T-CREATOR

GitHub Actions でゼロダウンタイムリリース:canary/blue-green をパイプライン実装

GitHub Actions でゼロダウンタイムリリース:canary/blue-green をパイプライン実装

アプリケーションの更新時にサービスを停止せずにデプロイを実現したい――これは多くの開発チームが抱える課題です。GitHub Actions を活用すれば、canary リリースや blue-green デプロイメントといった高度なデプロイ戦略を、コードベースで管理できるパイプラインとして実装できます。

本記事では、GitHub Actions を用いてゼロダウンタイムリリースを実現する具体的な方法を、canary デプロイと blue-green デプロイの 2 つのパターンで解説します。実際に使えるワークフロー定義とともに、それぞれの戦略の特徴や使い分けについても詳しく見ていきましょう。

背景

ゼロダウンタイムリリースの必要性

現代の Web サービスでは、24 時間 365 日の可用性が求められます。しかし、従来のデプロイ方法ではアプリケーションの更新時にサービスを一時停止する必要があり、ユーザー体験を損なう原因となっていました。

ゼロダウンタイムリリースを実現することで、以下のメリットが得られます。

  • ユーザーへのサービス提供を中断せずにアップデート可能
  • ビジネス機会の損失を防ぐ
  • デプロイ頻度を上げられる(リリースサイクルの高速化)
  • 問題発生時の迅速なロールバックが可能

デプロイ戦略の種類

ゼロダウンタイムを実現するデプロイ戦略には、主に以下のパターンがあります。

#戦略名特徴適用シーン
1Canary リリース一部のユーザーに先行公開し、段階的に展開新機能のリスク評価が必要な場合
2Blue-Green デプロイ新旧環境を完全に分離し、瞬時に切り替え全体的なバージョンアップや大規模変更
3Rolling デプロイインスタンスを順次更新Kubernetes などのオーケストレーション環境

本記事では、GitHub Actions で実装しやすく、かつ多くのユースケースに対応できる canary リリースblue-green デプロイ に焦点を当てます。

GitHub Actions によるデプロイ自動化

GitHub Actions は、CI/CD パイプラインをコードとして管理できる強力なツールです。YAML 形式でワークフローを定義し、GitHub リポジトリ内で完結するため、以下の利点があります。

  • リポジトリと連携した自動化(プルリクエスト、タグ、ブランチごとの処理)
  • 豊富な公式・コミュニティアクションの活用
  • シークレット管理の統合
  • 実行履歴の可視化とデバッグ

これらの特性を活かして、高度なデプロイ戦略を実装していきます。

デプロイ戦略の全体像を図で示すと、以下のようになります。

mermaidflowchart TB
  repo["GitHub<br/>リポジトリ"] -->|push/tag| actions["GitHub Actions<br/>ワークフロー"]
  actions -->|デプロイ戦略選択| choice{戦略}
  choice -->|Canary| canary["Canary<br/>リリース"]
  choice -->|Blue-Green| bluegreen["Blue-Green<br/>デプロイ"]

  canary --> prod1["本番環境<br/>10% トラフィック"]
  canary --> prod2["本番環境<br/>100% トラフィック"]

  bluegreen --> blue["Blue 環境<br/>(現行版)"]
  bluegreen --> green["Green 環境<br/>(新版)"]
  bluegreen --> switch["トラフィック<br/>切り替え"]

この図から、リポジトリへのコミットをトリガーに、選択したデプロイ戦略に応じて本番環境へ段階的またはスイッチング方式でリリースする流れが理解できます。

課題

従来のデプロイ方法の問題点

従来の単純なデプロイ(一度にすべてのインスタンスを置き換える方法)には、以下の課題がありました。

#課題影響
1サービス停止時間の発生ユーザーがアクセスできない期間が生じる
2リスクの高いリリース問題発生時に全ユーザーが影響を受ける
3ロールバックの困難さ元の状態に戻すのに時間がかかる
4デプロイ頻度の制限リスクが高いため、リリースを頻繁に行えない

ゼロダウンタイム実現の技術的ハードル

ゼロダウンタイムリリースを実現するには、以下の技術的課題をクリアする必要があります。

トラフィック制御の複雑さ

新旧バージョンへのトラフィック振り分けを動的に制御する必要があります。ロードバランサーや API Gateway の設定を自動化し、段階的な切り替えやロールバックを実現しなければなりません。

