Kubernetesデプロイ戦略:午前2時のロールバックの恐怖をいかにして解消したか

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

デプロイにおける不安の正体

かつて、デプロイの日は私たちのチームにとって悪夢でした。Kubernetes標準のRollingUpdateを使用していても、頻繁に壁にぶつかっていました。新しいバージョンがすべてのCI/CDテストをパスしても、実際のトラフィックが流入した瞬間に崩壊するのです。移行中にエラー率は15%も急上昇し、ロールバックの作業はまるで「一度焼いたケーキを元に戻そうとする」ようなものでした。時間はかかり、手順は煩雑で、たいていはマネージャーが後ろで目を光らせている中で行われました。

多くの場合、コードそのものが悪者なのではなく、トラフィック制御の欠如が原因です。破壊的な変更を一度に100%のユーザーにリリースすることは、アップライトを賭けたハイリスクなギャンブルです。これを解決するために、私たちは基本的なアップデートから、ブルー/グリーン(Blue/Green)およびカナリア(Canary)パターンへと移行しました。この半年間で、この移行により200回以上のリリースを経ても本番環境を安定して維持できています。

ブルー/グリーンデプロイ:瞬時の切り替え

ブルー/グリーンデプロイは、2つの同一の本番環境を同時に実行することでリスクを排除します。「ブルー」は現在のライブバージョンであり、「グリーン」は新しいバージョンです。グリーンの環境が健全であると確信したときにのみ、スイッチを切り替えます。

Kubernetesでの仕組み

これを実現するために2つの独立したクラスターを用意する必要はありません。代わりに、Labels(ラベル)Selectors(セレクター)を活用します。KubernetesのServiceがトラフィックコントローラーとして機能します。Serviceのセレクターを更新するだけで、ユーザーをブルーのPodからグリーンのPodへと瞬時にルーティングし直すことができます。

実践:ブルー/グリーンの実装

具体的な例を見てみましょう。現在、ブルー(v1)のデプロイメントがトラフィックを処理しています。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-v1
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
      version: v1
  template:
    metadata:
      labels:
        app: my-app
        version: v1
    spec:
      containers:
      - name: app
        image: my-registry/app:v1
        ports:
        - containerPort: 8080

Serviceは現在、version: v1をターゲットにしています。

apiVersion: v1
kind: Service
metadata:
  name: app-service
spec:
  selector:
    app: my-app
    version: v1 # これがトラフィックの切り替えスイッチになります
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080

バージョン2の準備ができたら、新しいデプロイメント(app-v2)を立ち上げます。それらのPodがReadiness Probe(レディネスプローブ)をパスしたら、次のコマンド1つでServiceのセレクターを更新します。

kubectl patch service app-service -p '{"spec":{"selector":{"version":"v2"}}}'

もし新バージョンでエラーが発生し始めたら、5秒以内にv1に戻すことができます。これは、標準のローリングアップデートでは決して真似できないセーフティネットとなります。

カナリアデプロイ:実環境でのテスト

ブルー/グリーンは大きな切り替えに適していますが、カナリアデプロイは段階的な露出を目的としています。トラフィックのごく一部(例えば2%や5%)を新しいバージョンにルーティングします。メトリクスを監視し、すべてが正常であれば、徐々に水門を開いていきます。

カナリアデプロイの論理

カナリアは「サイレント」なバグを捕まえるために不可欠です。例えば、1,000人の同時接続ユーザーが発生して初めてトリガーされるような、緩やかなメモリリークやデータベース接続プールの枯渇といった問題です。「影響範囲(ブラストライジアス)」を制限することで、チームが調査している間、不具合を経験するのは一握りのユーザーだけで済みます。

実践:Nginx Ingressによるカナリア

単一のService内でPod의 比率を調整してカナリア構成を作ることもできますが、Nginx Ingressを使用する方がはるかにスマートです。アノテーションを介してトラフィックの割合を正確に制御できます。

まず、カナリアバージョンとそれ専用のサービス(app-v2-service)をデプロイします。次に、カナリア用のIngressオブジェクトを作成します。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-canary
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
  ingressClassName: nginx
  rules:
  - host: app.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: app-v2-service
            port:
              number: 80

この設定では、Nginxはリクエストのちょうど10%を新しいコードに送信します。私たちは通常、10%の状態で1時間維持し、p99レイテンシを監視します。レイテンシがしきい値の200ms以下に保たれていれば、重みを50%、そして100%へと引き上げていきます。

本番環境から得た教訓

これらの戦略を6か月間運用して、ドキュメントにはあまり書かれていない3つの教訓を学びました。1つ目は、データベースのスキーマ変更が最大のボトルネックになるということです。もしバージョン2がバージョン1を壊すような形でテーブルを移行してしまったら、ブルー/グリーンのロールバック計画は無意味になります。マイグレーションは常に後方互換性を持つように設計してください。

2つ目は、オブザーバビリティ(可観測性)は妥協できないということです。カナリアデプロイを「目隠し」状態で進めることはできません。私たちはGrafanaダッシュボードを使用して、v1とv2の5xxエラー率を並べて比較しています。カナリア側のエラー率が0.1%を超えたら、即座にデプロイを中止します。

3つ目は、スティッキーセッション(Sticky Sessions)に注意することです。Ingressでセッションアフィニティを使用している場合、重みの設定に関係なく、ユーザーが特定のバージョンに固定されてしまうことがあります。成功を測定する方法を慎重に検討しないと、データが歪む可能性があります。

どちらを選択すべきか?

選択は目的によります:

  • ブルー/グリーン:大規模なアーキテクチャの変更や、瞬時の「オール・オア・ナッシング」のリリースが必要な場合に使用します。一時的にリソース使用量が2倍になるためコストはかかりますが、非常に信頼性が高いです。
  • カナリア:日常的な機能アップデートに使用します。全ユーザーをリスクにさらすことなく、インフラが実際の負荷にどう耐えるかを確認する最適な方法です。

まとめ

ブルー/グリーンおよびカナリア戦略への移行は、私たちのチーム文化を変えました。もはや金曜日の午後のデプロイを恐れることはありません。KubernetesのラベルとIngressのアノテーションを活用することで、「サイトメンテナンス中」のページが過去の遺物となるような成熟度に達することができます。初期設定には労力がかかりますが、それによって得られる安心感は、YAMLの一行一行に見合う価値があります。

Share: