チュートリアルSep 8, 202624 分 READ

CircleCI でブルーグリーンデプロイメントを設定する方法

Roger Winter

Content Marketing Manager

CircleCI でブルーグリーンデプロイメントを設定する方法

ブルーグリーンデプロイメントは、2つの同一のプロダクション環境を実行し、新しいバージョンをリリースする際にそれらの間でトラフィックを切り替える手法です。

一方の環境は「アクティブ」な状態で、すべてのトラフィックを処理します。もう一方はアイドル状態で待機します。デプロイ時には、チームは新しいバージョンをアイドル環境にプッシュし、検証してからトラフィックを切り替えます。問題が発生した場合は、数秒でトラフィックを元に戻せます。Pod の再起動も、ローリングアップデートを逆方向に実行する必要もありません。

CI/CD ワークフロー(コードの変更が自動的にビルド、テスト、デプロイされる仕組み)では、ブルーグリーンはデプロイのステップとリリースのステップを明確に分離します。CircleCI は各ステージを自動化します。イメージのビルド、アイドル環境へのデプロイ、検証テストの実行、承認ゲートでの一時停止、そして人間が確認した後にのみトラフィックを切り替えます。

このチュートリアルでは、CircleCI パイプライン内で自動化された、Kubernetes 上でのブルーグリーンデプロイメントの設定方法を説明します。デュアル環境のマニフェスト、切り替え前の検証用の preview Service、承認ゲート付きワークフロー、デプロイの追跡、即時ロールバック、そしてブルーグリーンで最もよく発生する障害を防ぐデータベースマイグレーションのパターンについて解説します。

前提条件

開始する前に、チームには以下が必要です。

  • CircleCI アカウント(無料プランで十分です)。
  • kubectl が接続できるように設定された Kubernetes クラスタ(このチュートリアルでは GKE を使用しますが、EKS、AKS、minikube や kind などのローカルクラスタでも動作します)。
  • サンプルイメージをビルドするためにローカルにインストールされたDocker
  • コンテナレジストリ用のDocker Hub アカウント
  • GitHub からクローンしたサンプルリポジトリ: CIRCLECI-GWP/kubernetes-blue-green-deployment

Kubernetes におけるブルーグリーンデプロイメントの仕組み

2つの Kubernetes Deployment が並存し、一方には version: blue、もう一方には version: green というラベルが付けられます。Service は、そのセレクタに一致する Deployment にトラフィックをルーティングします。常に一方だけがアクティブで、もう一方はアイドル状態で次のリリースを待機します。

blue から green への切り替えとは、Service のセレクタを version: blue から version: green にパッチすることを意味します。これはメタデータの変更であり、Pod の操作ではありません。トラフィックは即座に切り替わります。Pod の作成、終了、再起動は発生しません。

ブルーグリーンデプロイメントの状態を比較する2つの Kubernetes アーキテクチャ図。切り替え前は、Service のセレクタが `version: blue` を対象とし、v1.0 を実行する3つの blue Pod にトラフィックをルーティングしており、v2.0 の3つの green Pod はアイドル状態です。切り替え後は、セレクタが `version: green` を対象とし、green Pod にルーティングされ、blue Pod はスタンバイ状態になります。

切り替え前に、チームはアイドル環境を検証できます。別途用意された「preview」Service は常に green Deployment を指しており、パイプラインが新しいバージョンに対してスモークテスト、統合テスト、負荷テストを実行できるエンドポイントを提供します。green Pod はプロダクションデータベースにアクセス可能なプロダクションインフラ上で稼働しているため、ここでのテストはステージング環境では見つけられない問題を検出できます。

両方の Deployment は同じデータベースと外部サービスを共有します。これがブルーグリーンで発生する障害の主な原因です。green のコードでは機能するが blue のコードを壊すようなスキーマ変更を行うと、ロールバックができなくなります。ステップ5では、これを防ぐマイグレーションのパターンについて説明します。

トレードオフとして、ブルーグリーンではデプロイ期間中、両方の環境が同時にフルレプリカセットを実行するため、コンピュートリソースが2倍必要になります。このコストが問題になるチームには、重複環境を必要とせずに Pod を段階的に更新するローリングデプロイメントが適しています。