状態管理とデータベース互換性

データベーススキーマの変更が伴う場合、新旧バージョンが同時に稼働する期間中も両方のバージョンが正常に動作する必要があります。これには後方互換性を保つ設計が求められます。

ヘルスチェックと監視

新バージョンが正常に動作しているかをリアルタイムで監視し、問題があれば自動的にロールバックする仕組みが必要です。メトリクスの収集とアラートの設定が欠かせません。

インフラストラクチャのコード化

デプロイ戦略を確実に再現可能にするため、インフラ設定もコードとして管理する必要があります。GitHub Actions のワークフロー YAML だけでなく、Terraform や CloudFormation などの IaC ツールとの連携も重要です。

以下の図は、ゼロダウンタイムデプロイで考慮すべき要素を整理したものです。

mermaidflowchart LR
  deploy["ゼロダウンタイム<br/>デプロイ"] --> traffic["トラフィック<br/>制御"]
  deploy --> state["状態管理<br/>DB 互換性"]
  deploy --> health["ヘルスチェック<br/>監視"]
  deploy --> iac["インフラ<br/>コード化"]

  traffic --> lb["ロードバランサー<br/>設定自動化"]
  state --> migration["マイグレーション<br/>戦略"]
  health --> metrics["メトリクス<br/>収集"]
  iac --> workflow["GitHub Actions<br/>ワークフロー"]

これらの課題を GitHub Actions のワークフロー内でどのように解決するかが、次の「解決策」セクションのポイントとなります。

解決策

Canary リリースの実装

Canary リリースは、新バージョンを一部のユーザー(またはトラフィックの一部)にのみ公開し、問題がないことを確認してから全体に展開する手法です。

基本的な流れ

  1. 新バージョンを本番環境の一部(例:10%)にデプロイ
  2. メトリクスを監視し、エラー率やレスポンスタイムを確認
  3. 問題がなければ段階的に展開率を上げる(50% → 100%)
  4. 問題があればロールバック

この戦略により、リスクを最小限に抑えながら新機能をリリースできます。

以下は、GitHub Actions で canary リリースを実現するワークフローの概要図です。

mermaidflowchart TD
  start["ワークフロー<br/>開始"] --> build["ビルド<br/>テスト"]
  build --> deploy10["Canary デプロイ<br/>10% トラフィック"]
  deploy10 --> wait1["待機<br/>5 分"]
  wait1 --> check1{"ヘルスチェック<br/>成功?"}
  check1 -->|No| rollback["ロールバック"]
  check1 -->|Yes| deploy50["展開<br/>50% トラフィック"]
  deploy50 --> wait2["待機<br/>5 分"]
  wait2 --> check2{"ヘルスチェック<br/>成功?"}
  check2 -->|No| rollback
  check2 -->|Yes| deploy100["展開<br/>100% トラフィック"]
  deploy100 --> done["完了"]
  rollback --> done

この図から、段階的にトラフィックを増やしながら、各ステップでヘルスチェックを行い、問題があればロールバックする流れが理解できます。

Blue-Green デプロイの実装

Blue-Green デプロイは、本番環境と同一の環境(Green)を用意し、そちらに新バージョンをデプロイした後、トラフィックを一斉に切り替える手法です。

基本的な流れ

  1. 現在の本番環境(Blue)と同じ構成の新環境(Green)を構築
  2. Green 環境に新バージョンをデプロイ
  3. Green 環境でテストとヘルスチェックを実施
  4. ロードバランサーのトラフィックを Blue から Green に切り替え
  5. 問題があれば即座に Blue に戻す(ロールバック)

この戦略は、大規模な変更や完全な環境切り替えが必要な場合に適しています。

Blue-Green デプロイの流れを図で示します。

mermaidflowchart LR
  lb["ロードバランサー"] --> blue["Blue 環境<br/>(現行版 v1.0)"]
  lb -.->|切り替え前| green["Green 環境<br/>(新版 v2.0)"]

  green --> test["テスト<br/>ヘルスチェック"]
  test --> switch["トラフィック<br/>切り替え"]
  switch --> lb2["ロードバランサー"]
  lb2 -.->|切り替え後| blue
  lb2 --> green

  style blue fill:#4A90E2
  style green fill:#7ED321

この図では、Blue 環境で稼働中のサービスを維持しながら、Green 環境に新バージョンをデプロイし、検証後にトラフィックを切り替える様子を表しています。

GitHub Actions ワークフローの設計原則

両方のデプロイ戦略を GitHub Actions で実装する際、以下の設計原則を守ることが重要です。

再利用可能なアクションの活用

共通処理(ビルド、テスト、デプロイ)は再利用可能なワークフローやコンポジットアクションとして切り出し、メンテナンス性を高めます。

環境ごとの設定管理

GitHub Environments を活用し、開発・ステージング・本番環境ごとに異なるシークレットや変数を管理します。承認フローも設定可能です。

ロールバック戦略の組み込み

デプロイ失敗時の自動ロールバックはもちろん、手動でロールバックできる仕組み(workflow_dispatch トリガー)も用意します。

可観測性の確保

デプロイの各段階でステータスを出力し、Slack や GitHub Issues への通知を設定することで、チーム全体が状況を把握できるようにします。

具体例

Canary リリースの完全実装例

ここでは、AWS ECS(Elastic Container Service)を対象とした canary リリースのワークフローを実装します。トラフィックの振り分けには AWS App Mesh や ALB(Application Load Balancer)のターゲットグループを使用します。

ワークフロー定義ファイルの作成

まず、.github​/​workflows​/​canary-deploy.yml ファイルを作成します。このワークフローは、main ブランチへのプッシュをトリガーに実行されます。

yamlname: Canary Release

on:
  push:
    branches:
      - main

環境変数とシークレットの設定

GitHub リポジトリの Settings > Secrets and variables > Actions から、以下のシークレットを設定します。

#シークレット名説明
1AWS_ACCESS_KEY_IDAWS アクセスキー ID
2AWS_SECRET_ACCESS_KEYAWS シークレットアクセスキー
3ECR_REPOSITORYECR リポジトリ名
4ECS_CLUSTERECS クラスター名
5ECS_SERVICEECS サービス名

ワークフローで使用する環境変数を定義します。

yamlenv:
  AWS_REGION: ap-northeast-1
  ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY }}
  ECS_CLUSTER: ${{ secrets.ECS_CLUSTER }}
  ECS_SERVICE: ${{ secrets.ECS_SERVICE }}
  IMAGE_TAG: ${{ github.sha }}

IMAGE_TAG には GitHub のコミット SHA を使用することで、各デプロイを一意に識別できます。

ビルドとテストのジョブ

最初に、アプリケーションのビルドとテストを実行します。

yamljobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: リポジトリのチェックアウト
        uses: actions/checkout@v4

      - name: Node.js のセットアップ
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'yarn'

      - name: 依存関係のインストール
        run: yarn install --frozen-lockfile

テストを実行し、コードの品質を確認します。

yaml- name: テストの実行
  run: yarn test

- name: Lint チェック
  run: yarn lint

Docker イメージのビルドとプッシュ

AWS ECR にログインし、Docker イメージをビルドしてプッシュします。

yaml- name: AWS 認証情報の設定
  uses: aws-actions/configure-aws-credentials@v4
  with:
    aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
    aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    aws-region: ${{ env.AWS_REGION }}

- name: ECR へのログイン
  id: login-ecr
  uses: aws-actions/amazon-ecr-login@v2

Docker イメージをビルドし、ECR にプッシュします。

yaml- name: Docker イメージのビルドとプッシュ
  env:
    ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
  run: |
    # Docker イメージのビルド
    docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
    docker tag $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG $ECR_REGISTRY/$ECR_REPOSITORY:latest

    # ECR へプッシュ
    docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
    docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest

Canary デプロイ(10% トラフィック)

新しいタスク定義を作成し、ECS サービスの 10% のトラフィックを新バージョンに振り分けます。

