本番Kubernetesが全滅した夜の話をする — Pod eviction地獄からの生還記

本番環境でKubernetesを運用していると、開発環境では決して再現しない障害に遭遇する。この記事では、筆者が実際に経験したKubernetes本番障害10件を振り返り、それぞれの原因と対策、そして組織として学んだ教訓を共有する。深夜のPagerDutyアラートに何度も叩き起こされた経験から得た知見が、あなたのクラスタ運用の助けになれば幸いだ。

教訓1: Pod Evictionの連鎖がサービスダウンを引き起こす

ある金曜日の夜、突然モニタリングダッシュボードが真っ赤に染まった。Podが次々とEvictされ、サービスが完全に停止した。原因はノードのメモリ逼迫だった。あるバッチ処理Podがメモリリークを起こし、ノードのメモリを食い尽くした結果、kubeletがOOM Killerを発動する前にPod Evictionを開始したのだ。

問題は、Evictionされたのがバッチ処理Podだけではなかったことだ。同じノード上で動いていたAPIサーバーのPodまで巻き添えを食らった。さらにPodDisruptionBudgetを設定していなかったため、Evictionに歯止めがかからなかった。

対策: Resource Quotaとの付き合い方

この障害から、以下の対策を導入した。

  • すべてのPodにresource requestsとlimitsを設定する。特にメモリのlimitsは必須
  • バッチ処理は専用のNode Poolで実行し、APIサーバーと物理的に分離する
  • PodDisruptionBudgetを全Deploymentに設定し、同時にEvictされるPod数を制限する
  • Vertical Pod Autoscaler (VPA) を導入し、適切なresource値を自動推定する
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: api-server-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: api-server

Resource Requestsの設計は、少なすぎるとスケジューリングが不安定になり、多すぎるとリソース効率が下がる。VPAのrecommendation modeで実際の使用量を観測してから値を決めるのが実践的なアプローチだ。

教訓2: Liveness ProbeのタイムアウトがCascading Failureを起こす

データベースの応答が一時的に遅延したとき、Liveness Probeがタイムアウトしてコンテナが再起動された。再起動したコンテナが一斉にDBへ接続しにいき、DBの負荷がさらに上がる。その結果、他のPodのLiveness Probeもタイムアウトし、再起動の連鎖が発生した。典型的なCascading Failureだ。

対策: ProbeとCircuit Breakerの見直し

  • Liveness ProbeからDB接続チェックを外し、プロセス生存のみを確認する
  • Readiness ProbeでDB接続を含むヘルスチェックを行い、トラフィックの受け入れを制御する
  • Startup Probeを導入し、初回起動時の猶予期間を設ける
  • アプリケーション側にCircuit Breakerパターンを実装する
livenessProbe:
  httpGet:
    path: /healthz/live
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 15
  timeoutSeconds: 5
  failureThreshold: 3

readinessProbe:
  httpGet:
    path: /healthz/ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 10
  timeoutSeconds: 3
  failureThreshold: 3

startupProbe:
  httpGet:
    path: /healthz/live
    port: 8080
  failureThreshold: 30
  periodSeconds: 10

Liveness ProbeとReadiness Probeの役割は明確に分離すべきだ。Livenessは「プロセスが壊れていないか」を確認し、Readinessは「リクエストを処理できる状態か」を確認する。この区別を曖昧にすると、今回のような障害を招く。

教訓3: ConfigMapの更新がPodに反映されないトラップ

設定変更のためにConfigMapを更新したが、Podに反映されなかった。envFromでConfigMapを参照している場合、ConfigMapを更新してもPodは自動的に再起動しないからだ。これを知らずに「設定変更が反映されない」と数時間悩んだ。

対策: 設定変更の反映を自動化する

  • Reloaderなどのコントローラを導入し、ConfigMap変更時にPodを自動再起動する
  • ConfigMapのイミュータブル戦略を採用し、名前にハッシュを付与して変更時は新しいConfigMapを作成する
  • volumeMountでConfigMapをマウントしている場合は自動反映されるが、envFromの場合は再起動が必要という挙動の違いを理解する
# Helmでのイミュータブル ConfigMap パターン
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config-{{ .Values.configHash }}
data:
  APP_ENV: production
  LOG_LEVEL: info

---
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    metadata:
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
    spec:
      containers:
      - name: app
        envFrom:
        - configMapRef:
            name: app-config-{{ .Values.configHash }}

教訓4: Horizontal Pod Autoscalerのメトリクス遅延

急激なトラフィック増加時に、HPAのスケールアウトが間に合わなかった。metrics-serverのメトリクス収集間隔がデフォルトで60秒、HPAのチェック間隔がデフォルトで15秒あり、実際のスケールアウトまでに2〜3分のラグがあった。その間にリクエストが捌ききれずタイムアウトが多発した。

対策: HPAの調整とバッファ

  • HPAのtarget utilizationを70〜80%に設定し、早めにスケールアウトを開始する
  • 最小レプリカ数を想定される最低トラフィックに合わせて設定する
  • カスタムメトリクス(リクエスト数やレイテンシ)をPrometheusで収集し、HPAのメトリクスとして利用する
  • predictive autoscalingを検討する。過去のトラフィックパターンから事前にスケールアウトする仕組みだ

教訓5: NetworkPolicyの設定ミスでPod間通信が遮断

