Kubernetesワークロードの本番ベースライン
リソース要求、プローブ、Disruption Budget、グレースフルシャットダウン、Podセキュリティ:本番のDeploymentが最初のインシデントの前に備えておくべき設定。
例は汎用的なもので、説明を目的としています。どの顧客環境から取ったものでもありません。しきい値や設定はご自身のシステムに合わせて調整してください。
なぜベースラインが必要か
Kubernetesはほとんど設定なしでほとんど何でも動かせます。そしてそれこそが問題です。デフォルト値は動かし始めるためのものであり、止まらずに動き続けるためのものではありません。以下の設定は初日に入れれば安く、障害中に気づけば高くつきます。対象はステートレスなDeploymentで、ステートフルなワークロードにはさらに多くの設定が必要です。
requestsとlimits
スケジューラはrequestsにもとづいてPodを配置し、Horizontal Pod Autoscalerもrequestsを基準に使用率を測ります。requestsのないPodは手探りで配置され、CPUによるオートスケールもできません。
メモリは圧縮できません。メモリ上限を超えたコンテナは強制終了されます。メモリのlimitはrequestと同じか近い値にし、確保し続けられないメモリにPodがスケジュールされないようにします。CPUは事情が異なり、CPU上限を超えたコンテナはスロットリングされ、それがレイテンシとして現れます。レイテンシに敏感なサービスでは、CPUのrequestsだけを設定してCPUのlimitsを外すチームも多くあります。どちらを選ぶにせよ、スロットリングを計測してください。
3つのプローブ、3つの役割
- readinessは、Podがトラフィックを受けるかどうかを決めます。共有依存先には注意が必要です。すべてのPodが同時にreadinessに失敗すると、サービスのエンドポイントが1つもなくなります。
- livenessは、コンテナを再起動するかどうかを決めます。対象はプロセス自体に限定してください。データベースを確認していると、データベース障害ですべてのPodが一斉に再起動します。
- startupは、起動の遅いアプリケーションが立ち上がるまで他のプローブを保留します。livenessの遅延を引き延ばす代わりに使います。
ノードのドレインとゾーン障害を乗り切る
ノードのアップグレードやオートスケーリングによって、Podは常に退避させられます。PodDisruptionBudgetは自発的な中断で同時に停止できるレプリカ数を制限し、トポロジー分散制約(topology spread constraints)はレプリカを異なるゾーンに配置して、1つのゾーンの障害でサービス全体が落ちないようにします。
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api
spec:
maxUnavailable: 1
selector:
matchLabels: { app: api }
グレースフルシャットダウン
Podが削除されると、サービスのエンドポイントからの除外と、コンテナの停止(preStopフック、続いてSIGTERM)が同時に始まります。即座に終了するアプリケーションは、まだルーティングされてくるリクエストを取りこぼします。ロードバランサーが追いつくよう短いpreStopの待機を入れ、SIGTERMを受けたら処理中の作業を終わらせ、terminationGracePeriodSecondsは待機時間とドレイン時間の合計より長くしておきます。
セキュリティコンテキストとイメージ
非rootユーザーで実行し、Linuxのcapabilitiesをすべて外し、権限昇格を禁止し、ルートファイルシステムを読み取り専用でマウントします。restrictedレベルのPod Security Standardは、その大部分をネームスペース単位で強制します。イメージは可変のタグではなくダイジェストで参照し、テストしたものがそのまま動くようにします。
まとめると
ベースラインを適用したDeploymentの例です。値は仮のものです。requestsはこの例ではなく、計測した使用量にもとづいて決めてください。
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector:
matchLabels: { app: api }
template:
metadata:
labels: { app: api }
spec:
terminationGracePeriodSeconds: 45
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels: { app: api }
securityContext:
runAsNonRoot: true
seccompProfile: { type: RuntimeDefault }
containers:
- name: api
image: registry.example.com/api@sha256:<digest>
resources:
requests: { cpu: 250m, memory: 256Mi }
limits: { memory: 256Mi }
readinessProbe:
httpGet: { path: /ready, port: 8080 }
periodSeconds: 5
livenessProbe:
httpGet: { path: /live, port: 8080 }
periodSeconds: 10
failureThreshold: 3
startupProbe:
httpGet: { path: /live, port: 8080 }
periodSeconds: 5
failureThreshold: 30
lifecycle:
preStop:
sleep: { seconds: 5 } # native sleep action, Kubernetes 1.30+
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }