「新しいコンテナを本番に出した直後に障害が起きたとき、どれだけ速く前の版に戻せるか」——デプロイの本質はここです。速さは二の次。壊れたとき即座に戻せるかが先です。
私は経済産業大臣賞を受賞した木材流通SaaSで、API Gateway → NLB → ALB → ECS on Fargate という構成の上に 221本のAPIエンドポイントを本番運用してきました。決済基盤は本番二重課金0件を維持しています。「一人 × 生成AI(Claude Code)」で本番品質を出すには、デプロイパイプラインそのものを壊れにくく・戻しやすい構造にしておくことが不可欠でした。
この記事は、ECS on Fargate の3つのデプロイ方式を整理し、それぞれをいつ・どう使うかを判断できるようにすることを目的にしています。ECS基盤の設計・ネットワーク・セキュリティの基本はピラー記事に譲り、本稿は**「デプロイそのもの」**に集中します。
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つのパラメータで決まります(公式)。
minimumHealthyPercent:デプロイ中に健全でなければならないタスク数の下限(%、切り上げ)。maximumPercent:デプロイ中に起動してよいタスク数の上限(%、切り下げ)。
min 100% / max 200%(ピラー記事の設定)は最も安全側です。desiredCount=2 なら、新版タスク2つを先に立ち上げ(合計4)、健全を確認してから旧版2つを停止します。一時的にコストが倍になりますが、可用性は一切下がりません。
デプロイサーキットブレーカー
新タスクがクラッシュループし続けているのに気付かずトラフィックを流す事故を防ぐのがデプロイサーキットブレーカーです。
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)
仕組み
デプロイコントローラをECSのまま、デプロイ戦略にBLUE_GREENを選ぶことで有効になります。
- サービス更新が始まると、greenタスク群を起動する。
- green が健全になったらALBのトラフィックをgreenへ切り替える。
bakeTimeInMinutesの間、blue(旧版)とgreen(新版)を両方保持する。この間はクリック一つでblueへ即座に戻せる(インスタントロールバック)。- 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設定例
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 で直接設定します。
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には以下が必要です(公式)。
- ALB(必須。NLBも可だがトラフィックシフト制限あり)
- 本番リスナー(ポート443等)と テストリスナー(オプションだが推奨:ポート8080等)、同一ALBに属す
- ターゲットグループ×2(blue用・green用)
- CodeDeployアプリ(コンピュートプラットフォーム
ECS)とデプロイグループ - AppSpec(タスク定義ARN・コンテナ名・ポート・フックのLambda ARN)
ecsCodeDeployRole(CodeDeployがECSとLBを操作するためのIAMロール)
AppSpec例
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 設定
# 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を使えば、IAMロールへの一時クレデンシャル取得で全て完結します。
パイプライン全体像
git push → GitHub Actions トリガー
→ OIDC で AWS 一時クレデンシャル取得
→ 品質ゲート(型チェック・テスト・脆弱性スキャン)
→ ECR へ CommitSHA タグで push
→ タスク定義の image を新タグに差し替え(新リビジョン登録)
→ ECS サービスへデプロイ(ローリングまたは Blue/Green)
完全な workflow.yml
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のデプロイを開始して完了を待機します。
- 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ロール設定(参考)
{
"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ガイドを参照してください。
⑤デプロイ前品質ゲートとコンテナ側の準備
デプロイ方式をどれだけ洗練させても、コンテナそのものが壊れていれば何も意味がない。サーキットブレーカーや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はタスクを停止します。停止の流れはこうです。
- タスクをALBターゲットグループから登録解除(
deregistration_delayの間、処理中リクエストを保護) - コンテナに
SIGTERMを送る stopTimeout(既定30秒・最大120秒)の間、終了を待つ- 終わらなければ
SIGKILLで強制終了(データ損失リスク)
Blue/Greenデプロイでも、旧版(blue)タスクを削除する際には同じSIGTERMシーケンスが走ります。bake時間の終了後にblueを落とすタイミングでも、SIGTERMを正しく処理できていなければデータ損失が起きます。
ヘルスチェックとの連携で特に重要なのが startPeriod です。コンテナ起動直後の初期化中(DBコネクション確立・キャッシュウォームアップ)にヘルスチェックを失敗させないよう、猶予時間を設定しておきます。
{
"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コード等)はピラー記事に記載しています。本番でのトラブルシュートはECSトラブルシューティング記事が役に立ちます。
ヘルスチェックの二層構造
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設定 |
どの方式を選んでも、「安全に出荷する」ための本質は変わりません。
- 品質ゲートをCIで固める(型・テスト・スキャン)
- CommitSHAタグでイメージの版を固定する(
latest依存をなくす) - OIDC鍵レスで認証し、長期アクセスキーを一切置かない
- SIGTERMを正しく処理して、デプロイ・スケールイン・中断のたびに綺麗に終わる
- 自動ロールバックを有効にする(サーキットブレーカー or Blue/Greenのbake時間)
私はこの型で、API Gateway → NLB → ALB → ECS on Fargate 上の221エンドポイントを本番運用し、決済基盤の二重課金0件を維持してきました。詳しくは木材流通SaaSの事例をご覧ください。
Fargateの計算基盤選定(ECS vs Lambda vs App Runner)は比較記事を、IaCのstate管理・モジュール設計はTerraform記事を、デプロイ後の可観測性はOpenTelemetry × ECSをあわせて参照してください。