# ECS on Fargate CI/CD 完全ガイド：ネイティブBlue/Green・CodeDeploy・GitHub Actions(OIDC)で安全に出荷する

> ECS Fargate の3つのデプロイ戦略（ローリング・ECSネイティブBlue/Green・CodeDeploy）を整理し、GitHub Actions OIDC の鍵レスパイプラインを実コードで示す。本番出荷の品質ゲートまで一気通貫。

- 公開日: 2026-06-26
- 著者: 友田 陽大
- タグ: AWS, ECS, Fargate, CI/CD, Blue/Green, CodeDeploy, GitHub Actions, デプロイ
- URL: https://tomodahinata.com/blog/aws-ecs-fargate-cicd-blue-green-codedeploy-github-actions-guide

## 要点

- ECSには3つのデプロイ方式がある：ローリング更新（ECSタイプ）・ECSネイティブBlue/Green（2025年GA、CodeDeploy不要）・CodeDeploy Blue/Green。まずECSネイティブを検討し、高度なLambdaフック検証が必要な場合のみCodeDeployを選ぶ
- ECSネイティブBlue/Greenは deployment controller=ECS + strategy=BLUE_GREEN で有効化。bakeTimeInMinutes でgreenへ切り替えた後もblueを保持しインスタントロールバックを維持。6つのライフサイクルフックでLambdaによる検証を差し込める
- CodeDeploy Blue/Greenは ALBに本番リスナー＋テストリスナー・ターゲットグループ2つ・AppSpecが必要。NLBの場合はトラフィックシフトが ECSAllAtOnce のみに制限される
- GitHub Actions OIDC で長期アクセスキーを一切置かず、role-to-assume で一時クレデンシャルを取得。ECR push → タスク定義更新 → ECSデプロイを完全自動化できる
- デプロイ成功の前提はコンテナ側の品質ゲート（型・テスト・脆弱性スキャン）と、SIGTERMを受けて stopTimeout 内に綺麗に終わるグレースフルシャットダウンの両立

---

「新しいコンテナを本番に出した直後に障害が起きたとき、どれだけ速く前の版に戻せるか」——デプロイの本質はここです。**速さは二の次。壊れたとき即座に戻せるか**が先です。

私は経済産業大臣賞を受賞した木材流通SaaSで、`API Gateway → NLB → ALB → ECS on Fargate` という構成の上に **221本のAPIエンドポイント**を本番運用してきました。決済基盤は[本番二重課金0件](/case-studies/lumber-industry-dx)を維持しています。「一人 × 生成AI(Claude Code)」で本番品質を出すには、デプロイパイプラインそのものを**壊れにくく・戻しやすい**構造にしておくことが不可欠でした。

この記事は、ECS on Fargate の3つのデプロイ方式を整理し、それぞれを**いつ・どう使うか**を判断できるようにすることを目的にしています。ECS基盤の設計・ネットワーク・セキュリティの基本は[ピラー記事](/blog/aws-ecs-fargate-production-guide)に譲り、本稿は**「デプロイそのもの」**に集中します。

---

## ECSの3つのデプロイ方式：まず地図を掴む

ECS on Fargate には、サービスの更新方式として3つの選択肢があります。どれを選ぶかでインフラの複雑さとロールバック速度が変わります。

| 方式 | デプロイコントローラ | Blue/Green | ロールバック速度 | 必要な追加リソース |
|------|------------------|-----------|-----------------|-----------------|
| ローリング更新 | ECS | なし | 新版を落として旧版を起動（数十秒〜） | なし |
| ECSネイティブBlue/Green | ECS | あり（2025年GA） | bake時間中はインスタント切替 | なし（ECS完結） |
| CodeDeploy Blue/Green | CODEDEPLOY | あり | bake時間中はインスタント切替 | CodeDeployアプリ・テストリスナー・TG×2 |

**基本的な選び方**：

