- 第7回スコープ・学習目標・今ここマップ
- Pod とは何か — Kubernetes の最小実行単位
- Pod 定義 YAML の構造 — apiVersion / kind / metadata / spec
- Pod ライフサイクル — Pending から Running・Succeeded / Failed まで
- マルチコンテナ Pod 設計パターン概観 — Init / Sidecar / Ephemeral の使い分け
- Init Container — メインコンテナ起動前の前処理を定義する
- Sidecar パターン — メインコンテナと並走するサポートコンテナ
- Ephemeral Container — kubectl debug で稼働 Pod にデバッグシェルを注入する
- JVM ヒープ vs resources.limits.memory — OOMKilled を防ぐ設計
- やってみよう①:Backend Pod 起動 + Init Container ログ確認
- やってみよう②:OOMKilled 演習 — limits.memory を過小設定してデバッグする
- やってみよう③:Ephemeral Container で稼働 Pod 内デバッグ
- 現場ヒヤリハット — Init Container が終わらない / imagePullPolicy: Always の本番問題
- 理解度チェック + まとめ + 次回予告 + シリーズ一覧
第7回スコープ・学習目標・今ここマップ
動作確認バージョン: kind v0.31.0 / kindest/node:v1.35.0 / kubectl v1.35.0 (Kustomize v5.7.1) / fanclub-backend:0.1.0 (eclipse-temurin:25-jre + Payara Micro 7.2026.4) / Docker CE 29.4.3 / containerd 2.2.3 / AlmaLinux 10.1(kernel 6.12.0-124.55.3.el10_1)(2026-05-10 時点・k8s-ops 実機検証済・SP_vol1-pre-11 起点)
本回は Kubernetes 実践教科書 第1巻(CKAD 対応・全 19 回)の第7回です。第3部「アプリリソース」の第1回として、Pod の YAML 定義・マルチコンテナパターン(Init / Sidecar / Ephemeral)・JVM ヒープと limits.memory の整合・OOMKilled デバッグ を扱います。
CKAD D1(Application Design and Build・20 %)の中核を網羅し、D4(Application Environment, Configuration and Security・25 %)の resources.requests / limits 部分も補完します。
第6回からの継承状態確認(SP_vol1-pre-11 状態):
| 項目 | 状態 | 出典 |
|---|---|---|
| kind クラスタ | kind-control-plane Ready(v1.35.0・37m old) | ep5 で作成済・継続稼働中 |
| nginx-test Pod | default ns で 1/1 Running(ep6 由来) | ep6 演習で作成・継続稼働中 |
| metrics-server | kube-system ns で 1/1 Running | ep6 H2-9 で導入済 |
| alias k=kubectl | ~/.bashrc に永続設定済 | ep6 完了済 |
| fanclub-backend:0.1.0 | ホスト Docker images に存在(657MB・ID 3bdcfa296cf6) | ep3 でビルド済 |
今ここマップ(第1巻 19 回中の現在位置):
第1部 コンテナとDocker
第1回: コンテナ技術概念 + Docker環境準備 [完了]
第2回: Docker基本操作 [完了]
第3回: Dockerfile + マルチステージビルド + JDK 25/Payara Micro [完了]
第4回: コンテナレジストリ + イメージタグ戦略 + Trivy スキャン [完了]
第2部 Kubernetes基礎
第5回: K8s全体像 + kind で軽量K8s [完了]
第6回: kubectl基本操作 + Observability・Debug [完了]
第3部 アプリリソース
★ 第7回: Pod + Multi-containerパターン ← 今ここ
第8回: Service とネットワーキング
第9回: ストレージ(PVC + StatefulSet)+ PostgreSQL DB追加
第10回: ConfigMap + Secret + ServiceAccount 基礎
第11回: Job + CronJob + DaemonSet
第4部 ワークロード戦略(第12〜14回)
第5部 セキュリティ基礎(第15〜16回)
第6部 パッケージ管理 + HTTPS公開(第17〜19回)
第7回を終えると、以下を習得した状態になります。
- Pod YAML の 4 要素(
apiVersion/kind/metadata/spec)を記述してkubectl apply -fで起動できる - Init Container で「依存リソース起動待ち」を実装し、コンテナ間の依存関係を解決できる
- Sidecar・Init Container・Ephemeral Container の用途を区別して使い分けを説明できる
limits.memoryと JVM の-XX:MaxRAMPercentage=75.0の整合を設計し、OOMKilled を回避できるkubectl debug -itで稼働中の Pod に Ephemeral Container を注入してデバッグできる
模擬アプリ進捗(第7回):第1巻で初めて fanclub-api を Kubernetes に載せる重要マイルストーンです。第6回まで「Docker で動かす」段階だった fanclub-backend が、今回から「Kubernetes で動かす」段階へ進みます。
第7回完了時点では Backend Pod が単独で稼働している状態となり、第8回で Service を追加してネットワーク経由のアクセスを実現します。
前回からの橋渡し:第6回で覚えた kubectl describe pod と kubectl logs --previous は、本回の OOMKilled デバッグ演習でそのまま使います。第6回で「概念のみ紹介した」--dry-run=client -o yaml テクニックは、本回が初の本格活用となります。
kubectl explain pod.spec.containers による YAML スキーマ確認も第6回で習得済みのため、本回の YAML 記述で活用していきます。
Pod とは何か — Kubernetes の最小実行単位
第6回まで Docker でコンテナを直接動かしてきました。Kubernetes では、コンテナを直接動かさず必ず Pod という単位で包んで動かします。なぜそんな手間をかけるのか、本節で Pod の本質を確認していきます。
Pod = 1 つ以上のコンテナの実行単位
Pod は 1 つ以上のコンテナをまとめて 1 つの実行単位として扱う 仕組みです。最も一般的な構成は「1 Pod に 1 コンテナ」で、本回の Backend Pod もこの構成を基本にします。「1 Pod に複数コンテナ」を入れる場合は、複数のコンテナが密結合に協調動作する必要があるとき(例:メインコンテナとログ転送 Sidecar)に限られます。
| 観点 | Docker のコンテナ単独実行 | Kubernetes の Pod |
|---|---|---|
| 実行単位 | コンテナ 1 つ | 1 つ以上のコンテナをまとめた論理単位 |
| ネットワーク | コンテナごとに独立した network namespace | 同一 Pod 内のコンテナは network namespace を共有 |
| ストレージ | コンテナごとに独立したファイルシステム | 共有 Volume を介して複数コンテナで同じファイルを参照可能 |
| スケジューリング | 該当なし(手動配置) | Pod 単位で 1 つのノードへ配置される(コンテナ単位ではない) |
| ライフサイクル | コンテナごとに独立 | Pod 単位で起動・停止する |
なぜコンテナを Pod に包むのか
Pod という抽象を導入する理由は、Kubernetes が スケジューリングとネットワーキングの最小単位を Pod に揃えている 点に集約されます。コンテナ単位でスケジューリングしてしまうと、本来同じノードに配置したい補助プロセス(ログ転送・プロキシ)が別のノードに散らばる可能性があり、設計が複雑になります。
Pod 単位でまとめておけば、密結合な補助プロセスは必ず同じノードに配置され、localhost で相互通信できます。
同一 Pod 内の全コンテナは以下の 2 種類のリソースを共有します。
- 共有 network namespace:全コンテナが同じ Pod IP を共有し、コンテナ間通信は
localhost:<ポート>で完結する。外部からのアクセスは Pod IP(または第8回で学ぶ Service)経由で行う - 共有 Volume:
spec.volumes[]で定義した Volume を複数コンテナでvolumeMountsすると、共有ファイルシステムとして使える。emptyDir / ConfigMap / PVC など複数の Volume タイプがある(PVC の詳細は第9回)
Pod 設計の基本方針
Pod に何を入れるかの判断基準は、現場では次のように整理されます。
- 密結合な補助プロセスは同じ Pod に入れる:例えば「アプリ本体 + ログ転送 Sidecar」「アプリ本体 + サービスメッシュのプロキシ」のように、必ず同居させたいプロセスはマルチコンテナ Pod として設計する
- 独立してスケールできるコンポーネントは別 Pod に分ける:例えば「Frontend と Backend」「Backend と DB」は別 Pod として設計する。fanclub-api でも Frontend / Backend / DB を別 Pod で構成する方針を取っている
本回では Backend だけを Pod 化します。Frontend Pod は第8回で追加し、第8回の Service を介して Frontend → Backend の通信を実装していきます。
Pod 定義 YAML の構造 — apiVersion / kind / metadata / spec
本回は第1巻で初めて YAML マニフェストを本格的に書く回です。Kubernetes のリソース定義 YAML はすべて apiVersion / kind / metadata / spec の 4 要素で構成されます。Pod に限らず Deployment(第12回)・Service(第8回)・ConfigMap(第10回)など全リソースで同じ構造を使うため、本回で確実に定着させます。
YAML 4 要素の役割
| 要素 | 型 | 役割 | Pod での例 |
|---|---|---|---|
apiVersion | string | Kubernetes API のバージョン | v1(Pod は Core API・group prefix なし) |
kind | string | リソースの種別 | Pod(大文字始まり) |
metadata | Object | リソースの識別情報 | name(必須)/ namespace(省略時は default)/ labels(任意のキー/バリュー) |
spec | Object | リソースの宣言的定義 | containers[](必須)/ initContainers[] / volumes[] / restartPolicy など |
apiVersion と kind の組み合わせで Kubernetes API がどのスキーマで検証するかが決まります。kubectl api-resources で全リソースの apiVersion / kind が一覧できます(第6回で習得済)。
–dry-run=client -o yaml で雛形を生成する
第6回で概念のみ紹介した --dry-run=client -o yaml テクニックを、本回で初めて実用に使います。Pod の YAML をゼロから書くのではなく、雛形を生成して必要なフィールドを追記する流れです。
実行コマンド:
$ kubectl run fanclub-backend --image=fanclub-backend:0.1.0 --dry-run=client -o yaml
実行結果:
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: fanclub-backend
name: fanclub-backend
spec:
containers:
- image: fanclub-backend:0.1.0
name: fanclub-backend
resources: {}
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
--dry-run=client は「クライアント側で検証のみ行い、API Server には送信しない」モードです。-o yaml で生成されたマニフェストを標準出力に表示します。CKAD 試験でも YAML を素早く準備する標準テクニックとして広く使われています。
生成された雛形に resources / env / imagePullPolicy / initContainers を追記して完全な Pod 定義に仕上げる流れを、次節で実際に行います。
本回で使用する Backend Pod の完全 YAML
本回の演習で作成する fanclub-backend Pod の完全な YAML を先に提示します。Init Container と resources 設定を含む完成版です。
apiVersion: v1
kind: Pod
metadata:
name: fanclub-backend
labels:
app: fanclub-backend
spec:
initContainers:
- name: wait-for-api
image: busybox:1.36
command:
- sh
- -c
- "until nc -z kubernetes.default.svc.cluster.local 443; do echo 'waiting for API server...'; sleep 2; done"
containers:
- name: fanclub-backend
image: fanclub-backend:0.1.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
env:
- name: JAVA_OPTS
value: "-XX:MaxRAMPercentage=75.0"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "1000m"
このマニフェストの設計ポイントを整理します。
metadata.name: fanclub-backend:Pod の一意識別名。同一 namespace 内で重複不可metadata.labels.app: fanclub-backend:第8回 Service のselectorで参照する SSOT ラベル。本回で正確に定義することが第8回以降の前提条件になるspec.initContainers[]:メインコンテナ起動前に実行する前処理コンテナ。本回では「API Server への接続確認」を Init Container で実装する。第9回で「PostgreSQL の接続待ち」に置き換える布石spec.containers[].name: fanclub-backend:コンテナ名。kubectl logs <pod> -c <container-name>でログを取得するときに使う識別子。第8回 Service のtargetPort名指し参照でも使われる SSOTimagePullPolicy: IfNotPresent:「ローカルキャッシュがあれば使う・なければ pull する」設定。kind 環境ではkind loadでロードしたイメージを使うため、この設定が必須(Always だと外部 registry を叩いて失敗する)containerPort: 8080:コンテナ内で Listen するポート番号。fanclub-backend の Payara Micro が--port 8080で起動する設定(app-spec で SSOT 確定済)env.JAVA_OPTS: -XX:MaxRAMPercentage=75.0:JVM ヒープをlimits.memoryの 75 % に設定する Container-aware JVM オプション。詳細は H2-9 で扱うresources.requests / limits:requests がスケジューリング基準、limits が実行時上限。本回では requests < limits の Burstable 構成を採用する
YAML スキーマ確認の手順:書いている途中で「このフィールドは何だっけ?」となったときは、kubectl explain pod.spec.containers で kubernetes.io を開かずにスキーマを確認できます。第6回で習得した kubectl explain <resource>.<field> の活用場面です。
実行コマンド(コンテナフィールドのスキーマ確認):
$ kubectl explain pod.spec.containers
実行コマンド(imagePullPolicy のスキーマだけ確認):
$ kubectl explain pod.spec.containers.imagePullPolicy
CKAD 試験中も kubectl explain はそのまま使えます。kubernetes.io 公式ドキュメントを開く時間が惜しい試験本番で、強力なフィールド確認手段になります。
Pod ライフサイクル — Pending から Running・Succeeded / Failed まで
Pod は kubectl apply -f した瞬間に Running になるわけではなく、いくつかの状態(Phase)を経由します。Phase の遷移を理解すると、Pod が起動しないときに「今どの段階で詰まっているのか」を切り分けられます。
Pod Phase の 5 状態
| Phase | 意味 | 典型例 |
|---|---|---|
| Pending | スケジューリング待ちまたはイメージ pull 中 | リソース不足で配置先ノードがない / ImagePullBackOff |
| Running | 少なくとも 1 つのコンテナが Running 状態 | 正常稼働中 |
| Succeeded | 全コンテナが正常終了(Exit Code 0) | Job のコンテナが完了(第11回で扱う) |
| Failed | 少なくとも 1 つのコンテナが異常終了 | CrashLoopBackOff の後に Failed |
| Unknown | ノードとの通信断などで状態不明 | ノード障害・kubelet 停止 |