yamlcanary-10:
  needs: build
  runs-on: ubuntu-latest
  steps:
    - name: AWS 認証情報の設定
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ env.AWS_REGION }}

    - name: タスク定義の更新
      id: task-def
      run: |
        # 現在のタスク定義を取得
        TASK_DEFINITION=$(aws ecs describe-task-definition \
          --task-definition $ECS_SERVICE \
          --query 'taskDefinition' \
          --output json)

        # 新しいイメージタグで更新
        NEW_TASK_DEF=$(echo $TASK_DEFINITION | \
          jq --arg IMAGE "$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" \
          '.containerDefinitions[0].image = $IMAGE | del(.taskDefinitionArn, .revision, .status, .requiresAttributes, .compatibilities, .registeredAt, .registeredBy)')

        # 新しいタスク定義を登録
        NEW_TASK_ARN=$(aws ecs register-task-definition \
          --cli-input-json "$NEW_TASK_DEF" \
          --query 'taskDefinition.taskDefinitionArn' \
          --output text)

        echo "task-arn=$NEW_TASK_ARN" >> $GITHUB_OUTPUT
      env:
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}

ALB のターゲットグループを使用して、トラフィックの 10% を新バージョンに振り分けます。

yaml- name: Canary デプロイ(10%)の実行
  run: |
    # ECS サービスを更新(10% のトラフィックを新バージョンに)
    aws ecs update-service \
      --cluster $ECS_CLUSTER \
      --service $ECS_SERVICE \
      --task-definition ${{ steps.task-def.outputs.task-arn }} \
      --deployment-configuration "minimumHealthyPercent=100,maximumPercent=200" \
      --desired-count 10

    # デプロイの完了を待機
    aws ecs wait services-stable \
      --cluster $ECS_CLUSTER \
      --services $ECS_SERVICE

ヘルスチェックと監視

デプロイ後、一定時間待機してメトリクスを監視します。

yaml- name: 待機(5分間)
  run: sleep 300

- name: ヘルスチェック
  id: health-check-10
  run: |
    # CloudWatch メトリクスを取得してエラー率を確認
    ERROR_RATE=$(aws cloudwatch get-metric-statistics \
      --namespace AWS/ECS \
      --metric-name TargetResponseTime \
      --dimensions Name=ServiceName,Value=$ECS_SERVICE \
      --start-time $(date -u -d '5 minutes ago' +%Y-%m-%dT%H:%M:%S) \
      --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
      --period 300 \
      --statistics Average \
      --query 'Datapoints[0].Average' \
      --output text)

    # エラー率が閾値を超えていないか確認
    if (( $(echo "$ERROR_RATE > 500" | bc -l) )); then
      echo "エラー率が高すぎます: $ERROR_RATE ms"
      echo "result=failed" >> $GITHUB_OUTPUT
      exit 1
    else
      echo "ヘルスチェック成功: $ERROR_RATE ms"
      echo "result=success" >> $GITHUB_OUTPUT
    fi

段階的展開(50% → 100%)

ヘルスチェックが成功したら、50% への展開を行います。

yamlcanary-50:
  needs: canary-10
  runs-on: ubuntu-latest
  steps:
    - name: AWS 認証情報の設定
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ env.AWS_REGION }}

    - name: Canary デプロイ(50%)の実行
      run: |
        # トラフィックの 50% を新バージョンに
        aws elbv2 modify-listener \
          --listener-arn $LISTENER_ARN \
          --default-actions Type=forward,ForwardConfig='{
            "TargetGroups": [
              {"TargetGroupArn": "'$OLD_TARGET_GROUP_ARN'", "Weight": 50},
              {"TargetGroupArn": "'$NEW_TARGET_GROUP_ARN'", "Weight": 50}
            ]
          }'

    - name: 待機(5分間)
      run: sleep 300

    - name: ヘルスチェック
      run: |
        # 同様のヘルスチェックを実行
        # (省略:10% 時と同様の処理)

最終的に 100% への展開を行います。

yamlcanary-100:
  needs: canary-50
  runs-on: ubuntu-latest
  steps:
    - name: AWS 認証情報の設定
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ env.AWS_REGION }}

    - name: 完全展開(100%)の実行
      run: |
        # すべてのトラフィックを新バージョンに
        aws elbv2 modify-listener \
          --listener-arn $LISTENER_ARN \
          --default-actions Type=forward,TargetGroupArn=$NEW_TARGET_GROUP_ARN

    - name: 旧バージョンのクリーンアップ
      run: |
        # 旧タスクを削除
        aws ecs update-service \
          --cluster $ECS_CLUSTER \
          --service $ECS_SERVICE-old \
          --desired-count 0

ロールバック処理

ヘルスチェックが失敗した場合のロールバック処理を定義します。