- **ローリング更新**：シンプルな構成で十分、デプロイの仕組みをシンプルに保ちたい、短い停止リスクが許容できる。
- **ECSネイティブBlue/Green**：Blue/Greenが欲しいが追加リソースを増やしたくない。**2025年以降の新規構成のデフォルト推奨**。
- **CodeDeploy Blue/Green**：すでにCodeDeploy資産がある、Lambdaフックで複雑な検証をしたい、NLBと組み合わせた既存構成を踏まえる場合。

---

## ①ローリング更新の復習：min/max と サーキットブレーカー

ECSの最もシンプルなデプロイは**ローリング更新**です。デプロイコントローラは`ECS`、デプロイ戦略は暗黙のローリングです。動きは2つのパラメータで決まります（[公式](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-ecs.html)）。

- **`minimumHealthyPercent`**：デプロイ中に健全でなければならないタスク数の下限（%、切り上げ）。
- **`maximumPercent`**：デプロイ中に起動してよいタスク数の上限（%、切り下げ）。

`min 100% / max 200%`（ピラー記事の設定）は**最も安全側**です。`desiredCount=2` なら、新版タスク2つを先に立ち上げ（合計4）、健全を確認してから旧版2つを停止します。一時的にコストが倍になりますが、可用性は一切下がりません。

### デプロイサーキットブレーカー

新タスクがクラッシュループし続けているのに気付かずトラフィックを流す事故を防ぐのが**デプロイサーキットブレーカー**です。

```hcl
resource "aws_ecs_service" "app" {
  # ...
  deployment_circuit_breaker {
    enable   = true
    rollback = true # 失敗を検知したら前リビジョンへ自動ロールバック
  }
  # CloudWatch アラームとの併用も可能（どちらかの条件成立で失敗扱い）
  deployment_controller {
    type = "ECS"
  }
}
```

`rollback = true` にすると、新タスクが規定回数ヘルスチェックを通らない場合に**デプロイ失敗として自動で前リビジョンへ戻します**。CloudWatchアラームとの連動も可能で、両方有効にすると「どちらかの条件が成立した時点」で失敗扱いになります。

### イメージダイジェストによる版の一貫性

ECSは既定でタグをイメージダイジェストへ解決し、サービス内の全タスクが同一バイナリで動くことを保証します（`versionConsistency`）。`latest`タグを使うと「ビルドし直したら`latest`の中身が変わって一部タスクだけ別バージョン」という事故が起きます。**CommitSHAをタグに使い、`latest`に依存しない**のが鉄則です。

---

## ②ECSネイティブBlue/Green：2025年GA、CodeDeploy不要

2025年7月にGAし、2025年10月にcanary/linear追加でCodeDeployと機能同等になった**ECSネイティブBlue/Green**が、今後の新規構成での推奨です。CodeDeployを別途管理せずにECSだけで完結します。

