本記事には広告(アフィリエイトリンク)が含まれます。

Blue/GreenとCanary戦略|CKAD第13回

広告

新卒インフラエンジニア向け「Kubernetes 実践教科書① CKAD アプリケーション開発編」の第13回です。第12回では Backend を Deployment 化し、ローリングアップデートで無停止更新を体験しました。今回はローリング以外のデプロイ戦略——RecreateBlue/GreenCanary——を実装し、ダウンタイム・リソース・ロールバック速度のトレードオフを手を動かして比べます。

動作確認バージョン:kind v0.31.0(K8s v1.35.0)/ kubectl v1.35.6 / fanclub-backend:0.3.0 ・ 0.3.1 / busybox:1.37 / postgres:18(2026-06-28 実機検証時点)

広告

今ここマップ(全 19 回中の現在地)

現在地は第 4 部「ワークロード戦略」の第 13 回です。第12回のローリングアップデートに続き、デプロイ戦略の引き出しを増やします。

  • 第 1 部 コンテナと Docker(第 1〜4 回)
  • 第 2 部 Kubernetes 基礎(第 5〜6 回)
  • 第 3 部 アプリリソース(第 7〜11 回)
  • 第 4 部 ワークロード戦略(第 12〜14 回)← 今ここ
  • 第 5 部 セキュリティ基礎(第 15〜16 回)
  • 第 6 部 パッケージ管理と HTTPS 公開(第 17〜19 回)

この回のゴール

  • Recreate / Blue/Green / Canary を実装し、3 戦略の動きを説明できる。
  • 各戦略のトレードオフ(ダウンタイム / リソース消費 / ロールバック速度)を比較できる。
  • kubectl patch で Service のセレクタを切り替え、レプリカ比でトラフィック割合を調整できる。

デプロイ戦略の全体像

Deployment の更新方法は strategy だけではありません。Service のセレクタやレプリカ比を組み合わせると、目的に応じた切り替え方ができます。代表的な 4 つを押さえます。

  • RollingUpdate(第12回):少しずつ入れ替える既定戦略。無停止だが、更新中は新旧が混在する。
  • Recreate:旧 Pod を全部消してから新 Pod を起動する。ダウンタイムが出るが、新旧を混在させたくないとき(破壊的なスキーマ変更など)に使う。
  • Blue/Green:旧版(Blue)と新版(Green)を2 つ並べておき、Service のセレクタを一気に切り替える。切り戻しもセレクタを戻すだけで速い。
  • Canary:新版を少数だけ混ぜ、レプリカ比でトラフィックの一部を新版へ流して様子を見る。
4 つのデプロイ戦略(RollingUpdate / Recreate / Blue-Green / Canary)の比較表。ダウンタイムの有無、リソース消費、ロールバック速度、主な用途を 4 戦略で並べて示す。
図1:デプロイ戦略の比較(ダウンタイム / リソース / ロールバック速度 / 用途)

本シリーズの学習環境(kind 単一ノード・CPU 2 コア)では Pod を何十個も並べられません。そこで本回はレプリカ数を小さくし、各 Deployment の requests.cpu を一時的に 100m に下げて複数戦略を同居させます(本番の値や大規模なレプリカ比は概念として補足します)。デモが終わったら最後に本番相当へ戻します。

版を見分ける準備:0.3.1 をビルドする

Blue/Green や Canary では「いまどの版が応答したか」を確かめたくなります。現在の Backend(0.3.0)には版を返す口がないので、バージョンを返す GET /api/version を足した 0.3.1 を作ります。第3回で用意したソース一式は developer のホーム ~/fanclub-api(一般ユーザー所有)にあります。次のファイルを ~/fanclub-api/src/main/java/com/example/fanclub/VersionResource.java として追加します。

package com.example.fanclub;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.Map;

@Path("/version")
@Produces(MediaType.APPLICATION_JSON)
public class VersionResource {