Pending の 2 つのパターン
Pending は混同されやすい Phase です。原因は大きく 2 系統に分かれ、対処方法も異なります。
- パターン A:Scheduler が配置先ノードを探している(リソース不足・
Unschedulable)。kubectl describe podの Events にFailedSchedulingが記録される。対処は requests を下げるかノードを追加する - パターン B:ノードは決まったがイメージ pull 中(
ImagePullBackOff/ErrImagePull)。Events にFailed to pull imageが記録される。対処は registry のアクセス権・タグの存在・imagePullPolicy の見直し
どちらのパターンも kubectl describe pod の Events で原因を特定できます。第6回で習得した「Events を必ず確認する」習慣がここで活きます。
コンテナ Status と Pod Phase の違い
Pod Phase は Pod 全体の大まかな状態を表しますが、コンテナ単位の詳細状態 は別に存在します。kubectl describe pod の Containers セクションに表示される State(Waiting / Running / Terminated)がそれです。
| コンテナ State | 意味 | サブ状態の例 |
|---|---|---|
| Waiting | 起動待ち・起動失敗中 | ContainerCreating / CrashLoopBackOff / ImagePullBackOff |
| Running | 稼働中 | —(Started 時刻のみ表示) |
| Terminated | 終了 | Completed(Exit 0)/ Error(Exit ≠ 0)/ OOMKilled(Exit 137) |
第6回の補足:第6回で扱った CrashLoopBackOff は Pod Phase ではなく、コンテナ State の Waiting サブ状態 です。Phase 自体は Running(または初回起動時は Pending)であることが多く、ここを混同しないことが重要です。
第6回の演習②でクラッシュループを観察したとき、kubectl get pod の STATUS 列に CrashLoopBackOff と表示されていましたが、あれはコンテナ State の表示でした。
kubectl get pod -w でリアルタイム監視
Pod Phase の遷移をリアルタイムで観察したいときは -w(watch)フラグを使います。状態変化があるたびに 1 行追記されます。
実行コマンド:
$ kubectl get pod fanclub-backend -w
本回の演習① Step 5 でこのコマンドを使い、Init Container 実行中(Init:0/1)→ PodInitializing → Running の遷移を観察します。
マルチコンテナ Pod 設計パターン概観 — Init / Sidecar / Ephemeral の使い分け
マルチコンテナ Pod には 3 つの代表的な設計パターンがあります。CKAD D1 でも頻出論点で、「いつ何を使うか」を直感的に区別できる状態が試験合格の前提になります。
3 パターンの比較
| 観点 | Init Container | Sidecar | Ephemeral Container |
|---|---|---|---|
| 定義場所 | spec.initContainers[] | spec.containers[] 複数定義(従来型)または spec.initContainers[] + restartPolicy: Always(ネイティブ) | kubectl debug で動的追加 |
| 実行タイミング | メインコンテナ起動前(順次・直列) | メインコンテナと同時(並走・並列) | 後から動的追加(デバッグ目的) |
| 完了後の扱い | 完了 → 次の Init Container またはメインコンテナが起動 | Pod 停止まで継続 | Pod 停止まで残る(削除不可) |
| 主な用途 | DB 接続待ち・設定ファイル生成・初期化 | ログ転送・プロキシ・監視エージェント | distroless イメージのデバッグ・一時調査 |
| Probe 設定 | 不可 | 可能(Liveness / Readiness / Startup) | 不可(resources も制限あり) |
| 削除可否 | Pod 再作成で消える | Pod 再作成で消える | 削除不可(Pod 終了まで残る) |