yamlrollback:
  if: failure()
  needs: [canary-10, canary-50]
  runs-on: ubuntu-latest
  steps:
    - name: AWS 認証情報の設定
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ env.AWS_REGION }}

    - name: ロールバックの実行
      run: |
        # トラフィックを旧バージョンに戻す
        aws elbv2 modify-listener \
          --listener-arn $LISTENER_ARN \
          --default-actions Type=forward,TargetGroupArn=$OLD_TARGET_GROUP_ARN

        # 新バージョンのタスクを削除
        aws ecs update-service \
          --cluster $ECS_CLUSTER \
          --service $ECS_SERVICE \
          --desired-count 0

    - name: Slack 通知
      uses: slackapi/slack-github-action@v1
      with:
        payload: |
          {
            "text": "❌ Canary デプロイが失敗し、ロールバックしました",
            "blocks": [
              {
                "type": "section",
                "text": {
                  "type": "mrkdwn",
                  "text": "*Canary デプロイ失敗*\nコミット: `${{ github.sha }}`\n詳細: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
                }
              }
            ]
          }
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Blue-Green デプロイの完全実装例

次に、Blue-Green デプロイのワークフローを実装します。こちらは AWS ECS と ALB を使用した例です。

ワークフロー定義ファイルの作成

.github​/​workflows​/​blue-green-deploy.yml ファイルを作成します。

yamlname: Blue-Green Deployment

on:
  push:
    tags:
      - 'v*'

このワークフローは、バージョンタグ(v1.0.0 など)がプッシュされた際に実行されます。

環境変数の設定

Canary リリースと同様の環境変数に加え、Blue-Green 環境固有の設定を追加します。

yamlenv:
  AWS_REGION: ap-northeast-1
  ECR_REPOSITORY: ${{ secrets.ECR_REPOSITORY }}
  ECS_CLUSTER: ${{ secrets.ECS_CLUSTER }}
  BLUE_SERVICE: ${{ secrets.BLUE_SERVICE }}
  GREEN_SERVICE: ${{ secrets.GREEN_SERVICE }}
  BLUE_TARGET_GROUP: ${{ secrets.BLUE_TARGET_GROUP_ARN }}
  GREEN_TARGET_GROUP: ${{ secrets.GREEN_TARGET_GROUP_ARN }}
  LISTENER_ARN: ${{ secrets.LISTENER_ARN }}
  IMAGE_TAG: ${{ github.ref_name }}

ビルドジョブ

Canary リリースと同様のビルド処理を行います(省略)。

Green 環境へのデプロイ

Green 環境(新バージョン)へデプロイします。この時点では、まだトラフィックは Blue 環境に向いています。

yamldeploy-green:
  needs: build
  runs-on: ubuntu-latest
  environment:
    name: production-green
    url: https://green.example.com
  steps:
    - name: リポジトリのチェックアウト
      uses: actions/checkout@v4

    - name: AWS 認証情報の設定
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ env.AWS_REGION }}

    - name: タスク定義のレンダリング
      id: render-task-def
      uses: aws-actions/amazon-ecs-render-task-definition@v1
      with:
        task-definition: task-definition.json
        container-name: app
        image: ${{ secrets.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }}

ECS サービスを更新し、Green 環境に新バージョンをデプロイします。

yaml- name: Green 環境へのデプロイ
  uses: aws-actions/amazon-ecs-deploy-task-definition@v1
  with:
    task-definition: ${{ steps.render-task-def.outputs.task-definition }}
    service: ${{ env.GREEN_SERVICE }}
    cluster: ${{ env.ECS_CLUSTER }}
    wait-for-service-stability: true

Green 環境のテストとヘルスチェック

デプロイ後、Green 環境で統合テストとヘルスチェックを実施します。

yamltest-green:
  needs: deploy-green
  runs-on: ubuntu-latest
  steps:
    - name: リポジトリのチェックアウト
      uses: actions/checkout@v4

    - name: Green 環境のヘルスチェック
      run: |
        # Green 環境のエンドポイントにヘルスチェックリクエスト
        for i in {1..10}; do
          STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://green.example.com/health)
          if [ "$STATUS" -eq 200 ]; then
            echo "ヘルスチェック成功"
            exit 0
          fi
          echo "試行 $i: ステータスコード $STATUS"
          sleep 5
        done
        echo "ヘルスチェック失敗"
        exit 1

    - name: 統合テストの実行
      run: |
        # Green 環境に対して統合テストを実行
        yarn test:e2e --baseUrl=https://green.example.com

