記事

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"] }
お問い合わせ

現在の構成についてセカンドオピニオンが必要ですか?

システムの概要を短くお送りください。シニアエンジニアが1営業日以内に書面でお返しします。

インフラについて相談する