    @GET
    public Response version() {
        String v = System.getenv().getOrDefault("APP_VERSION", "0.3.1");
        return Response.ok(Map.of("version", v)).build();
    }
}

ベースパスは第3回の AppConfig/api に固定済みなので、このクラスは GET /api/version として公開されます。環境変数 APP_VERSION があればそれを、なければ既定の 0.3.1 を返します。イメージをビルドして登録レジストリへ push します。実行コマンド:

$ cd ~/fanclub-api
$ docker build -t k8s-registry:5000/fanclub-backend:0.3.1 .
$ docker push k8s-registry:5000/fanclub-backend:0.3.1

実行結果(例):依存はキャッシュ済みなので短時間で完了します。

0.3.1: digest: sha256:caf2cc59... size: 856

これで 0.3.0(版を返さない=/api/version は 404)と 0.3.1(200 で版を返す)の 2 つが揃いました。コード変更を伴うので 0.3.0 → 0.3.1 とタグを上げます(中身が変わらない回はタグを据え置く方針でした)。この差で「どちらの版に届いたか」を判別します。

Recreate 戦略(ダウンタイムを体験する)

まずは Recreate です。本番の fanclub-backend を止めないよう、使い捨ての別 Deployment(ラベル app: fanclub-backend-rc・Service には載せない)で挙動だけ観察します。ここは「全入れ替えでダウンタイムが出る」点だけを見たいので、Init Container・Sidecar・liveness/startup は省いた最小構成にしています(本番の fanclub-backend は第12回のフル構成のままで、この後の Blue/Green 以降では継承します)。次を recreate-demo.yaml として作成します(developer 所有・root 不要)。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: fanclub-backend-recreate
  labels:
    app: fanclub-backend-rc
spec:
  replicas: 2
  strategy:
    type: Recreate
  selector:
    matchLabels:
      app: fanclub-backend-rc
  template:
    metadata:
      labels:
        app: fanclub-backend-rc
    spec:
      serviceAccountName: fanclub-backend
      automountServiceAccountToken: false
      containers:
      - name: backend
        image: k8s-registry:5000/fanclub-backend:0.3.0
        ports:
        - containerPort: 8080
        envFrom:
        - configMapRef:
            name: fanclub-config
        - secretRef:
            name: fanclub-db-secret
        readinessProbe:
          httpGet:
            path: /health/ready
            port: 8080
          periodSeconds: 10
          failureThreshold: 3
        resources:
          requests:
            memory: "480Mi"
            cpu: "100m"
          limits:
            memory: "640Mi"
            cpu: "600m"

適用して Ready を待ち、その後 0.3.1 へ更新します。実行コマンド:

$ kubectl apply -f recreate-demo.yaml
$ kubectl rollout status deployment/fanclub-backend-recreate
$ kubectl set image deployment/fanclub-backend-recreate backend=k8s-registry:5000/fanclub-backend:0.3.1
$ kubectl get deployment fanclub-backend-recreate -w

実行結果(例):更新の瞬間、READY が一度 0 になり(旧 2 Pod を全終了 → 新 Pod の起動完了を待つ)、しばらくして 2 に戻ります。この「全部落ちてから立ち上がる」間がダウンタイムです(-wCtrl-C で抜けます)。

NAME                       READY   UP-TO-DATE   AVAILABLE
fanclub-backend-recreate   2/2     2            2
fanclub-backend-recreate   0/2     2            0
fanclub-backend-recreate   0/2     2            0
fanclub-backend-recreate   2/2     2            2

確認できたら片付けます。実行コマンド:

$ kubectl delete -f recreate-demo.yaml

Blue/Green デプロイ