ステップ1 — サンプルアプリケーションのビルドとコンテナ化

サンプルアプリケーションは、依存関係を持たない Node.js の HTTP サーバーです。次の3つのエンドポイントを提供します。

  • GET / は、実行中のバージョン、デプロイスロット(blue または green)、Pod のホスト名、タイムスタンプを表示するスタイル付きの HTML ステータスページを返します。
  • GET /api は同じ情報を JSON 形式で返します。
  • GET /health は readiness probe と liveness probe 用に {"status":"ok"} を返します。

バージョンは APP_VERSION 環境変数から、スロットは DEPLOY_SLOT 環境変数から取得されます。どちらもデプロイ時に Kubernetes マニフェストによって設定されます。blue 環境と green 環境で GET /api を実行すると、異なる slot の値が返されるため、切り替えを目視で確認できます。

完全なソースコードはサンプルリポジトリにあります。Dockerfile は node:20-alpine を使用し、非 root ユーザーとして実行されます。

FROM node:20-alpine
WORKDIR /app
COPY package.json .
COPY index.js .
EXPOSE 3000
USER node
CMD ["node", "index.js"]

プッシュする前に、ローカルでビルドしてテストします。

docker build -t blue-green-tutorial-app:local ./app
docker run -p 3000:3000 -e DEPLOY_SLOT=blue blue-green-tutorial-app:local

# In another terminal:
curl localhost:3000/api
# {"version":"1.0.0","slot":"blue","hostname":"...","timestamp":"..."}

curl localhost:3000/health
# {"status":"ok"}

ステップ2 — Kubernetes マニフェストの作成

Kubernetes 上でのブルーグリーンには、4つのマニフェストが必要です。2つの Deployment(各環境に1つ)と2つの Service(production と preview)です。この4つすべてで、CircleCI パイプラインがデプロイ時に埋め込むenvsubstのプレースホルダーを使用します。envsubst はファイル内の ${VARIABLE} 参照を対応する環境変数の値に置き換えるため、パイプラインはマニフェストを適用する前にイメージタグと Docker Hub のユーザー名を注入できます。

Blue Deployment

k8s/deployment-blue.yml にある blue Deployment は、現在のプロダクションバージョンを実行します。version: blue ラベルによって、その Pod は green の Pod と区別されます。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: blue-green-tutorial-app-blue
  namespace: default
  annotations:
    circleci.com/project-id: ${CIRCLE_PROJECT_ID}
    circleci.com/operation-timeout: 10m
  labels:
    app: blue-green-tutorial-app
    version: blue
    circleci.com/component-name: blue-green-tutorial-app
    circleci.com/version: ${IMAGE_TAG}
spec:
  replicas: 3
  selector:
    matchLabels:
      app: blue-green-tutorial-app
      version: blue
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: blue-green-tutorial-app
        version: blue
        circleci.com/component-name: blue-green-tutorial-app
        circleci.com/version: ${IMAGE_TAG}
    spec:
      containers:
        - name: blue-green-tutorial-app
          image: ${DOCKERHUB_USERNAME}/blue-green-tutorial-app:${IMAGE_TAG}
          ports:
            - containerPort: 3000
          env:
            - name: APP_VERSION
              value: "${IMAGE_TAG}"
            - name: DEPLOY_SLOT
              value: "blue"
          readinessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 10
            periodSeconds: 5
            failureThreshold: 3
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 15
            periodSeconds: 10