> AWS released native Blue/Green deployments for Amazon ECS — you can now do blue/green deployments directly from ECS without needing AWS CodeDeploy.（— [AWS Blog](https://aws.amazon.com/blogs/aws/accelerate-safe-software-releases-with-new-built-in-blue-green-deployments-in-amazon-ecs/)）

### 仕組み

デプロイコントローラを`ECS`のまま、デプロイ戦略に`BLUE_GREEN`を選ぶことで有効になります。

1. サービス更新が始まると、**greenタスク群**を起動する。
2. green が健全になったらALBのトラフィックをgreenへ切り替える。
3. **`bakeTimeInMinutes`** の間、blue（旧版）とgreen（新版）を両方保持する。この間はクリック一つでblueへ即座に戻せる（インスタントロールバック）。
4. bake時間が切れたらblueタスクを削除する。

### 6つのデプロイライフサイクルフック

ECSネイティブBlue/Greenでは、Lambdaを差し込める6つのライフサイクルフックが用意されています。

| フック | タイミング |
|--------|----------|
| `PRE_SCALE_UP` | greenタスクのスケールアップ前 |
| `POST_SCALE_UP` | greenタスクのスケールアップ後（起動確認） |
| `TEST_TRAFFIC_SHIFT` | テストトラフィックをgreenへ向ける前 |
| `POST_TEST_TRAFFIC_SHIFT` | テストトラフィック切り替え後（スモークテスト） |
| `PRODUCTION_TRAFFIC_SHIFT` | 本番トラフィックをgreenへ向ける前 |
| `POST_PRODUCTION_TRAFFIC_SHIFT` | 本番トラフィック切り替え後（本番確認） |

フックのLambdaは検証結果をECSに返す。失敗した場合はその時点でデプロイが中断され、blueへ戻ります。

### トラフィックシフトの種類

| 種類 | 挙動 |
|------|------|
| `ALL_AT_ONCE` | 一括でgreenへ切り替え（最速、リスク集中） |
| `CANARY` | 最初に指定%をgreenへ、待機後に残りを切り替え |
| `LINEAR` | 一定%ずつ段階的にgreenへシフト |

### Terraform設定例

```hcl
resource "aws_ecs_service" "app" {
  name            = "web-api"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.app.arn
  desired_count   = 2
  launch_type     = "FARGATE"

  deployment_controller {
    type = "ECS" # ECSネイティブBlue/Greenはコントローラ "ECS" のまま
  }

  deployment_configuration {
    strategy {
      type = "BLUE_GREEN"
      bake_time_in_minutes = 10 # 切り替え後10分間、旧版を保持してインスタントロールバックを維持
    }
    # トラフィックシフト方式（CANARY例）
    deployment_circuit_breaker {
      enable   = true
      rollback = true
    }
  }

  # ライフサイクルフック（任意）
  # hooks を指定することで Lambdaによる各段階での検証が可能

  network_configuration {
    subnets          = var.private_subnet_ids
    security_groups  = [aws_security_group.task.id]
    assign_public_ip = false
  }
  load_balancer {
    target_group_arn = aws_lb_target_group.app.arn
    container_name   = "app"
    container_port   = 8080
  }
}
```

> **注意**：ECSネイティブBlue/Greenは比較的新しい機能（2025年GA）のため、Terraformプロバイダのバージョンによっては `deployment_configuration` ブロックの `strategy` サブブロックがまだサポートされていない場合があります。その場合はAWS CLIまたはコンソールで設定し、TerraformでIgnoreする過渡的対応が必要です。AWS CLIでの設定は後述します。

#### AWS CLI でのECSネイティブBlue/Green有効化

Terraformプロバイダが追いついていない場合、`create-service` または `update-service` で直接設定します。

```bash
aws ecs update-service \
  --cluster prod \
  --service web-api \
  --deployment-configuration '{
    "strategy": {
      "type": "BLUE_GREEN",
      "bakeTimeInMinutes": 10
    },
    "deploymentCircuitBreaker": {
      "enable": true,
      "rollback": true
    }
  }' \
  --region ap-northeast-1
```

---

## ③CodeDeploy Blue/Green：既存資産・高度検証のとき

ECSネイティブBlue/Greenで事足りる場合はCodeDeployを追加する理由はありません。ただし次のケースではCodeDeployが適しています。

- **既存のCodeDeploy資産**（他のサービスや承認フローがある）と統一したい。
- **Lambdaフックで高度な検証**（外部APIとの統合テスト、DBマイグレーション確認、Chaos Engineeringシナリオ）をきめ細かく差し込みたい。
- NLB構成で既存のターゲットグループ切り替え設定がある（ただしNLBの場合はAllAtOnceのみ）。

### 必要なリソース

CodeDeployによるECS Blue/Greenには以下が必要です（[公式](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-blue-green.html)）。

1. **ALB**（必須。NLBも可だがトラフィックシフト制限あり）
2. **本番リスナー**（ポート443等）と **テストリスナー**（オプションだが推奨：ポート8080等）、同一ALBに属す
3. **ターゲットグループ×2**（blue用・green用）
4. **CodeDeployアプリ**（コンピュートプラットフォーム`ECS`）と**デプロイグループ**
5. **AppSpec**（タスク定義ARN・コンテナ名・ポート・フックのLambda ARN）
6. **`ecsCodeDeployRole`**（CodeDeployがECSとLBを操作するためのIAMロール）

### AppSpec例

```yaml
version: 0.0
Resources:
  - TargetService:
      Type: AWS::ECS::Service
      Properties:
        TaskDefinition: "arn:aws:ecs:ap-northeast-1:111122223333:task-definition/web-api:42"
        LoadBalancerInfo:
          ContainerName: "app"
          ContainerPort: 8080
        PlatformVersion: "LATEST"
        NetworkConfiguration:
          AwsvpcConfiguration:
            Subnets:
              - "subnet-0abc123def456"
              - "subnet-0def789abc123"
            SecurityGroups:
              - "sg-0abcdef1234567890"
            AssignPublicIp: "DISABLED"
Hooks:
  - BeforeAllowTestTraffic: "arn:aws:lambda:ap-northeast-1:111122223333:function:pre-deploy-validation"
  - AfterAllowTestTraffic: "arn:aws:lambda:ap-northeast-1:111122223333:function:smoke-test"
  - BeforeAllowTraffic: "arn:aws:lambda:ap-northeast-1:111122223333:function:final-gate"
  - AfterAllowTraffic: "arn:aws:lambda:ap-northeast-1:111122223333:function:post-deploy-check"
```

### 事前定義デプロイ設定

| 設定名 | 挙動 |
|--------|------|
| `CodeDeployDefault.ECSAllAtOnce` | 一括切り替え（ALB・NLBどちらも利用可） |
| `CodeDeployDefault.ECSLinear10PercentEvery1Minutes` | 1分ごとに10%ずつシフト（10分で完了） |
| `CodeDeployDefault.ECSLinear10PercentEvery3Minutes` | 3分ごとに10%ずつシフト（30分で完了） |
| `CodeDeployDefault.ECSCanary10Percent5Minutes` | 最初に10%を5分確認し、残りを一括 |
| `CodeDeployDefault.ECSCanary10Percent15Minutes` | 最初に10%を15分確認し、残りを一括 |

> **NLBの制限**：NLBと組み合わせた場合はトラフィックシフトが `ECSAllAtOnce` のみに制限されます。

### Terraformでの CodeDeploy 設定

```hcl
# ECSサービスのデプロイコントローラを CODEDEPLOY に設定
resource "aws_ecs_service" "app" {
  name            = "web-api"
  cluster         = aws_ecs_cluster.main.id
  task_definition = aws_ecs_task_definition.app.arn
  desired_count   = 2
  launch_type     = "FARGATE"

  deployment_controller {
    type = "CODEDEPLOY"
  }

  # CodeDeploy管理下では load_balancer は2つのTGを参照
  load_balancer {
    target_group_arn = aws_lb_target_group.blue.arn
    container_name   = "app"
    container_port   = 8080
  }

  network_configuration {
    subnets          = var.private_subnet_ids
    security_groups  = [aws_security_group.task.id]
    assign_public_ip = false
  }

  # CodeDeployがTGを入れ替えるため、TFのplan差分を無視する
  lifecycle {
    ignore_changes = [task_definition, load_balancer]
  }
}

# IAMロール：CodeDeployがECSとLBを操作するための権限
resource "aws_iam_role" "ecs_codedeploy" {
  name               = "ecsCodeDeployRole"
  assume_role_policy = data.aws_iam_policy_document.codedeploy_assume.json
}

resource "aws_iam_role_policy_attachment" "ecs_codedeploy" {
  role       = aws_iam_role.ecs_codedeploy.name
  policy_arn = "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS"
}

# CodeDeployアプリ・デプロイグループ
resource "aws_codedeploy_app" "ecs" {
  compute_platform = "ECS"
  name             = "web-api"
}

resource "aws_codedeploy_deployment_group" "ecs" {
  app_name               = aws_codedeploy_app.ecs.name
  deployment_group_name  = "prod"
  service_role_arn       = aws_iam_role.ecs_codedeploy.arn
  deployment_config_name = "CodeDeployDefault.ECSCanary10Percent5Minutes"

  ecs_service {
    cluster_name = aws_ecs_cluster.main.name
    service_name = aws_ecs_service.app.name
  }

  load_balancer_info {
    target_group_pair_info {
      prod_traffic_route {
        listener_arns = [aws_lb_listener.https.arn]
      }
      test_traffic_route {
        listener_arns = [aws_lb_listener.test.arn]
      }
      target_group {
        name = aws_lb_target_group.blue.name
      }
      target_group {
        name = aws_lb_target_group.green.name
      }
    }
  }

  auto_rollback_configuration {
    enabled = true
    events  = ["DEPLOYMENT_FAILURE", "DEPLOYMENT_STOP_ON_ALARM"]
  }

  blue_green_deployment_config {
    deployment_ready_option {
      action_on_timeout = "CONTINUE_DEPLOYMENT"
    }
    terminate_blue_instances_on_deployment_success {
      action                           = "TERMINATE"
      termination_wait_time_in_minutes = 5 # bake時間。この間はblueが残りロールバック可能
    }
  }
}
```

> **`iam:PassRole`が必要**：GitHubActionsやCI/CDのIAMロールには、`ecsCodeDeployRole`を`PassRole`する権限も付与してください。

---

## ④GitHub Actions(OIDC)パイプライン：鍵レスで安全に

デプロイパイプラインに長期アクセスキーを置くのは**2026年のアンチパターン**です。[GitHub Actions OIDCによる鍵レスCI/CD](/blog/github-actions-oidc-keyless-cicd-aws-gcp-guide)を使えば、IAMロールへの一時クレデンシャル取得で全て完結します。

### パイプライン全体像

```text
git push → GitHub Actions トリガー
  → OIDC で AWS 一時クレデンシャル取得
  → 品質ゲート（型チェック・テスト・脆弱性スキャン）
  → ECR へ CommitSHA タグで push
  → タスク定義の image を新タグに差し替え（新リビジョン登録）
  → ECS サービスへデプロイ（ローリングまたは Blue/Green）
```

### 完全な workflow.yml

```yaml
name: Deploy to ECS Fargate

on:
  push:
    branches:
      - main

permissions:
  id-token: write   # OIDC トークン取得に必要
  contents: read

env:
  AWS_REGION: ap-northeast-1
  ECR_REPOSITORY: web-api
  ECS_CLUSTER: prod
  ECS_SERVICE: web-api
  CONTAINER_NAME: app
  TASK_DEFINITION_FAMILY: web-api

jobs:
  quality-gate:
    name: Quality Gate
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Type check
        run: npm run type-check

      - name: Unit tests
        run: npm test -- --run

      - name: Build
        run: npm run build

  deploy:
    name: Build & Deploy
    runs-on: ubuntu-latest
    needs: quality-gate  # 品質ゲートが通ってからデプロイ
    environment: production

    steps:
      - uses: actions/checkout@v4

      # ─── OIDC で AWS に認証（長期キー不要）───
      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-deploy
          aws-region: ${{ env.AWS_REGION }}
          # role-session-name でどのジョブが認証したか CloudTrail に残る
          role-session-name: github-${{ github.sha }}

      # ─── ECR へ push ───
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build, tag, and push image to ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}  # CommitSHA で版を固定（latest 非依存）
        run: |
          docker build \
            --platform linux/arm64 \
            --build-arg BUILD_SHA=$IMAGE_TAG \
            -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG \
            -t $ECR_REGISTRY/$ECR_REPOSITORY:latest \
            .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          # latest も更新（human確認用。ECSはSHAタグを使う）
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
          echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT

      # ─── ECR イメージスキャン結果を確認（HIGH/CRITICALがあれば失敗） ───
      - name: Wait for ECR scan and check results
        env:
          IMAGE_TAG: ${{ github.sha }}
        run: |
          aws ecr wait image-scan-complete \
            --repository-name $ECR_REPOSITORY \
            --image-id imageTag=$IMAGE_TAG \
            --region $AWS_REGION || true
          FINDINGS=$(aws ecr describe-image-scan-findings \
            --repository-name $ECR_REPOSITORY \
            --image-id imageTag=$IMAGE_TAG \
            --query 'imageScanFindings.findingSeverityCounts' \
            --output json 2>/dev/null || echo '{}')
          HIGH=$(echo $FINDINGS | jq '.HIGH // 0')
          CRITICAL=$(echo $FINDINGS | jq '.CRITICAL // 0')
          echo "HIGH=$HIGH CRITICAL=$CRITICAL"
          if [ "$HIGH" -gt 0 ] || [ "$CRITICAL" -gt 0 ]; then
            echo "::error::Image has HIGH or CRITICAL vulnerabilities. Blocking deploy."
            exit 1
          fi

      # ─── タスク定義の image を新タグへ差し替え ───
      - name: Download current task definition
        run: |
          aws ecs describe-task-definition \
            --task-definition $TASK_DEFINITION_FAMILY \
            --query taskDefinition \
            > task-definition.json

      - name: Render new task definition with updated image
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: task-definition.json
          container-name: ${{ env.CONTAINER_NAME }}
          image: ${{ steps.build-image.outputs.image }}

      # ─── ECS へデプロイ（ローリング or ECSネイティブBlue/Green） ───
      - name: Deploy to ECS
        uses: aws-actions/amazon-ecs-deploy-task-definition@v2
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          service: ${{ env.ECS_SERVICE }}
          cluster: ${{ env.ECS_CLUSTER }}
          wait-for-service-stability: true  # デプロイが安定するまで待機（失敗したらCI失敗）

      - name: Notify deployment result
        if: always()
        env:
          STATUS: ${{ job.status }}
          SHA: ${{ github.sha }}
        run: |
          echo "Deploy status: $STATUS | SHA: $SHA"
          # Slack通知などはここで実装
```

### CodeDeploy Blue/Green を使う場合の差分

`aws-actions/amazon-ecs-deploy-task-definition` は CodeDeploy にも対応しています。`codedeploy-appspec` オプションでAppSpecを渡すと、CodeDeployのデプロイを開始して完了を待機します。

```yaml
      - name: Deploy via CodeDeploy Blue/Green
        uses: aws-actions/amazon-ecs-deploy-task-definition@v2
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          service: ${{ env.ECS_SERVICE }}
          cluster: ${{ env.ECS_CLUSTER }}
          wait-for-service-stability: true
          codedeploy-appspec: appspec.yaml
          codedeploy-application: web-api
          codedeploy-deployment-group: prod
```

### OIDCのIAMロール設定（参考）

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:ref:refs/heads/main"
        }
      }
    }
  ]
}
```

このロールに付与するポリシーには最小限の権限（ECRへのpush・`ecs:RegisterTaskDefinition`・`ecs:UpdateService`・`iam:PassRole`（タスク実行ロール用））を絞り込むのが原則です。詳細は[GitHub Actions OIDCガイド](/blog/github-actions-oidc-keyless-cicd-aws-gcp-guide)を参照してください。

---

## ⑤デプロイ前品質ゲートとコンテナ側の準備

デプロイ方式をどれだけ洗練させても、**コンテナそのものが壊れていれば何も意味がない**。サーキットブレーカーやBlue/Greenは「デプロイを中断して戻す」仕組みであり、「正常なイメージを届ける」仕組みではありません。

### 出荷前品質ゲートの層

| レイヤ | 内容 | タイミング |
|--------|------|-----------|
| 型チェック | TypeScript `tsc --noEmit` / Pythonなら `mypy` | PR + main push |
| ユニットテスト | ビジネスロジック・バリデーション | PR + main push |
| コンテナビルド | `docker build` 成功を確認 | main push |
| 脆弱性スキャン | ECR Enhanced Scanning / Trivy | push後、デプロイ前 |
| インテグレーションテスト | ステージング環境での疎通確認（任意） | main push |

### グレースフルシャットダウンとの連携

デプロイ中にECSはタスクを停止します。停止の流れはこうです。

1. タスクをALBターゲットグループから**登録解除**（`deregistration_delay`の間、処理中リクエストを保護）
2. コンテナに **`SIGTERM`** を送る
3. **`stopTimeout`**（既定30秒・最大120秒）の間、終了を待つ
4. 終わらなければ **`SIGKILL`** で強制終了（データ損失リスク）

Blue/Greenデプロイでも、旧版（blue）タスクを削除する際には同じSIGTERMシーケンスが走ります。**bake時間の終了後にblueを落とすタイミングでも、SIGTERMを正しく処理できていなければデータ損失が起きます。**

ヘルスチェックとの連携で特に重要なのが `startPeriod` です。コンテナ起動直後の初期化中（DBコネクション確立・キャッシュウォームアップ）にヘルスチェックを失敗させないよう、猶予時間を設定しておきます。

```json
{
  "healthCheck": {
    "command": ["CMD-SHELL", "wget -q -O - http://localhost:8080/healthz || exit 1"],
    "interval": 15,
    "timeout": 5,
    "retries": 3,
    "startPeriod": 30
  },
  "stopTimeout": 60
}
```

グレースフルシャットダウンの実装詳細（SIGTERMハンドラのNode.jsコード等）は[ピラー記事](/blog/aws-ecs-fargate-production-guide)に記載しています。本番でのトラブルシュートは[ECSトラブルシューティング記事](/blog/aws-ecs-fargate-troubleshooting-task-stopped-reasons-guide)が役に立ちます。

### ヘルスチェックの二層構造

ECSには2つのヘルスチェックレイヤが存在し、両方が連動します。

- **コンテナヘルスチェック**（タスク定義の`healthCheck`）：コンテナが落ちているかどうかをECSが判断。失敗が続くとECSがタスクを置換する。
- **ALBターゲットヘルスチェック**（ターゲットグループの`health_check`）：ALBが「このIPへリクエストを送っていいか」を判断。失敗するとALBがタスクをターゲットから外す。

Blue/Greenデプロイ時、greenタスクが「ALBターゲットヘルスチェックをパス」して初めて本番トラフィックが移ります。ステージングや社内テスト用のリスナーをテストリスナーとして使うと、本番に流す前に追加の疎通確認ができます。

---

## まとめ：壊れたとき即座に戻せる状態を作る

ECS on Fargate のデプロイ戦略を3方式で整理しました。

| 方式 | 推奨場面 | 追加リソース |
|------|---------|------------|
| ローリング + サーキットブレーカー | シンプルな構成、短時間停止許容 | なし |
| ECSネイティブBlue/Green | **2025年以降の新規構成の第一選択** | なし |
| CodeDeploy Blue/Green | 既存CodeDeploy資産、高度なLambdaフック検証 | TG×2・テストリスナー・CodeDeploy設定 |

どの方式を選んでも、**「安全に出荷する」ための本質は変わりません**。

1. **品質ゲートをCIで固める**（型・テスト・スキャン）
2. **CommitSHAタグでイメージの版を固定する**（`latest`依存をなくす）
3. **OIDC鍵レスで認証**し、長期アクセスキーを一切置かない
4. **SIGTERMを正しく処理**して、デプロイ・スケールイン・中断のたびに綺麗に終わる
5. **自動ロールバックを有効にする**（サーキットブレーカー or Blue/Greenのbake時間）

私はこの型で、`API Gateway → NLB → ALB → ECS on Fargate` 上の221エンドポイントを本番運用し、決済基盤の二重課金0件を維持してきました。詳しくは[木材流通SaaSの事例](/case-studies/lumber-industry-dx)をご覧ください。

Fargateの計算基盤選定（ECS vs Lambda vs App Runner）は[比較記事](/blog/aws-ecs-fargate-vs-lambda-vs-app-runner-compute-selection-guide)を、IaCのstate管理・モジュール設計は[Terraform記事](/blog/terraform-module-design-state-isolation-drift-detection-guide)を、デプロイ後の可観測性は[OpenTelemetry × ECS](/blog/aws-observability-opentelemetry-sre-ecs)をあわせて参照してください。
