はじめに
Running はコンテナプロセスが存在することを示すだけであり、アプリケーションがトラフィックを受け付ける準備ができていることや、自動的に復旧できることを保証するものではありません。Kubernetes のプローブは、アプリケーション固有のヘルスチェックをコントローラーや Service の動作に反映させます。
この実験では、3 種類すべてのプローブを追加し、レディネスによって Pod が Service のエンドポイントから除外される様子を確認します。その後、問題を修正し、正常なシャットダウンフックを追加します。
それぞれの役割を明確に分けて考えましょう。スタートアッププローブは初期化に時間がかかるアプリケーションを保護し、レディネスプローブは Pod が Service のトラフィックを受け取るかどうかを制御し、ライブネスプローブは停止状態に陥ったコンテナを kubelet が再起動するタイミングを判断します。すべての目的に同じプローブを使うと、不要な再起動や早すぎるトラフィック送信を招くことがあります。
プローブ付きアプリケーションをデプロイする
このステップでは、名前付きの http ポートに対して / をチェックするスタートアップ、レディネス、ライブネスプローブ付きの NGINX をデプロイします。
3 つのプローブはコンテナ仕様の内部に記述されるため、マニフェストは少し長くなっています。適用する前に、次の順序で確認してください。Deployment とラベル、コンテナと名前付きポート、そしてスタートアップ、レディネス、ライブネスの各プローブです。この例では 3 つのチェックすべてが同じ HTTP エンドポイントを使用しますが、失敗した場合の影響はそれぞれ異なります。
1 行で記述されたマッピングやリストの一部には、YAML の簡潔な「フロースタイル」が使われています。たとえば {app: healthy-web} のような形式です。波括弧はマッピングを、角括弧はリストを表し、複数行で記述する YAML と同じ意味になります。ここでは、値全体を読みやすく保てる場合にのみ使用しています。
スタートアップのしきい値により、Kubernetes が起動失敗と判断するまで最大 24 秒(12 × 2)待機します。スタートアップに成功すると、レディネスは 3 秒ごと、ライブネスは 5 秒ごとに実行されます。
cd /home/labex/project/application-health
cat <<'EOF' > healthy-web.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: healthy-web
namespace: practice-health
spec:
replicas: 2
selector:
matchLabels: {app: healthy-web}
template:
metadata:
labels: {app: healthy-web}
spec:
containers:
- name: web
image: nginx:1.27-alpine
imagePullPolicy: IfNotPresent
ports:
- {name: http, containerPort: 80, protocol: TCP}
startupProbe:
httpGet: {path: /, port: http}
failureThreshold: 12
periodSeconds: 2
readinessProbe:
httpGet: {path: /, port: http}
periodSeconds: 3
livenessProbe:
httpGet: {path: /, port: http}
periodSeconds: 5
failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
name: healthy-web
namespace: practice-health
spec:
selector: {app: healthy-web}
ports:
- {name: http, port: 80, targetPort: http, protocol: TCP}
EOF
名前空間を作成し、マニフェストを適用します。rollout status はプロセスが起動したかどうかだけでなく、2 つのレプリカがどちらもレディネスを満たすまで待機します。
kubectl create namespace practice-health
kubectl apply -f healthy-web.yaml
kubectl rollout status deployment/healthy-web -n practice-health --timeout=60s
スタートアッププローブは初期化に時間がかかる場合に備え、レディネスはトラフィックを受け付ける資格を制御し、ライブネスは復旧不能な障害後の再起動を可能にします。
ロールアウトが完了すれば、「コンテナが実行中である」以上のことが確認できています。2 つの Pod はどちらもレディネスリクエストに正常に応答しています。
最後に表示されるロールアウト行は、最初の健全性の証拠です。
Waiting for deployment "healthy-web" rollout to finish: 0 of 2 updated replicas are available...
Waiting for deployment "healthy-web" rollout to finish: 1 of 2 updated replicas are available...
deployment "healthy-web" successfully rolled out
途中の行は正常です。Kubernetes は各 Pod がスタートアップとレディネスを通過するたびに進捗を報告します。最後の行が表示されて初めて、2 つのレプリカが利用可能になったことを意味します。
レディネスを Service エンドポイントに接続する
このステップでは、レディネス条件と EndpointSlice を確認します。
まず、見慣れた Pod の一覧を表示し、次にレディネス条件と再起動回数だけに絞り込みます。カスタムテーブルを使うと、「レディではない状態」と「再起動中」を簡単に区別できます。
kubectl get pods -n practice-health -l app=healthy-web
kubectl get pods -n practice-health -l app=healthy-web -o custom-columns='POD:.metadata.name,READY_CONDITION:.status.conditions[?(@.type=="Ready")].status,RESTARTS:.status.containerStatuses[0].restartCount'
kubectl get endpointslices -n practice-health -l kubernetes.io/service-name=healthy-web -o jsonpath='{range .items[].endpoints[*]}{.addresses[0]} ready={.conditions.ready}{"\n"}{end}'
2 つのアドレスには、どちらも ready=true と表示されるはずです。レディネスに失敗してもコンテナは実行中のままですが、そのアドレスは通常の Service ルーティングから除外されます。
EndpointSlice は、Service のセレクターと実際の Pod アドレスをつなぐ橋渡し役です。Pod がレディでなくても Service 自体は存在し続けます。通常のトラフィックをその Pod に送らないようにするのは、エンドポイントの条件です。
健全な状態では、通常次のような出力になります。
POD READY_CONDITION RESTARTS
healthy-web-... True 0
healthy-web-... True 0
10.244.104.11 ready=true
10.244.104.12 ready=true
Pod 名と IP アドレスは環境によって異なります。重要なのは対応関係です。Ready=True の Pod が 2 つあり、それに対応する ready=true のエンドポイントが 2 つ存在し、どちらのコンテナも再起動していないことを確認してください。
レディネス失敗を確認する
このステップでは、レディネスのパスだけを存在しない URL に意図的に変更します。
これは制御された障害です。JSON Patch はネストされた 1 つのフィールドだけを置き換えるため、スタートアッププローブとライブネスプローブは変更されません。NGINX は /not-ready に対して HTTP 404 を返し、レディネスはこれを失敗として扱います。
kubectl patch deployment healthy-web -n practice-health --type='json' -p='[{"op":"replace","path":"/spec/template/spec/containers/0/readinessProbe/httpGet/path","value":"/not-ready"}]'
kubectl rollout status deployment/healthy-web -n practice-health --timeout=20s || true
kubectl get pods -n practice-health -l app=healthy-web
kubectl get endpointslices -n practice-health -l kubernetes.io/service-name=healthy-web -o jsonpath='{range .items[].endpoints[*]}{.addresses[0]} ready={.conditions.ready}{"\n"}{end}'
|| true によって、想定されるロールアウトのタイムアウトを成功扱いにし、シェルが処理を続行できるようにしています。新しいコンテナは実行されますが、レディネスに失敗するため、Deployment は利用可能な状態になりません。
実行時には、次のような状態が確認できます。
error: timed out waiting for the condition
NAME READY STATUS RESTARTS
healthy-web-old-... 1/1 Running 0
healthy-web-old-... 1/1 Running 0
healthy-web-new-... 0/1 Running 0
...
10.244.104.17 ready=false
新しい Pod は Running ですがレディではなく、再起動もしていません。古いレディな Pod 2 つが残っているのは、Deployment のローリングアップデートが可用性を保護するためです。新しいリビジョンがレディになれない間、Kubernetes は健全なレプリカをこれ以上置き換えません。したがって、ロールアウトのタイムアウトからは、レディネスの動作とコントローラーの安全機構の両方を確認できます。EndpointSlice の出力では新しいアドレスが ready=false となるため、通常の Service ルーティングでは健全なエンドポイントだけが使用されます。
レディネスを修復し、トラフィックを確認する
このステップでは、レディネスのパスを元に戻し、Service のエンドポイントが復旧するまで待機します。
前のパッチの逆操作を行い、実験で変更する変数を 1 つだけにします。置き換え後の Pod が / に対して HTTP 200 を返すため、今回は rollout status が成功します。
kubectl run health-client は、リクエストを送信するための一時的な単独 Pod を作成します。--image は BusyBox を選択し、--image-pull-policy=IfNotPresent はキャッシュ済みのイメージを再利用し、--restart=Never はコントローラーではなく Pod を作成するよう指定します。--rm -i はコマンドを接続状態で実行し、終了時に Pod を削除します。2 つ目の -- は kubectl のオプションと、コンテナ内で実行する wget コマンドを分離します。
wget -qO- では、-q が進捗メッセージを非表示にし、-O- がレスポンス本文を標準出力に書き出します。パイプによってその本文を head に渡し、先頭部分だけを表示します。URL に Service 名 healthy-web を使用しているのは、クラスタ内 DNS が名前空間内でこの名前を解決できるためです。
kubectl patch deployment healthy-web -n practice-health --type='json' -p='[{"op":"replace","path":"/spec/template/spec/containers/0/readinessProbe/httpGet/path","value":"/"}]'
kubectl rollout status deployment/healthy-web -n practice-health --timeout=60s
kubectl run health-client -n practice-health --image=busybox:1.36 --image-pull-policy=IfNotPresent --restart=Never --rm -i -- wget -qO- http://healthy-web | head
一時クライアントはクラスタ内 DNS を使用します。表示する HTML を制限することで、確認結果を読みやすく保ちながら、正常なレスポンスによってレディネスと Service ルーティングが一致していることを証明できます。
レスポンスは、次のような NGINX の HTML で始まります。
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
ロールアウト成功後に HTML が表示されれば、DNS が Service を解決し、Service がレディなエンドポイントを選択し、NGINX がリクエストを処理したという一連の経路全体が正常であることを確認できます。
診断用クライアントを名前空間に残さないよう、終了後は一時 Pod を削除します。
正常な終了処理を追加する
このステップでは、preStop フックと終了猶予期間を追加します。フックによってシャットダウンを短時間遅延させ、プロセスが終了する前にエンドポイントの削除を伝播させます。
2 つの JSON Patch 操作を分けて確認しましょう。1 つ目は Pod の終了猶予を 20 秒に設定します。2 つ目はコンテナインデックス 0 に 5 秒間のスリープフックを追加します。Kubernetes は終了処理の開始時にフックを実行し、残りの猶予時間はその間も減少し続けます。
kubectl patch deployment healthy-web -n practice-health --type=json -p='[
{"op":"add","path":"/spec/template/spec/terminationGracePeriodSeconds","value":20},
{"op":"add","path":"/spec/template/spec/containers/0/lifecycle","value":{"preStop":{"sleep":{"seconds":5}}}}
]'
kubectl rollout status deployment/healthy-web -n practice-health --timeout=60s
kubectl get deployment healthy-web -n practice-health -o jsonpath='grace={.spec.template.spec.terminationGracePeriodSeconds}s preStop={.spec.template.spec.containers[0].lifecycle.preStop.sleep.seconds}s{"\n"}'
JSON パッチが変更するのは指定された 2 つのフィールドだけなので、プローブやその他のコンテナ設定はそのまま維持されます。正常な終了処理はレディネスを補完します。レディネスがトラフィックの受け入れ可否を制御するのに対し、正常な終了処理は終了完了前に Pod をトラフィックから除外する時間を Kubernetes に与えます。
最後の JSONPath の出力は grace=20s preStop=5s になるはずです。5 秒のフックは 20 秒の猶予内に収まるため、NGINX は通常どおり終了できます。
新しい preStop フックによって終了処理が意図的に 5 秒追加されるため、ロールアウト中に 1 old replicas are pending termination のようなメッセージが表示され、一時的に停止することがあります。最終的な出力は次のようになります。
deployment "healthy-web" successfully rolled out
grace=20s preStop=5s
まとめ
スタートアップ、レディネス、ライブネスの各チェックを Kubernetes の動作に結び付け、コンテナを停止させずにレディネスによってエンドポイントから除外される様子を確認し、トラフィックを復旧させ、正常な終了処理を追加しました。これらの仕組みにより、健全性は単に可視化されるだけでなく、実際のアクションにつながる情報になります。


