新卒インフラエンジニア向け「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 describe の Events や kubectl 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は、initContainers に restartPolicy: 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 を起こします。両者の関係を次の図に示します。

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: {}
initContainersのwait-for-dbが依存の名前解決を待ち、完了後に本体が起動します。log-shipperはrestartPolicy: Always付きのネイティブ Sidecarで、本体とapplogボリュームを共有して並走します。backendのimageは第4回で push したk8s-registry:5000/fanclub-backend:0.2.0。第5回で設定したノードの registry 設定(hosts.toml)経由で取得されます。resourcesで要求 512Mi・上限 768Mi を設定。JVM ヒープ(制限の 75%)は約 576Mi に収まります。

適用して状態を確認します。
実行コマンド:
$ 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-forward は kill %1(または fg で前面に戻して Ctrl-C)で停止します。これで自社アプリ Backend が Kubernetes 上で初めて稼働しました。Pod 内ではメモリ・CPU の制限がかかるため、起動はローカルの Docker 単体(第3回)より少し遅く、応答までおおむね 10〜12 秒前後です。
OOMKilled を起こしてデバッグする
メモリ制限が小さすぎると何が起きるかを体験します。同じイメージで limits.memory を 256Mi に絞った 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
実行結果(例):起動と終了を繰り返し、STATUS が OOMKilled になり 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 を作り直すのが基本です。
やってみよう
fanclub-backend-pod.yaml(Init Container + ネイティブ Sidecar + 本体 + resources)を作成し、kubectl applyしてREADY 2/2を確認する。kubectl logs fanclub-backend -c wait-for-dbで Init の依存解決ログ、-c backendでデプロイ完了ログを確認する。backendログにdeployedが出た(デプロイ完了)のを待ってから、kubectl port-forwardで/health/liveがUPを返すことを確認する(早く叩くと Payara が 404 を返す)。limits.memory: 256Miの Pod を作って OOMKilled を発生させ、kubectl describeでReason: OOMKilled / Exit Code: 137を確認する。kubectl debug -it fanclub-backend --image=busybox:1.37 --target=backend --profile=general -- shで一時コンテナを差し込み、Pod 内から名前解決を試す。
理解度チェック
次の各文が正しいか(○)誤りか(×)を判断してください。解答は下にまとめています。
- 同じ Pod 内のコンテナは、同一 IP とボリュームを共有できる。
- Init Container は本体コンテナと同時に起動し、並走し続ける。
- ネイティブ Sidecar は
initContainersにrestartPolicy: Alwaysを付けて定義する。 - Ephemeral Container は
kubectl debugで稼働中の Pod に後から追加できる。 - コンテナのメモリ使用量が
limits.memoryを超えると OOMKilled(Exit Code 137)になる。 -XX:MaxRAMPercentage=75.0はヒープ上限をコンテナのメモリ制限の 75% に抑える。- 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
- 第1回 コンテナ技術概念 + Docker 環境準備
- 第2回 Docker 基本操作
- 第3回 Dockerfile + マルチステージ + JDK 25 / Payara Micro イメージビルド
- 第4回 コンテナレジストリ + イメージタグ戦略 + Trivy スキャン
第2部:Kubernetes 基礎
第3部:アプリリソース
- 第7回 Pod + Multi-container パターン ← 今ここ
- 第8回 Service とネットワーキング
- 第9回 ストレージ(PVC + StatefulSet)+ PostgreSQL DB 追加
- 第10回 ConfigMap + Secret + ServiceAccount 基礎
- 第11回 Job + CronJob + DaemonSet
第4部:ワークロード戦略
- 第12回 Deployment + 3 Probe + Rolling Update + Probe デバッグ実践
- 第13回 Deployment 戦略補完(Blue/Green + Canary + Recreate)
- 第14回 ResourceQuota + LimitRange + Multi-tenant Namespace