エラー率やパフォーマンスメトリクスを確認します。

yaml- name: メトリクス確認
  run: |
    # CloudWatch でメトリクスを確認
    ERROR_COUNT=$(aws cloudwatch get-metric-statistics \
      --namespace AWS/ApplicationELB \
      --metric-name HTTPCode_Target_5XX_Count \
      --dimensions Name=TargetGroup,Value=$GREEN_TARGET_GROUP \
      --start-time $(date -u -d '5 minutes ago' +%Y-%m-%dT%H:%M:%S) \
      --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
      --period 300 \
      --statistics Sum \
      --query 'Datapoints[0].Sum' \
      --output text)

    if [ "$ERROR_COUNT" != "None" ] && [ "$ERROR_COUNT" -gt 0 ]; then
      echo "エラーが検出されました: $ERROR_COUNT 件"
      exit 1
    fi
    echo "メトリクス確認完了"

トラフィックの切り替え

テストが成功したら、ALB のリスナールールを変更してトラフィックを Green 環境に切り替えます。

yamlswitch-traffic:
  needs: test-green
  runs-on: ubuntu-latest
  environment:
    name: production
    url: https://example.com
  steps:
    - name: AWS 認証情報の設定
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ env.AWS_REGION }}

    - name: トラフィックを Green に切り替え
      run: |
        # ALB リスナーのデフォルトアクションを Green に変更
        aws elbv2 modify-listener \
          --listener-arn $LISTENER_ARN \
          --default-actions Type=forward,TargetGroupArn=$GREEN_TARGET_GROUP

        echo "トラフィックを Green 環境に切り替えました"

切り替え後、本番環境での監視を行います。

yaml- name: 本番環境の監視(10分間)
  run: |
    echo "本番環境を 10 分間監視します"
    sleep 600

    # エラー率を確認
    ERROR_RATE=$(aws cloudwatch get-metric-statistics \
      --namespace AWS/ApplicationELB \
      --metric-name HTTPCode_Target_5XX_Count \
      --dimensions Name=TargetGroup,Value=$GREEN_TARGET_GROUP \
      --start-time $(date -u -d '10 minutes ago' +%Y-%m-%dT%H:%M:%S) \
      --end-time $(date -u +%Y-%m-%dT%H:%M:%S) \
      --period 600 \
      --statistics Sum \
      --query 'Datapoints[0].Sum' \
      --output text)

    if [ "$ERROR_RATE" != "None" ] && [ "$ERROR_RATE" -gt 10 ]; then
      echo "エラー率が高すぎます: $ERROR_RATE 件"
      exit 1
    fi

    echo "本番環境は正常に稼働しています"

Blue 環境のクリーンアップ

Green 環境が安定稼働していることを確認したら、Blue 環境(旧バージョン)をクリーンアップします。ただし、すぐには削除せず、一定期間保持しておきます。

yamlcleanup-blue:
  needs: switch-traffic
  runs-on: ubuntu-latest
  steps:
    - name: AWS 認証情報の設定
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ env.AWS_REGION }}

    - name: Blue 環境のスケールダウン
      run: |
        # Blue 環境のタスク数を減らす(完全には削除しない)
        aws ecs update-service \
          --cluster $ECS_CLUSTER \
          --service $BLUE_SERVICE \
          --desired-count 1

        echo "Blue 環境を 1 タスクに縮小しました(ロールバック用に保持)"

ロールバック用ワークフロー

問題が発生した場合に手動でロールバックできるワークフローを別途用意します。