使い分けの判断基準
3 パターンの選択は、次の質問に答えれば自然に決まります。
- Q1:メインコンテナの起動前に 1 回だけ実行したい? → Init Container を選ぶ(DB 接続確認・設定ファイル生成)
- Q2:メインコンテナと並行して常時動作させたい? → Sidecar を選ぶ(ログ転送・プロキシ・監視)
- Q3:稼働中の Pod に後から一時的にデバッグ用シェルを追加したい? → Ephemeral Container を選ぶ(distroless イメージのトラブルシュート)
本回では Init Container と Ephemeral Container を実機演習し、Sidecar は概念と YAML 例示にとどめます。Sidecar の実機演習は第12回(Deployment + 3 Probe)以降と第2巻第13回(Loki + Fluent Bit)で本格的に行います。
Init Container — メインコンテナ起動前の前処理を定義する
Init Container は メインコンテナの起動前に 1 回だけ実行される前処理コンテナ です。「DB が起動するまで Backend を待たせる」「設定ファイルを動的生成する」「権限ディレクトリを作成する」など、メインアプリ起動の前提条件を整える役割を担います。本回では「API Server への接続確認」を実装し、第9回で「PostgreSQL の接続待ち」に差し替える布石を打ちます。
Init Container の動作原理
- 順次実行(直列):
spec.initContainers[]に列挙した順に 1 つずつ実行される。並列ではない - 前のコンテナの完了が次の起動条件:前の Init Container が Exit Code 0 で終了してから次が起動する
- 全 Init Container の完了がメインコンテナ起動の前提:1 つでも Init Container が完了していなければ、メインコンテナは起動しない
- 失敗時は restartPolicy に従う:
restartPolicy: Always(デフォルト)の場合は Pod 全体が再起動する。Neverの場合は Pod が Failed Phase に遷移する
Init Container の制限事項
Init Container はメインコンテナと異なるいくつかの制限があります。CKAD 試験で問われやすい論点です。
- Liveness Probe / Readiness Probe / Startup Probe は使用不可(Init Container は完了させて捨てる前提のため)
lifecycle.postStart/lifecycle.preStopHook も使用不可- resources / volumeMounts / env / envFrom は使用可能
- 共有 Volume を使えば Init Container で生成したファイルをメインコンテナに渡せる(emptyDir Volume の典型用途)
本回で採用する Init Container の YAML
本回の演習では、busybox イメージで nc -z(netcat の port scan)を使い、Kubernetes API Server への接続確認を行う Init Container を採用します。
kubernetes.default.svc.cluster.local は kind クラスタ内で必ず解決可能な API Server の DNS 名で、待ち時間がほぼゼロのため演習が短時間で完了します。
initContainers:
- name: wait-for-api
image: busybox:1.36
command:
- sh
- -c
- "until nc -z kubernetes.default.svc.cluster.local 443; do echo 'waiting for API server...'; sleep 2; done"
各フィールドの意味を整理します。
name: wait-for-api:Init Container の識別名。kubectl logs <pod> -c wait-for-apiでログを取得する際に使うimage: busybox:1.36:軽量な Linux 環境イメージ。nc(netcat)が内蔵されているcommand3 要素配列:sh -c "<シェルコマンド文字列>"の標準パターン。3 つ目の要素は単一文字列でなければならず、本回では"(ダブルクォート)で囲み、内部のシングルクォートとの混在を明確にしているnc -z <host> <port>:ポートに接続できるかだけを確認するモード(-z = データ送受信なし)kubernetes.default.svc.cluster.local:443:Kubernetes API Server の well-known DNS 名 + HTTPS ポート。kind クラスタ内では即座に応答するため、Init Container はほぼ即完了する
第9回への橋渡し
本回の Init Container は「API Server を待つ」という、教育目的の練習用設定です。本番では「DB が起動するまで Backend を待たせる」など、依存リソースの起動を待つ用途で使います。
第9回(PVC + StatefulSet + DB 追加)では、本回の Init Container の待ち先を nc -z fanclub-db 5432(PostgreSQL の待ち受けポート)に差し替えます。Pod YAML の他の部分はそのまま流用できる構造で本回の YAML を組んでいます。
kubectl describe pod での Init Container 確認
Init Container の状態は kubectl describe pod の Init Containers セクションで確認できます。完了後は次のように表示されます。
Init Containers:
wait-for-api:
State: Terminated
Reason: Completed
Exit Code: 0
Ready: True
Init Container 専用のログは -c <init-container-name> で別途取得できます。第6回で「マルチコンテナ Pod の特定コンテナのみ指定する」場面として説明していた使い方が、ここで具体化されます。
実行コマンド:
$ kubectl logs fanclub-backend -c wait-for-api
Sidecar パターン — メインコンテナと並走するサポートコンテナ
Sidecar はメインコンテナと並走する補助コンテナのパターンです。ログ転送・プロキシ・監視エージェント・セキュリティスキャナなど、メインアプリと並行して動かす補助プロセスに使います。
本回では概念と YAML 例示にとどめ、実機演習は第12回(Deployment + 3 Probe)以降に委ねます(学習負荷管理のため)。CKAD 試験では Sidecar の YAML 記述問題も出題されるため、本節では完全な YAML を提示します。
従来型 Sidecar(spec.containers[] に複数定義)
従来から使われている Sidecar の書き方です。spec.containers[] にメインコンテナとサポートコンテナの両方を列挙します。Kubernetes 初期から存在するシンプルな構成で、現在も広く使われています。
apiVersion: v1
kind: Pod
metadata:
name: fanclub-backend-with-sidecar
labels:
app: fanclub-backend-with-sidecar
spec:
containers:
- name: fanclub-backend
image: fanclub-backend:0.1.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
volumeMounts:
- name: log-volume
mountPath: /var/log
- name: log-shipper
image: busybox:1.36
command:
- sh
- -c
- "tail -f /var/log/app.log"
volumeMounts:
- name: log-volume
mountPath: /var/log
volumes:
- name: log-volume
emptyDir: {}
このマニフェストは「メインの fanclub-backend が /var/log/app.log にログを書き込み、Sidecar の log-shipper がそのログを tail -f で読み取る」という典型構造です。emptyDir Volume を両コンテナで volumeMounts することで、ファイルシステムを共有しています。
emptyDir は Pod の生存期間だけ存在する一時ボリュームで、Pod が削除されると消えます(永続化が必要なケースは第9回 PVC で扱います)。
ネイティブ Sidecar(K8s v1.29+ Beta・v1.35 で GA)
Kubernetes v1.29 で Beta、v1.35 で GA となった新しい Sidecar 記法があります。spec.initContainers[] 内で restartPolicy: Always を指定すると、その Init Container は Sidecar として Pod 生存期間全体に渡って稼働する 動作になります。
Init Container の特徴(メインコンテナより前に起動する)と Sidecar の特徴(並走する)を合わせ持つ仕組みです。
apiVersion: v1
kind: Pod
metadata:
name: fanclub-backend-native-sidecar
labels:
app: fanclub-backend-native-sidecar
spec:
initContainers:
- name: log-shipper
image: busybox:1.36
restartPolicy: Always
command:
- sh
- -c
- "tail -f /var/log/app.log"
volumeMounts:
- name: log-volume
mountPath: /var/log
containers:
- name: fanclub-backend
image: fanclub-backend:0.1.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
volumeMounts:
- name: log-volume
mountPath: /var/log
volumes:
- name: log-volume
emptyDir: {}
ネイティブ Sidecar の利点は次の 2 点です。
- 起動順序が明確:Sidecar はメインコンテナより前に起動する。「ログ転送 Sidecar が立ち上がってからメインアプリが起動する」という順序を保証できる
- 終了順序も整理される:Pod 終了時、メインコンテナの完了を待ってから Sidecar が終了する。Job ワークロード(第11回)で「メインコンテナ完了後に Sidecar が残ってしまう」問題が解消される
Sidecar の主な用途
- ログ転送:Fluent Bit / Filebeat(第2巻第13回で Loki + Fluent Bit を実機運用)
- プロキシ:Envoy / istio-proxy(サービスメッシュの基礎)
- 監視エージェント:Prometheus exporter / Datadog agent
- セキュリティスキャナ:ランタイムでのプロセス監視
CKAD 試験では従来型・ネイティブ両方の Sidecar 記述が出題範囲です。本回の理解度チェック Q9 でネイティブ Sidecar の GA バージョンを問うため、本節の内容を押さえておいてください。
Ephemeral Container — kubectl debug で稼働 Pod にデバッグシェルを注入する
Ephemeral Container は 稼働中の Pod に後から動的に追加するデバッグ専用コンテナ です。Pod spec の一部として最初からデプロイするものではなく、トラブルシューティング目的で kubectl debug コマンド経由で注入します。
第6回 H2-12「ヒヤリハット事例 2」で扱った「kubectl exec セッション中に Pod が削除されてセッション切断された」問題への、より安全な代替手段としての位置づけです。
kubectl exec との違い
| 観点 | kubectl exec | kubectl debug(Ephemeral Container) |
|---|---|---|
| 前提 | 対象コンテナに shell が存在する必要がある | shell がない distroless イメージでも可 |
| 起動コスト | 低い(既存コンテナに attach) | やや高い(新規コンテナを Pod に追加) |
| 使用イメージ | 対象コンテナのイメージ(変更不可) | --image で任意指定(busybox / alpine 等) |
| Pod 削除時の挙動 | セッション即切断 | Ephemeral Container も Pod と同時に削除 |
| 削除可否 | セッション切断で完了 | Pod 終了まで残る(個別削除は不可) |
| 主な用途 | 環境変数確認・短時間の読み取り調査 | distroless イメージのデバッグ・詳細トラブルシュート |
kubectl debug の基本構文
本回の演習③で実際に使うコマンドの基本形を確認しておきます。
実行コマンド(基本構文):
$ kubectl debug -it fanclub-backend --image=busybox:1.36 --target=fanclub-backend
-it:interactive(標準入力接続)+ TTY(仮想端末割り当て)の組み合わせ。シェル操作のために必須--image=busybox:1.36:デバッグ用に注入するコンテナイメージ。shell が含まれているものを指定する--target=fanclub-backend:Process Namespace を共有する対象コンテナ名。共有することで対象コンテナのプロセス(PID)が Ephemeral Container 内のpsで見える
distroless イメージのデバッグ
本番運用では「shell を含まないイメージ」を採用するセキュリティ対策が広く行われています。Google 提供の distroless イメージ(gcr.io/distroless/java25-debian12 など)には /bin/sh も /bin/bash も含まれず、攻撃者が侵入しても shell コマンドを実行できません。第3巻(CKS)でも詳しく扱う設計手法です。
distroless イメージを採用すると、kubectl exec でデバッグできなくなります。kubectl exec ... -- /bin/sh が executable not found で失敗するためです。Ephemeral Container はこの問題を解決します。
busybox や alpine のような shell 入りイメージを --image で指定して Pod に追加することで、distroless で稼働しているコンテナと同じネットワーク・ストレージにアクセスしながらデバッグできます。
Ephemeral Container の制限事項
- 削除不可:一度追加した Ephemeral Container は Pod が終了するまで残る。
kubectl debugで同じ Pod に複数回 shell を注入すると、Ephemeral Container が累積して残り続ける - resources / ports / readinessProbe 等は設定不可:通常コンテナで使えるフィールドの一部が制限されている
--targetで指定するコンテナが存在しない場合はエラー:コンテナ名のタイプミスに注意- Process Namespace 共有が必要な場合がある:対象コンテナのプロセスを見るには Pod spec の
shareProcessNamespace: trueが必要なケースがある(--targetで代替できる場合も多い)
JVM ヒープ vs resources.limits.memory — OOMKilled を防ぐ設計
fanclub-backend は Java アプリケーション(JDK 25 LTS + Payara Micro 7.2026.4)です。Java アプリを Kubernetes で動かすときに最も問題になるのが JVM ヒープと limits.memory の整合 です。本節では設計の原則と、本回の演習②で実際に再現する OOMKilled の仕組みを定量的に押さえます。
Container-aware JVM の動作
JDK 11 以降、JVM はコンテナ環境を自動認識する -XX:+UseContainerSupport がデフォルトで ON になっています。この設定により、JVM は ホストの物理メモリではなく、cgroup の制限値(= Pod の limits.memory)を基準にヒープサイズを計算する 動作になります。
limits.memory指定あり:JVM は cgroup 制限値のMaxRAMPercentage%(デフォルト 25 %)をヒープ上限として使用するlimits.memory指定なし:JVM はホスト物理メモリ全体を参照する。ノードの全メモリを単一コンテナが消費する可能性があり危険
本番では limits.memory を必ず指定することが鉄則です。本回の Backend Pod でも limits.memory: 512Mi を明示しています。
JDK 25 のデフォルト 25 % vs 推奨 75 % の差
第3回(Dockerfile + マルチステージビルド)の重要発見でも触れた通り、JDK 25 のデフォルト MaxRAMPercentage は 25 % です。これは保守的な値で、limits.memory: 512Mi の場合のヒープが 128Mi しかなく、Java アプリが OutOfMemoryError を起こしやすくなります。
limits.memory: 512Mi の場合
デフォルト(MaxRAMPercentage=25.0): 512 × 0.25 = 128 Mi → 小さすぎて OutOfMemoryError しやすい
推奨(MaxRAMPercentage=75.0): 512 × 0.75 = 384 Mi → 残り 25 % が Metaspace / Native Thread 等に使われる