注目すべき主要な設定は次のとおりです。

  • version: blue ラベルselector.matchLabels、Pod テンプレート、production Service のセレクタに登場します。これにより blue の Pod が独立したセットとして分離され、Service がそれらを個別にターゲットできるようになります。
  • maxUnavailable: 0maxSurge: 1。パイプラインが green Deployment に新しいイメージを適用する際、代替の Pod が準備できるまで既存の Pod はオフラインになりません。また、同時に稼働する追加の Pod は1つだけです。
  • circleci.com/* のラベルとアノテーション。CircleCI のデプロイ追跡機能は、実行中の Pod からこれらを読み取ります。これらは Kubernetes が不変(immutable)として扱う spec.selector.matchLabels には含めないようにしてください。完全なリファレンスについては、コンポーネント設定のドキュメントを確認してください。

k8s/deployment-green.yml にある green Deployment は、blue に関するすべての参照が green に置き換わっている点を除いて同一です。メタデータの名前、version ラベル、DEPLOY_SLOT の値が該当します。

Production Service

k8s/service.yml にある production Service は、アクティブな方の環境にトラフィックをルーティングします。初期状態では version: blue を選択します。

apiVersion: v1
kind: Service
metadata:
  name: blue-green-tutorial-app
  namespace: default
spec:
  selector:
    app: blue-green-tutorial-app
    version: blue
  ports:
    - port: 80
      targetPort: 3000
  type: LoadBalancer

トラフィックの切り替えとは、この Service のセレクタを version: blue から version: green にパッチすることを意味します。この1つの変更だけで、すべてのプロダクショントラフィックが green の Pod にリダイレクトされます。

Preview Service

k8s/service-preview.yml にある preview Service は、常に green 環境を指します。これにより、パイプラインは production への切り替え前に新しいバージョンを検証するためのエンドポイントを得られます。

apiVersion: v1
kind: Service
metadata:
  name: blue-green-tutorial-app-preview
  namespace: default
spec:
  selector:
    app: blue-green-tutorial-app
    version: green
  ports:
    - port: 80
      targetPort: 3000
  type: LoadBalancer

クラスタの作成

このチュートリアルではクラスタに GKE を使用しますが、どの Kubernetes クラスタでも動作します。マニフェストとパイプラインはクラウドに依存しません。EKS、AKS、DigitalOcean、あるいは minikube や kind などのローカルクラスタでも、同じワークフローが実行できます。

GKE の場合、1つの gcloud コマンドでクラスタを作成できます。

gcloud container clusters create blue-green-tutorial \
  --zone=us-central1-a \
  --num-nodes=3 \
  --machine-type=e2-medium

次に、kubectl が新しいクラスタと通信できるように設定します。

gcloud container clusters get-credentials blue-green-tutorial --zone=us-central1-a

get-credentials は、クラスタのエンドポイントと認証情報を ~/.kube/config に書き込み、それをアクティブなコンテキストとして設定します。これ以降、すべての kubectl コマンドは新しいクラスタを対象にします。別途接続手順を行う必要はありません。

Google Cloud のGKE クイックスタートでは、プロジェクトの設定、認証、課金設定を含む完全なセットアップ手順を確認できます。他のプロバイダーを使用する場合は、それぞれの同等のドキュメントに従ってください。パターンは同じです。クラスタをプロビジョニングし、kubectl をそれに向けて設定します。

次に進む前に、接続を確認します。

kubectl get nodes

ノードの一覧が返ってきたら、このチュートリアルの残りの部分はクラウドに依存しません。ここから先のすべてのコマンドは、クラスタがどこで実行されていても同じように動作します。

初期セットアップ

パイプラインを最初に実行する前に、両方の Deployment と両方の Service を手動で適用します。

export IMAGE_TAG=initial
export DOCKERHUB_USERNAME=<your-dockerhub-username>
export CIRCLE_PROJECT_ID=<your-circleci-project-id>

envsubst < k8s/deployment-blue.yml | kubectl apply -f -
envsubst < k8s/deployment-green.yml | kubectl apply -f -
kubectl apply -f k8s/service.yml
kubectl apply -f k8s/service-preview.yml

両方の環境は同じイメージから開始します。これ以降は、green 環境へのすべての更新をパイプラインが処理します。

ステップ3 — 承認ゲート付きの CircleCI パイプラインを作成する

リポジトリを接続する

CircleCI で、左側のサイドバーの Projects をクリックし、対象のリポジトリを見つけて Set Up Project をクリックします。既存の .circleci/config.yml を使用するために Fastest オプションを選択します。詳細な手順については、Getting started with CircleCI を参照してください。

コンテキストを作成する

このパイプラインには、リポジトリに保存すべきではないシークレットが必要です。CircleCI のコンテキストは、実行時にジョブが利用できる環境変数を保存します。blue-green-tutorial という名前のコンテキストを作成します。

  1. CircleCI で Organization Settings > Contexts に移動します。
  2. Create Context をクリックし、blue-green-tutorial という名前を付けます。
  3. 次の3つの環境変数を追加します。
変数
DOCKERHUB_USERNAME Docker Hub のユーザー名
DOCKERHUB_PASSWORD Docker Hub のパスワードまたはアクセストークン
KUBECONFIG_DATA クラスタ用の Base64 エンコードされた kubeconfig

GKE 用に KUBECONFIG_DATA を生成するには:

gcloud container clusters get-credentials <cluster-name> --zone <zone> --project <project-id>
cat ~/.kube/config | base64 | tr -d '[:space:]'

EKS、AKS、その他のプロバイダーを使用する場合は、各プロバイダーのドキュメントに従って kubectl アクセスを設定し、生成された kubeconfig を同じ方法でエンコードします。

パイプラインの設定

このパイプラインは、線形のワークフローの中に5つのジョブを持ちます。ここが、ブルーグリーンがローリングと異なる点です。デプロイとリリースを1つのステップで行うのではなく、パイプラインは検証ゲートによってそれらを分離します。

version: 2.1

commands:
  setup-kubectl:
    description: Install kubectl and configure cluster access
    steps:
      - run:
          name: Install kubectl
          command: |
            KUBECTL_VERSION=$(curl -L -s https://dl.k8s.io/release/stable.txt)
            curl -LO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
            chmod +x kubectl
            sudo mv kubectl /usr/local/bin/kubectl
      - run:
          name: Configure kubeconfig
          command: |
            mkdir -p ~/.kube
            echo "$KUBECONFIG_DATA" | tr -d '[:space:]' | base64 --decode > ~/.kube/config
            chmod 600 ~/.kube/config

jobs:
  build-and-push:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - setup_remote_docker:
          docker_layer_caching: true
      - run:
          name: Build Docker image
          command: |
            docker build -t $DOCKERHUB_USERNAME/blue-green-tutorial-app:$CIRCLE_SHA1 ./app
      - run:
          name: Push to Docker Hub
          command: |
            echo "$DOCKERHUB_PASSWORD" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin
            docker push $DOCKERHUB_USERNAME/blue-green-tutorial-app:$CIRCLE_SHA1

  deploy-to-green:
    docker:
      - image: cimg/base:stable
    steps:
      - checkout
      - setup-kubectl
      - run:
          name: Install envsubst
          command: sudo apt-get update -qq && sudo apt-get install -y gettext-base
      - run:
          name: Deploy to green environment
          command: |
            export IMAGE_TAG=$CIRCLE_SHA1
            envsubst < k8s/deployment-green.yml | kubectl apply -f -
            kubectl apply -f k8s/service.yml
            kubectl apply -f k8s/service-preview.yml
      - run:
          name: Wait for green rollout
          command: kubectl rollout status deployment/blue-green-tutorial-app-green --timeout=5m

  validate-green:
    docker:
      - image: cimg/base:stable
    steps:
      - setup-kubectl
      - run:
          name: Get preview service endpoint
          command: |
            for i in $(seq 1 30); do
              PREVIEW_IP=$(kubectl get svc blue-green-tutorial-app-preview \
                -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
              if [ -n "$PREVIEW_IP" ]; then
                echo "Preview IP: $PREVIEW_IP"
                echo "export PREVIEW_IP=$PREVIEW_IP" >> "$BASH_ENV"
                break
              fi
              sleep 10
            done
            if [ -z "$PREVIEW_IP" ]; then
              echo "Preview service did not receive an external IP"
              exit 1
            fi
      - run:
          name: Smoke test - health check
          command: |
            curl -sf http://$PREVIEW_IP/health | grep -q '"status":"ok"'
      - run:
          name: Smoke test - version check
          command: |
            curl -sf http://$PREVIEW_IP/api | grep -q "$CIRCLE_SHA1"
      - run:
          name: Smoke test - slot check
          command: |
            curl -sf http://$PREVIEW_IP/api | grep -q '"slot":"green"'

  switch-traffic:
    docker:
      - image: cimg/base:stable
    steps:
      - setup-kubectl
      - run:
          name: Plan deployment
          command: |
            circleci run release plan "${CIRCLE_JOB}" \
              --environment-name="production" \
              --component-name="blue-green-tutorial-app" \
              --target-version="$CIRCLE_SHA1"
      - run:
          name: Switch production traffic to green
          command: |
            kubectl patch svc blue-green-tutorial-app \
              -p '{"spec":{"selector":{"version":"green"}}}'
      - run:
          name: Update deployment status to running
          command: circleci run release update "${CIRCLE_JOB}" --status=RUNNING
      - run:
          name: Log deploy marker
          command: |
            circleci run release log \
              --component-name=blue-green-tutorial-app \
              --environment-name=production \
              --target-version=$CIRCLE_SHA1
      - run:
          name: Update deployment status to success
          command: circleci run release update "${CIRCLE_JOB}" --status=SUCCESS
          when: on_success
      - run:
          name: Update deployment status to failed
          command: circleci run release update "${CIRCLE_JOB}" --status=FAILED
          when: on_fail

workflows:
  build-deploy:
    jobs:
      - build-and-push:
          context: blue-green-tutorial
      - deploy-to-green:
          requires:
            - build-and-push
          context: blue-green-tutorial
          filters:
            branches:
              only: main
      - validate-green:
          requires:
            - deploy-to-green
          context: blue-green-tutorial
      - hold-for-approval:
          type: approval
          requires:
            - validate-green
      - switch-traffic:
          requires:
            - hold-for-approval
          context: blue-green-tutorial

各ジョブを順に見ていきます。

build-and-push は Docker イメージをビルドし、Git のコミット SHA($CIRCLE_SHA1)でタグ付けします。これにより、すべてのイメージがそれを生成した正確なコミットまで追跡できます。

deploy-to-green は、新しいイメージタグを付けた green Deployment マニフェストと、両方の Service マニフェストを適用します。その後、green のロールアウトが完了するまで待機します。この時点で、新しいコードは green 環境で実行されていますが、production のトラフィックは依然として blue に向かっています。

validate-green は preview Service に対してスモークテストを実行します。health エンドポイントが応答すること、API が期待するバージョンを返すこと、レスポンスが green スロットから返されていることを確認します。いずれかのテストが失敗すると、そのジョブは失敗し、承認ゲートは表示されません。production は blue のままになります。

hold-for-approval は CircleCI のマニュアル承認ジョブです。パイプラインはここで一時停止します。エンジニアが検証結果を確認し、必要に応じて preview エンドポイントに対して追加の手動チェックを行い、CircleCI の UI で「Approve」をクリックすると処理が進みます。これは、コードのデプロイとユーザーへのリリースの間にある、人間によるチェックポイントです。

build-deploy ワークフローが hold-for-approval で一時停止している CircleCI のパイプライン表示。ジョブ一覧には、完了した build-and-push、deploy-to-green、validate-green と、入力待ちの hold-for-approval が表示されています。サイドパネルには Approve job と Cancel job のボタンがあります。

switch-traffic は、production Service のセレクタを version: blue から version: green にパッチします。トラフィックは即座に切り替わります。これがリリースの瞬間です。

このジョブは、一連の流れの中でデプロイマーカーも記録します。release plan は、作業が始まる前に計画中のデプロイを登録します。切り替え後、release update --status=RUNNING がそれをアクティブとしてマークし、release log がそのイベントを記録します。最後の release updateSUCCESS または FAILED でライフサイクルを終了させます。これらのマーカーは CircleCI のDeploys ダッシュボードに反映され、各リリースがパイプラインとコミットに紐づいたタイムラインエントリとして表示されます。

このデプロイチェーンは main でのみ実行されます。プルリクエストのビルドは build-and-push の後で停止し、デプロイはトリガーされません。

正常に完了した Retrigger パイプライン実行の CircleCI ワークフロー表示。build-deploy ワークフロー内のすべての5つのジョブ(build-and-push、deploy-to-green、validate-green、hold-for-approval、switch-traffic)に緑のチェックマークが表示され、全体のステータスは Success です。

ステップ4 — 切り替え前に green 環境を検証する

validate-green ジョブは、preview Service のエンドポイントに対して3つのチェックを実行します。

  1. ヘルスチェック: curl -sf http://$PREVIEW_IP/health{"status":"ok"} を返す。
  2. バージョンチェック: /api のレスポンスに期待するコミット SHA が含まれている。
  3. スロットチェック: /api のレスポンスで "slot":"green" が確認できる。

これら3つは最低限の基準です。実際のプロダクション環境では、重要な API エンドポイントを網羅するスモークテストスイート、preview エンドポイントに対する軽量な負荷テスト、アプリケーションが必要とするデータベース接続やマイグレーションのチェックなどを追加することになります。preview Service が安定したターゲットを提供するため、検証ジョブは HTTP リクエストを行うものであれば何でも実行できます。

どのようなテストスイートであっても、それは production インフラに対して実行されます。コネクションプールの上限、実際のデータに起因するエッジケース、production 専用の設定値による障害は、トラフィックが切り替わる前にここで表面化します。

検証が失敗すると、パイプラインは停止します。承認ゲートは表示されません。production のトラフィックは blue のままです。チームは green 環境を調査し、kubectl logs -l version=green でログを確認し、次のパイプライン実行前に問題を修正できます。

パイプラインが実行され、トラフィックが green に切り替わると、アプリケーションはデプロイを確認できるステータスページを表示します。

"Deployment successful" のステータスカードを表示している、デプロイ済みサンプルアプリケーションのブラウザ表示。カードには、バージョン 901ebbb、スロット green、green の Pod 名、デプロイのタイムスタンプが表示されています。このページは GKE のロードバランサー IP から提供されています。

ステップ5 — データベースマイグレーションを安全に処理する

expand-migrate-contract パターンを使うと、アプリのどちらのバージョンも壊すことなく、ブルーグリーンパイプラインを通じてスキーマ変更をリリースできます。これは3つのフェーズで機能します。

Expand フェーズ。 既存のカラムやテーブルに加えて、新しいカラムやテーブルを追加します。何も削除しません。このスキーマ変更は、アプリケーションの変更よりも前にデプロイします。既存の blue コードが依存しているものは何も削除されていないため、動作を続けます。新しい green コードは、新しい構造が既に存在するため動作します。

Migrate フェーズ。 古い構造から新しい構造へデータをバックフィルします。これはバックグラウンドジョブとして、またはスキーマの expand が適用された後のパイプラインのステップとして実行できます。

Contract フェーズ。 すべてのトラフィックが green バージョンで実行されるようになり、チームが blue 環境がロールバックのために不要であることを確認したら、古いカラムを削除します。これは別のデプロイとして、数日から数週間後に実行します。

たとえば、カラム名を user_name から username に変更する場合は、次のように進みます。

フェーズ スキーマの状態 Blue コード Green コード
Before user_name が存在 user_name を読み取る
Expand 両方のカラムが存在し、トリガーが同期 user_name を読み取る username を読み取る
Switch 両方のカラムが存在 稼働継続中、user_name を読み取る アクティブ、username を読み取る
Contract username のみが存在 スケールダウン済み username を読み取る

一貫して守るべき制約は N-1 互換性です。新しいアプリのバージョンは、古いスキーマと新しいスキーマの両方で動作しなければなりません。古いアプリのバージョンも、新しいスキーマで動作しなければなりません。どちらかの互換性が崩れると、ロールバックが失敗します。

スキーママイグレーションは、deploy-to-green ジョブより前に、パイプライン内の別のジョブとして実行します。これにより、新しいコードが動作する前にデータベースの準備が整います。

ステップ6 — 即時ロールバックを設定する

ロールバックは、production Service のセレクタを version: blue に戻すようパッチします。トラフィックは即座に以前のバージョンに戻ります。Pod の再起動、新しい ReplicaSet の作成、ローリングアップデートの逆方向実行は発生しません。blue Deployment は以前のイメージのまま稼働を続けているため、元に戻す操作はネットワークレベルの変更であり、数秒で反映されます。

.circleci/rollback.yml にあるロールバックパイプラインは、CircleCI の Deploys ダッシュボードからこれを自動化します。

version: 2.1

commands:
  setup-kubectl:
    description: Install kubectl and configure cluster access
    steps:
      - run:
          name: Install kubectl
          command: |
            KUBECTL_VERSION=$(curl -L -s https://dl.k8s.io/release/stable.txt)
            curl -LO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
            chmod +x kubectl && sudo mv kubectl /usr/local/bin/kubectl
      - run:
          name: Configure kubeconfig
          command: |
            mkdir -p ~/.kube
            echo "$KUBECONFIG_DATA" | tr -d '[:space:]' | base64 --decode > ~/.kube/config
            chmod 600 ~/.kube/config

jobs:
  rollback:
    docker:
      - image: cimg/base:stable
    environment:
      COMPONENT_NAME: << pipeline.deploy.component_name >>
      ENVIRONMENT_NAME: << pipeline.deploy.environment_name >>
      TARGET_VERSION: << pipeline.deploy.target_version >>
    steps:
      - setup-kubectl
      - run:
          name: Plan rollback release
          command: |
            circleci run release plan "${CIRCLE_JOB}" \
              --component-name=${COMPONENT_NAME} \
              --environment-name=${ENVIRONMENT_NAME} \
              --target-version=${TARGET_VERSION} \
              --rollback
      - run:
          name: Switch production traffic back to blue
          command: |
            kubectl patch svc blue-green-tutorial-app \
              -p '{"spec":{"selector":{"version":"blue"}}}'
      - run:
          name: Verify rollback
          command: |
            PROD_IP=$(kubectl get svc blue-green-tutorial-app \
              -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
            curl -sf http://$PROD_IP/api | grep -q '"slot":"blue"'
      - run:
          name: Update rollback status to SUCCESS
          command: circleci run release update "${CIRCLE_JOB}" --status=SUCCESS
          when: on_success
      - run:
          name: Update rollback status to FAILED
          command: circleci run release update "${CIRCLE_JOB}" --status=FAILED
          when: on_fail

  cancel-rollback:
    docker:
      - image: cimg/base:stable
    steps:
      - run:
          name: Update rollback status to CANCELED
          command: circleci run release update "${CIRCLE_JOB}" --status=CANCELED

workflows:
  rollback:
    jobs:
      - rollback:
          context: blue-green-tutorial
      - cancel-rollback:
          context: blue-green-tutorial
          requires:
            - rollback:
              - canceled

いくつか注目すべき点があります。

  • pipeline.deploy.* パラメータ。CircleCI がロールバックをトリガーする際に自動的に注入されます。
  • release plan --rollback。ダッシュボード上で通常のデプロイと区別するためのものです。
  • cancel-rollback ジョブ。明示的なキャンセル時にのみ発火し、古い「in progress」のエントリが残ることを防ぎます。

ロールバックパイプラインを接続する

ステップ3のデプロイマーカーが main にコミットされた状態で、プロジェクトの Overview ページから Rollback > Create rollback pipeline をクリックします。ウィザードは次の4つのステップを処理します。

  1. GitHub App のインストール。この機能は GitHub 専用であり、必要に応じてウィザードが App をインストールします。
  2. パイプラインの定義。プロジェクトのリポジトリを選択します。
  3. デプロイマーカー。すでに switch-traffic ジョブに含まれているため、I’ll make the config changes myself を選択し、続けて I’ve updated my config を選択します。
  4. ロールバック設定.circleci/rollback.yml を使用するために I already have a rollback config を選択し、Setup rollback pipeline をクリックします。

これでロールバックは、プロジェクトの Overview ページからトリガーできるようになります。後で Project Settings > Deploys からパイプラインを切り替える方法については、ロールバックパイプラインのドキュメントを参照してください。

CircleCI の「Run rollback」モーダル。「Rolling back」セクションには現在の blue-green-tutorial-app のバージョンが表示され、「To the following deployment」には以前のデプロイの SHA とそのコミットメッセージが表示されます。このダイアログには、任意で入力できるロールバック理由のフィールドと、Cancel および Rollback のボタンがあります。

ステップ7 — リリースを監視して確認する

トラフィックを切り替えた後は、切り替え前と切り替え後の期間でエラー率とレイテンシを比較します。切り替え直後にエラーが急増した場合、通常は green のコードが検証テストで想定していなかった production の条件に遭遍したことを意味します。

データベースのクエリパターンも監視します。green のコードからの新しいクエリが、予期しない負荷を生み出す可能性があります。まず確認すべきなのは、コネクションプールの使用状況とスロークエリログです。

各切り替えには CircleCI の Deploys ダッシュボードでタイムスタンプが記録されるため、リリースイベントを Prometheus、Datadog、Grafana などのメトリクスの変化と対応付けられます。

blue-green-tutorial-app コンポーネントの CircleCI Deploys ダッシュボード。タイムラインには、Logged と Success が混在する4件の production デプロイが表示されています。下部には、同じコンポーネント、バージョンの SHA、環境、switch-traffic トリガー、公開時刻を示す2つのデプロイ行があり、一方は Success、もう一方は Logged と表示されています。

切り替え後は、あらかじめ定めた検証期間(30分、1時間など、チームのリスク許容度に応じて決める期間)だけ blue 環境を稼働させ続けます。リリースが安定しており、ロールバックが不要だとチームが確信できた時点でのみスケールダウンします。blue Deployment がまだ稼働している間は、ロールバックは数秒で完了します。スケールダウンした後にロールバックするには、以前のイメージを再デプロイする必要があります。

両方の環境の現在の状態を確認するには:

kubectl get pods -l app=blue-green-tutorial-app -L version

これにより、すべての Pod がその version ラベルとともに表示され、どの環境で何が実行されているかが明確になります。

CircleCI がブルーグリーンデプロイメントを実現する仕組み

このパイプラインにおける CircleCI の役割は、単にステップを実行することだけではありません。承認ジョブこそが、ブルーグリーンのデプロイをゲート付きリリースに変えるものです。コードは main へのプッシュごとに production インフラに到達しますが、トラフィックが切り替わるのは、エンジニアが CircleCI の UI で Approve をクリックしたときだけです。このデプロイとリリースの分離こそが、ブルーグリーンのロールバックを高速にしています。以前のバージョンはまだ稼働しているため、切り戻しは再デプロイではなく Service セレクタの切り替えで済みます。デプロイマーカーは Deploys ダッシュボードで各リリースを追跡し、ロールバックパイプラインは同じダッシュボードから、切り戻しをワンクリックの操作に変えます。

ブルーグリーンは、数多くあるデプロイ戦略の1つです。他のデプロイ戦略は、速度、リスク、インフラコストについて異なるトレードオフを取ります。同じ CircleCI のプリミティブ(承認ゲート、デプロイマーカー、ロールバックパイプライン)は、それらすべてに適用できます。

ブルーグリーンを使うべきとき(使うべきでないとき)

次のような場合、ブルーグリーンは適した選択です。

  • 即時ロールバックが必須要件である場合(金融サービス、E コマースのチェックアウト、ヘルスケアなど)。
  • チームがデプロイ期間中のコンピュートコストの2倍を許容できる場合。
  • 切り替え前に production 環境で検証を行うことが重要な場合。
  • リリースの頻度が低く、リスクが高い場合。

次のような場合、ブルーグリーンは適切な選択ではありません。

  • インフラの予算が限られている場合(カナリアリリースはリソースの一部しか使用しません)。
  • チームが1日に何度もデプロイする場合(2つの環境を維持するオーバーヘッドは、高頻度のデプロイでは見合いません)。
  • パーセンテージベースのトラフィック移行やメトリクス駆動のプロモーションが必要な場合(これはカナリアリリースの領域です)。

各戦略の比較については、Kubernetes 上でのローリングデプロイメントを参照してください。

まとめ

このチュートリアルでは、Kubernetes 上に完全なブルーグリーンパイプラインを構築しました。並行環境へのデプロイ、production インフラに対する検証、人間による承認の待機、トラフィックの切り替え、そして問題が発生した場合のダッシュボードからのロールバックです。パイプライン全体はリポジトリ内の YAML として存在し、CircleCI の無料プランで実行でき、GKE、EKS、AKS、あるいは自己管理の Kubernetes クラスタのいずれに対しても動作します。

構築を始めるには、無料の CircleCI アカウントにサインアップし、パイプラインの設定を自分のプロジェクトに合わせて調整してください。