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

Pod・マルチコンテナパターン実践【CKAD第7回】

広告
広告

第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 Poddefault ns で 1/1 Running(ep6 由来)ep6 演習で作成・継続稼働中
metrics-serverkube-system ns で 1/1 Runningep6 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 podkubectl 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)経由で行う
  • 共有 Volumespec.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 での例
apiVersionstringKubernetes API のバージョンv1(Pod は Core API・group prefix なし)
kindstringリソースの種別Pod(大文字始まり)
metadataObjectリソースの識別情報name(必須)/ namespace(省略時は default)/ labels(任意のキー/バリュー)
specObjectリソースの宣言的定義containers[](必須)/ initContainers[] / volumes[] / restartPolicy など

apiVersionkind の組み合わせで 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 名指し参照でも使われる SSOT
  • imagePullPolicy: 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 停止
Pod ライフサイクル状態遷移図 - Pending から Running への矢印、Running から Succeeded・Failed への分岐矢印、Pending が ImagePullBackOff で停滞するパターンを示すループ。各 Phase の代表的な発生条件を併記。

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)→ PodInitializingRunning の遷移を観察します。

マルチコンテナ Pod 設計パターン概観 — Init / Sidecar / Ephemeral の使い分け

マルチコンテナ Pod には 3 つの代表的な設計パターンがあります。CKAD D1 でも頻出論点で、「いつ何を使うか」を直感的に区別できる状態が試験合格の前提になります。

3 パターンの比較

観点Init ContainerSidecarEphemeral 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 終了まで残る)
マルチコンテナパターン比較図 - 左に Init Container(起動前・順次実行・完了後消える)・中央に Sidecar(並走・Pod 生存中は稼働)・右に Ephemeral Container(後付け・デバッグ専用)。それぞれの代表的なユースケース(DB 接続待ち / ログ転送 / kubectl debug)をアイコン付きで示す。

使い分けの判断基準

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.preStop Hook も使用不可
  • 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)が内蔵されている
  • command 3 要素配列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 podInit 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 execkubectl 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/shexecutable 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 等に使われる
JVM ヒープと limits.memory 整合図 - 横棒グラフで limits.memory: 512Mi の中身を可視化。MaxRAMPercentage=75.0 の場合は左 384Mi(緑・JVM Heap)+ 右 128Mi(オレンジ・Metaspace + Direct Memory + Native Thread)。デフォルト 25.0 % との比較も並列表示。

limits.memory 別のヒープ計算表

limits.memory75 % 設定時の JVM ヒープ残り 25 % の用途
512 Mi384 MiMetaspace / 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)で扱います。

  • Guaranteedrequests == limits の構成。OOMKilled されにくい優先度
  • Burstablerequests < limits の構成。本回の Backend Pod が該当(requests.memory 256Mi < limits.memory 512Mi)
  • BestEffortrequests / 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 podReason: 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 列が OOMKilledCrashLoopBackOff を繰り返します。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 の BackOff Warning:「再起動を遅延している」kubelet のメッセージ。x2 over 46s は同種イベントが 2 回・46 秒の間に集約されていることを示す表示。`STATUS` 列が OOMKilledCrashLoopBackOff を交互に示すのと対応している

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 podReason: 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-backend Pod:Running(演習①作成・Ephemeral Container 1 個 Terminated 状態で同居)
  • fanclub-backend-oom Pod:削除済(演習② Step 5 で削除)
  • nginx-test Pod:削除済(演習① Step 1 で削除)
  • metrics-server:稼働中

現場ヒヤリハット — Init Container が終わらない / imagePullPolicy: Always の本番問題

事例 1:Init Container の nc が DNS 解決できない宛先を永遠に待った

状況:本番(kubeadm)の新環境に Backend Pod をデプロイしたところ、kubectl get podInit: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 podInit Containers セクションで State / Started 時刻も確認する
  • nc -ztimeout を必ず設定する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:devkind 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 はスキップされてメインコンテナが起動する。

問 4limits.memory: 512Mi-XX:MaxRAMPercentage=75.0 を設定した場合、JVM ヒープ上限は 384 Mi になる。

問 5:コンテナが OOMKilled された場合、kubectl describe pod の Exit Code は 137 になる。

問 6kubectl debug -it <pod> --image=busybox で追加した Ephemeral Container は、kubectl delete を専用フラグ付きで実行すれば個別削除できる。

問 7imagePullPolicy: IfNotPresent を設定した場合、ローカルにキャッシュがあれば registry への pull は行われない。

問 8kind 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 に従って再起動される。スキップして次に進むことはない
問 4512 Mi × 0.75 = 384 Mi。残り 25 % は Metaspace / Direct Memory / Native Thread Stack に使われる
問 5OOMKilled は Linux カーネルが SIGKILL(Signal 9)でプロセスを終了する。Exit Code = 128 + 9 = 137
問 6×Ephemeral Container は削除不可。Pod が終了するまで Pod spec 内に残る。Pod 自体を再作成すれば消える
問 7IfNotPresent はローカルキャッシュ優先。ない場合のみ pull を試みる。kind load でロードしたイメージを使う際の標準設定
問 8kind load docker-image はホスト Docker のイメージを kind ノード(コンテナ)に直接転送する。registry 経由ではない
問 9Kubernetes 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 podReason: 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 定義の selectortargetPort でそのまま参照されます。

kubectl port-forward でブラウザから一時アクセスする確認手順も扱います。

シリーズ一覧

第1部:コンテナと Docker

第2部:Kubernetes 基礎

第3部:アプリリソース

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

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

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

広告
kubernetes
スポンサーリンク