Blue/Green は、旧版(Blue=0.3.0)と新版(Green=0.3.1)を2 本同時に立てておき、Service のセレクタを Blue から Green へ一気に向け替える方式です。第12回の fanclub-backend Deployment を、Blue と Green の 2 本に作り替えます。まず Blue を developer のホームに fanclub-backend-blue.yaml として作成します(一般ユーザー所有・root 不要。以降の green / stable / canary もこのファイルをコピーして作るので、すべて同じ ~/ 配下に置きます)。第12回の構成(Init Container・Sidecar・3 Probe・applog ボリューム)をそのまま引き継ぎ、ラベルに track: blue を足し、requests.cpu だけ 100m に下げています。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: fanclub-backend-blue
  labels:
    app: fanclub-backend
    track: blue
spec:
  replicas: 1
  selector:
    matchLabels:
      app: fanclub-backend
      track: blue
  template:
    metadata:
      labels:
        app: fanclub-backend
        track: blue
    spec:
      serviceAccountName: fanclub-backend
      automountServiceAccountToken: false
      initContainers:
      - name: wait-for-db
        image: busybox:1.37
        command:
        - sh
        - -c
        - 'until nslookup fanclub-db.default.svc.cluster.local >/dev/null 2>&1; do echo "waiting for fanclub-db..."; sleep 2; done; echo "fanclub-db resolved"'
      - name: log-shipper
        image: busybox:1.37
        restartPolicy: Always
        command:
        - sh
        - -c
        - 'echo "sidecar started"; tail -F /var/log/app/app.log 2>/dev/null || sleep infinity'
        volumeMounts:
        - name: applog
          mountPath: /var/log/app
      containers:
      - name: backend
        image: k8s-registry:5000/fanclub-backend:0.3.0
        ports:
        - containerPort: 8080
        envFrom:
        - configMapRef:
            name: fanclub-config
        - secretRef:
            name: fanclub-db-secret
        startupProbe:
          httpGet:
            path: /health/started
            port: 8080
          failureThreshold: 30
          periodSeconds: 10
        livenessProbe:
          httpGet:
            path: /health/live
            port: 8080
          periodSeconds: 10
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /health/ready
            port: 8080
          periodSeconds: 10
          failureThreshold: 3
        resources:
          requests:
            memory: "512Mi"
            cpu: "100m"
          limits:
            memory: "768Mi"
            cpu: "1000m"
        volumeMounts:
        - name: applog
          mountPath: /var/log/app
      volumes:
      - name: applog
        emptyDir: {}

Green は Blue をコピーして、名前・track・イメージだけ変えれば作れます。実行コマンド:

$ cp fanclub-backend-blue.yaml fanclub-backend-green.yaml
$ sed -i 's/-blue/-green/; s/track: blue/track: green/; s|fanclub-backend:0.3.0|fanclub-backend:0.3.1|' fanclub-backend-green.yaml

第12回の fanclub-backend Deployment を削除し、Blue と Green を適用します(Service fanclub-backend 自体は第8回のものをそのまま使い、セレクタだけ切り替えます)。実行コマンド:

$ kubectl delete deployment fanclub-backend
$ kubectl apply -f fanclub-backend-blue.yaml
$ kubectl apply -f fanclub-backend-green.yaml
$ kubectl wait --for=condition=available deployment/fanclub-backend-blue deployment/fanclub-backend-green --timeout=180s

Service のセレクタを track: blue に向けます。実行コマンド:

$ kubectl patch service fanclub-backend -p '{"spec":{"selector":{"app":"fanclub-backend","track":"blue"}}}'
$ kubectl run c --rm -i --restart=Never --image=curlimages/curl:latest -- \
    curl -s -o /dev/null -w "version=%{http_code}\n" http://fanclub-backend:8080/api/version

実行結果(例):Blue(0.3.0)は /api/version を持たないので 404 です。

version=404

セレクタを track: green に切り替えます。track は同じキーの値を上書きするだけなので、この patch で即時に向き先が変わります。実行コマンド:

