本文へスキップ

CKAD 概念ノート: マルチコンテナ Pod のパターン

  • kubernetes
  • ckad
  • pod
  • init-container
  • sidecar

基準: Kubernetes v1.35 (kind kindest/node:v1.35.8 でコマンド・YAML を確認)

目次

Pod の中の container の種類

種類定義する場所いつ実行されるか再起動probe
app containerspec.containersinit 段階が終わった後、並列Pod restartPolicy対応
init containerspec.initContainersapp より先に、順番に、完了まで失敗時に再試行 (後述)不可
sidecar (native)spec.initContainers + restartPolicy: Always定義順に起動し、その後ずっと常に対応
ephemeral containerephemeralcontainers subresourceユーザーが追加したときしない不可

共有するもの

  • network: Pod IP を一つ共有する。container 同士は localhost:<port> で通信でき、その代わりポートが重複してはいけない
  • storage: spec.volumes に定義した volume を各 container が volumeMounts で mount する。受け渡しするファイルには普通 emptyDir を使う
  • IPC: SystemV semaphore、POSIX shared memory も使える
  • process namespace はデフォルトで分離。必要なら process namespace sharing を有効にする

init container

Pod の起動前に準備作業をする container。ドキュメントが挙げる用途は、Service ができるまで待つ、Git repository を volume に clone する、設定ファイルをテンプレートからレンダリングする、など。

  • app とは別のイメージなので、sed や dig のようなツールを app のイメージに入れなくて済む
  • app container からは見えない Secret にアクセスさせられる
  • 条件が揃うまで app の起動を止めておく用途に使う

動作の流れ

  1. kubelet は network と storage の準備ができてから、spec に書かれた順に init container を実行する
  2. 各 init container が成功 (exit 0) しないと次のものは始まらない
  3. すべて終わると app container を起動する

失敗したとき:

  • Pod restartPolicy が Never: Pod 全体が失敗扱い
  • OnFailure: その init container を再試行
  • Always: init container には OnFailure として適用される

その他のルール:

  • init が終わるまで Pod phase は Pending、condition Initialized は false。kubectl get の STATUS には Init:0/2 のように表示される
  • 通常の init container には lifecycle、livenessProbe、readinessProbe、startupProbe を書けない。書くと API server が Forbidden: may not be set for init containers without restartPolicy=Always で拒否する
  • 再実行されることがあるので idempotent に書く。emptyDir に書き込むファイルがすでにあるかもしれない
  • 実行中の Pod では init container の image だけ変更でき、変えても Pod は再起動しない
  • container 名は init・app 全体で一意でなければならない
  • init container のポートは Service に紐付かない

リソースの計算

  • init container の中で最も大きい request/limit = effective init request/limit
  • Pod の effective request/limit = max(app・sidecar container の合計, effective init の値) + pod overhead
  • スケジューリングはこの effective な値で行われるので、init が大きいと初期化のときにしか使わないリソースも予約される

sidecar

メインのアプリの隣で、ログ収集、モニタリング、データ同期のような補助機能を担う container。アプリのコードを変えずに機能を足すのが目的です。

native sidecar

SidecarContainers feature gate (v1.29 からデフォルト有効) で入った方式。initContainers の項目に restartPolicy: Always を付けると sidecar になります。

  • init container と同じく順序が保証される。sidecar が started になると次の init container が始まる。startupProbe があれば、それが成功して初めて started
  • 起動後は Pod が終わるまで動き続ける。終了すると Pod restartPolicy に関係なく再起動
  • probe に対応。readinessProbe の結果は Pod の ready 判定に含まれる
  • Pod 終了時: メイン container が完全に止まってから sidecar に TERM が送られ、定義順の逆順に止まる
  • Job ではメイン container が終われば、sidecar が残っていても Job は完了する
  • image を変えると Pod ではなくその container だけが再起動
  • 終了処理でメイン container が grace period を使い切ると、sidecar はすぐに SIGKILL を受けることがある。その場合の 0 以外の exit code は正常とみなしてよい
    • grace period: Pod の削除リクエストから強制終了までの猶予時間。spec.terminationGracePeriodSeconds で指定し、デフォルトは 30 秒。この間に終わらなかった container は SIGKILL で終了する

    • container ごとではなく、Pod 全体で 1 つのタイマーを共有する。メインが SIGTERM を受けて後処理をしている間も時間は進むので、メインが長引くほど sidecar の持ち時間が減る

    • 例: grace period 30 秒、メインの後処理に 28 秒、ログ収集 sidecar は残りのログを送るのに 5 秒かかる場合

      t=0s   Pod 削除リクエスト。メインに SIGTERM
      t=28s  メイン終了 (exit 0)。ここでやっと sidecar に SIGTERM
      t=30s  期限到達。後処理中の sidecar に SIGKILL -> exit 137 (128+9)

      メインが 30 秒を使い切ると、sidecar は SIGTERM すら受けずにメインと一緒に SIGKILL を受ける

    • sidecar はメインを補助する container なので、メインが正常に終了していれば Pod の終了は役目を果たしている。公式ドキュメントも、この場合の sidecar の 0 以外の exit code は外部ツールで無視するよう案内している。モニタリングが sidecar の 137 を障害として検知しないようにしておく

従来型の sidecar

