drain が30分たっても終わらない
目標
drainがブロックされたときにエビクションAPIを直接呼び出して何が止めているのかを読み取り、パーセンテージで指定したバジェットの切り上げルール、セレクターが重複したバジェットのデッドロック、準備ができていないPodのエビクションポリシーを、クラスターが計算した数値で確認したうえで、バジェットのないワークロードをクラスター全体から探して一覧にします。
なぜ重要なのか
kubectl drainはPodを削除しません。Podごとにエビクション(Eviction)サブリソースを呼び出し、そのリクエストをPodDisruptionBudgetが審査します。バジェットが足りなければ、削除ではなくHTTP 429が返り、drainは成功するまで再試行します。そのため、ブロックされたdrainは失敗で終わらず、永遠に続きます。この仕組みを知らないと、画面のerror when evicting podsだけを見ても、原因を見つけられません。バジェットは数字1つではなく、currentHealthy・desiredHealthy・disruptionsAllowedの3つの数字で説明され、その数字の読み方を知っていれば、ブロックされた理由は、たいてい3つのうちのどれかに絞られます。バジェットが0であるか、セレクターが重複しているか、準備ができていないPodがバジェットをすでに壊しているか、です。
ステップ
- ネームスペース
ops-pdbを作成し、/root/ops-disruption/api.yamlにDeploymentapiを書いてください。レプリカは4、ラベルはapp: api、イメージはnginx:1.27.3、nodeSelectorはkubernetes.io/hostname: lab-node-0です。適用して、4つのPodがすべてlab-node-0でRunningになるまで待ってください。 /root/ops-disruption/api-pdb.yamlにPodDisruptionBudgetapi-pdbを書いてください。ネームスペースはops-pdb、minAvailable: 4、セレクターはapp: apiです。適用したあと、コントローラーが状態を計算するまで待ってから、/root/ops-disruption/pdb-status.tsvに1行で保存してください。api-pdb、<currentHealthy>、<desiredHealthy>、<disruptionsAllowed>、<expectedPods>の順に、タブで区切ります。apiのPodを1つ選んで、/root/ops-disruption/eviction.jsonにEvictionオブジェクトを書いてください。apiVersionはpolicy/v1、kindはEviction、metadata.nameはそのPod名、metadata.namespaceはops-pdbです。そのファイルでkubectl create --raw /api/v1/namespaces/ops-pdb/pods/<파드이름>/eviction -f /root/ops-disruption/eviction.jsonを実行し(プレースホルダーはPod名です)、エラー出力を/root/ops-disruption/eviction-denied.txtに保存してください(標準エラー出力も一緒に保存する必要があります)。kubectl drain lab-node-0 --ignore-daemonsets --delete-emptydir-data --force --timeout=20sを実行して、出力を/root/ops-disruption/drain-blocked.txtに保存してください(標準エラー出力を含む)。drainは最初にノードをスケジュール不可の状態に変えるので、コマンドが終わったあと、必ずkubectl uncordon lab-node-0で元に戻しておいてください。/root/ops-disruption/api-pdb-fixed.yamlに、同じ名前のPDBapi-pdbを書き直し、minAvailableの代わりにmaxUnavailable: 1を使って適用してください(2つのフィールドは同時に使えないので、新しいファイルにはminAvailableがあってはいけません)。許容中断数が1になるまで待ったあと、/root/ops-disruption/pdb-after.tsvに、ステップ2と同じ5つの列で保存し、ステップ3のリクエストに?dryRun=Allを付けてもう一度送り、その出力を/root/ops-disruption/eviction-allowed.txtに保存してください。/root/ops-disruption/batch.yamlにDeploymentbatchを書いてください。ネームスペースはops-pdb、レプリカは7、ラベルはapp: batch、イメージはnginx:1.27.3です。そして/root/ops-disruption/batch-pdb.yamlにPDBbatch-pdbを書いてください。minAvailable: 50%で、セレクターはapp: batchです。両方を適用して状態が計算されたら、/root/ops-disruption/rounding.tsvに1行で保存してください。batch-pdb、7、50%、<desiredHealthy>、<disruptionsAllowed>の順に、タブで区切ります。/root/ops-disruption/batch-extra.yamlに、PDBbatch-extraをもう1つ書いてください。ネームスペースはops-pdb、maxUnavailable: 1、セレクターはbatch-pdbとまったく同じapp: batchです。適用したあと、batchのPod1つに対して?dryRun=Allを付けたエビクションのリクエストを送り、その出力を/root/ops-disruption/overlap.txtに保存してください(リクエストの本文は/root/ops-disruption/overlap-eviction.jsonに置きます)。/root/ops-disruption/flaky.yamlにDeploymentflaky(レプリカ3、ラベルapp: flaky、イメージnginx:1.27.3)を、/root/ops-disruption/flaky-pdb.yamlにPDBflaky-pdb(minAvailable: 3、セレクターapp: flaky)を書いて適用してください。そのあと、flakyのPod1つのstatus.conditionsをパッチして、ReadyをFalseにしてください(kubectl -n ops-pdb patch pod <이름> --subresource=status --type=merge -p ...、プレースホルダーはPod名です)。そのPodに対して?dryRun=Allのエビクションを送って、拒否メッセージを/root/ops-disruption/unhealthy-denied.txtに保存し、次にflaky-pdbのspec.unhealthyPodEvictionPolicyをAlwaysAllowに変えてから、同じリクエストをもう一度送って、/root/ops-disruption/unhealthy-allowed.txtに保存してください。/root/ops-disruption/orphan.yamlにDeploymentorphanを書いてください。ネームスペースはops-pdb、レプリカは3、ラベルはapp: orphan、イメージはnginx:1.27.3で、PDBは作成しません。そのあと、/root/ops-disruption/pdb-audit.shを作成してください。すべてのネームスペースのDeploymentのうち、spec.replicasが2以上なのに、同じネームスペースのどのPDBにもカバーされていないものを、<네임스페이스>と<이름>のタブ区切りで標準出力にだけ出力し、名前順にソートします(プレースホルダーはネームスペースと名前です)。PDBのspec.selector.matchLabelsが、Deploymentのspec.template.metadata.labelsの部分集合であれば、カバーされているとみなします。その出力を/root/ops-disruption/pdb-audit.txtに保存してください。
参考
- エビクションサブリソースは、
kubectl create --raw /api/v1/namespaces/<ns>/pods/<pod>/eviction -f <파일>で直接呼び出せます(プレースホルダーはファイル名です)。 ?dryRun=Allを付けると、Podを削除せずに判定だけを受け取れます。- PDBの状態は、
kubectl get pdb <이름> -o jsonの.statusにすべて入っています。 - よくある間違い: 出力を保存するときに
2>&1を前に書いてしまい、標準エラー出力がファイルに保存されないことです。 - よくある間違い: drainがブロックされたあと、ノードをuncordonせず、容量が静かに減ったまま残ることです。
- 参考: https://kubernetes.io/docs/concepts/workloads/pods/disruptions/
- 参考: https://kubernetes.io/docs/tasks/run-application/configure-pdb/
1つのノードに集めたワークロード
ネームスペースops-pdbを作成し、/root/ops-disruption/api.yamlにDeploymentapiを書いてください。レプリカは4、ラベルはapp: api、イメージはnginx:1.27.3、nodeSelectorはkubernetes.io/hostname: lab-node-0です。適用して、4つのPodがすべてlab-node-0でRunningになるまで待ってください。
drainはノード単位の作業なので、対象のノードが決まっていないと再現できません。1つのノードに集めておくと、あとでそのノードを空にしようとしたときに、何が止めているのかがはっきりします。新しいネームスペースは、defaultサービスアカウントができるまで少し時間がかかります。
許容中断数0という数字を作る
/root/ops-disruption/api-pdb.yamlにPodDisruptionBudgetapi-pdbを書いてください。ネームスペースはops-pdb、minAvailable: 4、セレクターはapp: apiです。適用したあと、コントローラーが状態を計算するまで待ってから、/root/ops-disruption/pdb-status.tsvに1行で保存してください。api-pdb、<currentHealthy>、<desiredHealthy>、<disruptionsAllowed>、<expectedPods>の順に、タブで区切ります。
PDBを作成した直後は、statusが空のことがあります。中断コントローラーがセレクターに該当するPodを数えてstatusを埋めるまで待ってください。expectedPodsが0でなくなれば、計算が終わったということです。4つすべてを守るよう指定したので、許容中断数がいくつになるのかを先に予想してみてください。
エビクションAPIに直接問い合わせる
apiのPodを1つ選んで、/root/ops-disruption/eviction.jsonにEvictionオブジェクトを書いてください。apiVersionはpolicy/v1、kindはEviction、metadata.nameはそのPod名、metadata.namespaceはops-pdbです。そのファイルでkubectl create --raw /api/v1/namespaces/ops-pdb/pods/<파드이름>/eviction -f /root/ops-disruption/eviction.jsonを実行し(プレースホルダーはPod名です)、エラー出力を/root/ops-disruption/eviction-denied.txtに保存してください(標準エラー出力も一緒に保存する必要があります)。
drainはPodを削除するのではなく、エビクションサブリソースを呼び出します。そのため、PDBがブロックすると、削除ではなくHTTP 429が返ります。応答の本文に、なぜブロックされたのかが書かれています。出力をファイルに残すには、2>&1 > 파일ではなく> 파일 2>&1の順序である必要があります(プレースホルダーはファイル名です)。
drainが終わらないことを自分の目で確かめる
kubectl drain lab-node-0 --ignore-daemonsets --delete-emptydir-data --force --timeout=20sを実行して、出力を/root/ops-disruption/drain-blocked.txtに保存してください(標準エラー出力を含む)。drainは最初にノードをスケジュール不可の状態に変えるので、コマンドが終わったあと、必ずkubectl uncordon lab-node-0で元に戻しておいてください。
drainコマンドは、2つのことをします。ノードにスケジュール不可の印を残し、その上のPodをエビクションAPIで1つずつ追い出します。前者が成功して後者がブロックされると、ノードはスケジュール不可のまま残ります。これが、現場で容量が静かに減っていく、よくある経路です。
バジェットを修正すると、同じリクエストが通過する
/root/ops-disruption/api-pdb-fixed.yamlに、同じ名前のPDBapi-pdbを書き直し、minAvailableの代わりにmaxUnavailable: 1を使って適用してください(2つのフィールドは同時に使えないので、新しいファイルにはminAvailableがあってはいけません)。許容中断数が1になるまで待ったあと、/root/ops-disruption/pdb-after.tsvに、ステップ2と同じ5つの列で保存し、ステップ3のリクエストに?dryRun=Allを付けてもう一度送り、その出力を/root/ops-disruption/eviction-allowed.txtに保存してください。
エビクションサブリソースはdryRunをサポートしています。実際にはPodを削除せず、今このPodを追い出せるかどうかだけを問い合わせます。運用でメンテナンスウィンドウを確保する前に確認する方法であり、ここでは、あとのステップで使うPodを削除しないために使います。成功すると、code 201を含むStatusが返ります。
パーセンテージは、どちらへ切り上げられるのか
/root/ops-disruption/batch.yamlにDeploymentbatchを書いてください。ネームスペースはops-pdb、レプリカは7、ラベルはapp: batch、イメージはnginx:1.27.3です。そして/root/ops-disruption/batch-pdb.yamlにPDBbatch-pdbを書いてください。minAvailable: 50%で、セレクターはapp: batchです。両方を適用して状態が計算されたら、/root/ops-disruption/rounding.tsvに1行で保存してください。batch-pdb、7、50%、<desiredHealthy>、<disruptionsAllowed>の順に、タブで区切ります。
7の50%は3.5です。Kubernetesがどちらへ丸めるかを先に予想してから、クラスターが計算した数値と突き合わせてみてください。予想が外れたなら、その方向がなぜ安全な側なのかを考えてみてください。YAMLでは、パーセンテージを引用符で囲むほうが安全です。
セレクターが重複した2つのバジェットは、デッドロックを作る
/root/ops-disruption/batch-extra.yamlに、PDBbatch-extraをもう1つ書いてください。ネームスペースはops-pdb、maxUnavailable: 1、セレクターはbatch-pdbとまったく同じapp: batchです。適用したあと、batchのPod1つに対して?dryRun=Allを付けたエビクションのリクエストを送り、その出力を/root/ops-disruption/overlap.txtに保存してください(リクエストの本文は/root/ops-disruption/overlap-eviction.jsonに置きます)。
2つのバジェットそれぞれには余裕があるのに、リクエストがブロックされます。エビクションサブリソースは、1つのPodが2つのバジェットに該当する状況そのものをサポートしていないからです。応答の文をそのまま読んでみてください。このメッセージを一度見た人は、次からセレクターの重複をすぐに疑うようになります。
準備ができていないPodが、drainを掴んで離さない
/root/ops-disruption/flaky.yamlにDeploymentflaky(レプリカ3、ラベルapp: flaky、イメージnginx:1.27.3)を、/root/ops-disruption/flaky-pdb.yamlにPDBflaky-pdb(minAvailable: 3、セレクターapp: flaky)を書いて適用してください。そのあと、flakyのPod1つのstatus.conditionsをパッチして、ReadyをFalseにしてください(kubectl -n ops-pdb patch pod <이름> --subresource=status --type=merge -p ...、プレースホルダーはPod名です)。そのPodに対して?dryRun=Allのエビクションを送って、拒否メッセージを/root/ops-disruption/unhealthy-denied.txtに保存し、次にflaky-pdbのspec.unhealthyPodEvictionPolicyをAlwaysAllowに変えてから、同じリクエストをもう一度送って、/root/ops-disruption/unhealthy-allowed.txtに保存してください。
PDBが数える健全なPodは、ReadyコンディションがTrueのPodです。3つを守るよう指定したのに、1つがReadyでなければ、バジェットはすでに壊れた状態で、デフォルトのポリシーでは、その壊れたPodでさえ追い出せません。drainが永遠に終わらない、よくある理由です。ポリシーを変えると、同じリクエストが通過します。
バジェットのないワークロードをクラスター全体から探す
/root/ops-disruption/orphan.yamlにDeploymentorphanを書いてください。ネームスペースはops-pdb、レプリカは3、ラベルはapp: orphan、イメージはnginx:1.27.3で、PDBは作成しません。そのあと、/root/ops-disruption/pdb-audit.shを作成してください。すべてのネームスペースのDeploymentのうち、spec.replicasが2以上なのに、同じネームスペースのどのPDBにもカバーされていないものを、<네임스페이스>と<이름>のタブ区切りで標準出力にだけ出力し、名前順にソートします(プレースホルダーはネームスペースと名前です)。PDBのspec.selector.matchLabelsが、Deploymentのspec.template.metadata.labelsの部分集合であれば、カバーされているとみなします。その出力を/root/ops-disruption/pdb-audit.txtに保存してください。
この一覧が、メンテナンスウィンドウを設計するときに最初に必要な資料です。バジェットのないワークロードは、drainに何の抵抗もなく、まるごと落とされます。部分集合の判定は、jqでPDBセレクターの各項目が、Podのラベルに同じ値で存在するかを数えればよいです。スクリプトがファイルを直接書き込むと、採点ツールが再実行するときに学習者の成果物を上書きしてしまうので、標準出力にだけ出力してください。