$ kubectl patch service fanclub-backend -p '{"spec":{"selector":{"app":"fanclub-backend","track":"green"}}}'
$ kubectl run c --rm -i --restart=Never --image=curlimages/curl:latest -- \
    curl -s http://fanclub-backend:8080/api/version

実行結果(例):Green(0.3.1)に切り替わり、版が返ります。CRUD も問題なく動きます。

{"version":"0.3.1"}
Blue/Green と Canary の図。上段は Blue(0.3.0)と Green(0.3.1)の 2 Deployment に対し Service のセレクタを track:blue から track:green に切り替える様子。下段は stable(0.3.0)3 Pod と canary(0.3.1)1 Pod に対し Service が app ラベルで両方へ振り分け、レプリカ比でトラフィックが約 25%対75%に分かれる様子。
図2:Blue/Green のセレクタ切替(上)と Canary のレプリカ比トラフィック分配(下)

切り戻しはセレクタを blue に戻すだけです。Green に不具合が見つかってもすぐ戻せます。実行コマンド:

$ kubectl patch service fanclub-backend -p '{"spec":{"selector":{"app":"fanclub-backend","track":"blue"}}}'

Blue/Green は「新旧を丸ごと用意して一気に切り替える」ぶん、切替・切り戻しが速い反面、2 版分のリソースを同時に使うのが弱点です。確認できたら次の Canary に進むため、Blue と Green を削除します。実行コマンド:

$ kubectl delete deployment fanclub-backend-blue fanclub-backend-green

Canary リリース

Canary は、安定版(stable)を多めに、新版(canary)を少しだけ並べ、両方を同じ Service の配下に入れて一部のトラフィックを新版へ流す方式です。振り分けは特別な機能ではなく、Ready な Pod 数の比で決まります(kube-proxy が各 Pod へほぼ均等に分配)。共通ラベル app: fanclub-backend を両者に付け、識別用に track: stable / track: canary を足します。

stable は Blue(0.3.0)を、canary は Green(0.3.1)をコピーして作ります。実行コマンド:

$ cp fanclub-backend-blue.yaml fanclub-backend-stable.yaml
$ sed -i 's/-blue/-stable/; s/track: blue/track: stable/; s/replicas: 1/replicas: 3/' fanclub-backend-stable.yaml
$ cp fanclub-backend-green.yaml fanclub-backend-canary.yaml
$ sed -i 's/-green/-canary/; s/track: green/track: canary/' fanclub-backend-canary.yaml
$ kubectl apply -f fanclub-backend-stable.yaml
$ kubectl apply -f fanclub-backend-canary.yaml
$ kubectl wait --for=condition=available deployment/fanclub-backend-stable deployment/fanclub-backend-canary --timeout=200s

Service のセレクタを app: fanclub-backend だけに広げ、stable・canary の両方へ届くようにします。ここで注意点があります。直前の Blue/Green で track: blue が残っているので、単純に app を指定するだけでは track が消えません(kubectl patch の既定はキーをマージするため)。不要になった tracknull で明示的に外します。実行コマンド:

$ kubectl patch service fanclub-backend --type=merge \
    -p '{"spec":{"selector":{"app":"fanclub-backend","track":null}}}'
$ kubectl get service fanclub-backend -o jsonpath='{.spec.selector}{"\n"}'

実行結果(例):セレクタが app だけになりました。

{"app":"fanclub-backend"}

Service の宛先(エンドポイント)に stable 3 + canary 1 の計 4 Pod が並んでいるかを確認します。kubectl get endpoints は v1.33 以降で非推奨になったため、EndpointSlice を見ます。実行コマンド:

$ kubectl get endpointslices -l kubernetes.io/service-name=fanclub-backend

実行結果(例):宛先は計 4 つ(stable 3 + canary 1)です。ENDPOINTS 列は 3 つまで表示され、残りは + N more... と省略されます。