セキュリティ強化のためにNetworkPolicyを導入した際、デフォルトで全通信を拒否し、必要な通信のみ許可するホワイトリスト方式を採用した。しかし、DNS通信(kube-dns / CoreDNS)の許可を忘れており、Pod内からの名前解決が全く動かなくなった。

対策: NetworkPolicy導入の段階的アプローチ

  • まずobserve-onlyモードで通信パターンを可視化する(Calicoのログ機能など)
  • DNS通信の許可を最初に設定する。これを忘れると全サービスが動かなくなる
  • 名前空間単位でNetworkPolicyを段階的に適用する
  • CIパイプラインにNetworkPolicyのテストを組み込む
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53

教訓6: PVCのリサイズで学んだストレージの制約

永続ボリュームの容量が足りなくなり、PVCをリサイズしようとしたが、使用していたStorageClassがallowVolumeExpansionをサポートしていなかった。結局、新しいPVCを作成してデータをマイグレーションする羽目になった。深夜3時の作業は過酷だった。

対策: ストレージ設計の事前準備

  • StorageClassにallowVolumeExpansion: trueを設定しておく
  • ボリュームの使用率を監視し、80%でアラートを出す
  • 定期的にストレージの使用量をレビューし、計画的に拡張する
  • ステートフルなワークロードはマネージドサービス(RDS, Cloud SQLなど)の利用を検討する

教訓7: RollingUpdateのmaxSurge/maxUnavailable設定ミス

デプロイ時にmaxUnavailableを50%に設定していたため、レプリカ数4のDeploymentで2つのPodが同時に停止した。新しいPodの起動に時間がかかり、残り2つのPodでは全トラフィックを捌けなかった。レスポンスタイムが急上昇し、ユーザーから苦情が殺到した。

対策: 安全なデプロイ戦略

  • maxUnavailableを0に設定し、既存Podを停止する前に新しいPodが起動していることを保証する
  • maxSurgeを25%〜50%に設定し、追加リソースの利用を許容する
  • Canary Deploymentの導入を検討し、段階的にトラフィックを移行する
  • デプロイ前にreadiness gateを確認し、新しいPodが完全に準備できてからトラフィックを切り替える

教訓8: Secretの平文管理によるセキュリティインシデント

Kubernetes Secretがbase64エンコードされているだけで暗号化されていないことを知らないメンバーがいた。GitリポジトリにSecretマニフェストがコミットされ、平文の認証情報が漏洩した。幸い早期に発見できたが、全認証情報のローテーションが必要になった。

対策: Secret管理のベストプラクティス

  • Sealed Secrets、External Secrets Operator、またはVault を導入して暗号化を担保する
  • .gitignoreにSecretマニフェストを追加し、git-secretsなどのpre-commitフックでシークレットの漏洩を防止する
  • etcdの暗号化を有効にし、Secret保存時の暗号化を担保する
  • RBAC設定でSecretへのアクセスを最小権限に制限する

教訓9: Ingress ControllerのRate Limit未設定

ある日、特定のAPIエンドポイントに異常な量のリクエストが押し寄せた。Rate Limitを設定していなかったため、全リクエストがバックエンドに到達し、DBのコネクションプールが枯渇してサービス全体がダウンした。

対策: 多層防御のRate Limiting

  • Ingress Controllerレベルでグローバルなrate limitingを設定する
  • アプリケーションレベルでエンドポイント別のrate limitingを実装する
  • CDN / WAFでの事前フィルタリングを導入する
  • 異常なトラフィックパターンを検知してアラートを出す仕組みを構築する
# nginx-ingress のアノテーション例
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    nginx.ingress.kubernetes.io/limit-rps: "50"
    nginx.ingress.kubernetes.io/limit-burst-multiplier: "5"
    nginx.ingress.kubernetes.io/limit-connections: "20"

教訓10: クラスタアップグレードの失敗と復旧

Kubernetesのマイナーバージョンアップグレード時に、非推奨APIを使用しているマニフェストの更新を忘れていた。アップグレード後にDeploymentやIngressが作成できなくなり、新しいデプロイが一切できない状態に陥った。

対策: アップグレード前の準備

  • kubent(Kube No Trouble)やplutoなどのツールで非推奨API使用箇所を事前に検出する
  • ステージング環境で先行アップグレードし、問題を洗い出す
  • アップグレード手順書を作成し、ロールバック手順も含める
  • 1つのマイナーバージョンずつアップグレードする。バージョンスキップは避ける
# 非推奨API検出
$ kubent
>>> Deprecated APIs removed in 1.29 <<<
-------------------------------------------------
KIND           NAMESPACE  NAME       API_VERSION
Ingress        default    my-ingress networking.k8s.io/v1beta1
PodSecurityPolicy <none> restricted policy/v1beta1

まとめ: 障害から学ぶ文化を作る

これらの障害すべてに共通する教訓がある。それは「障害は必ず起きる」ということだ。重要なのは障害を防ぐことではなく、障害が起きたときに素早く検知し、迅速に復旧し、そして同じ障害を二度と起こさない仕組みを作ることだ。

障害はシステムの弱点を教えてくれる最良の教師である。ただし、その教えを記録し、共有し、対策を実装しなければ、同じ授業料を何度も払うことになる。

筆者のチームでは、すべての障害についてポストモーテムを書き、blamelessな振り返りを行い、改善アクションをチケット化して追跡している。この文化が定着してから、同じ障害の再発は劇的に減った。Kubernetesの運用は奥が深いが、一つひとつの障害と真摯に向き合うことで、クラスタはより堅牢になっていく。