Certified Kubernetes Application Developer (CKAD) #8 デプロイ戦略: Blue-green、canary

新しいバージョンをデプロイしながらサービスを途切れさせないことは、運用の基本です。クラウドのマネージドなデプロイツールやサービスメッシュはこの作業を華やかに自動化しますが、CKAD はそうしたツールのない バニラ Kubernetes で同じ結果を作り出せるかを問います。つまり Deployment と Service、そして label だけで blue-green と canary を自分の手で実装する力です。

核心はただ 1 つです。Service は selector (label) で Pod を選ぶ という事実です。この selector をどう変えるか、どの Pod がその selector に同時に掛かるかを調整すれば、トラフィックの流れを手で制御できます。今回の記事では Deployment の rolling update を復習したあと、label の切り替えだけで blue-green と canary を構成します。

出発点: rolling update と recreate の復習 #

デプロイ戦略を自分の手で実装する前に、Deployment が標準で提供する 2 つの戦略を押さえておきます。Deployment の spec.strategy.type には RollingUpdate (デフォルト値) と Recreate の 2 つがあります。

RollingUpdate は既存の Pod を一度に落とさず、新しい Pod を少しずつ立ち上げながら置き換えます。置き換え速度は maxSurge (一時的に追加で立ち上げられる Pod 数) と maxUnavailable (同時に落とせる Pod 数) で調整します。イメージを変えると、同じ Service の後ろで旧バージョンと新バージョンの Pod がしばらく混ざって動き、無停止で移行します。詳しい動作は #5K8s 実務 #4 で扱いました。

spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0

Recreate は既存の Pod をすべて落としたあとに新しい Pod を立ち上げます。その間にダウンタイムが生じますが、旧バージョンと新バージョンが同時に立っていてはいけない場合 (例: 単一書き込みのデータベースマイグレーション) に使います。

spec:
  strategy:
    type: Recreate

rolling update は強力ですが限界があります。旧バージョンと新バージョンが置き換えの途中で混ざってトラフィックを受ける という点、そして トラフィックの比率を精密に制御しにくい という点です。この限界を補うために blue-green と canary が登場します。どちらの戦略も Kubernetes が別のリソースとして提供するものではなく、Deployment と Service、label を組み合わせて自分で作ります。

Blue-green: selector の切り替えで即時カットオーバー #

blue-green は 現在のバージョン (blue) と新しいバージョン (green) を同時に立ち上げておき、Service の selector を一度に green へ回してトラフィックをまるごと移す方式です。置き換えの途中で 2 つのバージョンが混ざることはなく、問題が起きれば selector を blue に戻して即座にロールバックします。

1) blue Deployment と Service #

まず blue を立ち上げ、その Pod だけを指す Service を作ります。核心は selector に version: blue を入れることです。

# blue Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-blue
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
      version: blue
  template:
    metadata:
      labels:
        app: web
        version: blue
    spec:
      containers:
      - name: web
        image: nginx:1.25
---
# 2 つの label を両方とも selector に掛ける Service
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:
    app: web
    version: blue
  ports:
  - port: 80
    targetPort: 80

この時点で web Service は app: web でありかつ version: blue の Pod だけを選ぶので、トラフィックはすべて blue へ向かいます。

2) green Deployment を別に立ち上げる #

新しいバージョンは label だけを version: green と変えて 別の Deployment として立ち上げます。まだ Service の selector は blue なので、green はトラフィックを受けません。この状態で green を十分に検証します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-green
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
      version: green
  template:
    metadata:
      labels:
        app: web
        version: green
    spec:
      containers:
      - name: web
        image: nginx:1.27

green に直接アクセスして検証したい場合は、一時的なテスト用 Service を別に作るか、kubectl port-forward deploy/web-green 8080:80 で覗き込みます。

3) カットオーバー: Service の selector を green へ #

検証が終わったら Service の selector を green に変えます。この 1 回の変更で、すべてのトラフィックが即座に green へ移ります。命令形で素早く処理する方法は 2 つあります。

# 方法 1: kubectl set selector
k set selector svc web 'app=web,version=green'

# 方法 2: kubectl patch
k patch svc web -p '{"spec":{"selector":{"app":"web","version":"green"}}}'

k get endpoints web で、Service の後ろの Pod IP が green Pod に変わったかを確認します。

4) ロールバック: selector を blue に戻す #

green で問題が見つかったら、selector をまた blue に戻すだけで済みます。blue Deployment をそのまま残しておいたので、ロールバックは即座に終わります。

k patch svc web -p '{"spec":{"selector":{"app":"web","version":"blue"}}}'

green が安定したと判断したら、blue Deployment を削除してリソースを回収します。blue-green のコストは、カットオーバーまで 2 つのバージョンを同時に立ち上げてリソースを 2 倍使う という点です。代わりにカットオーバーとロールバックが selector の変更 1 回で終わるので、最も速いです。

Canary: 共通 label と replicas の比率でトラフィック分配 #

canary は新しいバージョンを全体に一度に晒さず、少数のトラフィックにだけ先に流して リスクを減らす方式です。blue-green が selector をまるごと変えて即時に切り替えたのに対し、canary は stable と canary の Pod を 1 つの Service が一緒に選ぶ ようにしてトラフィックを分けます。

核心アイデア: 共通 label で同時選択 #

Service の selector を、2 つの Deployment が 共有する label (例: app: web) だけで掛けます。すると stable Pod と canary Pod が両方とも同じ Service の後ろにまとまります。Service は自分が選んだ Pod 群におおよそ均等にトラフィックを分散するので、replicas の数の比率 がそのままトラフィック比率の近似になります。