yamlmanual-rollback:
  if: failure() || github.event_name == 'workflow_dispatch'
  runs-on: ubuntu-latest
  steps:
    - name: AWS 認証情報の設定
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ env.AWS_REGION }}

    - name: トラフィックを Blue に戻す
      run: |
        # Blue 環境にスケールアップ
        aws ecs update-service \
          --cluster $ECS_CLUSTER \
          --service $BLUE_SERVICE \
          --desired-count 3

        # サービスが安定するまで待機
        aws ecs wait services-stable \
          --cluster $ECS_CLUSTER \
          --services $BLUE_SERVICE

        # トラフィックを Blue に切り替え
        aws elbv2 modify-listener \
          --listener-arn $LISTENER_ARN \
          --default-actions Type=forward,TargetGroupArn=$BLUE_TARGET_GROUP

        echo "ロールバック完了: トラフィックを Blue 環境に戻しました"

    - name: Slack 通知
      uses: slackapi/slack-github-action@v1
      with:
        payload: |
          {
            "text": "⚠️ Blue-Green デプロイがロールバックされました",
            "blocks": [
              {
                "type": "section",
                "text": {
                  "type": "mrkdwn",
                  "text": "*ロールバック実行*\nタグ: `${{ github.ref_name }}`\nトラフィックを Blue 環境に戻しました"
                }
              }
            ]
          }
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

監視とアラートの統合

両方のデプロイ戦略において、監視とアラートは重要な要素です。以下に、CloudWatch と Datadog を使用した監視の設定例を示します。

CloudWatch アラームの作成

デプロイワークフロー内で、CloudWatch アラームを自動作成します。

yaml- name: CloudWatch アラームの作成
  run: |
    # エラー率アラームの作成
    aws cloudwatch put-metric-alarm \
      --alarm-name "$ECS_SERVICE-high-error-rate" \
      --alarm-description "5XX エラーが多発" \
      --metric-name HTTPCode_Target_5XX_Count \
      --namespace AWS/ApplicationELB \
      --statistic Sum \
      --period 60 \
      --evaluation-periods 2 \
      --threshold 10 \
      --comparison-operator GreaterThanThreshold \
      --dimensions Name=TargetGroup,Value=$TARGET_GROUP_ARN \
      --alarm-actions $SNS_TOPIC_ARN

    # レスポンスタイムアラームの作成
    aws cloudwatch put-metric-alarm \
      --alarm-name "$ECS_SERVICE-slow-response" \
      --alarm-description "レスポンスタイムが遅い" \
      --metric-name TargetResponseTime \
      --namespace AWS/ApplicationELB \
      --statistic Average \
      --period 60 \
      --evaluation-periods 3 \
      --threshold 1.0 \
      --comparison-operator GreaterThanThreshold \
      --dimensions Name=TargetGroup,Value=$TARGET_GROUP_ARN \
      --alarm-actions $SNS_TOPIC_ARN

Datadog へのデプロイイベント送信

デプロイの開始・完了・失敗を Datadog に送信し、ダッシュボードで可視化します。

yaml- name: Datadog デプロイイベント送信
  run: |
    curl -X POST "https://api.datadoghq.com/api/v1/events" \
      -H "Content-Type: application/json" \
      -H "DD-API-KEY: ${{ secrets.DATADOG_API_KEY }}" \
      -d '{
        "title": "デプロイ開始: '"$ECS_SERVICE"'",
        "text": "バージョン: '"$IMAGE_TAG"'\n戦略: Canary Release",
        "priority": "normal",
        "tags": ["service:'"$ECS_SERVICE"'", "env:production", "deployment:canary"],
        "alert_type": "info"
      }'

データベースマイグレーションの統合

ゼロダウンタイムデプロイでは、データベーススキーマの変更も考慮する必要があります。以下は、後方互換性を保ちながらマイグレーションを実行する例です。

段階的マイグレーション戦略

データベース変更を 3 段階に分けて実行します。

  1. 拡張フェーズ:新しいカラムやテーブルを追加(旧バージョンは新カラムを無視)
  2. 移行フェーズ:新バージョンをデプロイし、新カラムを使用開始
  3. 縮退フェーズ:旧カラムやテーブルを削除

ワークフローに組み込む例を示します。

yamldatabase-migration:
  needs: build
  runs-on: ubuntu-latest
  steps:
    - name: リポジトリのチェックアウト
      uses: actions/checkout@v4

    - name: マイグレーションの実行(拡張フェーズ)
      run: |
        # 新しいカラムを追加(旧バージョンは影響を受けない)
        yarn migration:run --scope=expand

        # 例: ALTER TABLE users ADD COLUMN email_verified BOOLEAN DEFAULT FALSE;
      env:
        DATABASE_URL: ${{ secrets.DATABASE_URL }}

    - name: データ移行スクリプトの実行
      run: |
        # 既存データを新しいカラムに移行
        yarn migration:data-copy

