本記事には広告(アフィリエイトリンク)が含まれます。

Podとマルチコンテナパターン|CKAD第7回

広告

新卒インフラエンジニア向け「Kubernetes 実践教科書① CKAD アプリケーション開発編」の第7回です。第6回までで kubectl を使いこなし、クラスタを観測できるようになりました。今回からは第3部「アプリリソース」に入り、第4回でレジストリに push した自社アプリ fanclub-backend を、いよいよ Pod として Kubernetes に初投入します。Pod 定義 YAML の構造、Init Container・Sidecar・Ephemeral Container というマルチコンテナ設計パターン、そして JVM ヒープと Pod のメモリ制限の整合(OOMKilled の再現)までを実機で扱います。

動作確認バージョン:kind v0.31.0(K8s v1.35.0)/ kubectl v1.35.6 / fanclub-backend:0.2.0 / busybox:1.37(2026-06-25 実機検証時点)

広告

今ここマップ(全 19 回中の現在地)

現在地は第 3 部「アプリリソース」の第 7 回です。ここから模擬アプリ fanclub-api を Kubernetes 上に組み上げていきます。

  • 第 1 部 コンテナと Docker(第 1〜4 回)
  • 第 2 部 Kubernetes 基礎(第 5〜6 回)
  • 第 3 部 アプリリソース(第 7〜11 回)← 今ここ
  • 第 4 部 ワークロード戦略(第 12〜14 回)
  • 第 5 部 セキュリティ基礎(第 15〜16 回)
  • 第 6 部 パッケージ管理と HTTPS 公開(第 17〜19 回)

この回のゴール

  • Pod 定義 YAML を作成し、自社イメージをクラスタにデプロイできる。
  • Init Container・Sidecar・Ephemeral Container の使い分けを説明できる。
  • JVM ヒープと resources.limits.memory の関係を実装し、OOMKilled を再現・デバッグできる。

Pod とは何か

Pod は Kubernetes が扱う最小のデプロイ単位で、1 つ以上のコンテナをまとめた実行単位です。同じ Pod 内のコンテナはネットワーク(同一 IP・localhost で相互通信)とストレージ(同じ Volume をマウント可能)を共有します。アプリ本体と、それを補助するコンテナ(後述の Sidecar など)を 1 つの Pod にまとめるのはこのためです。

Pod 定義 YAML の構造

Kubernetes のリソースはすべて同じ 4 つの最上位フィールドで記述します。Pod も例外ではありません。

  • apiVersion:リソースの API バージョン(Pod は v1)。
  • kind:リソースの種類(Pod)。
  • metadata:名前・ラベルなどのメタ情報。
  • spec:望ましい状態の定義(どのコンテナをどう動かすか)。

Pod のライフサイクル

Pod は status.phase で状態を表します。スケジュールやイメージ取得の段階が Pending、コンテナが動き出すと Running、(バッチ的に)正常終了すると Succeeded、異常終了すると Failed です。Web サーバーのような常駐アプリは Running のままになります。各段階の詳細は第6回で学んだ kubectl describeEventskubectl get events で追えます。

マルチコンテナ Pod 設計パターン

1 つの Pod に複数のコンテナを入れるときの定番パターンが 3 つあります。CKAD でも頻出です。

Init Container(前提条件を待つ)

Init Container は本体コンテナより先に・順番に・最後まで完了するまで実行されるコンテナです。完了して初めて本体が起動するため、「DB が起動するまで待つ」「設定ファイルを用意する」といった前提条件の準備に使います。本シリーズでは Backend が依存する DB の起動待ちに使いますが、DB(PostgreSQL)の追加は第9回のため、本回ではまだ DB がありません。そこで本回は、必ず存在する kubernetes.default サービスの名前解決を待機対象にして「依存の起動を待つ」パターンだけを実演し、第9回で待機対象を fanclub-db に差し替えます。

Sidecar(本体に並走して補助する)

Sidecar は本体コンテナと並走して補助機能(ログ転送・プロキシなど)を提供するコンテナです。Kubernetes 1.33 で GA したネイティブ Sidecarは、initContainersrestartPolicy: Always を付けて定義します。通常の Init Container と違い、本体より先に起動して動き続け、本体の停止後に停止します。本回では、本体と emptyDir ボリュームを共有してログを転送する想定の Sidecar を 1 つ並べます。