1) stable Deployment と共通 selector の Service #

# stable: replicas 9
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-stable
spec:
  replicas: 9
  selector:
    matchLabels:
      app: web
      track: stable
  template:
    metadata:
      labels:
        app: web
        track: stable
    spec:
      containers:
      - name: web
        image: nginx:1.25
---
# Service は共通 label の app: web だけを selector に掛ける
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:
    app: web
  ports:
  - port: 80
    targetPort: 80

Service の selector に track がないという点が核心です。app: web だけを見るので、stable であれ canary であれこの label さえあればすべてトラフィックを受けます。

2) canary Deployment を少数で追加 #

新しいバージョンを track: canary label で、しかし共通 label の app: web はそのまま付けたまま、少ない replicas で立ち上げます。stable 9 個と canary 1 個なら約 9:1、つまり全体トラフィックの約 10% だけが canary へ流れます。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-canary
spec:
  replicas: 1
  selector:
    matchLabels:
      app: web
      track: canary
  template:
    metadata:
      labels:
        app: web
        track: canary
    spec:
      containers:
      - name: web
        image: nginx:1.27

この状態で k get endpoints web を見ると、stable 9 個と canary 1 個の Pod IP が一緒に入っています。トラフィック比率をさらに上げたい場合は canary の replicas を増やします。

# canary の比重を約 30% に (stable 7 : canary 3)
k scale deploy web-canary --replicas=3
k scale deploy web-stable --replicas=7

3) 昇格または廃棄 #

canary の指標 (エラー率、レイテンシ) が正常なら、stable を新しいバージョンに置き換えて canary を引き上げます。stable のイメージを上げたあと、canary を 0 に減らすか削除します。

# stable を新しいバージョンに置き換えて元の replicas に復旧
k set image deploy/web-stable web=nginx:1.27
k scale deploy web-stable --replicas=9
# canary を整理
k delete deploy web-canary

問題が見つかったら、canary を削除することで即座に露出を止めます。stable には手を付けていないので、ユーザーへの影響が少ないです。

canary の限界もはっきりしています。トラフィック分配は replicas の数に比例する近似 にすぎず、ヘッダーやユーザー基準の精密なルーティングは不可能です。精密な制御が必要なら Ingress の canary アノテーションやサービスメッシュを使いますが、CKAD の範囲はここまでの replicas ベースの実装です。

3 つの戦略の比較 #

項目rolling updateblue-greencanary
実装Deployment の標準 strategy2 つの Deployment + selector 切り替え2 つの Deployment + 共通 label
追加リソースほぼなし (maxSurge の分)2 倍 (2 バージョン同時維持)少し (canary replicas の分)
ロールバック速度普通 (k rollout undo)最も速い (selector の戻し)速い (canary の削除)
トラフィック制御不可 (漸進的な置き換え)すべてかゼロかreplicas 比率で近似
バージョンの混在置き換えの途中で混ざる混ざらない意図的に共存
検証の機会少ないカットオーバー前に十分少数トラフィックで漸進的

3 つの戦略は優劣ではなく、状況に応じた選択です。リソースの余裕が少なく無難な置き換えなら rolling update、速いカットオーバーと即時ロールバックが重要なら blue-green、リスクを漸進的に検証したいなら canary です。

試験ポイント #

CKAD でこのテーマは selector の切り替え がすべてだと言っても言いすぎではありません。

  • blue-green のカットオーバーは Service の selector の変更 です。k set selector svc <name> 'app=web,version=green' または k patch svc を手に馴染ませておけば数秒で終わります。
  • ロールバックは selector を戻すこと です。blue Deployment をあらかじめ消さないことがロールバックの前提です。
  • canary は Service selector を共通 label だけで 掛け、2 つの Deployment がその label を共有するようにすることが核心です。selector に track のような区別用 label を入れてしまうと分配されません。
  • トラフィック比率は k scale deploy ... --replicas=N で調整します。9:1 なら約 10% という点だけ覚えておけば十分です。
  • 確認はいつも k get endpoints <svc>k get pods --show-labels で行います。どの Pod が Service の後ろにまとまっているかを目で検証する習慣が誤答を防ぎます。

label と selector の動作が紛らわしいなら、K8s 実務 #5 で Service がどのように Pod を見つけるかを改めて確認するとよいです。

まとめ #

今回の記事で押さえたこと:

  • デプロイ戦略は別のリソースではなく Deployment + Service + label の組み合わせ です。CKAD はマネージドなツールなしで自分の手で実装する力を問います。
  • rolling update (デフォルト、漸進的な置き換え) と recreate (全体を落としてから立ち上げ) は Deployment の strategy として提供されます。
  • blue-green は 2 つのバージョンを同時に立ち上げ、Service selector を一度に切り替えて即時カットオーバーします。ロールバックは selector を戻すことで最も速いです。
  • canary は Service が共通 label で stable と canary を一緒に選ぶようにし、replicas の比率でトラフィックを近似分配します。
  • すべての戦略の検証は、k get endpoints--show-labels でどの Pod がまとまっているかを確認することから始まります。

次へ: Helm #

ここまではマニフェストを手で作り、命令形で操作してきました。しかし実務では同じアプリを環境ごとに少しずつ違えて繰り返しデプロイしなければなりません。#9 Helm: install、upgrade、rollback、values では、マニフェストをテンプレートにまとめて values で環境差を注入し、install と upgrade、rollback をパッケージ単位で扱う Helm を整理します。

X