limits.memory 別のヒープ計算表
limits.memory | 75 % 設定時の JVM ヒープ | 残り 25 % の用途 |
|---|---|---|
| 512 Mi | 384 Mi | Metaspace / Direct Memory / Native Thread Stack |
| 1 Gi(1024 Mi) | 768 Mi | 同上 |
| 2 Gi(2048 Mi) | 1536 Mi | 同上 |
残り 25 % は Java の以下のメモリ領域で使用されます。
- Metaspace:クラス定義のメタ情報。アプリ規模に比例して増える
- Direct Memory:NIO(New I/O)が使うネイティブメモリ。HTTP クライアントやファイル I/O で消費される
- Native Thread Stack:JVM のネイティブスレッド 1 本あたり数百 KB を消費。スレッド数が多いアプリで増加する
- JIT Compiled Code:JIT コンパイラが生成したネイティブコード
Payara Micro のような JavaEE / Jakarta EE 系アプリは Metaspace を比較的多く使うため、25 % のバッファが妥当です。さらにメモリを切り詰めたい場合は 50 % まで下げる選択肢もありますが、まずは 75 % で運用し、実測値を見ながら調整するのが現場の流儀です。
OOMKilled の仕組み — Linux カーネルの OOM Killer
コンテナが limits.memory を超えてメモリを使おうとすると、Linux カーネルの OOM Killer がプロセスに SIGKILL(Signal 9)を送って強制終了させます。コンテナは Exit Code 137(128 + 9)で終了し、Kubernetes は restartPolicy に従って Pod を再起動します。
limits.memory: 128 Mi ← cgroup memory.max = 128 MiB
↓
Container がメモリを 128 MiB 以上使おうとする
↓
Linux カーネル: OOM Killer 発動 → SIGKILL(Signal 9)
↓
Container 終了(Exit Code 137 = 128 + 9)
↓
Kubernetes: restartPolicy に従って Pod 再起動
↓
すぐ再び OOMKilled → CrashLoopBackOff
OOMKilled は kubectl describe pod の Containers セクションで明示的に確認できます。
Containers:
fanclub-backend:
State: Waiting
Reason: CrashLoopBackOff
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Exit Code 137 の意味:Linux 慣習で「シグナル N で終了したプロセスの Exit Code は 128 + N」と定義されています。SIGKILL は Signal 9 のため、128 + 9 = 137 となります。同様に SIGTERM(Signal 15)で終了すると Exit Code 143(128 + 15)になります。
Kubernetes でコンテナが「137 で死んでいる」のを見たら OOMKilled、「143 で死んでいる」のを見たら正常な graceful shutdown と判断できます。
QoS クラスの概要(詳細は第14回)
Pod の resources 設定によって Kubernetes が自動的に QoS クラス(Quality of Service クラス)を割り当てます。本回では概念だけ押さえ、詳細な OOMKilled 優先順や cgroup スコアリングは第14回(ResourceQuota + LimitRange + Multi-tenant Namespace)で扱います。
- Guaranteed:
requests == limitsの構成。OOMKilled されにくい優先度 - Burstable:
requests < limitsの構成。本回の Backend Pod が該当(requests.memory 256Mi < limits.memory 512Mi) - BestEffort:
requests / limits両方未指定。OOMKilled される優先度が最も高い
本回での推奨設定
本回の Backend Pod では以下の組み合わせを採用します。これが本回の演習①で apply する設定で、演習②で OOMKilled を発生させた後に「正解」として戻ってくる設定でもあります。
env:
- name: JAVA_OPTS
value: "-XX:MaxRAMPercentage=75.0"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "1000m"
cpu の単位 m は millicore(1000m = 1 コア)の意味です。requests.cpu: 250m は「0.25 コア相当を確保する」設定で、limits.cpu: 1000m は「最大 1 コアまで使用可能」の意味になります。
CPU は memory と異なり、limits を超えてもプロセスが kill されることはなく、スロットリング(速度低下)するだけです。OOMKilled に相当する厳しい制裁は memory 固有の挙動と覚えてください。
やってみよう①:Backend Pod 起動 + Init Container ログ確認
本回最初の演習です。第1巻で初めて fanclub-api を Kubernetes に載せる重要マイルストーンを、Init Container 付きの Pod として実現します。所要時間の目安は約 30〜35 分です。
前提状態の確認
- kind クラスタ(kind-control-plane)が稼働中
- nginx-test Pod が default namespace で Running(ep6 由来・本演習冒頭で削除する)
- metrics-server が kube-system で Running
- fanclub-backend:0.1.0 イメージがホスト Docker images に存在
- alias k=kubectl 設定済
Step 1:nginx-test Pod のクリーンアップ
第6回演習用に作成した nginx-test Pod を削除し、default namespace をクリーンな状態に戻します。
実行コマンド:
$ kubectl delete pod nginx-test
実行結果:
pod "nginx-test" deleted
実行コマンド(削除確認):
$ kubectl get pods
実行結果:
No resources found in default namespace.
Step 2:マニフェストディレクトリの作成
本回以降の Kubernetes マニフェスト YAML は ~/fanclub-manifests/ 配下に格納します。Docker 関連ファイル(Dockerfile・compose.yml)が ~/fanclub-api/ 配下にある構造との分離原則に従います。
実行コマンド:
$ mkdir -p ~/fanclub-manifests/
$ cd ~/fanclub-manifests/
Step 3:kind ノードへのイメージロード
fanclub-backend:0.1.0 はホストの Docker images にしか存在しないため、kind クラスタ(コンテナとして動作するノード)にイメージをロードします。kind load docker-image コマンドはホストから kind ノードにイメージを直接転送する仕組みで、外部 registry を経由しないため高速です。
実行コマンド:
$ kind load docker-image fanclub-backend:0.1.0
実行結果:
Image: "fanclub-backend:0.1.0" with ID "sha256:3bdcfa296cf6b545aa2878f46b7c41bc5b65b3e05c497f19ae3cec13eb4b24ce" not yet present on node "kind-control-plane", loading...
実行コマンド(kind ノード内のイメージ確認):
$ docker exec kind-control-plane crictl images | grep fanclub
実行結果:
docker.io/library/fanclub-backend 0.1.0 bb00cc9c36c00 206MB
crictl は containerd(kind ノードのコンテナランタイム)を直接操作する CLI です。kind ノード内でイメージが正しく登録されていることが確認できます。
Step 4:Pod YAML の作成
fanclub-backend Pod の YAML マニフェストをファイルとして作成します。
実行コマンド:
$ cat << 'EOF' > ~/fanclub-manifests/fanclub-backend-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: fanclub-backend
labels:
app: fanclub-backend
spec:
initContainers:
- name: wait-for-api
image: busybox:1.36
command:
- sh
- -c
- "until nc -z kubernetes.default.svc.cluster.local 443; do echo 'waiting for API server...'; sleep 2; done"
containers:
- name: fanclub-backend
image: fanclub-backend:0.1.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
env:
- name: JAVA_OPTS
value: "-XX:MaxRAMPercentage=75.0"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "1000m"
EOF
YAML 検証のおすすめ手順:apply する前に --dry-run=server で構文と API Server 側のスキーマ検証を行えます。
実行コマンド(dry-run 検証):
$ kubectl apply -f ~/fanclub-manifests/fanclub-backend-pod.yaml --dry-run=server
実行結果:
pod/fanclub-backend created (server dry run)
Step 5:Pod の apply とライフサイクル観察
マニフェストを適用し、Pod の状態遷移をリアルタイムで観察します。
実行コマンド:
$ kubectl apply -f ~/fanclub-manifests/fanclub-backend-pod.yaml
実行結果:
pod/fanclub-backend created
実行コマンド(リアルタイム監視):
$ kubectl get pod fanclub-backend -w
実行結果:
NAME READY STATUS RESTARTS AGE
fanclub-backend 0/1 Init:0/1 0 0s
fanclub-backend 0/1 PodInitializing 0 1s
fanclub-backend 1/1 Running 0 5s
STATUS 列の遷移を読み解きます。
Init:0/1:Init Container 1 個中 0 個完了。wait-for-apiが実行中PodInitializing:Init Container 完了 → メインコンテナ起動準備中(イメージ展開・コンテナ作成)Running:メインコンテナ稼働中。READY 列が 1/1(コンテナ 1 個中 1 個 Ready)
Ctrl+C で -w 監視を抜けてください。
Step 6:Init Container のログ確認
Init Container は完了後もログだけは保持されています。-c <init-container-name> でログを取得します。
実行コマンド:
$ kubectl logs fanclub-backend -c wait-for-api
実行結果:
ログは空です。API Server が即座に応答したため、until ループの “waiting for API server…” メッセージは一度も出力されませんでした。
Init Container は nc -z kubernetes.default.svc.cluster.local 443 が成功した時点で until ループを抜けて Exit 0 で終了します。kind クラスタ内では API Server が即応答するため、待ち時間はほぼゼロです。
Step 7:Pod 詳細確認(Init Container + メインコンテナ)
kubectl describe pod で Init Container とメインコンテナの両方の状態を確認します。
実行コマンド:
$ kubectl describe pod fanclub-backend
実行結果(抜粋):
Name: fanclub-backend
Namespace: default
Labels: app=fanclub-backend
Status: Running
IP: 10.244.0.10
Init Containers:
wait-for-api:
Image: busybox:1.36
State: Terminated
Reason: Completed
Exit Code: 0
Started: Sun, 10 May 2026 09:55:26 +0900
Finished: Sun, 10 May 2026 09:55:26 +0900
Ready: True
Restart Count: 0
Containers:
fanclub-backend:
Image: fanclub-backend:0.1.0
State: Running
Started: Sun, 10 May 2026 09:55:27 +0900
Ready: True
Restart Count: 0
Limits:
cpu: 1
memory: 512Mi
Requests:
cpu: 250m
memory: 256Mi
Environment:
JAVA_OPTS: -XX:MaxRAMPercentage=75.0
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 59s default-scheduler Successfully assigned default/fanclub-backend to kind-control-plane
Normal Pulled 59s kubelet spec.initContainers{wait-for-api}: Container image "busybox:1.36" already present on machine and can be accessed by the pod
Normal Created 59s kubelet spec.initContainers{wait-for-api}: Container created
Normal Started 59s kubelet spec.initContainers{wait-for-api}: Container started
Normal Pulled 58s kubelet spec.containers{fanclub-backend}: Container image "fanclub-backend:0.1.0" already present on machine and can be accessed by the pod
Normal Created 58s kubelet spec.containers{fanclub-backend}: Container created
Normal Started 58s kubelet spec.containers{fanclub-backend}: Container started
Events セクションから、Init Container(wait-for-api)が完了してからメインコンテナ(fanclub-backend)が起動している順序が読み取れます。
fanclub-backend:0.1.0 イメージは already present on machine となっており、kind load でロードしたキャッシュが使われたことが確認できます(imagePullPolicy: IfNotPresent の効果)。
Step 8:メインコンテナの起動ログ確認
Payara Micro の起動ログを確認し、Backend が正しく動作していることを確かめます。
実行コマンド:
$ kubectl logs fanclub-backend -c fanclub-backend
実行結果(抜粋):
[2026-05-10T00:55:28.510+0000] [WARNING] [PayaraMicro] [tid: 3] Payara Micro Runtime directory is located in a temporary file location which can be cleaned by system processes.
[2026-05-10T00:55:28.545+0000] [INFO] [PayaraMicro] [tid: 3] Payara Micro Runtime directory is located at /tmp/payaramicro-rt14420631468863963553tmp
[2026-05-10T00:55:28.551+0000] [INFO] [fish.payara.micro.boot.runtime.PayaraMicroRuntimeBuilder] [tid: 3] Built Payara Micro Runtime
[2026-05-10T00:55:34.438+0000] [INFO] [NCLS-CORE-00101] [javax.enterprise.system.core] [tid: 27] Network Listener http-listener started in: 9ms - bound to [/0.0.0.0:8080]
[2026-05-10T00:55:34.442+0000] [INFO] [NCLS-CORE-00058] [javax.enterprise.system.core] [tid: 27] Network listener https-listener on port 8443 disabled per domain.xml
[2026-05-10T00:55:34.442+0000] [INFO] [NCLS-CORE-00087] [javax.enterprise.system.core] [tid: 27] Grizzly 4.1.0 started in: 3,906ms - bound to [http-listener:8080]
Payara Micro が 8080 ポートでリクエストを受け付けている状態です。第8回で Service と kubectl port-forward を使って実際にブラウザからアクセスできるようにします。
Step 9:メモリ使用量確認
第6回で導入した metrics-server を使い、実際の Pod のメモリ使用量を確認します。
実行コマンド:
$ kubectl top pod fanclub-backend
実行結果:
NAME CPU(cores) MEMORY(bytes)
fanclub-backend 10m 277Mi
limits.memory: 512Mi 設定下で、Payara Micro + Backend アプリの実メモリ使用量がいくらに収まっているかを確認できます。kubectl top pod はメモリ設計を見直す際の実測値を得る基本ツールです。
演習①完了:Backend Pod が稼働中の状態を達成しました。この Pod は本回の演習③(Ephemeral Container)と第8回(Service 演習)で使い続けるため、削除せずそのまま残しておきます。
やってみよう②:OOMKilled 演習 — limits.memory を過小設定してデバッグする
本回 2 つ目の演習です。limits.memory を意図的に過小設定して OOMKilled を発生させ、kubectl describe pod の Reason: OOMKilled + Exit Code: 137 を自分の目で確認します。所要時間の目安は約 20〜25 分です。
前提状態
演習①完了後の状態(fanclub-backend Pod が Running)からスタートします。本演習で作成する fanclub-backend-oom は別名の Pod として作成するため、演習①の Pod とは共存します。
Step 1:OOMKilled 用 Pod YAML の作成(limits.memory 過小設定)
意図的に OOMKilled を発生させる構成として、limits.memory: 128Mi + JAVA_OPTS: -Xmx256M を組み合わせます。JVM ヒープ最大値(-Xmx256M = 256 MB)が cgroup memory 制限(128 MiB)を上回るため、JVM がヒープを確保しようとした瞬間に OOMKilled されます。
実行コマンド:
$ cat << 'EOF' > ~/fanclub-manifests/fanclub-backend-oom.yaml
apiVersion: v1
kind: Pod
metadata:
name: fanclub-backend-oom
labels:
app: fanclub-backend-oom
spec:
containers:
- name: fanclub-backend
image: fanclub-backend:0.1.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
env:
- name: JAVA_OPTS
value: "-Xmx256M"
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "500m"
EOF
設計の意図:-Xmx256M は JVM ヒープ最大値を 256 MB に設定する古典的なオプションです。limits.memory: 128Mi(128 MiB ≒ 134 MB)を超える設定のため OOMKilled が確実に発生します。
本番では -Xmx の代わりに -XX:MaxRAMPercentage=75.0 を使い、limits.memory と整合を取ることが正解です(演習①の Pod がその構成)。
Step 2:apply とステータス確認
実行コマンド:
$ kubectl apply -f ~/fanclub-manifests/fanclub-backend-oom.yaml
実行結果:
pod/fanclub-backend-oom created
実行コマンド(ステータス監視):
$ kubectl get pod fanclub-backend-oom -w
実行結果:
NAME READY STATUS RESTARTS AGE
fanclub-backend-oom 0/1 ContainerCreating 0 1s
fanclub-backend-oom 1/1 Running 1 11s
fanclub-backend-oom 0/1 OOMKilled 1 21s
fanclub-backend-oom 0/1 CrashLoopBackOff 1 (16s ago) 31s
fanclub-backend-oom 0/1 OOMKilled 2 41s
fanclub-backend-oom 0/1 OOMKilled 2 51s
STATUS 列が OOMKilled → CrashLoopBackOff を繰り返します。Kubernetes の restartPolicy: Always(デフォルト)により、OOMKilled で終了した Pod が自動再起動を試み続け、再起動するたびに同じ理由で OOMKilled される無限ループです。
RESTARTS 列が増え続ける点も観察してください。Ctrl+C で監視を抜けます。
Step 3:kubectl describe で OOMKilled を確認
OOMKilled の詳細情報を kubectl describe pod で確認します。
実行コマンド:
$ kubectl describe pod fanclub-backend-oom
実行結果(Containers セクション抜粋):
Containers:
fanclub-backend:
Image: fanclub-backend:0.1.0
State: Terminated
Reason: OOMKilled
Exit Code: 137
Started: Sun, 10 May 2026 09:56:56 +0900
Finished: Sun, 10 May 2026 09:57:04 +0900
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
Started: Sun, 10 May 2026 09:56:33 +0900
Finished: Sun, 10 May 2026 09:56:40 +0900
Ready: False
Restart Count: 2
Limits:
cpu: 500m
memory: 128Mi
Requests:
cpu: 100m
memory: 64Mi
Environment:
JAVA_OPTS: -Xmx256M
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 61s default-scheduler Successfully assigned default/fanclub-backend-oom to kind-control-plane
Normal Pulled 30s (x3 over 60s) kubelet spec.containers{fanclub-backend}: Container image "fanclub-backend:0.1.0" already present on machine and can be accessed by the pod
Normal Created 30s (x3 over 60s) kubelet spec.containers{fanclub-backend}: Container created
Normal Started 30s (x3 over 60s) kubelet spec.containers{fanclub-backend}: Container started
Warning BackOff 22s (x2 over 46s) kubelet spec.containers{fanclub-backend}: Back-off restarting failed container fanclub-backend in pod fanclub-backend-oom_default(9bf3ac8e-0d18-4084-a706-f691d0c659d3)
確認すべき項目を整理します。
State: Terminated / Reason: OOMKilled / Exit Code: 137:現在のコンテナ状態(直近の終了理由が OOMKilled・取得タイミングで「Terminated 直後」「次の Waiting 待ち」のいずれかが見える)Last State: Terminated / Reason: OOMKilled / Exit Code: 137:その前の終了理由。同じく OOMKilled が記録され、再起動を繰り返していることが分かるRestart Count:再起動回数。時間経過とともに増加する(実機検証時は 2 回)- Events の
BackOffWarning:「再起動を遅延している」kubelet のメッセージ。x2 over 46sは同種イベントが 2 回・46 秒の間に集約されていることを示す表示。`STATUS` 列がOOMKilledとCrashLoopBackOffを交互に示すのと対応している
Step 4:kubectl logs –previous で直前のログを確認
第6回で習得した --previous フラグで、直前に終了したコンテナのログを確認します。OOMKilled で死ぬ直前の Payara Micro の状態が読めます。
実行コマンド:
$ kubectl logs fanclub-backend-oom --previous
実行結果:
unable to retrieve container logs for containerd://441cbb913e6dc25031eb43aae7b109da787a3ca07ad60306505f0d7853a1b4a5
OOMKilled は Linux カーネルが SIGKILL を即座に送るため、JVM 自体は OOM を検知してログを出す余裕がありません。アプリケーションログは「途中で唐突に途切れる」のが OOMKilled の典型的な症状です。kubectl describe pod の Reason: OOMKilled がなければ、ログだけ見ても原因特定が難しい点を実機で確認してください。
Step 5:OOMKilled Pod の削除
OOMKilled が発生する Pod を削除します。本演習の確認は完了しました。
実行コマンド:
$ kubectl delete pod fanclub-backend-oom
実行結果:
pod "fanclub-backend-oom" deleted
Step 6:演習①の Pod が稼働している確認
演習①で作成した fanclub-backend Pod は引き続き Running です。本演習②では別名の Pod を作成・削除しただけなので、演習①の Pod は影響を受けていません。
実行コマンド:
$ kubectl get pods
実行結果:
NAME READY STATUS RESTARTS AGE
fanclub-backend 1/1 Running 0 2m
演習②完了:「limits.memory: 512Mi + -XX:MaxRAMPercentage=75.0」の組み合わせ(演習①の構成)が正解であることを、対比で実感できる演習でした。本番でメモリ設定を変更するときは「JVM のヒープ計算」と「limits.memory」の整合を必ずペアで確認することを習慣にしてください。
やってみよう③:Ephemeral Container で稼働 Pod 内デバッグ
本回 3 つ目の演習です。演習①で稼働中の fanclub-backend Pod に busybox の Ephemeral Container を注入し、環境変数・ホスト名・プロセスを確認します。所要時間の目安は約 15〜20 分です。
前提状態
演習①完了後の状態(fanclub-backend Pod が Running)からスタートします。演習②で作成した fanclub-backend-oom は削除済です。
Step 1:Ephemeral Container の注入
kubectl debug で稼働中の Pod に Ephemeral Container を追加します。
実行コマンド:
$ kubectl debug -it fanclub-backend --image=busybox:1.36 --target=fanclub-backend
実行結果:
Targeting container "fanclub-backend". If you don't see processes from this container it may be because the container runtime doesn't support this feature.
--profile=legacy is deprecated and will be removed in the future. It is recommended to explicitly specify a profile, for example "--profile=general".
Defaulting debug container name to debugger-27v2c.
If you don't see a command prompt, try pressing enter.
/ #
busybox の / # プロンプトが表示されたら、Ephemeral Container 内のシェルに接続できています。debugger-27v2c はランダムな英数字で、Ephemeral Container の自動命名です。
Step 2:Ephemeral Container 内での環境変数確認
Ephemeral Container 自身の環境変数を確認します。--target フラグは Process Namespace を対象コンテナと共有するためのフラグであり、環境変数を継承するわけではありません。
ここで env コマンドで見えるのは Ephemeral Container 自身の環境(Kubernetes が自動注入する KUBERNETES_* 系のみ)で、対象コンテナの JAVA_OPTS は表示されません。
対象コンテナの環境変数を確認したいときは、後段の ps -ef で対象プロセスのコマンドラインから読み取るか、cat /proc/<PID>/environ | tr '\0' '\n' を使います。
実行コマンド(Ephemeral Container 内):
/ # env
実行結果(抜粋):
KUBERNETES_SERVICE_PORT=443
KUBERNETES_PORT=tcp://10.96.0.1:443
HOSTNAME=fanclub-backend
SHLVL=1
HOME=/root
TERM=xterm
KUBERNETES_PORT_443_TCP_ADDR=10.96.0.1
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
KUBERNETES_PORT_443_TCP_PORT=443
KUBERNETES_PORT_443_TCP_PROTO=tcp
HOSTNAME=fanclub-backend が表示されることに注目してください。Ephemeral Container は対象 Pod のネットワーク名前空間を共有しているため、Pod のホスト名がそのまま Ephemeral Container の HOSTNAME になります。
Step 3:プロセス確認(共有 Process Namespace)
--target フラグで Process Namespace を共有しているため、Java プロセス(Payara Micro)が Ephemeral Container から見えます。
実行コマンド(Ephemeral Container 内):
/ # ps -ef
実行結果(抜粋):
PID USER TIME COMMAND
1 root 0:00 sh -c java $JAVA_OPTS -jar payara-micro.jar --deploy app.w
14 root 0:11 java -XX:MaxRAMPercentage=75.0 -jar payara-micro.jar --dep
117 root 0:00 sh -c echo 'EPHEMERAL CONTAINER START'; env | head -10; ec
132 root 0:00 ps -ef
PID 14 の Java プロセスが -XX:MaxRAMPercentage=75.0 付きで起動していることが確認できます(PID 1 は親シェル、PID 14 が Java 本体)。これは演習① Step 8 の Payara Micro 起動ログとも整合する情報です。
--target がない通常の kubectl exec ではこの形でプロセスを観察するのは難しく、Ephemeral Container の特性が活きる場面です。
Step 4:ネットワーク疎通確認
同一 Pod 内のメインコンテナ(Payara Micro)の 8080 ポートに localhost でアクセスできるか確認します。これは Pod 内の共有 network namespace の動作確認でもあります。
実行コマンド(Ephemeral Container 内):
/ # nc -z localhost 8080
/ # echo $?
実行結果:
port 8080 OK
0
Exit Code 0 = ポート接続成功です。同一 Pod 内のコンテナは localhost で互いにアクセスできることが実機で確認できました。
Step 5:Ephemeral Container から終了
exit で Ephemeral Container のシェルから抜けます。Ephemeral Container 自体は Pod 内に残り続ける点に注意してください。
実行コマンド(Ephemeral Container 内):
/ # exit
k8s-ops のシェルプロンプトに戻ります。
Step 6:Ephemeral Container の存在確認
Pod の状態を再度確認すると、Ephemeral Container が Terminated 状態で残っていることが分かります。
実行コマンド:
$ kubectl describe pod fanclub-backend
実行結果(Ephemeral Containers セクション抜粋):
Ephemeral Containers:
debugger-27v2c:
Container ID: containerd://dee2e1de97a84a127c3581980f3bcb62aa32c4177829c4e9b2e8c99fc50914e9
Image: busybox:1.36
State: Terminated
Reason: Completed
Exit Code: 0
Started: Sun, 10 May 2026 09:57:43 +0900
Finished: Sun, 10 May 2026 09:57:43 +0900
Ready: False
Restart Count: 0
Ephemeral Container は完了済(Terminated / Completed)ですが、Pod の spec から削除することはできません。Pod 自体を削除(または再作成)するまで残ります。
Step 7:Pod の status.ephemeralContainerStatuses 確認
YAML 出力で詳細を確認できます。
実行コマンド:
$ kubectl get pod fanclub-backend -o yaml | grep -A 15 ephemeralContainerStatuses
実行結果:
ephemeralContainerStatuses:
- containerID: containerd://dee2e1de97a84a127c3581980f3bcb62aa32c4177829c4e9b2e8c99fc50914e9
image: docker.io/library/busybox:1.36
imageID: docker.io/library/busybox@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662
lastState: {}
name: debugger-27v2c
ready: false
restartCount: 0
state:
terminated:
containerID: containerd://dee2e1de97a84a127c3581980f3bcb62aa32c4177829c4e9b2e8c99fc50914e9
exitCode: 0
finishedAt: "2026-05-10T00:57:43Z"
reason: Completed
startedAt: "2026-05-10T00:57:43Z"
本番運用で「distroless で動いているコンテナの中身を調査したい」「kubectl exec でデバッグ中に Pod が再作成されるリスクを避けたい」場面で、kubectl debug + Ephemeral Container は安全な調査手段になります。第6回 H2-12 の「exec セッション中に Pod が消えてしまった」ヒヤリハットへの、構造的な解決策がここで出揃いました。
演習③完了:本回完了時点のクラスタ状態は以下の通りです。fanclub-backend Pod は第8回 Service 演習の起点として残します。
fanclub-backendPod:Running(演習①作成・Ephemeral Container 1 個 Terminated 状態で同居)fanclub-backend-oomPod:削除済(演習② Step 5 で削除)nginx-testPod:削除済(演習① Step 1 で削除)- metrics-server:稼働中
現場ヒヤリハット — Init Container が終わらない / imagePullPolicy: Always の本番問題
事例 1:Init Container の nc が DNS 解決できない宛先を永遠に待った
状況:本番(kubeadm)の新環境に Backend Pod をデプロイしたところ、kubectl get pod が Init:0/1 のまま 30 分以上進まなくなった。Init Container の nc -z fanclub-db 5432 が成功せず、ループし続けている状態。
Pod が起動しないため、ヘルスチェックも走らず、アラートも上がらず、デプロイ担当者が手動で確認するまで気付けなかった。
原因:DB Pod が同じ namespace に存在していなかった(StatefulSet のデプロイ順序ミス)ため、fanclub-db という Service 名の DNS 解決ができなかった。nc -z は接続失敗時に短時間で戻るため、until ループが繰り返し実行される。
Init Container は終わらず、Pod 全体が Init:0/1 から進まない。さらに悪いことに、Init Container のログを見ない限り原因が分からない(kubectl get pod だけでは「待ち中」しか見えない)。
対策:
- Init Container が長時間
Init:0/1から進まない場合は、必ずkubectl logs <pod> -c <init-container-name>で Init Container のログを確認する kubectl describe podの Init Containers セクションで State / Started 時刻も確認するnc -zに timeout を必ず設定する。nc -z -w 3 <host> <port>で 3 秒タイムアウト。永遠に待たないように設計する- 本番では Init Container 自体に「最大試行回数」のロジックを入れる(例:100 回試行で失敗時は exit 1 して Pod を Failed に落とす)
- 依存リソース(DB Pod / Service)が同じ namespace に存在することを
kubectl get all -n <ns>で事前に確認する
教訓:Init Container は「依存先の準備が完了してから起動する」優れた設計ですが、依存先が永遠に来ない場合は無限に待ち続けます。本番では timeout を必ず設定し、ログ確認の習慣を Pod デバッグの初動に組み込みます。
本回の演習では kubernetes.default.svc.cluster.local という即応答する宛先を使ったため待ち時間がほぼゼロでしたが、本番の依存先(DB / Cache / 外部 API)はそうではありません。第9回(PVC + StatefulSet + DB 追加)で PostgreSQL の接続待ちに切り替えるとき、この教訓を再確認します。
事例 2:imagePullPolicy: Always のままで kind ローカルロードのイメージが pull 失敗した
状況:開発者が kind 環境で動作確認したいと思い、ホストでビルドした my-app:dev を kind load docker-image my-app:dev で kind ノードにロードした。しかし Pod を apply すると ImagePullBackOff でエラーになる。
kubectl describe pod を見ると Failed to pull image "my-app:dev": rpc error: ... not found と表示された。kind load は成功しているはずなのに、なぜ pull に失敗するのかが理解できず、デバッグに 1 時間以上を費やした。
原因:Pod の imagePullPolicy が明示されていなかった。Kubernetes のデフォルト挙動では、イメージタグが latest または省略の場合は imagePullPolicy: Always、それ以外(:dev や :0.1.0 等)は imagePullPolicy: IfNotPresent がデフォルトです。
問題のケースでは my-app:dev という非 latest タグだったため理論上は IfNotPresent がデフォルトのはずでしたが、開発者が「最新を取りたい」と思って imagePullPolicy: Always を明示的に書いていた。Always は外部 registry から毎回 pull を試みるため、registry に存在しないローカルロード済イメージは取得できず失敗する仕組み。
対策:
kind load docker-imageを使う場合は、必ずimagePullPolicy: IfNotPresentまたはNeverを Pod YAML に明示するIfNotPresent:ローカルにキャッシュがある場合はそちらを使う。なければ pull を試みる(推奨デフォルト)Never:必ずローカルキャッシュを使う。外部 pull を一切試みない(kind 学習用に明示的にキャッシュ依存を強調する用途)Always:毎回 registry から pull する。本番でlatestタグやmainタグを意図的に追従するときに使う(イメージタグ戦略は第4回で扱った通り、本番では原則 immutable タグを推奨)- 本回の Backend Pod YAML(演習①)では
imagePullPolicy: IfNotPresentを明示している。本シリーズの全マニフェストで同じルールを採用する
教訓:imagePullPolicy のデフォルト値はタグによって変わるため、明示しない場合の挙動が読みにくくなります。本シリーズでは原則として imagePullPolicy を明示する方針です。
第4回で学んだイメージタグ戦略(latest 禁止・immutable な version タグを使う)と整合する運用ルールでもあります。kind load + IfNotPresent の組み合わせは、ローカル開発における kind 環境のお手本です。
理解度チェック + まとめ + 次回予告 + シリーズ一覧
理解度チェック(○× 形式・全 9 問)
第7回の理解度を確認します。○か×で答えてください。
問 1:同一 Pod 内の複数コンテナは、localhost を使って互いに通信できる。
問 2:Init Container は spec.containers[] に定義する。
問 3:Init Container が 1 つ失敗した場合、残りの Init Container はスキップされてメインコンテナが起動する。
問 4:limits.memory: 512Mi で -XX:MaxRAMPercentage=75.0 を設定した場合、JVM ヒープ上限は 384 Mi になる。
問 5:コンテナが OOMKilled された場合、kubectl describe pod の Exit Code は 137 になる。
問 6:kubectl debug -it <pod> --image=busybox で追加した Ephemeral Container は、kubectl delete を専用フラグ付きで実行すれば個別削除できる。
問 7:imagePullPolicy: IfNotPresent を設定した場合、ローカルにキャッシュがあれば registry への pull は行われない。
問 8:kind load docker-image fanclub-backend:0.1.0 を使えば、外部 registry に push しなくても kind クラスタで fanclub-backend:0.1.0 を使える。
問 9:Sidecar コンテナのネイティブサポート(spec.initContainers[] + restartPolicy: Always)は Kubernetes v1.29 で Beta になり、v1.35 で GA になっている。
解答:
| 問 | 解答 | 解説 |
|---|---|---|
| 問 1 | ○ | 同一 Pod 内のコンテナは共有 network namespace を持ち、Pod IP を共有する。コンテナ間通信は localhost で完結する |
| 問 2 | × | Init Container は spec.initContainers[] に定義する。spec.containers[] はメインコンテナ(および従来型 Sidecar)の定義場所 |
| 問 3 | × | Init Container が失敗すると Pod は restartPolicy に従って再起動される。スキップして次に進むことはない |
| 問 4 | ○ | 512 Mi × 0.75 = 384 Mi。残り 25 % は Metaspace / Direct Memory / Native Thread Stack に使われる |
| 問 5 | ○ | OOMKilled は Linux カーネルが SIGKILL(Signal 9)でプロセスを終了する。Exit Code = 128 + 9 = 137 |
| 問 6 | × | Ephemeral Container は削除不可。Pod が終了するまで Pod spec 内に残る。Pod 自体を再作成すれば消える |
| 問 7 | ○ | IfNotPresent はローカルキャッシュ優先。ない場合のみ pull を試みる。kind load でロードしたイメージを使う際の標準設定 |
| 問 8 | ○ | kind load docker-image はホスト Docker のイメージを kind ノード(コンテナ)に直接転送する。registry 経由ではない |
| 問 9 | ○ | Kubernetes v1.29 で Sidecar Containers が Beta、v1.35 で GA。CKAD v1.35 試験の出題対象 |
第7回まとめ
第7回では以下を実施しました。
- Pod は Kubernetes の最小実行単位。同一 Pod 内のコンテナは network namespace と Volume を共有する。最も一般的な構成は 1 Pod 1 Container だが、密結合な補助プロセス(ログ転送・プロキシ)はマルチコンテナ Pod にまとめる
- Pod YAML の 4 要素(
apiVersion/kind/metadata/spec)を記述し、--dry-run=client -o yamlで雛形を生成する流れを習得した。kubectl explainでフィールドのスキーマを kubernetes.io を開かずに確認できる - Init Container(
spec.initContainers[])はメインコンテナ起動前に順次実行される前処理コンテナ。本回は API Server への接続確認を実装し、第9回で PostgreSQL 接続待ちに差し替える - Sidecar は
spec.containers[]に複数定義する従来型と、spec.initContainers[]+restartPolicy: Alwaysのネイティブ型(v1.29 Beta・v1.35 GA)の 2 通りがある。CKAD 試験では両方の YAML 記述が出題範囲 limits.memoryと JVM-XX:MaxRAMPercentage=75.0の整合が JVM アプリの本番運用の基本。設定を誤ると OOMKilled(Exit Code 137)が発生し、kubectl describe podでReason: OOMKilledとして確認できる- Ephemeral Container(
kubectl debug -it)は稼働中の Pod に後付けするデバッグ専用コンテナ。distroless イメージのデバッグや、exec セッション切断のリスクを避けた調査に有効。一度追加すると削除不可で、Pod 終了まで残る - CKAD D1(Application Design and Build・20 %)の中核(マルチコンテナ Pod 設計パターン・適切なワークロードリソースの選択)と、D4 の resources 関連を網羅した
次回予告
第8回 Service とネットワーキングでは、本回起動した fanclub-backend Pod に外部からアクセスするための Service リソースを学びます。Pod の IP は再起動のたびに変わるため、Service が安定したアクセス先を提供する仕組みを実装します。
Frontend Pod も追加し、Frontend → Backend を Service で接続するマルチ Pod 構成を実現します。本回確定した containers[].name: fanclub-backend / labels.app: fanclub-backend / containerPort: 8080 が、第8回 Service 定義の selector と targetPort でそのまま参照されます。
kubectl port-forward でブラウザから一時アクセスする確認手順も扱います。
シリーズ一覧
第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