Ephemeral Container(後から差し込むデバッグ用)

Ephemeral Container は、稼働中の Pod に後から一時的に追加するデバッグ用コンテナです。本番イメージにシェルやネットワークツールを含めない(軽量・安全)方針でも、調査時だけ kubectl debug でツール入りコンテナを差し込めます。記事後半で実際に使います。

JVM ヒープと resources.limits.memory

Pod のコンテナには resources.requests(確保したい量)と resources.limits(上限)を設定できます。コンテナのメモリ使用量が limits.memory を超えるとカーネルにより強制終了され、これが OOMKilled(Out Of Memory Killed)です。

Java は要注意です。第3回で見たとおり、本イメージには JAVA_OPTS="-XX:MaxRAMPercentage=75.0" が焼き込まれており、JVM はヒープ上限を「コンテナのメモリ制限の 75%」に抑えます。たとえば limits.memory: 768Mi ならヒープ上限は約 576Mi、残りは Metaspace やスレッドスタックに使われます。この整合が取れていないと、JVM がヒープを確保した瞬間に制限を超えて OOMKilled になります。記事後半で、わざと limits.memory を小さくして OOMKilled を起こします。両者の関係を次の図に示します。

limits.memory 768Mi ではヒープ 576Mi(75%)+非ヒープ 192Mi が上限内に収まり Running、256Mi ではヒープ 192Mi に非ヒープを足した総量が上限を超えて OOMKilled(Exit 137)になることを示すメモリ配分の比較図。
図1:JVM ヒープと limits.memory の関係(768Mi は収まり、256Mi は超過して OOMKilled)

fanclub-api Backend を Pod で起動する

ここまでのパターンを 1 つの Pod にまとめます。次のマニフェストを developer のホームに fanclub-backend-pod.yaml として作成します(一般ユーザー所有のファイルで、kubectl apply はクラスタへ送るだけなので root 権限は不要です)。Init Container(依存待ち)+ネイティブ Sidecar(ログ転送想定)+本体 Backend の 3 コンテナ構成で、YAML は省略せず全量を示します。

apiVersion: v1
kind: Pod
metadata:
  name: fanclub-backend
  labels:
    app: fanclub-backend
spec:
  initContainers:
  - name: wait-for-db
    image: busybox:1.37
    command:
    - sh
    - -c
    - 'until nslookup kubernetes.default.svc.cluster.local >/dev/null 2>&1; do echo "waiting for dependency..."; sleep 2; done; echo "dependency resolved"'
  - name: log-shipper
    image: busybox:1.37
    restartPolicy: Always
    command:
    - sh
    - -c
    - 'echo "sidecar started"; tail -F /var/log/app/app.log 2>/dev/null || sleep infinity'
    volumeMounts:
    - name: applog
      mountPath: /var/log/app
  containers:
  - name: backend
    image: k8s-registry:5000/fanclub-backend:0.2.0
    ports:
    - containerPort: 8080
    resources:
      requests:
        memory: "512Mi"
        cpu: "250m"
      limits:
        memory: "768Mi"
        cpu: "1000m"
    volumeMounts:
    - name: applog
      mountPath: /var/log/app
  volumes:
  - name: applog
    emptyDir: {}
  • initContainerswait-for-db が依存の名前解決を待ち、完了後に本体が起動します。
  • log-shipperrestartPolicy: Always 付きのネイティブ Sidecarで、本体と applog ボリュームを共有して並走します。
  • backendimage は第4回で push した k8s-registry:5000/fanclub-backend:0.2.0。第5回で設定したノードの registry 設定(hosts.toml)経由で取得されます。
  • resources で要求 512Mi・上限 768Mi を設定。JVM ヒープ(制限の 75%)は約 576Mi に収まります。
fanclub-backend Pod 内で、Init Container wait-for-db が依存解決まで待って完走し、その後に本体 backend とネイティブ Sidecar log-shipper が並走して emptyDir ボリューム applog を共有する構成図。Init は READY 数に含まれず READY は 2/2。
図2:マルチコンテナ Pod の構成(Init 完走 → 本体 + ネイティブ Sidecar が並走・Volume 共有)