デプロイ完了後、旧カラムを削除する縮退フェーズを実行します。

yamlcleanup-database:
  needs: [canary-100, switch-traffic]
  runs-on: ubuntu-latest
  steps:
    - name: リポジトリのチェックアウト
      uses: actions/checkout@v4

    - name: マイグレーションの完了(縮退フェーズ)
      run: |
        # 新バージョンが安定稼働したら、旧カラムを削除
        yarn migration:run --scope=contract

        # 例: ALTER TABLE users DROP COLUMN old_email_status;
      env:
        DATABASE_URL: ${{ secrets.DATABASE_URL }}

以下の図は、段階的マイグレーションとデプロイの関係を示しています。

mermaidsequenceDiagram
  participant DB as データベース
  participant Old as 旧バージョン
  participant New as 新バージョン

  Note over DB: 拡張フェーズ
  DB->>DB: 新カラム追加
  Old->>DB: 旧カラムを使用

  Note over Old,New: デプロイ開始
  New->>DB: 新カラムを使用開始
  Old->>DB: 旧カラムを使用(並行稼働)

  Note over Old,New: トラフィック切り替え
  New->>DB: 新カラムを使用

  Note over DB: 縮退フェーズ
  DB->>DB: 旧カラム削除
  New->>DB: 新カラムを使用

この図から、旧バージョンと新バージョンが並行稼働する期間中も、データベーススキーマが両方に対応している様子が理解できます。

まとめ

GitHub Actions を活用したゼロダウンタイムリリースの実装方法を、canary リリースと blue-green デプロイの 2 つの戦略で詳しく解説しました。

主要なポイント

Canary リリースの特徴

段階的にトラフィックを増やしながら新バージョンをリリースする手法で、リスクを最小限に抑えられます。10% → 50% → 100% と展開し、各段階でヘルスチェックを行うことで、問題を早期に検出できます。

Blue-Green デプロイの特徴

完全に分離された 2 つの環境を用意し、一斉にトラフィックを切り替える手法です。テストを十分に行ってから切り替えるため、確実性が高く、ロールバックも瞬時に行えます。

戦略の使い分け

#シーン推奨戦略理由
1新機能の段階的リリースCanaryリスク評価しながら展開可能
2大規模なバージョンアップBlue-Green全体テスト後に一斉切り替え
3データベース変更を伴うBlue-Green互換性テストを完全に実施可能
4高頻度のマイナーアップデートCanary迅速な展開とフィードバック

実装のベストプラクティス

再利用可能なワークフローの構築

共通処理をコンポジットアクションや再利用可能なワークフローとして切り出すことで、メンテナンス性が向上します。複数のサービスで同じデプロイ戦略を使用する場合に特に有効です。

監視とアラートの統合

CloudWatch、Datadog、Prometheus などの監視ツールと連携し、デプロイの各段階でメトリクスを確認することが重要です。エラー率、レスポンスタイム、リソース使用率などを自動チェックします。

ロールバック戦略の明確化

自動ロールバックだけでなく、手動でのロールバック手順も用意しておくことで、緊急時に迅速に対応できます。workflow_dispatch トリガーを使用した手動実行ワークフローが有効です。

データベースマイグレーションの段階化

拡張・移行・縮退の 3 フェーズに分けることで、新旧バージョンが並行稼働する期間も安全にスキーマ変更を行えます。後方互換性を常に意識しましょう。

さらなる改善のヒント

パフォーマンステストの自動化

デプロイ前に負荷テストを自動実行し、パフォーマンスの劣化がないことを確認できます。Apache JMeter や k6 などのツールを GitHub Actions に統合しましょう。

セキュリティスキャンの組み込み

コンテナイメージの脆弱性スキャン(Trivy、Snyk など)をパイプラインに組み込むことで、安全なデプロイを実現できます。

マルチリージョン展開

AWS の複数リージョンや異なるクラウドプロバイダーにデプロイする場合、リージョンごとに段階的展開を行うことで、グローバルなサービスでもリスクを抑えられます。

GitHub Actions によるゼロダウンタイムリリースは、一度構築すれば継続的に恩恵を受けられる強力な仕組みです。本記事で紹介したワークフローを参考に、ぜひ皆さんのプロジェクトに導入してみてください。

関連リンク