containers に app container をもう一つ入れる方式。container 単位の restartPolicy がない古いバージョンでも動き、今でも有効です。ただし普通の app container なので:

  • アプリより先に起動する保証がない
  • Job では問題になる。Pod が Succeeded になるにはすべての container が終了する必要があり、動き続ける sidecar がいると終わらない

起動・終了の順序が問題にならず、すべての container が Pod の動作に必要なら、この方式で十分だと公式ドキュメントも説明しています。

ambassador と adapter

Kubernetes ブログの 2015 年の記事 “Patterns for Composite Containers” で sidecar と一緒に紹介されたパターン。現在の concepts ドキュメントには独立した項目がなく、実装も専用フィールドなしで container を追加するだけです。

パターン役割例 (ブログより)
sidecarメイン container の機能拡張nginx + Git リポジトリを同期する container、ファイルシステムを共有
ambassador外部接続を代わりに受け持つローカル proxyアプリは localhost の Redis につなぎ、ambassador が読み書きを振り分けて実際のサーバーに転送
adapter出力を標準形式に変換アプリごとに違うモニタリングデータの形式を共通の形式にして出す

ambassador は network namespace の共有、adapter は volume か localhost の共有に頼る構造。三つとも実装は containers か native sidecar で行います。

ephemeral container

実行中の Pod に一時的に付けるデバッグ用 container。v1.25 から stable。

  • Pod には作成後に container を追加できないが、ephemeral container は ephemeralcontainers subresource で追加する。だから kubectl edit では入れられない
  • 追加した後は変更・削除できない
  • 自動再起動なし、リソース保証なし
  • ports、livenessProbe、readinessProbe、resources は使えない
  • static Pod では使えない
  • 使う場面: container がクラッシュした、または distroless のように shell がなくて kubectl exec ができないとき
kubectl debug -it <pod> --image=busybox:1.28 --target=<container>

--target は指定した container の process namespace に入り、そのプロセスを見られるようにする。container runtime が対応している必要があり、対応していなければ process namespace が分かれたまま起動することがある。

例

init container で設定ファイルを作り、native sidecar がアプリのログを読む Pod。

apiVersion: v1
kind: Pod
metadata:
  name: web
spec:
  initContainers:
    - name: init-config
      image: busybox:1.36
      command: ["sh", "-c", 'echo "server_name=web" > /config/app.conf']
      volumeMounts:
        - name: config
          mountPath: /config
    - name: log-shipper
      image: busybox:1.36
      restartPolicy: Always
      command: ["sh", "-c", "touch /logs/app.log; tail -F /logs/app.log"]
      volumeMounts:
        - name: logs
          mountPath: /logs
  containers:
    - name: app
      image: busybox:1.36
      command:
        [
          "sh",
          "-c",
          "while true; do cat /config/app.conf >> /logs/app.log; sleep 5; done",
        ]
      volumeMounts:
        - name: config
          mountPath: /config
        - name: logs
          mountPath: /logs
  volumes:
    - name: config
      emptyDir: {}
    - name: logs
      emptyDir: {}

実行順: init-config 完了 → log-shipper 起動 (started) → app 起動。log-shipper は app と一緒に動き続ける。

同じ sidecar を従来型で書くなら、containers の下に移して restartPolicy を消せばよい。この場合 log-shipper と app は順序なしで一緒に起動します。

kubectl apply -f web.yaml
kubectl get pod web                      # STATUS: Init:0/2 → Running
kubectl logs web -c init-config          # init container のログ
kubectl logs web -c log-shipper -f       # sidecar のログ
kubectl logs web --all-containers
kubectl exec -it web -c app -- sh
kubectl describe pod web                 # Init Containers セクションの State/Reason

紛らわしいものの比較

init container、native sidecar、従来型の sidecar

init containernative sidecar従来型の sidecar
場所initContainersinitContainerscontainers
container restartPolicyなしAlwaysなし
実行期間終わるまでPod の生存期間全体Pod の生存期間全体
app より先に起動はい (完了まで)はい (started まで)保証なし
probe不可対応対応
app とのデータ受け渡し一方向 (volume に書いておく)双方向双方向
Job の完了を妨げるいいえいいえはい
終了順該当なしメインの後、逆順順序なし

kubectl exec と kubectl debug

kubectl execkubectl debug (ephemeral)
使うイメージ既存の container のイメージ新しく指定したイメージ
shell のないイメージ不可可能
クラッシュした container不可可能
Pod spec の変化なしephemeral container が残る

試験のポイント

コマンド

マルチ container の Pod を一発で作る imperative コマンドはありません。骨組みを作ってから YAML を直す流れです。

kubectl run web --image=busybox:1.36 --dry-run=client -o yaml \
  --command -- sh -c 'sleep 3600' > web.yaml
# initContainers、追加の container、volumes を手で書く

kubectl logs <pod> -c <container>
kubectl exec -it <pod> -c <container> -- sh
kubectl debug -it <pod> --image=busybox:1.28 --target=<container>

kubectl explain

kubectl explain pod.spec.initContainers
kubectl explain pod.spec.initContainers.restartPolicy  # Always = native sidecar
kubectl explain pod.spec.shareProcessNamespace         # container 間のプロセス共有
kubectl explain pod.spec.volumes.emptyDir              # container 間のファイル共有
kubectl explain pod.spec.containers.volumeMounts

参考ドキュメント