適用して状態を確認します。

実行コマンド:

$ kubectl apply -f fanclub-backend-pod.yaml
$ kubectl get pod fanclub-backend -o wide

実行結果(例):Init 完了後に本体と Sidecar が起動し、READY 2/2(本体 + ネイティブ Sidecar)になります。

NAME              READY   STATUS    RESTARTS   AGE   IP             NODE
fanclub-backend   2/2     Running   0          20s   10.244.82.13   kind-control-plane

各コンテナのログを -c(コンテナ指定)で確認します。Init は依存解決、Sidecar は起動、Backend はデプロイ完了が見えます。

実行コマンド:

$ kubectl logs fanclub-backend -c wait-for-db
$ kubectl logs fanclub-backend -c backend | grep deployed

実行結果(例):

dependency resolved
... app was successfully deployed in 5,018 milliseconds.

動作確認として、Liveness のヘルスチェックを叩きます。Pod 内の 8080 番をローカルに転送する kubectl port-forward を使います。ここで 1 つ注意があります。Pod が 2/2 Running になっても、Payara がアプリをデプロイし終える(コンテナ起動から約 10〜12 秒)まで /health/live は応答せず、早く叩くと Payara が HTTP 404 を返します。上の backend ログに app was successfully deployed(または ready in ...)が出たのを確認してから叩いてください。DB はまだ無いため /health/ready は DOWN ですが、/health/live はプロセスが生きていれば UP を返します。

実行コマンド:

$ kubectl port-forward pod/fanclub-backend 8080:8080 &
$ curl -s http://localhost:8080/health/live

実行結果(例):

{"status":"UP","checks":[{"name":"fanclub-api-live","status":"UP","data":{}}]}

確認できたら、バックグラウンド実行した port-forwardkill %1(または fg で前面に戻して Ctrl-C)で停止します。これで自社アプリ Backend が Kubernetes 上で初めて稼働しました。Pod 内ではメモリ・CPU の制限がかかるため、起動はローカルの Docker 単体(第3回)より少し遅く、応答までおおむね 10〜12 秒前後です。

OOMKilled を起こしてデバッグする

メモリ制限が小さすぎると何が起きるかを体験します。同じイメージで limits.memory256Mi に絞った Pod を、先ほどと同様に developer のホームへ backend-oom.yaml として作成します(一般ユーザー所有・root 不要)。ヒープ上限は約 192Mi まで下がり、Payara Micro の起動に必要なメモリを賄えず OOMKilled になります。

apiVersion: v1
kind: Pod
metadata:
  name: backend-oom
spec:
  containers:
  - name: backend
    image: k8s-registry:5000/fanclub-backend:0.2.0
    resources:
      requests:
        memory: "200Mi"
      limits:
        memory: "256Mi"

実行コマンド:

$ kubectl apply -f backend-oom.yaml
$ kubectl get pod backend-oom -w

実行結果(例):起動と終了を繰り返し、STATUSOOMKilled になり RESTARTS が増えていきます。

NAME          READY   STATUS      RESTARTS      AGE
backend-oom   1/1     Running     1 (7s ago)    15s
backend-oom   0/1     OOMKilled   1 (12s ago)   20s

原因は kubectl describe で確定します。第6回で学んだ「詳細とイベントを見る」手順です。

実行コマンド:

$ kubectl describe pod backend-oom | grep -A2 "Last State"

実行結果(例):

    Last State:     Terminated
      Reason:       OOMKilled
      Exit Code:    137

Reason: OOMKilled / Exit Code: 137 がメモリ超過の決定的な証拠です。対処は「limits.memory を増やす」または「MaxRAMPercentage を下げる」のいずれかで、JVM が使う実メモリと制限を整合させます。確認できたら片付けます。

実行コマンド:

$ kubectl delete pod backend-oom

kubectl debug で一時コンテナを差し込む

稼働中の fanclub-backend に、調査用の Ephemeral Container を差し込みます。本体イメージ(JRE のみ・シェルやネットワークツールは最小)に手を入れず、busybox のツールで Pod 内のネットワークなどを調べられます。--target で対象コンテナのプロセス名前空間を共有します。--profile=general を指定します(指定しないと legacy プロファイルが使われ、非推奨の警告が出ます)。

