はじめに
Pod の起動順序では、コンテナの起動前に Pod sandbox やネットワークなどの準備が行われることを説明しました。Pod sandbox の役割は、名前だけでは分かりにくいと思います。この記事では、その目的と役割、containerd による実装、再作成時の挙動を整理します。
対象コンポーネントとバージョン
| コンポーネント | バージョン | リポジトリ | 本記事での役割 |
|---|---|---|---|
| Kubernetes | v1.37.1 | kubernetes/kubernetes | CRI の Go インターフェース定義、kubelet による PodSandboxConfig の組み立て・sandbox 再作成判定、pause バイナリのソース |
| containerd | v2.4.1 / v2.3.4 | containerd/containerd | v2.4.1 はソース確認用、v2.3.4 は kind ノード内で実地検証したバージョン |
| kind | v0.33.0 | kubernetes-sigs/kind | 検証環境。ノードイメージは kindest/node:v1.37.0
|
TL;DR
-
Pod sandbox は、Pod 内のコンテナが共有する実行環境です。kubelet は CRI の
RunPodSandboxを通じて作成を依頼します。 -
containerd の Linux 向け
podsandbox実装では、pause コンテナを使います。VM を使うランタイムなどでは実装が異なります。 -
通常の Pod では network / IPC namespace を共有します。PID namespace はコンテナごとに分かれますが、
shareProcessNamespaceやホストの namespace を使う設定によって変わります。 -
pause は待機とシグナル処理を行う小さなプログラムです。アプリケーションコンテナは、pause の PID に対応する
/proc/<pid>/ns/...を使って共有 namespace に参加します。 - sandbox が利用できなくなると、kubelet は再作成を判断します。アプリケーションコンテナも再作成され、Pod IP が変わる場合があります。
Pod sandbox の概要
Pod sandbox は、同じ Pod のコンテナが共有する実行環境です。例えば、アプリケーションと sidecar が同じ IP アドレスを使い、localhost で通信できるように、コンテナを起動する前に共有するネットワークなどを用意します。アプリケーションコンテナが終了・再起動しても、その共有環境を維持できるようにする役割があります。
kubelet は CRI の RunPodSandbox でランタイムに作成を依頼します。kubelet の役割や、コンテナランタイム・CNI・OCI Runtime との関係は、kubelet の機能一覧で整理しています。
この記事で扱う containerd の Linux 向け実装では、主に次の処理を行います。
- Pod 用の network namespace を用意し、CNI を通じてネットワーク接続や IP アドレスを設定する(
hostNetworkを使う場合はホストのネットワークを使う) - pause コンテナを起動し、コンテナ間で共有する namespace を維持する
- 後から起動するアプリケーションコンテナが、その共有環境に参加できるようにする
Pod sandbox の実現方法はランタイムによって異なり、pause コンテナを使う方式のほか、VM を使う方式もあります。以降では、共有する環境の指定方法、containerd による実装、sandbox が再作成される条件を順に確認します。
containerd の Linux 向け podsandbox 実装で、ホストの namespace を使わず、shareProcessNamespace を有効にしていない場合の構成です。A・B・C は別々の PID namespace を示します。線は network / IPC namespace の共有を示し、プロセスの親子関係を表すものではありません。
第1章: なぜ複数コンテナでネットワークなどを共有できるのか
Pod の定義には「同じ network namespace を共有する複数コンテナのグループ」という説明がよく出てきます。この章では、Linux コンテナを対象に、共有の指定を CRI の仕様から確認します。
1.1 Linux namespace の共有という発想
Linux のコンテナは、プロセスに network/PID/IPC/UTS/mount などの namespace を割り当てることで分離を実現しています。Pod 内のアプリケーションと sidecar が同じ IP アドレスを使い、localhost で通信できるのは、同じ network namespace を共有するためです。
CRI はこれを NamespaceMode という列挙型で表現しています。
| NamespaceMode | 共有の範囲 |
|---|---|
POD |
同じ Pod 内で共有する |
CONTAINER |
コンテナごとに分離する |
NODE |
ノードの namespace を使う |
TARGET |
target_id で指定したコンテナの namespace を使う |
(参考: api.proto#L397-L417)
1.2 NamespaceOption: sandbox とコンテナがどの namespace を使うか
NamespaceMode を実際に namespace の種類ごとに指定するのが NamespaceOption です。
message NamespaceOption {
// Network namespace for this container/sandbox.
NamespaceMode network = 1;
// PID namespace for this container/sandbox.
// Note: The CRI default is POD, but the v1.PodSpec default is CONTAINER.
// The kubelet's runtime manager will set this to CONTAINER explicitly for v1 pods.
NamespaceMode pid = 2;
// IPC namespace for this container/sandbox.
NamespaceMode ipc = 3;
// Target Container ID for NamespaceMode of TARGET.
string target_id = 4;
UserNamespace userns_options = 5;
}
(参考: api.proto#L433-L457)
CRI では PID namespace のデフォルトも POD ですが、kubelet は通常の Pod に対して CONTAINER を明示します。そのため、ホストの namespace を使う設定がない通常の Pod では、network / IPC は共有し、PID はコンテナごとに分かれます。shareProcessNamespace: true を指定すると、他のコンテナのプロセスも同じ PID namespace 内で確認できます。
1.3 kubelet は Pod spec から NamespaceOption をどう組み立てるか
この NamespaceOption は、kubelet の NamespacesForPod が Pod の HostNetwork/HostPID/HostIPC/ShareProcessNamespace から組み立てます。
// pkg/kubelet/kuberuntime/util/util.go
func NetworkNamespaceForPod(pod *v1.Pod) runtimeapi.NamespaceMode {
if pod != nil && pod.Spec.HostNetwork {
return runtimeapi.NamespaceMode_NODE
}
return runtimeapi.NamespaceMode_POD
}
func PidNamespaceForPod(pod *v1.Pod) runtimeapi.NamespaceMode {
if pod != nil {
if pod.Spec.HostPID {
return runtimeapi.NamespaceMode_NODE
}
if pod.Spec.ShareProcessNamespace != nil && *pod.Spec.ShareProcessNamespace {
return runtimeapi.NamespaceMode_POD
}
}
// Note that PID does not default to the zero value for v1.Pod
return runtimeapi.NamespaceMode_CONTAINER
}
func NamespacesForPod(ctx context.Context, pod *v1.Pod, runtimeHelper kubecontainer.RuntimeHelper, rcManager RuntimeHandlerResolver) (*runtimeapi.NamespaceOption, error) {
...
return &runtimeapi.NamespaceOption{
Ipc: IpcNamespaceForPod(pod),
Network: NetworkNamespaceForPod(pod),
Pid: PidNamespaceForPod(pod),
UsernsOptions: userNs,
}, nil
}
(参考: util.go#L74-L133)
| Pod spec のフィールド | 対応する namespace | デフォルト |
true にした場合 |
|---|---|---|---|
hostNetwork |
network |
POD (Pod 内で共有) |
NODE (ホストの network namespace を直接使う) |
hostIPC |
ipc | POD |
NODE |
hostPID |
pid |
CONTAINER (コンテナごとに別) |
NODE |
shareProcessNamespace |
pid | CONTAINER |
POD (Pod 内で共有) |
この NamespaceOption は、generatePodSandboxConfig の中で PodSandboxConfig.Linux.SecurityContext.NamespaceOptions としてセットされ、sandbox 作成時に CRI 経由でランタイムに渡されます。
// pkg/kubelet/kuberuntime/kuberuntime_sandbox.go
namespaceOptions, err := runtimeutil.NamespacesForPod(ctx, pod, m.runtimeHelper, m.runtimeClassManager)
...
lc.SecurityContext.NamespaceOptions = namespaceOptions
(参考: kuberuntime_sandbox.go#L202-L206)
第2章: containerd は sandbox をどう実装するか
CRI の Pod sandbox と、その実装で使う pause コンテナは区別します。この章では、Linux 上で動く containerd の podsandbox 実装を確認します。
2.1 CRI の PodSandboxManager インターフェース
The CRI is a plugin interface which enables the kubelet to use a wide variety of container runtimes, without having a need to recompile the cluster components.
(参考: Container Runtime Interface (CRI))
CRI は sandbox の作成・停止・削除・状態取得などを gRPC API として定義しています。kubelet 側では、これらの操作を次の Go インターフェースを通じて呼び出します。
// staging/src/k8s.io/cri-api/pkg/apis/services.go
type PodSandboxManager interface {
// RunPodSandbox creates and starts a pod-level sandbox. Runtimes should ensure
// the sandbox is in ready state.
RunPodSandbox(ctx context.Context, config *runtimeapi.PodSandboxConfig, runtimeHandler string) (string, error)
// StopPodSandbox stops the sandbox. If there are any running containers in the
// sandbox, they should be force terminated.
StopPodSandbox(ctx context.Context, podSandboxID string) error
// RemovePodSandbox removes the sandbox. If there are running containers in the
// sandbox, they should be forcibly removed.
RemovePodSandbox(ctx context.Context, podSandboxID string) error
// PodSandboxStatus returns the Status of the PodSandbox.
PodSandboxStatus(ctx context.Context, podSandboxID string, verbose bool) (*runtimeapi.PodSandboxStatusResponse, error)
// ListPodSandbox returns a list of Sandbox.
ListPodSandbox(ctx context.Context, filter *runtimeapi.PodSandboxFilter) ([]*runtimeapi.PodSandbox, error)
...
}
(参考: services.go#L67-L95)
kubelet はこのインターフェースの実装(containerd や CRI-O 等)に対して RunPodSandbox を呼び出すだけで、sandbox を実際にどう実現するかはランタイム側の裁量に委ねられています。containerd では、これは別の sandbox.Controller インターフェースとして差し替え可能な拡張点になっており、デフォルトの podsandbox モードとは別に、VM ベースの隔離(Kata Containers 等)を使う shim sandboxer を実装として差し込むこともできます。
criuses containerd internal to create and start a special pause container (the sandbox container) and put that container inside the pod's cgroups and namespace
(参考: containerd CRI architecture)
2.2 デフォルト実装: pause イメージを使った最小限のコンテナ
containerd の Sandbox API の説明では、pause コンテナが共有 namespace を維持し、アプリケーションコンテナが起動時にその namespace に参加する構成が紹介されています。
(参考: containerd Sandbox API)
デフォルトの sandbox イメージは registry.k8s.io/pause です。
// internal/cri/config/config.go
// DefaultSandboxImage is the default image to use for sandboxes when empty or
// for default configurations.
DefaultSandboxImage = "registry.k8s.io/pause:3.10.2"
(参考: config.go#L73-L75)
sandbox を作成する RunPodSandbox の実装は、(1) sandbox ID の予約、(2) (hostNetwork でなければ) network namespace の新規作成、(3) CNI によるネットワーク設定、(4) pause イメージが無ければ pull、(5) 実際に pause コンテナを作成・起動、という順に進みます。
// internal/cri/server/sandbox_run.go
func (c *criService) RunPodSandbox(ctx context.Context, r *runtime.RunPodSandboxRequest) (_ *runtime.RunPodSandboxResponse, retErr error) {
...
}
// internal/cri/server/sandbox_run.go
sandbox.NetNS, err = netns.NewNetNS(netnsMountDir)
(参考: sandbox_run.go#L54、sandbox_run.go#L209)
CRI 層で pause イメージを事前取得するのは、多くのランタイム実装が pause コンテナを使うためです。コメントには、この処理が Sandbox API の抽象化とは整合しない面があり、将来変更される可能性も記されています。
(参考: sandbox_run.go#L285-L304)
pause コンテナの OCI spec では、ホストの namespace を使う場合に、その種類の namespace を新規作成する設定を外します。network namespace は、ホストを使わない場合も新規作成のデフォルトに任せず、先に用意した nsPath を指定します。
// internal/cri/server/podsandbox/sandbox_run_linux.go
if nsOptions.GetNetwork() == runtime.NamespaceMode_NODE {
specOpts = append(specOpts, customopts.WithoutNamespace(runtimespec.NetworkNamespace))
specOpts = append(specOpts, customopts.WithoutNamespace(runtimespec.UTSNamespace))
} else {
specOpts = append(specOpts, oci.WithLinuxNamespace(
runtimespec.LinuxNamespace{
Type: runtimespec.NetworkNamespace,
Path: nsPath,
}))
}
if nsOptions.GetPid() == runtime.NamespaceMode_NODE {
specOpts = append(specOpts, customopts.WithoutNamespace(runtimespec.PIDNamespace))
}
if nsOptions.GetIpc() == runtime.NamespaceMode_NODE {
specOpts = append(specOpts, customopts.WithoutNamespace(runtimespec.IPCNamespace))
}
(参考: sandbox_run_linux.go#L76-L91)
2.3 pause バイナリの中身
containerd は pause イメージを取得し、コンテナとして起動します。イメージ内で動く pause バイナリのソースは Kubernetes 本体にあります。
// build/pause/linux/pause.c
static void sigdown(int signo) {
psignal(signo, "Shutting down, got signal");
exit(0);
}
static void sigreap(int signo) {
while (waitpid(-1, NULL, WNOHANG) > 0)
;
}
int main(int argc, char **argv) {
...
if (sigaction(SIGINT, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
return 1;
if (sigaction(SIGTERM, &(struct sigaction){.sa_handler = sigdown}, NULL) < 0)
return 2;
if (sigaction(SIGCHLD, &(struct sigaction){.sa_handler = sigreap,
.sa_flags = SA_NOCLDSTOP},
NULL) < 0)
return 3;
for (;;)
pause();
...
}
(参考: pause.c#L32-L66)
SIGINT / SIGTERM を受け取ると終了し、SIGCHLD を受け取ると waitpid で終了した子プロセスを回収します。それ以外の間は pause() で待機します。アプリケーションの起動やヘルスチェックは行いません。
子プロセスの回収は、共有 PID namespace の PID 1 として孤児プロセスを引き受ける場合などに関わります。PID namespace をコンテナごとに分けた通常の Pod で、pause が各コンテナ内のすべてのゾンビプロセスを回収するわけではありません。
2.4 アプリケーションコンテナは pause の namespace にどう参加するか
sandbox (pause コンテナ) 自体は、CNI で設定したネットワーク namespace のパスを直接参照します。一方で、アプリケーションコンテナは、sandbox の PID に対応する /proc/<pid>/ns/net を参照して同じ namespace に参加します。
// internal/cri/opts/spec_opts.go
const (
netNSFormat = "/proc/%v/ns/net"
ipcNSFormat = "/proc/%v/ns/ipc"
utsNSFormat = "/proc/%v/ns/uts"
pidNSFormat = "/proc/%v/ns/pid"
)
func WithPodNamespaces(config *runtime.LinuxContainerSecurityContext, sandboxPid uint32, targetPid uint32, uids, gids []runtimespec.LinuxIDMapping) oci.SpecOpts {
namespaces := config.GetNamespaceOptions()
opts := []oci.SpecOpts{
oci.WithLinuxNamespace(runtimespec.LinuxNamespace{Type: runtimespec.NetworkNamespace, Path: GetNetworkNamespace(sandboxPid)}),
oci.WithLinuxNamespace(runtimespec.LinuxNamespace{Type: runtimespec.IPCNamespace, Path: GetIPCNamespace(sandboxPid)}),
oci.WithLinuxNamespace(runtimespec.LinuxNamespace{Type: runtimespec.UTSNamespace, Path: GetUTSNamespace(sandboxPid)}),
}
if namespaces.GetPid() != runtime.NamespaceMode_CONTAINER {
opts = append(opts, oci.WithLinuxNamespace(runtimespec.LinuxNamespace{Type: runtimespec.PIDNamespace, Path: GetPIDNamespace(targetPid)}))
}
...
}
(参考: spec_opts.go#L348-L411)
sandboxPid は CreateContainer の実装の中で、sandbox の状態取得 (SandboxStatus) から取り出されます。
// internal/cri/server/container_create.go
sandboxPid = cstatus.Pid
(参考: container_create.go#L68-L76、WithPodNamespaces 呼び出しは container_create.go#L925)
PID namespace は、CONTAINER の場合は共有先のパスを追加せず、コンテナごとの namespace を使います。POD の場合は sandbox、TARGET の場合は指定した別コンテナの PID namespace に参加します。NODE ではホストの PID namespace を使います。
2.5 kind で実際に確認する
2コンテナの Pod を使い、pause と各コンテナの namespace を比較します。以下の YAML を sandbox-demo-pod.yaml として保存します。
シェル例は Bash で実行し、ホスト側で jq を使います。出力は2026年10月22日の検証記録です。PID・sandbox ID・IP アドレスは実行ごとに変わります。ノード内の containerd は v2.3.4 で、ソース確認に使った v2.4.1 とは異なります。
apiVersion: v1
kind: Pod
metadata:
name: sandbox-demo
spec:
containers:
- name: c1
image: nginx:1.27
- name: c2
image: busybox:1.36
command: ["sh", "-c", "sleep 3600"]
kind create cluster --name sandbox-review --image kindest/node:v1.37.0 --kubeconfig /tmp/sandbox-review-kubeconfig
export KUBECONFIG=/tmp/sandbox-review-kubeconfig
kubectl apply -f sandbox-demo-pod.yaml
kubectl wait --for=condition=Ready pod/sandbox-demo --timeout=90s
kind のノードコンテナの中には crictl/ctr が同梱されているので、docker exec 経由でそのまま使えます。まず sandbox の NamespaceOption を見ます。
PODID=$(docker exec sandbox-review-control-plane crictl pods --name sandbox-demo --namespace default --state Ready -q)
docker exec sandbox-review-control-plane crictl inspectp "$PODID"
"security_context": {
"namespace_options": {
"pid": 1,
"userns_options": { "mode": 2 }
},
...
}
この出力では network / ipc が省略されています。どちらも proto3 のゼロ値である POD = 0 に対応し、"pid": 1 は CONTAINER を示します。1.2節で説明した通り、kubelet が PID namespace に CONTAINER を明示していることを確認できます。
次に、sandbox (pause) と各コンテナのプロセスの namespace を /proc/<pid>/ns で直接比較します。ここで使う PID は kind のノードコンテナ内から見た値です。sandbox の PID は crictl inspectp の info.pid、各コンテナの PID は crictl inspect の info.pid で取得できます。
SANDBOX_PID=$(docker exec sandbox-review-control-plane crictl inspectp "$PODID" | jq '.info.pid')
CIDS=$(docker exec sandbox-review-control-plane crictl ps --pod "$PODID" -q)
for cid in $CIDS; do
docker exec sandbox-review-control-plane crictl inspect "$cid" | jq -r '"\(.status.metadata.name) pid=\(.info.pid)"'
done
c2 pid=1793
c1 pid=1730
(SANDBOX_PID は 1448)
CONTAINER_PIDS=()
for cid in $CIDS; do
CONTAINER_PIDS+=("$(docker exec sandbox-review-control-plane crictl inspect "$cid" | jq -r '.info.pid')")
done
docker exec sandbox-review-control-plane sh -c '
for pid do
echo "pid=$pid net=$(readlink /proc/$pid/ns/net) pid_ns=$(readlink /proc/$pid/ns/pid) ipc=$(readlink /proc/$pid/ns/ipc)"
done' sh "$SANDBOX_PID" "${CONTAINER_PIDS[@]}"
pid=1448 net=net:[4026532360] pid_ns=pid:[4026532682] ipc=ipc:[4026532681]
pid=1793 net=net:[4026532360] pid_ns=pid:[4026532700] ipc=ipc:[4026532681]
pid=1730 net=net:[4026532360] pid_ns=pid:[4026532697] ipc=ipc:[4026532681]
(pid=1448 が pause、1730 が nginx、1793 が busybox の sh)
net/ipc の namespace inode 番号 (4026532360/4026532681) は3プロセスとも完全に一致している一方、pid_ns だけは3プロセスともすべて異なります。2.4節の実装で確認した、network / IPC は共有し、PID はコンテナごとに分ける動作と一致します。
プロセスツリーを見ると、3つのプロセスは親子関係ではなく、同じ containerd-shim の子として並んでいます。
$ docker exec sandbox-review-control-plane ps -o pid,ppid,comm -p 1448,1730,1793
PID PPID COMMAND
1448 1361 pause
1730 1361 nginx
1793 1361 sh
この例では、pause はアプリケーションプロセスの直接の親ではありません。namespace の共有と、ホストから見たプロセスの親子関係は分けて確認します。pause の実行バイナリは /pause、実行ユーザーの UID は 65535、この時点の RSS は 384 KiB でした。
$ docker exec sandbox-review-control-plane ps -o uid,pid,rss,args -p 1448
UID PID RSS COMMAND
65535 1448 384 /pause
第3章でも同じクラスタと Pod を使います。
第3章: sandbox のライフサイクルと再作成
sandbox は一度作られたら Pod が削除されるまでずっと同じもの、というわけではありません。この章では、sandbox がいつ作られ、どういう条件で作り直されるかを見ます。
3.1 sandbox 作成を呼び出す箇所
kubelet が sandbox を新規作成する処理は createPodSandbox が担います。
// pkg/kubelet/kuberuntime/kuberuntime_sandbox.go
func (m *kubeGenericRuntimeManager) createPodSandbox(ctx context.Context, pod *v1.Pod, attempt uint32) (string, string, error) {
podSandboxConfig, err := m.generatePodSandboxConfig(ctx, pod, attempt)
...
podSandBoxID, err := m.runtimeService.RunPodSandbox(ctx, podSandboxConfig, runtimeHandler)
...
return podSandBoxID, "", nil
}
(参考: kuberuntime_sandbox.go#L42-L75)
3.2 PodSandboxChanged: 既存の sandbox を使い回せるかの判定
sandbox を新規作成する必要があるかは PodSandboxChanged が判定します。5つの条件を上から順に評価し、いずれかに該当すれば true (新しい sandbox が必要) を返します。以下のコードは、ログ出力などを省略した抜粋です。
初回作成も含む判定フローです。true は新しい sandbox が必要という判定で、作成処理自体は呼び出し元で行います。最後の IP 判定は、以下のコードの条件に対応しています。
// pkg/kubelet/kuberuntime/util/util.go
func PodSandboxChanged(pod *v1.Pod, podStatus *kubecontainer.PodStatus) (bool, uint32, string, string) {
if len(podStatus.SandboxStatuses) == 0 {
return true, 0, "", kubetypes.PodSandboxNotReadyMsgNoPodSandbox
}
readySandboxCount := 0
for _, s := range podStatus.SandboxStatuses {
if s.State == runtimeapi.PodSandboxState_SANDBOX_READY {
readySandboxCount++
}
}
sandboxStatus := podStatus.SandboxStatuses[0]
if readySandboxCount > 1 {
return true, sandboxStatus.Metadata.Attempt + 1, sandboxStatus.Id, kubetypes.PodSandboxNotReadyMsgMultipleSandboxes
}
if sandboxStatus.State != runtimeapi.PodSandboxState_SANDBOX_READY {
return true, sandboxStatus.Metadata.Attempt + 1, sandboxStatus.Id, kubetypes.PodSandboxNotReadyMsgSandboxNotReady
}
if sandboxStatus.GetLinux().GetNamespaces().GetOptions().GetNetwork() != NetworkNamespaceForPod(pod) {
return true, sandboxStatus.Metadata.Attempt + 1, "", kubetypes.PodSandboxNotReadyMsgNetworkNamespaceMode
}
if !kubecontainer.IsHostNetworkPod(pod) && sandboxStatus.Network != nil && sandboxStatus.Network.Ip == "" {
return true, sandboxStatus.Metadata.Attempt + 1, sandboxStatus.Id, kubetypes.PodSandboxNotReadyMsgNoIPAddress
}
return false, sandboxStatus.Metadata.Attempt, sandboxStatus.Id, ""
}
(参考: util.go#L31-L70)
| 条件 | 意味 |
|---|---|
| sandbox が1つも無い | 初回作成前、または既存の sandbox が失われている |
| READY な sandbox が複数ある | 複数の sandbox が利用可能と報告されているため、再作成して状態を整える |
| 最新の sandbox が READY でない | ランタイムが sandbox を利用可能と報告していない。pause の終了などが考えられる |
| network namespace のモードが異なる | Pod が要求するモードと、ランタイムが報告する既存 sandbox のモードが一致しない |
hostNetwork ではなく、Network 情報はあるが IP アドレスが空 |
Pod 用の IP アドレスが報告されていないため、ネットワーク設定を確認する |
この関数は、sandbox の現在の状態から再作成の要否を判断します。判定理由はログと戻り値に残しますが、pause が終了した原因やネットワーク障害の原因まで特定するものではありません。呼び出し元は computePodActions で、戻り値は podContainerChanges.CreateSandbox として SyncPod に渡され、true であれば古いコンテナを Kill した上で createPodSandbox が呼ばれます。
(参考: kuberuntime_manager.go#L1278-L1321)
3.3 sandbox が失われるケース
公式ドキュメントは、Pod が Eviction されないままノードが再起動する場合と、VM による隔離を使うランタイムで sandbox の VM が再起動する場合を挙げています。
(参考: Pod Lifecycle)
どちらも sandbox とネットワーク設定を再び準備する必要がありますが、pause コンテナを使うかどうかはランタイムの実装によって異なります。
3.4 PodReadyToStartContainers Condition との関係
PodReadyToStartContainers は、コンテナを起動するための sandbox やネットワークなどの準備状態を表す Condition です。pause の起動だけを表すものではなく、hostNetwork や VM ベースのランタイムにも適用されます。
sandbox が失われ、ランタイムから利用可能な sandbox が報告されなくなると、この Condition は False になります。再作成後、必要な準備が整うと再び True になります。kubelet は GeneratePodReadyToStartContainersCondition の中で PodSandboxChanged を呼び出し、再作成が必要なら False、不要なら True として Condition を計算します。API への反映は非同期なので、すべての途中状態が観測できるとは限りません。起動準備の流れは、Pod の起動順序でも説明しています。
(参考: Pod Lifecycle、generate.go#L271-L291)
3.5 kind で実際に確認する
第2章で作成した検証用クラスタで、pause プロセスを停止し、sandbox の再作成を確認します。以下のシェル例は Bash で実行します。
PODID_BEFORE=$(docker exec sandbox-review-control-plane crictl pods --name sandbox-demo --namespace default --state Ready -q)
SANDBOX_PID=$(docker exec sandbox-review-control-plane crictl inspectp "$PODID_BEFORE" | jq -r '.info.pid')
kubectl get pod sandbox-demo -o wide
docker exec sandbox-review-control-plane kill -9 "$SANDBOX_PID"
$ docker exec sandbox-review-control-plane crictl pods --name sandbox-demo --namespace default
POD ID CREATED STATE NAME NAMESPACE ATTEMPT RUNTIME
83d2bb8fa2f45 36 seconds ago Ready sandbox-demo default 0 (default)
停止直後の取得では、まだ Ready と表示されました。状態の反映には時間差があります。その後、kubelet が利用できない sandbox を検出し、再作成を行いました。再作成後の一覧では、古い sandbox が NotReady になっていることを確認できます。
$ kubectl get events --field-selector involvedObject.name=sandbox-demo --sort-by=.lastTimestamp
...
SandboxChanged Pod sandbox changed, it will be killed and re-created.
Killing Stopping container c1
Killing Stopping container c2
この Event は、SyncPod が新しい sandbox を作成すると判断し、podContainerChanges.SandboxID に既存の sandbox ID が入っている場合に記録されます。初回作成では記録されません。
// pkg/kubelet/kuberuntime/kuberuntime_manager.go
m.recorder.WithLogger(logger).Eventf(ref, v1.EventTypeNormal, events.SandboxChanged, "Pod sandbox changed, it will be killed and re-created.")
(参考: kuberuntime_manager.go#L1594)
再作成後の状態を見ると、新しい sandbox ID・新しい Pod IP で、両方のコンテナが再起動していることが分かります。
$ docker exec sandbox-review-control-plane crictl pods --name sandbox-demo --namespace default
POD ID CREATED STATE NAME NAMESPACE ATTEMPT RUNTIME
dec647060111e Less than a second ago Ready sandbox-demo default 1 (default)
83d2bb8fa2f45 About a minute ago NotReady sandbox-demo default 0 (default)
$ kubectl get pod sandbox-demo -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
sandbox-demo 2/2 Running 2 (0s ago) 76s 10.244.0.6 sandbox-review-control-plane <none> <none>
ATTEMPT が 0 から 1 に上がった新しい sandbox が Ready になり、Pod の IP アドレスも 10.244.0.3 から 10.244.0.6 に変わりました。この検証では Pod の UID は変わらず、sandbox の再作成に伴って両コンテナの restartCount がそれぞれ 0 から 1 になりました。Pod IP が変わるかどうかはネットワークの実装や割り当て状況にもよるため、再作成のたびに変わるとは限りません。
kind delete cluster --name sandbox-review
おわりに
Pod sandbox は普段意識することが少ないと思いますが、Pod を理解するうえで大事な仕組みなので、記事にまとめました。
参考
参考リンク
本文で登場する順に並べています。