NAME                    ADDRESSTYPE   PORTS   ENDPOINTS                                  AGE
fanclub-backend-abcde   IPv4          8080    10.244.82.54,10.244.82.55,10.244.82.56 + 1 more...   60s

計測用の Pod を 1 つ立て、その中から /api/version を 40 回叩いて版ごとの応答数を数えます。0.3.1(canary)は版を返し、0.3.0(stable)は 404 になるので、その数でトラフィック割合がわかります(default の Service はセッションアフィニティが無く、接続ごとに分散します)。実行コマンド:

$ kubectl run loadtest --rm -i --restart=Never --image=curlimages/curl:latest --quiet -- \
    sh -c 'for i in $(seq 1 40); do curl -s -o /dev/null -w "%{http_code}\n" http://fanclub-backend:8080/api/version; done' | sort | uniq -c

実行結果(例):4 Pod 中 1 つが canary なので、おおむね 4 分の 1(約 25%)が 200(canary=0.3.1)に届きます。試行回数が少ないと割合はぶれます。

     10 200
     30 404

※上の例では 200 が canary、404 が stable です(200 の本文が {"version":"0.3.1"})。canary を増やしたい(割合を上げたい)ときは canary のレプリカを増やすか、stable を減らします。たとえば stable を 2 に減らすと canary の比率は約 33% に上がります。実行コマンド:

$ kubectl scale deployment fanclub-backend-stable --replicas=2

このように レプリカ比 = おおよそのトラフィック比です。本番では数十レプリカで「10% → 30% → 100%」と段階的に広げますが、本シリーズの 2 コアノードでは Pod 数に上限があるため、ここでは小さな比率で原理を確認しています。

戦略の使い分け

  • RollingUpdate:通常はこれで十分。無停止で、追加リソースも少ない(第12回)。
  • Recreate:新旧を混ぜられない更新(破壊的なスキーマ変更など)。ダウンタイムを許容できるときだけ。
  • Blue/Green:切替・切り戻しの速さが要るとき。2 版分のリソースを使える前提。
  • Canary:新版を少しずつ試して様子を見たいとき。観測(メトリクス)と組み合わせると効果的。

本番では、Blue/Green や Canary を自動化する Argo Rollouts のようなツールがよく使われます(重み付けや自動解析つきの段階リリースができます)。本巻では概念紹介にとどめ、ここまでに学んだ「Deployment・Service・セレクタ・ラベル」の組み合わせで戦略の土台を理解しておきます。

後片付けと統合

検証が済んだので、新版 0.3.1 を正式採用し、第12回と同じ形の単一 fanclub-backend Deployment に戻します(学習用に下げていた requests.cpu も本番相当の 250m へ戻します)。第12回で作った backend-deploy.yaml のイメージを 0.3.1 に書き換えて再適用すれば、自己回復・3 Probe・Init/Sidecar はそのまま、版だけ上がった状態になります。実行コマンド:

$ kubectl delete deployment fanclub-backend-stable fanclub-backend-canary
$ sed -i 's|fanclub-backend:0.3.0|fanclub-backend:0.3.1|' backend-deploy.yaml
$ kubectl apply -f backend-deploy.yaml
$ kubectl patch service fanclub-backend --type=merge \
    -p '{"spec":{"selector":{"app":"fanclub-backend","track":null}}}'
$ kubectl rollout status deployment/fanclub-backend

最後に、単一 Deployment(2/2)に戻り、版が 0.3.1・CRUD も動くことを確認します。実行コマンド:

$ kubectl get deployment fanclub-backend
$ kubectl run c --rm -i --restart=Never --image=curlimages/curl:latest -- \
    curl -s http://fanclub-backend:8080/api/version
$ kubectl run c --rm -i --restart=Never --image=curlimages/curl:latest -- \
    curl -s -o /dev/null -w "members=%{http_code}\n" http://fanclub-backend:8080/api/members

実行結果(例):