実行コマンド:

$ kubectl debug -it fanclub-backend --image=busybox:1.37 --target=backend --profile=general -- sh

差し込んだコンテナ内で、たとえば依存先の名前解決を確認できます。

実行コマンド(一時コンテナ内):

# nslookup kubernetes.default.svc.cluster.local

追加した Ephemeral Container は kubectl get pod fanclub-backend -o jsonpath='{.spec.ephemeralContainers[*].name}'debugger-xxxxx として確認できます。Ephemeral Container は後から削除できず Pod の再作成で消える一時的な存在で、調査が終わったら Pod を作り直すのが基本です。

やってみよう

  1. fanclub-backend-pod.yaml(Init Container + ネイティブ Sidecar + 本体 + resources)を作成し、kubectl apply して READY 2/2 を確認する。
  2. kubectl logs fanclub-backend -c wait-for-db で Init の依存解決ログ、-c backend でデプロイ完了ログを確認する。
  3. backend ログに deployed が出た(デプロイ完了)のを待ってから、kubectl port-forward/health/liveUP を返すことを確認する(早く叩くと Payara が 404 を返す)。
  4. limits.memory: 256Mi の Pod を作って OOMKilled を発生させ、kubectl describeReason: OOMKilled / Exit Code: 137 を確認する。
  5. kubectl debug -it fanclub-backend --image=busybox:1.37 --target=backend --profile=general -- sh で一時コンテナを差し込み、Pod 内から名前解決を試す。

理解度チェック

次の各文が正しいか(○)誤りか(×)を判断してください。解答は下にまとめています。

  1. 同じ Pod 内のコンテナは、同一 IP とボリュームを共有できる。
  2. Init Container は本体コンテナと同時に起動し、並走し続ける。
  3. ネイティブ Sidecar は initContainersrestartPolicy: Always を付けて定義する。
  4. Ephemeral Container は kubectl debug で稼働中の Pod に後から追加できる。
  5. コンテナのメモリ使用量が limits.memory を超えると OOMKilled(Exit Code 137)になる。
  6. -XX:MaxRAMPercentage=75.0 はヒープ上限をコンテナのメモリ制限の 75% に抑える。
  7. Pod 定義の最上位フィールドは apiVersion / kind / metadata / spec である。

解答

  • 1. ○:同一 Pod 内はネットワーク(localhost)とボリュームを共有できる。
  • 2. ×:Init Container は本体より先に・完了するまで動く。並走するのは Sidecar。
  • 3. ○:ネイティブ Sidecar は restartPolicy: Always 付きの Init Container。
  • 4. ○:本番イメージに手を入れず、調査時だけツールを差し込める。
  • 5. ○:メモリ制限超過で OOMKilled・Exit Code 137 になる。
  • 6. ○:制限の 75% をヒープ上限とし、残りを Metaspace 等に充てる。
  • 7. ○:すべての Kubernetes リソースに共通の 4 フィールド。

まとめ

本記事では、Pod の構造(apiVersion / kind / metadata / spec)とライフサイクルを押さえ、Init Container・ネイティブ Sidecar・Ephemeral Container の 3 パターンを実装しました。そのうえで自社アプリ fanclub-backend を Pod として Kubernetes に初投入し、JVM ヒープと limits.memory の整合、OOMKilled の再現とデバッグ(Reason: OOMKilled / Exit Code: 137)まで実機で確認しました。第4回で push したイメージが、第5回で整えたレジストリ設定を通じてクラスタで動いた点も、ここまでの積み上げがつながった瞬間です。

次回予告

次回(第8回)は Service を扱います。Pod の IP は再作成のたびに変わるため、安定した名前でアクセスする仕組みが要ります。ClusterIP / NodePort などの Service タイプを学び、Frontend(Nginx)Pod を追加して Frontend → Backend のクラスタ内通信を確立します。

シリーズ一覧

第1部:コンテナと Docker

第2部:Kubernetes 基礎

第3部:アプリリソース

第4部:ワークロード戦略

第5部:セキュリティ基礎

第6部:パッケージ管理 + HTTPS 公開

広告
kubernetes
スポンサーリンク