NAME              READY   UP-TO-DATE   AVAILABLE
fanclub-backend   1/1     1            1
{"version":"0.3.1"}
members=200

これで Backend は第12回と同じ構成のまま、版だけ 0.3.1 に進みました。会員データ(第9・11回で入れた 3 件)も PVC に残ったままです。

やってみよう

  1. VersionResource.java を追加して 0.3.1 をビルド・push し、0.3.0(404)と 0.3.1(版を返す)の差を作る。
  2. Recreate の Deployment を更新し、READY が一度 0 になる(ダウンタイム)ことを -w で観察する。
  3. Blue(0.3.0)/ Green(0.3.1)を立て、Service セレクタを track: blue → green に切り替えて応答が 404 → {"version":"0.3.1"} に変わることを確認し、blue に切り戻す。
  4. stable(3)+ canary(1)を共通ラベルで並べ、セレクタを track: nullapp だけに広げ、/api/version を 40 回叩いて canary の割合(約 25%)を数える。
  5. backend-deploy.yaml0.3.1 にして単一 fanclub-backend へ統合し、戦略用 Deployment を片付ける。

理解度チェック

次の各文が正しいか(○)誤りか(×)を判断してください。解答は下にまとめています。

  1. Recreate は旧 Pod を全て終了してから新 Pod を起動するため、更新中にダウンタイムが発生する。
  2. Blue/Green では Service のセレクタを切り替えることで新版へ一気に向き先を変えられる。
  3. Blue/Green のロールバックは、セレクタを旧版(blue)に戻すだけでよい。
  4. Canary のトラフィック割合は、Service の weight フィールドに割合を書いて指定する。
  5. Canary では、新版(canary)のレプリカを増やすほど新版へ届くトラフィックの割合が増える。
  6. kubectl get endpoints は v1.35 でも推奨される宛先確認コマンドである。
  7. Service のセレクタを kubectl patch でマージするとき、不要になったラベルキーは自動的に消える。
  8. Recreate は本番の無停止リリースに最も適した戦略である。

解答

  • 1. ○:全終了 → 起動の間が空くためダウンタイムが出る。
  • 2. ○:セレクタの向き先を blue → green に変えるだけで切り替わる。
  • 3. ○:セレクタを blue に戻せば即座に切り戻せる。
  • 4. ×:標準 Service に weight はない。割合はレプリカ比で決まる(重み付けは Gateway API や Argo Rollouts の領域)。
  • 5. ○:レプリカ比 = おおよそのトラフィック比。
  • 6. ×:kubectl get endpoints は v1.33+ で非推奨。kubectl get endpointslices を使う。
  • 7. ×:マージでは消えない。track: null のように明示して外す。
  • 8. ×:Recreate はダウンタイムを伴う。無停止なら RollingUpdate / Blue/Green。

まとめ

本記事では、ローリング以外の 3 つのデプロイ戦略を実装しました。Recreate は全入れ替えでダウンタイムを伴い、Blue/Green は 2 版を並べてセレクタで一気に切替・切り戻し、Canary は共通ラベルとレプリカ比で一部トラフィックを新版へ流します。版の判別には 0.3.1 で足した /api/version を使い、最後は第12回と同じ形の単一 fanclub-backend(版だけ 0.3.1)へ統合しました。戦略の正体は、これまで学んだ Deployment・Service・ラベル・セレクタの組み合わせです。

次回予告

次回(第14回)は、Namespace で環境を分け、ResourceQuotaLimitRange で「Namespace 全体の上限」「Pod 既定値」を制御します。今回 CPU で苦労した「リソースの取り合い」を、ルールで管理する回です。

シリーズ一覧

第1部:コンテナと Docker

第2部:Kubernetes 基礎

第3部:アプリリソース

第4部:ワークロード戦略

第5部:セキュリティ基礎

第6部:パッケージ管理 + HTTPS 公開

広告
kubernetes
スポンサーリンク