- 1 1. この記事について
- 2 2. 前提と全体構成図
- 3 3. CodeDeploy ECS 基礎 — DeploymentGroup / TaskSet / primary-test listener
- 3.1 3-1. CodeDeploy とは — 3つのコンピュートプラットフォーム
- 3.2 3-2. デプロイフェーズと TaskSet — ECS 固有の概念
- 3.3 3-3. DeploymentGroup の設計
- 3.4 3-4. primary listener と test listener の役割
- 3.5 3-5. Terraform: aws_codedeploy_app
- 3.6 3-6. Terraform: aws_codedeploy_deployment_group
- 3.7 3-7. デプロイ状況の確認 — aws deploy get-deployment
- 3.8 3-8. §3 まとめ
- 4 4. ALB 設定 — Blue/Green TG × 2 + 本番/テスト listener
- 4.1 4-1. Blue/Green TG 2本構成の設計思想
- 4.2 4-2. target_type=ip が必須の理由
- 4.3 4-3. healthcheck 設計
- 4.4 4-4. Terraform: aws_lb_target_group × 2
- 4.5 4-5. 本番 / テスト listener の役割分担
- 4.6 4-6. Terraform: aws_lb_listener × 2
- 4.7 4-7. Terraform: aws_lb_listener_rule(priority 設計)
- 4.8 4-8. Blue TG のみ attach — ECS service の設定とドリフト防止
- 4.9 4-9. ALB access log の S3 出力設定(監査証跡)
- 4.10 4-10. §4 まとめ
- 5 Section 5. CodePipeline 拡張 — Blue/Green 対応 Deploy stage
- 6 Section 6. appspec.yaml 設計 — lifecycle hook 実装
- 7 Section 7. Terraform で全体をコード化
- 7.1 7-1. ディレクトリ構成とモジュール拡張方針
- 7.2 7-2. backend.tf / provider — Terraform 1.9 / hashicorp/aws ~> 5.0
- 7.3 7-3. tfvars 拡張 — Blue/Green 固有変数
- 7.4 7-4. modules/codedeploy-bluegreen/alb.tf — target group × 2 + listener × 2
- 7.5 7-5. modules/codedeploy-bluegreen/iam.tf — CodeDeploy サービスロール
- 7.6 7-6. modules/codedeploy-bluegreen/main.tf — CodeDeploy app + deployment_group + Alarm
- 7.7 7-7. modules/codedeploy-bluegreen/lambda_hooks.tf — lifecycle hook Lambda
- 7.8 7-8. modules/ecs-fargate の変更点 — deployment_controller + lifecycle
- 7.9 7-9. environments/dev/main.tf — モジュール統合(one-shot apply)
- 7.10 7-10. one-shot apply 手順
- 7.11 7-11. apply 後の確認
- 7.12 7-12. 本記事で追加・変更する Terraform リソース一覧
- 8 8. deployment_config 選択 — Canary/Linear/AllAtOnce と自動ロールバック
- 8.1 8-1. TrafficShiftStrategy — 3 種の概念と選択基準
- 8.2 8-2. Terraform での 3 種切替 — deployment_config_name 変数
- 8.3 8-3. カスタム deployment_config の作成 — Canary30%/10 分の例
- 8.4 8-4. auto_rollback_configuration — 自動ロールバック設定
- 8.5 8-5. CloudWatch Alarm 設定 — 5xx レートと Unhealthy Host
- 8.6 8-6. ロールバック完了時間の目安
- 8.7 8-7. tfvars への deployment_config 設計指針まとめ
- 9 9. IAM 最小権限設計 — CodeDeploy 実行 Role と PassRole 制約
- 9.1 9-1. 登場するロールの整理
- 9.2 9-2. codedeploy-ecs-service-role — AWS 管理ポリシーの問題点
- 9.3 9-3. 最小権限版 codedeploy-ecs-service-role の Terraform 実装
- 9.4 9-4. PassRole NG 例と修正手順
- 9.5 9-5. variables.tf への PassRole 関連変数追加
- 9.6 9-6. CodePipeline サービスロールの拡張 — CodeDeploy 呼び出し権限
- 9.7 9-7. permission_boundary(任意適用例)
- 9.8 9-8. IAM 設計の全体まとめ
- 10 Section 10. 手動承認ゲートと運用 — wait time / 監視 / destroy 手順
- 11 Section 11. まとめと次の発展
1. この記事について
- ECS×Fargate×CodePipeline AWS Native CI/CD — ECR rolling update 基礎編 — 本シリーズ第1弾。CodePipeline / CodeBuild / ECR / IAM 構成は本記事でも踏襲
- Amazon EventBridge × VPC Lattice × Fargate — Fargate 起動の基礎
- Terraform 1.9.x / hashicorp/aws ~> 5.0 の動作環境
- ALB target group・listener・listener_rule の基本操作
本シリーズの位置づけ:
- 第1弾(公開済): Rolling Update 基礎編 — CodePipeline + CodeBuild + ECR + Fargate の1本完結
- 本記事(第2弾): Blue/Green デプロイ編 — CodeDeploy × ALB target group swap で無停止切替
- 第3弾(予告): マルチ環境パイプライン編 — dev/stg/prod + 手動承認ゲート
本記事は「Git push で CodeDeploy が起動し、ALB target group swap による無停止デプロイが走る環境」を Terraform 1.9 で構築するハンズオンです。第1弾(Rolling Update)で構築した CodePipeline / CodeBuild / ECR / IAM の資産を引き継ぎ、Blue/Green 切替という次のステップへ進みます。
本記事のゴール状態
本記事を最後まで実施すると、以下のパイプラインが動作します。
Git push (main branch)
└─ CodePipeline トリガー(CodeStar Connections 経由)
├─ Source ステージ: GitHub からソース取得
├─ Build ステージ: CodeBuild
│ └─ docker build → docker push → ECR にイメージ登録
│ └─ imageDetail.json 生成(CodeDeploy 用)
└─ Deploy ステージ: CodeDeploy ECS Blue/Green
├─ Green TG に新 Fargate タスク群を起動
├─ テスト listener (port 8080) で疎通確認
├─ Lifecycle hook 実行(BeforeAllowTraffic / AfterAllowTraffic)
└─ 本番 listener (port 80) を Green TG へ一括切替 → Blue TG 解体
「git push するだけで、ALB を無停止のまま新バージョンへ一括切替できる」状態が完成します。切替中もユーザーへの HTTP 応答は途切れず、新バージョンに問題があれば CodeDeploy が自動的にロールバックします。
Rolling Update(第1弾)vs Blue/Green(本記事)
どちらも ECS × Fargate で稼働しますが、タスク切替の仕組みが根本的に異なります。
| 比較軸 | Rolling Update(第1弾) | Blue/Green(本記事) |
|---|---|---|
| 切替方式 | 旧タスクを順次置換 | 新タスク群を別 TG に起動 → 一括切替 |
| 混在期 | あり(新旧タスクが一時共存) | ゼロ(切替は瞬間的) |
| ロールバック | ECS サービス更新で再デプロイ | CodeDeploy が自動ロールバック |
| テスト機会 | デプロイ中は困難 | テスト listener (port 8080) で切替前検証可 |
| ALB リソース数 | 1 listener / 1 target group | 2 listener / 2 target group |
| Terraform 追加リソース | 少ない | CodeDeploy / Blue TG / Green TG / CloudWatch Alarm 等 |
| 適した用途 | 開発・検証環境 / 小規模サービス | 本番環境 / SLO 厳格なサービス |
| 月コスト目安 | 低い(ALB 1 台分) | やや高い(ALB 2 listener + CodeDeploy) |
Blue/Green の核心は「混在期をゼロにする」ことです。 Rolling は旧タスクを 1 つずつ落として新タスクを起動するため、デプロイ中は新旧両バージョンのタスクが同時にリクエストを受けます。ユーザーによってリクエストが旧バージョンに当たるか新バージョンに当たるかが変わり、データスキーマ変更を伴うデプロイでは整合性が崩れるリスクがあります。
Blue/Green は新タスク群(Green TG)を完全起動してから ALB の向き先を一括変更するため、ユーザーからは「ある瞬間を境に全タスクが新バージョンになる」ように見えます。また切替前にテスト listener (port 8080) で新バージョンのヘルスチェックや smoke test を実行できるため、問題を本番トラフィックに影響させる前に検知できます。
AWS Native vs GitHub Actions — 本シリーズの位置づけ
CI/CD を AWS Native(CodePipeline + CodeDeploy)で組むか、GitHub Actions(GHA)で組むかは設計上の重要な選択です。本シリーズは AWS Native 縛りで一貫しています。
| 観点 | AWS Native(本シリーズ) | GitHub Actions |
|---|---|---|
| ツール管理 | AWS マネージドサービスのみ | GitHub Actions runner 管理 |
| IAM 連携 | 自然(IAM ロールで完結) | OIDC + AssumeRole の設定が必要 |
| コスト | CodePipeline / CodeDeploy の従量課金 | Actions 無料枠 + runner コスト |
| 可視性 | AWS コンソールで一元管理 | GitHub UI で管理 |
| 適した組織 | AWS 中心のインフラチーム | GitHub 中心の開発チーム |
どちらが優れているわけではなく、チームのツールスタックと既存の IAM 設計に合わせて選択します。本シリーズでは「AWS サービスだけで完結させる」という学習目的から、AWS Native を選択しています。
所要時間とコスト目安
| 項目 | 目安 |
|---|---|
| 初回構築(Terraform apply + 動作確認) | 90〜120 分 |
| 2回目以降(変更適用) | 30 分 |
| コスト(稼働中 / 時間) | 約 $0.25/h |
| コスト(放置した場合 / 月) | $26 前後 |
terraform destroy を忘れずに実行してください。ALB / Fargate タスク / CodePipeline が稼働したまま放置すると、月 $26 超の課金が発生します。本記事では検証完了後に terraform destroy で全リソースを削除する手順も §11 に含めています。
ECS/Fargate が初めての方へ
第1弾(Rolling Update 基礎編)を先にお読みください。ECS / Fargate の概念・CodePipeline の構成・Terraform による IAM 設計まで、本シリーズで共通して使う基盤をゼロから解説しています。
本記事はその続編として、CodeDeploy と ALB Blue/Green 切替に特化した構成を追加します。第1弾の Terraform コードを流用するため、第1弾を完了していることを前提としています。第1弾で構築した ECR / CodeBuild / CodePipeline(Source + Build)/ IAM の Terraform コードはそのまま残し、本記事では Deploy ステージと追加リソースのみを差分追記します。
第3弾へのつながり
本記事(第2弾)で Blue/Green デプロイの基盤が整うと、次の自然なステップは「環境ごとのパイプライン分離と手動承認ゲート」です。第3弾(マルチ環境パイプライン編)では dev / stg / prod の3環境を単一パイプラインで管理し、stg→prod 間に手動承認を挟む構成を Terraform で実装します。本記事で理解する CodeDeploy DeploymentGroup の設計がそのまま応用できます。

本記事の全体構成
本記事は以下の 11 セクションで構成されています。
| セクション | タイトル | 概要 |
|---|---|---|
| §1(本節) | この記事について | ゴール・Rolling vs Blue/Green・コスト |
| §2 | 前提と全体構成図 | 前提環境・アーキテクチャ・新規 vs 引継ぎリソース |
| §3 | CodeDeploy ECS 基礎 | DeploymentGroup / TaskSet / listener 設計 |
| §4 | Terraform: CodeDeploy 設定 | aws_codedeploy_app / aws_codedeploy_deployment_group |
| §5 | Terraform: ALB Blue/Green 設定 | Blue TG / Green TG / テスト listener |
| §6 | Terraform: CloudWatch Alarm 設定 | 自動ロールバックトリガー設計 |
| §7 | appspec.yaml 解説 | TaskSet マッピング・Lifecycle hook 設定 |
| §8 | buildspec.yml 更新 | imageDetail.json 生成・ECR push 手順 |
| §9 | CodePipeline Deploy ステージ差替え | Blue/Green 用 action 設定 |
| §10 | 動作確認とトラブルシューティング | デプロイ検証・ロールバック確認・よくあるエラー |
| §11 | クリーンアップ | terraform destroy 手順・コスト検証 |
§3〜§9 が本記事のメインコンテンツです。Terraform コードは実際に動作するものを全量掲載しており、コピー&ペーストで環境を構築できます。§10 は実際のデプロイで遭遇しやすいエラーとその対処法をまとめています。
各セクションは独立して参照できるよう設計していますが、初回は §1 から順番に読み進めることを推奨します。特に §3(CodeDeploy ECS 基礎)は Blue/Green の内部動作を理解するうえで重要です。Terraform コードを先に見たい場合は §4 から読み始め、動作原理が気になった時点で §3 に戻る読み方もできます。
本記事で学べること
本記事を通じて、以下のスキルが身につきます。
- CodeDeploy ECS デプロイ設計: DeploymentGroup / TaskSet / primary listener と test listener の役割分担を理解し、Terraform で記述できる
- traffic shift 戦略の選択: AllAtOnce / Canary10%5分 / Linear10%毎1分 の違いと使い分けを実装ベースで習得
- Lifecycle hook の活用:
BeforeAllowTraffic/AfterAllowTrafficで本番切替前後に Lambda や ECS タスクを挟む設計パターン - CloudWatch Alarm 連携ロールバック: 5xx エラー率・CPU 過負荷などのメトリクスで自動ロールバックをトリガーする構成
- Terraform 1.9 での全体 IaC 化:
aws_codedeploy_app/aws_codedeploy_deployment_group/ ALB listener / CloudWatch Alarm を一元管理する実装パターン
これらは本番環境で Blue/Green デプロイを運用するために必要な知識の核心部分です。
2. 前提と全体構成図
前提環境
本記事の手順を実施するには、以下の環境が整っていることを前提としています。
| ツール / リソース | バージョン / 条件 |
|---|---|
| Terraform | 1.9.x 以上 |
| hashicorp/aws provider | ~> 5.0 |
| AWS CLI | v2 |
| Docker | 任意バージョン(ローカルビルド用) |
| GitHub リポジトリ | CodeStar Connections で AWS に接続済み |
| 第1弾の Terraform コード | Rolling Update 編の terraform apply 完了済み |
第1弾で作成した ECR リポジトリ・CodeBuild プロジェクト・CodePipeline(Source + Build ステージ)・IAM ロール群は、本記事でも そのまま引き継ぎます。本記事では Deploy ステージを CodeDeploy ECS Blue/Green に差し替え、必要なリソースを追加します。
全体アーキテクチャ
本記事で構築するシステムの全体像は以下のとおりです。
GitHub (main branch)
│
▼ CodeStar Connections
CodePipeline
├─ Source ステージ (CodeStar)
├─ Build ステージ (CodeBuild)
│ └─ ECR push → imageDetail.json
└─ Deploy ステージ (CodeDeploy ECS Blue/Green)
│
▼
CodeDeploy DeploymentGroup
├─ Green TG に新 Fargate タスク群を起動
│ └─ VPC / サブネット / セキュリティグループ
├─ ALB テスト listener (port 8080) で検証
├─ Lifecycle hook 実行(BeforeAllowTraffic / AfterAllowTraffic)
└─ ALB 本番 listener (port 80) を Green TG へ切替
└─ Blue TG を解体
ALB
├─ listener: port 80 → 本番 TG(Blue または Green)
└─ listener: port 8080 → テスト TG(デプロイ中の新バージョン)
Fargate Service(CodeDeploy 管理)
├─ Blue TG: 旧バージョンタスク群(切替後に解体)
└─ Green TG: 新バージョンタスク群(切替後に本番へ)
ポイントは ALB に 2 本の listener を持たせる点です。port 80 は本番トラフィック用で、常に「現在の安定バージョン」の TG を向いています。port 8080 はテスト用で、デプロイ中の新バージョン(Green TG)を向きます。切替前にテスト listener に対して smoke test を実行し、問題がなければ CodeDeploy が port 80 を Green TG に切り替えます。
本記事で新規追加するリソース(第1弾との差分)
| Terraform リソース | 用途 |
|---|---|
aws_codedeploy_app | ECS コンピュートプラットフォームのアプリ定義 |
aws_codedeploy_deployment_group | Blue/Green 切替ルール・Lifecycle hook・ロールバック設定 |
aws_lb_target_group × 2 | Blue TG(旧バージョン)/ Green TG(新バージョン) |
aws_lb_listener | port 8080 テスト listener |
aws_lb_listener_rule | テスト listener のルーティング設定 |
aws_cloudwatch_metric_alarm × 2 | エラー率上昇時の自動ロールバックトリガー |
appspec.yaml | CodeDeploy へのタスク定義・TG マッピング指示 |
| IAM ポリシー追加 | CodeDeploy サービスロールへの ECS / ALB / CloudWatch 権限 |
第1弾から引き継ぐリソース
以下のリソースは第1弾の Terraform コードから変更なしで引き継ぎます。
- ECR リポジトリ: Docker イメージの保存先。タグ戦略(
:latest+ コミット SHA)もそのまま - CodeBuild プロジェクト: ビルド・ECR プッシュ処理。
buildspec.ymlにimageDetail.json生成を追記するのみ - CodePipeline(Source + Build ステージ): GitHub 連携とビルド実行。Deploy ステージのみ差し替え
- IAM ロール基盤: CodePipeline 実行ロール・CodeBuild サービスロール・ECS タスクロール・タスク実行ロール
- VPC / サブネット / セキュリティグループ: ネットワーク基盤は第1弾のものをそのまま利用
Deploy ステージのみ aws_codepipeline の stage ブロックを Blue/Green 用に書き換えます。既存のロールには CodeDeploy 用のポリシーを追加アタッチします。
ALB コスト最適化: 単一 ALB × 2 listener 方式
ALB を 2 台(本番用 / テスト用)用意する構成も可能ですが、コスト上の理由から本記事では単一 ALB に 2 本の listener を持たせる方式を採用します。
単一 ALB
├─ listener port 80 (本番)
└─ listener port 8080(テスト)
ALB は台数単位で課金されるため、2 台構成では月間コストが 2 倍になります。単一 ALB + 2 listener 方式にすることで、テスト listener の費用($0.0225 × listener 時間)のみで Blue/Green 検証ができます。テスト listener は CodeDeploy デプロイ中のみ新バージョンへトラフィックを流し、切替完了後は本番 listener のみが機能する点も注意してください。

3. CodeDeploy ECS 基礎 — DeploymentGroup / TaskSet / primary-test listener
3-1. CodeDeploy とは — 3つのコンピュートプラットフォーム
AWS CodeDeploy は、アプリケーションのデプロイを自動化するマネージドサービスです。2種類の AWS サービス(ECS・Lambda)とオンプレミス/EC2 の合わせて 3つのコンピュートプラットフォームをサポートし、それぞれデプロイ戦略が大きく異なります。
CodeDeploy 未経験者向け:まず全体像を把握しよう
Rolling Update(第1弾)では aws ecs update-service を使ってタスクを順次入れ替えましたが、Blue/Green では CodeDeploy が ALB のトラフィック切替を完全制御します。「新しいバージョンを別グループに起動し、問題なければ本番に切り替え、問題があれば即座に戻す」という仕組みです。
自動ロールバック・承認ウィンドウ・段階的トラフィックシフトが標準機能として提供されており、運用負荷を大幅に削減できます。
| プラットフォーム | 対象サービス | デプロイ戦略 | Agent インストール | CodeDeploy 料金 |
|---|---|---|---|---|
| EC2 / On-Premises | EC2 インスタンス・オンプレサーバー | In-Place / Blue/Green | 必要(CodeDeploy Agent をインスタンスに導入) | 無料 |
| Lambda | Lambda 関数のバージョン/エイリアス | Canary / Linear / AllAtOnce | 不要(Lambda はサーバーレス) | 無料 |
| ECS(本記事) | ECS サービス + ALB Target Group | Canary / Linear / AllAtOnce(Blue/Green のみ) | 不要(Fargate は AWS 管理・Agent 概念なし) | 無料 |
ポイント: ECS プラットフォームの最大の特徴は「Blue/Green デプロイのみ」をサポートする点です。Rolling Update は ECS 組み込みの機能(第1弾)であり、CodeDeploy とは独立しています。本記事では ECS + CodeDeploy の組み合わせで ダウンタイムゼロのデプロイ を実現します。
3-2. デプロイフェーズと TaskSet — ECS 固有の概念
CodeDeploy ECS が導入する最重要概念が TaskSet です。通常の ECS サービスは「タスク群」を1セットだけ管理しますが、Blue/Green デプロイ中は ORIGINAL(旧タスク群) と REPLACEMENT(新タスク群) の2セットを同時に維持します。
デプロイの進行はフェーズとして管理されます。以下の Python Enum は本記事で定義する DeploymentPhase です(後続セクションの監視実装でも使用します):
from enum import Enum
class DeploymentPhase(Enum):
"""Blue/Green デプロイのフェーズ。"""
ORIGINAL = "Original"# 旧タスク群のみ稼働(デプロイ前)
REPLACEMENT = "Replacement"# 新タスク群起動中・test listener で検証可能
SHIFTED= "Shifted" # 本番 listener 切替完了・旧タスク群は待機中
TERMINATED= "Terminated" # termination_wait 経過後・旧タスク群削除完了
各フェーズの詳細を時系列で解説します:
| フェーズ | 状態 | ALB の振り向け先 | ロールバック可否 |
|---|---|---|---|
ORIGINAL | デプロイ前・旧タスク群100%稼働 | Blue TG(旧) | — |
REPLACEMENT | 新タスク群が Green TG に起動完了・test listener(8080)で疎通検証可能 | Blue TG(旧)100% + test listener は Green TG | ✅ 即時 |
SHIFTED | 本番 listener(80)が Green TG に切替完了・旧タスク群は termination_wait 分だけ維持 | Green TG(新)100% | ✅ termination_wait 内は可 |
TERMINATED | 旧タスク群削除完了・完全移行 | Green TG(新)100% | ❌ 不可 |
Q3 対応(未経験者向け):
REPLACEMENTフェーズ中は、エンドユーザーへの影響は一切ありません。新タスク群はまだトラフィックを受けておらず、テスト用ポート(8080)でのみアクセス可能です。ここで不具合が見つかればaws deploy stop-deploymentで即座にロールバックできます。
3-3. DeploymentGroup の設計
DeploymentGroup は「どの ECS サービスに・どのように・どの ALB を使って」デプロイするかを定義する設定の集合体です。ECS プラットフォームの場合、以下の主要ブロックで構成されます。
主要設定項目
| 設定キー | 役割 | 本記事の設定値 |
|---|---|---|
deployment_config_name | トラフィックシフト戦略の選択 | CodeDeployDefault.ECSCanary10Percent5Minutes |
deployment_style.deployment_type | デプロイ種別 | BLUE_GREEN(ECS プラットフォームでは固定) |
deployment_style.deployment_option | トラフィック制御有無 | WITH_TRAFFIC_CONTROL(ECS では固定) |
blue_green_deployment_config | Blue/Green 固有設定(後述) | termination_wait: 10分 |
ecs_service | 対象 ECS サービスとクラスター | my-app-service / my-app-cluster |
load_balancer_info | 本番 listener ARN + テスト listener ARN + TG ペア | Blue TG / Green TG + listener × 2 |
auto_rollback_configuration | 自動ロールバック条件 | DEPLOYMENT_FAILURE / STOP_ON_ALARM |
alarm_configuration | CloudWatch Alarm 連携 | 5xx count / unhealthy host count |
ECS プラットフォームでは deployment_type = BLUE_GREEN と deployment_option = WITH_TRAFFIC_CONTROL が必須固定値です。他の組み合わせ(IN_PLACE 等)はサポートされず、Terraform でエラーになります。
blue_green_deployment_config の詳細
# deployment_ready_option
wait_time_in_minutes: 10# test listener 疎通後、手動承認を10分待機(0=即時切替)
# green_fleet_provisioning_option
action: DISCOVER_EXISTING # CodeDeploy が ECS API 経由で新タスクを自動起動
# terminate_blue_instances_on_deployment_success
action: TERMINATE
termination_wait_time_in_minutes: 10 # 本番切替後、旧タスク群を10分維持してから削除
3-4. primary listener と test listener の役割
ALB に2本のリスナーを設定することが Blue/Green デプロイの核心です。
| listener | ポート | 役割 | デプロイ中の挙動 |
|---|---|---|---|
| primary(本番) | 80 | エンドユーザーが実際にアクセスする | デプロイ前: Blue TG → 本番切替後: Green TG |
| test(テスト) | 8080 | 新タスク群の疎通確認専用(開発者・監視向け) | REPLACEMENT フェーズ中のみ Green TG に接続 |
test listener は REPLACEMENT → SHIFTED の切替完了後に CodeDeploy が自動的にトラフィックを停止します。8080 ポートは「本番前最終確認ゾーン」として機能します。CodeDeploy Agent 不要: ECS Fargate ではインスタンスの概念がなく、CodeDeploy は ECS API と ALB API を直接呼び出してデプロイを実行するため Agent のインストール作業は不要です。
3-5. Terraform: aws_codedeploy_app
最小構成の CodeDeploy アプリケーションを Terraform で定義します。
# ============================================================
# CodeDeploy: アプリケーション定義
# Terraform 1.9.x / hashicorp/aws ~> 5.0
# ============================================================
terraform {
required_version = "~> 1.9"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
resource "aws_codedeploy_app" "main" {
name = "${var.project_name}-app"
compute_platform = "ECS" # ECS プラットフォーム固定(EC2 / Lambda は別記事)
tags = {
Project = var.project_name
Environment = var.environment
ManagedBy= "terraform"
}
}
compute_platform = "ECS" を明示的に指定することが重要です。省略するとデフォルト(Server = EC2/On-Premises)が適用され、DeploymentGroup 作成時にエラーが発生します。
3-6. Terraform: aws_codedeploy_deployment_group
DeploymentGroup は CodeDeploy 設定の中核です。blue_green_deployment_config ブロックで Blue/Green 固有の挙動を制御します。
# ============================================================
# CodeDeploy: デプロイメントグループ(Blue/Green ECS)
# ============================================================
resource "aws_codedeploy_deployment_group" "main" {
app_name= aws_codedeploy_app.main.name
deployment_group_name = "${var.project_name}-dg"
service_role_arn = aws_iam_role.codedeploy.arn
deployment_config_name = var.deployment_config_name # デフォルト: ECSCanary10Percent5Minutes
# ECS プラットフォームでは BLUE_GREEN + WITH_TRAFFIC_CONTROL が必須固定値
deployment_style {
deployment_type= "BLUE_GREEN"
deployment_option = "WITH_TRAFFIC_CONTROL"
}
# Blue/Green 固有設定
blue_green_deployment_config {
# test listener 疎通後の手動承認待機時間(0 = 即時切替)
deployment_ready_option {
action_on_timeout = "STOP_DEPLOYMENT" # タイムアウト時は自動ロールバック
wait_time_in_minutes = var.deployment_ready_wait_minutes
}
# 新タスク群(Green Fleet)の起動方式
# ECS の場合は DISCOVER_EXISTING 固定(CodeDeploy が ECS API 経由で起動)
green_fleet_provisioning_option {
action = "DISCOVER_EXISTING"
}
# 本番切替後の旧タスク群(Blue Fleet)の廃棄設定
terminate_blue_instances_on_deployment_success {
action= "TERMINATE"
termination_wait_time_in_minutes = var.termination_wait_minutes # 推奨: 10
}
}
# 対象 ECS サービス(サービス名 + クラスター名のペアで一意に識別)
ecs_service {
cluster_name = aws_ecs_cluster.main.name
service_name = aws_ecs_service.main.name
}
# ALB リスナー × 2 + TG ペアの設定
load_balancer_info {
target_group_pair_info {
# 本番 listener(port 80)
prod_traffic_route {
listener_arns = [aws_lb_listener.prod.arn]
}
# テスト listener(port 8080)— REPLACEMENT フェーズ中の疎通確認専用
test_traffic_route {
listener_arns = [aws_lb_listener.test.arn]
}
# Blue TG(現行)と Green TG(新規)のペア
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"]
}
# CloudWatch Alarm 連携(5xx 過多 or 不健全タスクが増えたら自動ロールバック)
alarm_configuration {
alarms = ["${var.project_name}-alb-5xx-count", "${var.project_name}-ecs-unhealthy-count"]
enabled = true
}
tags = {
Project = var.project_name
Environment = var.environment
ManagedBy= "terraform"
}
}
変数定義(terraform.tfvars 参考値)
# terraform.tfvars(参考値)
project_name = "my-app"
environment = "prod"
deployment_config_name= "CodeDeployDefault.ECSCanary10Percent5Minutes"
deployment_ready_wait_minutes = 10# 手動承認ウィンドウ(分)
termination_wait_minutes = 10# 旧タスク廃棄待機時間(分)
3-7. デプロイ状況の確認 — aws deploy get-deployment
CodeDeploy のデプロイ実行後は、以下のコマンドでリアルタイムに状況を追跡できます。
# 最新のデプロイ ID を取得してステータス確認
DEPLOYMENT_ID=$(aws deploy list-deployments \
--application-name "my-app-app" \
--deployment-group-name "my-app-dg" \
--include-only-statuses "InProgress" \
--query "deployments[0]" --output text --region ap-northeast-1)
aws deploy get-deployment \
--deployment-id "${DEPLOYMENT_ID}" \
--region ap-northeast-1 \
--query "deploymentInfo.{Status:status,CreateTime:createTime,CompleteTime:completeTime}" \
--output table
# ライフサイクルイベントの詳細(どの hook で止まっているかを特定)
aws deploy get-deployment-instance \
--deployment-id "${DEPLOYMENT_ID}" \
--instance-id "i-ecs-replacement" \
--region ap-northeast-1 \
--query "instanceSummary.lifecycleEvents[*].{Event:lifecycleEventName,Status:status}" \
--output table
Status フィールドは InProgress → Ready → Succeeded の順に遷移します。ECS の instance-id はコンソールの「Deployment targets」タブから ARN 形式で確認できます。

3-8. §3 まとめ
| 概念 | ポイント |
|---|---|
| コンピュートプラットフォーム | EC2/Lambda/ECS の3種。ECS は Agent 不要・Blue/Green のみ |
| TaskSet(ORIGINAL/REPLACEMENT) | Blue/Green デプロイ中に2セットのタスク群を同時維持 |
| deployment_style | ECS では BLUE_GREEN + WITH_TRAFFIC_CONTROL 固定 |
| primary / test listener | port 80(本番)/ port 8080(検証)の2本構成 |
| auto_rollback | DEPLOYMENT_FAILURE と STOP_ON_ALARM で自動復旧 |
次のセクションでは、この CodeDeploy 設定が参照する ALB Target Group × 2 と listener の Terraform 実装 を詳解します。
4. ALB 設定 — Blue/Green TG × 2 + 本番/テスト listener
4-1. Blue/Green TG 2本構成の設計思想
Blue/Green デプロイでは、ALB に接続された 2つのターゲットグループ(TG) が役割を交互に担います。
デプロイ前: 本番 listener(80) → Blue TG → 旧タスク群
デプロイ中: 本番 listener(80) → Blue TG → 旧タスク群(継続)
テスト listener(8080) → Green TG → 新タスク群(疎通確認)
デプロイ後: 本番 listener(80) → Green TG → 新タスク群
テスト listener(8080) → Blue TG → (空・次回の旧タスク用)
重要な設計原則は「ネーミングは固定・役割はデプロイ毎に反転」です。Blue が常に「旧バージョン」を意味するわけではなく、次のデプロイでは Green が「旧」になります。本記事では AWS ドキュメントの慣例に従い blue/green を使用します。
4-2. target_type=ip が必須の理由
Fargate タスクは VPC 内の ENI に直接 IP アドレスが割り当てられます。ホストインスタンスの概念がないため、TG の target_type は必ず ip を指定します(instance を指定するとデプロイ時にターゲット登録エラーが発生します)。
4-3. healthcheck 設計
Blue/Green デプロイの安全性はヘルスチェックの精度に直結します。CodeDeploy は 新タスク群が TG ヘルスチェックをパスしてから 本番 listener を切り替えるため、ヘルスチェックが緩すぎると不健全なタスクが本番に流入します。
| 設定 | 推奨値 | 理由 |
|---|---|---|
path | /healthz | アプリ専用ヘルスエンドポイント(DB接続チェックなど含む) |
port | traffic-port | タスク定義の containerPort と同じポートを自動使用 |
interval | 30 秒 | タスク起動に数十秒かかるため余裕を持たせる |
timeout | 10 秒 | interval の 1/3 未満で設定 |
healthy_threshold | 2 | 2回連続成功でヘルシー判定(チャタリング防止) |
unhealthy_threshold | 3 | 3回連続失敗でアンヘルシー判定 |
matcher | 200-299 | 2xx 系レスポンスをヘルシーとみなす |
4-4. Terraform: aws_lb_target_group × 2
# ============================================================
# ALB Target Group: Blue(現行)と Green(新規)の2本構成
# Terraform 1.9.x / hashicorp/aws ~> 5.0
# ============================================================
# Blue TG — ECS service 起動時に最初に attach する TG
resource "aws_lb_target_group" "blue" {
name = "${var.project_name}-blue-tg"
port = var.container_port# コンテナが listen するポート(例: 8080)
protocol = "HTTP"
target_type = "ip" # Fargate: ENI の IP アドレスで登録(必須)
vpc_id= aws_vpc.main.id
health_check {
enabled = true
path = "/healthz"
protocol= "HTTP"
port = "traffic-port" # container_port と同一ポートを使用
interval= 30
timeout = 10
healthy_threshold= 2
unhealthy_threshold = 3
matcher = "200-299"
}
# Blue/Green デプロイ中は CodeDeploy が target_group を動的に操作する
# Terraform の lifecycle で外部変更(CodeDeploy による登録/解除)を無視する
lifecycle {
create_before_destroy = true
}
tags = {
Name = "${var.project_name}-blue-tg"
Role = "blue"
Project = var.project_name
Environment = var.environment
ManagedBy= "terraform"
}
}
# Green TG — CodeDeploy がデプロイ時に新タスクを登録する TG
resource "aws_lb_target_group" "green" {
name = "${var.project_name}-green-tg"
port = var.container_port
protocol = "HTTP"
target_type = "ip"
vpc_id= aws_vpc.main.id
health_check {
enabled = true
path = "/healthz"
protocol= "HTTP"
port = "traffic-port"
interval= 30
timeout = 10
healthy_threshold= 2
unhealthy_threshold = 3
matcher = "200-299"
}
lifecycle {
create_before_destroy = true
}
tags = {
Name = "${var.project_name}-green-tg"
Role = "green"
Project = var.project_name
Environment = var.environment
ManagedBy= "terraform"
}
}
4-5. 本番 / テスト listener の役割分担
2本の listener は役割が明確に分離されています。
| listener | ポート | ターゲット(デプロイ前) | 用途 |
|---|---|---|---|
| 本番(prod) | 80 | Blue TG | エンドユーザーのリクエスト受付 |
| テスト(test) | 8080 | Green TG(REPLACEMENT フェーズ中のみ) | 開発者・監視システムの事前検証 |
テスト listener のポートは一般的に 8080 を使用しますが、443(HTTPS)でテスト TLS 検証を行うことも可能です。本記事では HTTP の 8080 を採用します。
セキュリティ注意: テスト listener のポート(8080)は、エンドユーザーに公開する必要はありません。セキュリティグループで 8080 へのアクセスを開発者・監視サーバーの IP レンジのみに制限することを強く推奨します。
4-6. Terraform: aws_lb_listener × 2
# ============================================================
# ALB: 本番 listener(port 80)
# ============================================================
resource "aws_lb_listener" "prod" {
load_balancer_arn = aws_lb.main.arn
port = 80
protocol = "HTTP"
# デフォルトアクション: Blue TG に転送(初期状態)
# CodeDeploy がデプロイ時に Green TG へ自動切替する
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.blue.arn
}
# CodeDeploy が listener のデフォルトアクションを動的に書き換えるため
# Terraform の plan で差分が出ることがある → ignore_changes で回避
lifecycle {
ignore_changes = [default_action]
}
tags = {
Name = "${var.project_name}-prod-listener"
Port = "80"
Role = "production"
Project = var.project_name
Environment = var.environment
ManagedBy= "terraform"
}
}
# ============================================================
# ALB: テスト listener(port 8080)— REPLACEMENT フェーズ専用
# ============================================================
resource "aws_lb_listener" "test" {
load_balancer_arn = aws_lb.main.arn
port = 8080
protocol = "HTTP"
# テスト listener の初期アクション(デプロイ前は 404 を返す)
# CodeDeploy が REPLACEMENT フェーズ中に Green TG へ自動切替する
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
message_body = "No active deployment"
status_code = "404"
}
}
lifecycle {
ignore_changes = [default_action]
}
tags = {
Name = "${var.project_name}-test-listener"
Port = "8080"
Role = "test"
Project = var.project_name
Environment = var.environment
ManagedBy= "terraform"
}
}
4-7. Terraform: aws_lb_listener_rule(priority 設計)
listener_rule は、特定の条件に合致するリクエストを特定の TG に転送するルールです。Blue/Green デプロイでは default_action に加えて、テスト listener に認証ヘッダー付きルールを追加するパターンが有効です。priority は数値が小さいほど優先度が高く(1-50000)、テスト専用ルールには 50〜99 を割り当てるのが一般的です。
# テスト listener: X-Deploy-Test: true ヘッダー付きリクエストのみ Green TG へ転送
# 監視システムや開発者が新タスクの疎通確認に使用する
resource "aws_lb_listener_rule" "test_canary" {
listener_arn = aws_lb_listener.test.arn
priority = 50
action {
type = "forward"
target_group_arn = aws_lb_target_group.green.arn
}
condition {
http_header {
http_header_name = "X-Deploy-Test"
values = ["true"]
}
}
lifecycle {
ignore_changes = [action]
}
tags = {
Name= "${var.project_name}-test-canary-rule"
Priority = "50"
ManagedBy = "terraform"
}
}
4-8. Blue TG のみ attach — ECS service の設定とドリフト防止
ECS service の load_balancer ブロックは Blue TG のみを指定します。Green TG は CodeDeploy がデプロイ実行時に動的にアタッチするため、Terraform の設定に含めません。
# ============================================================
# ECS Service: load_balancer は Blue TG のみ指定(drift 防止)
# ============================================================
resource "aws_ecs_service" "main" {
name= "${var.project_name}-service"
cluster= aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.main.arn
desired_count= var.desired_count
# Blue/Green デプロイ: deployment_controller を CODE_DEPLOY に切替
# Rolling Update(第1弾)では ECS を指定していたが、本記事では CODE_DEPLOY が必須
deployment_controller {
type = "CODE_DEPLOY"
}
# load_balancer は Blue TG のみ — Green TG は CodeDeploy が動的に attach
load_balancer {
target_group_arn = aws_lb_target_group.blue.arn
container_name= var.container_name
container_port= var.container_port
}
network_configuration {
subnets = aws_subnet.private[*].id
security_groups = [aws_security_group.ecs_tasks.id]
assign_public_ip = false
}
# CRITICAL: ignore_changes で CodeDeploy との競合を防止
# task_definition: CodeDeploy がデプロイ毎に新しい task def を設定する
# load_balancer:CodeDeploy が Blue/Green を切り替える際に書き換える
lifecycle {
ignore_changes = [task_definition, load_balancer]
}
depends_on = [
aws_lb_listener.prod,
aws_iam_role_policy_attachment.ecs_execution,
]
tags = {
Project = var.project_name
Environment = var.environment
ManagedBy= "terraform"
}
}
ドリフト防止の重要性:
ignore_changes = [task_definition, load_balancer]を省略すると、次回のterraform planで「CodeDeploy が書き換えた load_balancer 設定が Terraform のコードと異なる」として変更差分が検出されます。これをterraform applyすると Blue TG に戻る → デプロイが失敗するという悪循環が発生します。
4-9. ALB access log の S3 出力設定(監査証跡)
Blue/Green デプロイでは、トラフィック切替のタイミングと実際のリクエスト分布を記録することが重要です。ALB access log を S3 に出力することで、監査証跡とポストモーテム分析が可能になります。
resource "aws_s3_bucket" "alb_logs" {
bucket = "${var.project_name}-alb-logs-123456789012"
force_destroy = false
tags = {
Name = "${var.project_name}-alb-logs"
Project = var.project_name
Environment = var.environment
ManagedBy= "terraform"
}
}
# 東京リージョン ELB サービスアカウント(582318560864)に PutObject を許可
resource "aws_s3_bucket_policy" "alb_logs" {
bucket = aws_s3_bucket.alb_logs.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Sid = "AllowALBLogDelivery"
Effect = "Allow"
Principal = { AWS = "arn:aws:iam::582318560864:root" }
Action = "s3:PutObject"
Resource = "${aws_s3_bucket.alb_logs.arn}/alb-logs/AWSLogs/123456789012/*"
}]
})
}
# ALB 本体の access_logs ブロックで S3 出力を有効化
resource "aws_lb" "main" {
name= "${var.project_name}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets= aws_subnet.public[*].id
access_logs {
bucket = aws_s3_bucket.alb_logs.bucket
prefix = "alb-logs"
enabled = true
}
tags = {
Name = "${var.project_name}-alb"
Project = var.project_name
Environment = var.environment
ManagedBy= "terraform"
}
}
access log の target_group_arn フィールド(16列目)で Blue/Green のトラフィック分布を確認できます。zcat *.gz | awk '{print $17}' で TG ARN ごとに集計し、切替タイミングを把握できます。S3 lifecycle rule で 90日後に自動削除するよう設定することを推奨します。
4-10. §4 まとめ
| 設定 | 設計ポイント |
|---|---|
target_type = "ip" | Fargate(ENI 直接接続)では必須 |
healthcheck /healthz | アプリ専用エンドポイントで DB 含む総合チェック |
| TG × 2(Blue / Green) | 役割はデプロイ毎に反転・ネーミングは固定 |
| listener × 2(80 / 8080) | 80: 本番、8080: 疎通検証専用(セキュリティグループで制限推奨) |
ignore_changes | task_definition / load_balancer 必須(CodeDeploy との競合防止) |
| ALB access log | S3 出力で監査証跡確保・lifecycle で 90日後自動削除 |
§3 と §4 の設定が揃ったことで、CodeDeploy による Blue/Green トラフィック制御の基盤が完成しました。次のセクション(§5・§6)では、CodePipeline から CodeDeploy を呼び出す Deploy stage の設定と appspec.yaml の実装を詳解します。
Section 5. CodePipeline 拡張 — Blue/Green 対応 Deploy stage
第1弾(Rolling Update 編)の Deploy stage provider を ECS から CodeDeployToECS に変更し、CodeDeploy が必要とする3種類の artifact を供給する構成に更新する。第1弾との差分を中心に解説する。
5-1. 第1弾との設計差分
| 比較項目 | 第1弾(Rolling Update) | 第2弾(Blue/Green) |
|---|---|---|
| Deploy stage provider | ECS | CodeDeployToECS |
| Deploy に必要な artifact | imagedefinitions.json × 1 | appspec.yaml + taskdef.json + イメージ URI × 3 |
| artifact 生成元 | CodeBuild primary artifact | CodeBuild secondary-artifacts × 2 + primary artifact |
| デプロイエンジン | ECS サービス自体(ローリング置換) | CodeDeploy ECS(TaskSet swap) |
| ロールバック方法 | タスク定義を前バージョンに戻す | CodeDeploy が ORIGINAL TaskSet を保持・自動復元 |
| テスト検証機会 | なし(即時本番反映) | test listener(port 8080)でトラフィック確認後に本番切替 |
最重要変更点: 第1弾の imagedefinitions.json は不要になる。代わりに appspec.yaml(デプロイ定義)・taskdef.json(タスク定義テンプレート)・imageDetail.json(イメージ URI)を生成する。
5-2. Blue/Green 対応 buildspec.yml(完全版)
# buildspec.yml(Blue/Green 対応完全版)
version: 0.2
env:
variables:
AWS_DEFAULT_REGION: "ap-northeast-1"
parameter-store:
ECR_REGISTRY: "/myapp/ecr/registry"
ECR_REPOSITORY: "/myapp/ecr/repository"
CONTAINER_NAME: "/myapp/ecs/container_name"
TASK_DEFINITION_ARN: "/myapp/ecs/task_definition_arn"
phases:
pre_build:
commands:
- aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $ECR_REGISTRY
- IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c1-8)
- IMAGE_URI="${ECR_REGISTRY}/${ECR_REPOSITORY}:${IMAGE_TAG}"
- aws ecs describe-task-definition --task-definition $TASK_DEFINITION_ARN --query taskDefinition > taskdef-current.json
build:
commands:
- docker build -t $IMAGE_URI .
- docker push $IMAGE_URI
post_build:
commands:
# imageDetail.json: CodeDeployToECS が Image1ArtifactName として参照
- printf '{"ImageURI":"%s"}' $IMAGE_URI > imageDetail.json
# taskdef.json: <IMAGE1_NAME> placeholder を埋め込むことで CodeDeployToECS が自動置換
- |
jq --arg PLACEHOLDER "<IMAGE1_NAME>" \
--arg CONTAINER "$CONTAINER_NAME" \
'.containerDefinitions |= map(
if .name == $CONTAINER then .image = $PLACEHOLDER else . end
) |
del(.taskDefinitionArn, .revision, .status, .requiresAttributes,
.placementConstraints, .compatibilities, .registeredAt, .registeredBy)' \
taskdef-current.json > taskdef.json
# appspec.yaml: lifecycle hook ARN を指定(§6 で詳述)
- |
cat > appspec.yaml << 'EOF'
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: <TASK_DEFINITION>
LoadBalancerInfo:
ContainerName: "$CONTAINER_NAME"
ContainerPort: 8080
Hooks:
- BeforeAllowTraffic: "arn:aws:lambda:ap-northeast-1:ACCOUNT_ID:function:myapp-before-allow-traffic"
- AfterAllowTraffic: "arn:aws:lambda:ap-northeast-1:ACCOUNT_ID:function:myapp-after-allow-traffic"
EOF
sed -i "s/\$CONTAINER_NAME/$CONTAINER_NAME/g" appspec.yaml
artifacts:
files:
- imageDetail.json
name: BuildOutput
secondary-artifacts:
AppSpecOutput:
files:
- appspec.yaml
name: AppSpecOutput
TaskDefOutput:
files:
- taskdef.json
name: TaskDefOutput
<IMAGE1_NAME>placeholder: CodeDeployToECS action が taskdef.json 内の<IMAGE1_NAME>をimageDetail.jsonのイメージ URI で自動置換する。
5-3. CodePipeline Terraform 定義(Source + Build + Deploy 3ステージ完成形)
CodeBuild project(secondary-artifacts 追加)
# Terraform 1.9.x / hashicorp/aws ~> 5.0
resource "aws_codebuild_project" "myapp" {
name = "myapp-build"
build_timeout = 20
service_role = aws_iam_role.codebuild.arn
source {
type= "CODEPIPELINE"
buildspec = "buildspec.yml"
}
artifacts {
type = "CODEPIPELINE"
name = "BuildOutput"
}
secondary_artifacts {
type = "CODEPIPELINE"
artifact_identifier = "AppSpecOutput"
name = "AppSpecOutput"
}
secondary_artifacts {
type = "CODEPIPELINE"
artifact_identifier = "TaskDefOutput"
name = "TaskDefOutput"
}
environment {
compute_type = "BUILD_GENERAL1_SMALL"
image = "aws/codebuild/standard:7.0"
type= "LINUX_CONTAINER"
privileged_mode = true
}
logs_config {
cloudwatch_logs {
group_name = "/aws/codebuild/myapp-build"
}
}
tags = { Project = "myapp", ManagedBy = "terraform" }
}
CodePipeline 本体
# Terraform 1.9.x / hashicorp/aws ~> 5.0
resource "aws_codepipeline" "myapp" {
name = "myapp-pipeline"
role_arn = aws_iam_role.codepipeline.arn
artifact_store {
location = aws_s3_bucket.pipeline_artifacts.bucket
type = "S3"
encryption_key {
id= aws_kms_key.pipeline.arn
type = "KMS"
}
}
stage {
name = "Source"
action {
name = "Source"
category= "Source"
owner= "AWS"
provider= "CodeStarSourceConnection"
version = "1"
output_artifacts = ["SourceOutput"]
configuration = {
ConnectionArn = aws_codestarconnections_connection.github.arn
FullRepositoryId = "your-org/myapp"
BranchName = "main"
DetectChanges = "true"
}
}
}
stage {
name = "Build"
action {
name = "Build"
category = "Build"
owner = "AWS"
provider = "CodeBuild"
version = "1"
input_artifacts = ["SourceOutput"]
output_artifacts = ["BuildOutput", "AppSpecOutput", "TaskDefOutput"]
configuration = {
ProjectName= aws_codebuild_project.myapp.name
PrimarySource = "SourceOutput"
}
}
}
stage {
name = "Deploy"
action {
name = "Deploy"
category = "Deploy"
owner = "AWS"
provider = "CodeDeployToECS"
version = "1"
# 第1弾(ECS provider)との最大の差分: 3つの artifact を受け取る
input_artifacts = ["BuildOutput", "AppSpecOutput", "TaskDefOutput"]
configuration = {
ApplicationName = aws_codedeploy_app.myapp.name
DeploymentGroupName= aws_codedeploy_deployment_group.myapp.deployment_group_name
TaskDefinitionTemplateArtifact = "TaskDefOutput"
TaskDefinitionTemplatePath = "taskdef.json"
AppSpecTemplateArtifact = "AppSpecOutput"
AppSpecTemplatePath= "appspec.yaml"
Image1ArtifactName = "BuildOutput"
Image1ContainerName= "app"
}
}
}
tags = { Project = "myapp", ManagedBy = "terraform" }
}
5-4. CodePipeline IAM ロールへの追加権限(第1弾差分)
# Terraform 1.9.x / hashicorp/aws ~> 5.0
data "aws_iam_policy_document" "codepipeline_codedeploy" {
statement {
sid = "CodeDeployActions"
effect = "Allow"
actions = [
"codedeploy:CreateDeployment",
"codedeploy:GetDeployment",
"codedeploy:GetDeploymentConfig",
"codedeploy:GetApplicationRevision",
"codedeploy:RegisterApplicationRevision",
]
resources = [
aws_codedeploy_app.myapp.arn,
aws_codedeploy_deployment_group.myapp.arn,
"arn:aws:codedeploy:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:deploymentconfig:*",
]
}
statement {
sid = "ECSTaskDefinition"
effect = "Allow"
actions = ["ecs:RegisterTaskDefinition", "ecs:DescribeTaskDefinition"]
resources = ["*"]
}
statement {
sid = "PassRoleToECS"
effect = "Allow"
actions = ["iam:PassRole"]
resources = [
aws_iam_role.ecs_task_execution.arn,
aws_iam_role.ecs_task.arn,
]
condition {
test = "StringEquals"
variable = "iam:PassedToService"
values= ["ecs-tasks.amazonaws.com"]
}
}
}
resource "aws_iam_role_policy" "codepipeline_codedeploy" {
name= "codedeploy-access"
role= aws_iam_role.codepipeline.id
policy = data.aws_iam_policy_document.codepipeline_codedeploy.json
}
Section 6. appspec.yaml 設計 — lifecycle hook 実装
appspec.yaml は CodeDeploy が deployment を実行する際の「設計図」で、Blue/Green のターゲットサービス設定と lifecycle hook を記述する。本セクションでは appspec.yaml の構造と、自動ロールバックを実現する hook の Lambda 実装を解説する。

6-1. appspec.yaml 完全版
# appspec.yaml(ECS プラットフォーム用・完全版)
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
# <TASK_DEFINITION>: CodeDeployToECS action がタスク定義登録後に自動置換
TaskDefinition: <TASK_DEFINITION>
LoadBalancerInfo:
ContainerName: "app"
ContainerPort: 8080
PlatformVersion: "1.4.0"
Hooks:
- BeforeInstall: "arn:aws:lambda:ap-northeast-1:ACCOUNT_ID:function:myapp-before-install"
- AfterInstall: "arn:aws:lambda:ap-northeast-1:ACCOUNT_ID:function:myapp-after-install"
- AfterAllowTestTraffic: "arn:aws:lambda:ap-northeast-1:ACCOUNT_ID:function:myapp-after-allow-test-traffic"
- BeforeAllowTraffic: "arn:aws:lambda:ap-northeast-1:ACCOUNT_ID:function:myapp-before-allow-traffic"
- AfterAllowTraffic: "arn:aws:lambda:ap-northeast-1:ACCOUNT_ID:function:myapp-after-allow-traffic"
version: 0.0 は ECS プラットフォーム専用の必須指定。<TASK_DEFINITION> は taskdef.json の ECS 登録後に自動置換されるため手動入力不要。
6-2. lifecycle hook 全5種類と実行タイミング
# bluegreen_deployment_contract.py(dataclass 先出し契約)
from enum import Enum
class LifecycleHook(Enum):
"""appspec.yaml で指定可能な lifecycle hook(ECS プラットフォーム)。"""
BEFORE_INSTALL = "BeforeInstall"
AFTER_INSTALL = "AfterInstall"
AFTER_ALLOW_TEST_TRAFFIC = "AfterAllowTestTraffic"
BEFORE_ALLOW_TRAFFIC = "BeforeAllowTraffic"
AFTER_ALLOW_TRAFFIC = "AfterAllowTraffic"
| hook 名 | フェーズ | 実行タイミング | 主な用途 | 重要度 |
|---|---|---|---|---|
BeforeInstall | ORIGINAL → REPLACEMENT | 新タスク ECS 登録前 | DBスキーマ変更・フィーチャーフラグ有効化 | 低 |
AfterInstall | REPLACEMENT 起動後 | 新タスク起動・test listener アクセス可能後 | 新タスク起動確認・DB接続テスト | 中 |
AfterAllowTestTraffic | テストトラフィック流入後 | test listener(port 8080)転送開始後 | 統合テスト・E2E・パフォーマンス計測 | 高 |
BeforeAllowTraffic | 本番切替直前 | 本番 listener 切替前の最終検証 | ヘルスチェック → Failed で自動ロールバック | 最重要 |
AfterAllowTraffic | 本番切替後 | 本番 listener 切替後 | スモークテスト・監視アラート確認 | 高 |
BeforeAllowTrafficが最重要: Lambda がFailedを返した瞬間に CodeDeploy は本番 listener を ORIGINAL TaskSet に戻す。本番切替前の最後の防衛線。
6-3. hook は Lambda 関数で実装する
ECS プラットフォームの hook は必ず Lambda 関数で実装する(EC2 のように Shell スクリプト直接指定は不可)。
実装上の必須要件:
– event 経由で DeploymentId と LifecycleEventHookExecutionId を受け取る
– 処理後に必ず codedeploy.put_lifecycle_event_hook_execution_status でステータスを報告する(省略するとタイムアウトまで待機)
– Lambda timeout = hook timeout − 30 秒 に設定(hook timeout 超過による強制終了を防ぐ)
6-4. BeforeAllowTraffic hook 実装(Python)
本番切替直前に test listener(port 8080)経由でヘルスチェックし、失敗時に自動ロールバックを発動する。
# lambda/before_allow_traffic/handler.py
from __future__ import annotations
import json
import os
import urllib.request
import urllib.error
from dataclasses import dataclass
from enum import Enum
from typing import Any
import boto3
class HookStatus(Enum):
SUCCEEDED = "Succeeded"
FAILED = "Failed"
@dataclass(frozen=True)
class HealthCheckConfig:
test_listener_url: str
health_check_path: str
timeout_seconds: int
retry_count: int
expected_status_code: int
def _build_config() -> HealthCheckConfig:
return HealthCheckConfig(
test_listener_url=os.environ["TEST_LISTENER_URL"],
health_check_path=os.environ.get("HEALTH_CHECK_PATH", "/healthz"),
timeout_seconds=int(os.environ.get("HEALTH_CHECK_TIMEOUT_SECONDS", "5")),
retry_count=int(os.environ.get("HEALTH_CHECK_RETRY_COUNT", "3")),
expected_status_code=int(os.environ.get("EXPECTED_STATUS_CODE", "200")),
)
def _check_health(config: HealthCheckConfig) -> tuple[bool, str]:
url = f"{config.test_listener_url}{config.health_check_path}"
last_error = ""
for attempt in range(1, config.retry_count + 1):
try:
with urllib.request.urlopen(url, timeout=config.timeout_seconds) as response:
if response.status == config.expected_status_code:
return True, f"Health check passed (attempt {attempt}, status={response.status})"
last_error = f"Unexpected status: {response.status}"
except urllib.error.HTTPError as e:
last_error = f"HTTP error: {e.code} {e.reason} (attempt {attempt})"
except urllib.error.URLError as e:
last_error = f"URL error: {e.reason} (attempt {attempt})"
except TimeoutError:
last_error = f"Timeout after {config.timeout_seconds}s (attempt {attempt})"
return False, f"Failed after {config.retry_count} attempts. Last: {last_error}"
def _report_status(
deployment_id: str,
lifecycle_event_hook_execution_id: str,
status: HookStatus,
) -> None:
boto3.client("codedeploy").put_lifecycle_event_hook_execution_status(
deploymentId=deployment_id,
lifecycleEventHookExecutionId=lifecycle_event_hook_execution_id,
status=status.value,
)
def handler(event: dict[str, Any], context: Any) -> dict[str, Any]:
"""
event: {"DeploymentId": "d-XXXXXXXXX", "LifecycleEventHookExecutionId": "..."}
"""
deployment_id: str = event["DeploymentId"]
hook_execution_id: str = event["LifecycleEventHookExecutionId"]
print(f"[BeforeAllowTraffic] deployment_id={deployment_id}")
config = _build_config()
success, message = _check_health(config)
status = HookStatus.SUCCEEDED if success else HookStatus.FAILED
print(f"[BeforeAllowTraffic] {status.value}: {message}")
_report_status(deployment_id, hook_execution_id, status)
return {"statusCode": 200, "body": json.dumps({"message": message})}
Failed返却後の挙動: CodeDeploy が即座に ORIGINAL TaskSet に本番 listener をロールバックし、REPLACEMENT TaskSet を削除する。ユーザーへの影響は最小限に抑えられる。
6-5. AfterAllowTraffic hook 実装(Python)
本番切替後のスモークテスト。本番 listener(port 80/443)に直接アクセスして最終確認する。
# lambda/after_allow_traffic/handler.py
from __future__ import annotations
import json
import os
import urllib.request
from typing import Any
import boto3
def handler(event: dict[str, Any], context: Any) -> dict[str, Any]:
deployment_id: str = event["DeploymentId"]
hook_execution_id: str = event["LifecycleEventHookExecutionId"]
codedeploy = boto3.client("codedeploy")
production_url = os.environ["PRODUCTION_URL"]
smoke_path = os.environ.get("SMOKE_TEST_PATH", "/healthz")
timeout = int(os.environ.get("SMOKE_TEST_TIMEOUT_SECONDS", "10"))
expected_code = int(os.environ.get("EXPECTED_STATUS_CODE", "200"))
try:
with urllib.request.urlopen(f"{production_url}{smoke_path}", timeout=timeout) as resp:
if resp.status != expected_code:
raise ValueError(f"Unexpected status: {resp.status}")
print(f"[AfterAllowTraffic] Smoke test passed")
codedeploy.put_lifecycle_event_hook_execution_status(
deploymentId=deployment_id,
lifecycleEventHookExecutionId=hook_execution_id,
status="Succeeded",
)
return {"statusCode": 200, "body": json.dumps({"message": "Smoke test passed"})}
except Exception as e:
print(f"[AfterAllowTraffic] FAILED: {e}")
codedeploy.put_lifecycle_event_hook_execution_status(
deploymentId=deployment_id,
lifecycleEventHookExecutionId=hook_execution_id,
status="Failed",
)
return {"statusCode": 200, "body": json.dumps({"message": f"Failed: {e}"})}
6-6. hook Lambda の Terraform リソース
# Terraform 1.9.x / hashicorp/aws ~> 5.0
resource "aws_lambda_function" "before_allow_traffic" {
function_name = "myapp-before-allow-traffic"
filename= "${path.module}/lambda/before_allow_traffic.zip"
handler = "handler.handler"
runtime = "python3.12"
role = aws_iam_role.lambda_hook.arn
timeout = 270 # hook timeout(300s) - 30s の余裕を持たせる
environment {
variables = {
TEST_LISTENER_URL= "http://${aws_lb.myapp.dns_name}:8080"
HEALTH_CHECK_PATH= "/healthz"
HEALTH_CHECK_TIMEOUT_SECONDS = "5"
HEALTH_CHECK_RETRY_COUNT = "3"
EXPECTED_STATUS_CODE= "200"
}
}
source_code_hash = filebase64sha256("${path.module}/lambda/before_allow_traffic.zip")
tags = { Project = "myapp", ManagedBy = "terraform" }
}
resource "aws_lambda_function" "after_allow_traffic" {
function_name = "myapp-after-allow-traffic"
filename= "${path.module}/lambda/after_allow_traffic.zip"
handler = "handler.handler"
runtime = "python3.12"
role = aws_iam_role.lambda_hook.arn
timeout = 270
environment {
variables = {
PRODUCTION_URL = "https://${aws_lb.myapp.dns_name}"
SMOKE_TEST_PATH= "/healthz"
SMOKE_TEST_TIMEOUT_SECONDS = "10"
EXPECTED_STATUS_CODE = "200"
}
}
source_code_hash = filebase64sha256("${path.module}/lambda/after_allow_traffic.zip")
tags = { Project = "myapp", ManagedBy = "terraform" }
}
resource "aws_iam_role" "lambda_hook" {
name = "myapp-lambda-hook-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "lambda.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
tags = { Project = "myapp", ManagedBy = "terraform" }
}
data "aws_iam_policy_document" "lambda_hook" {
statement {
sid = "CloudWatchLogs"
effect = "Allow"
actions = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
]
resources = ["arn:aws:logs:*:*:*"]
}
statement {
sid = "CodeDeployHookStatus"
effect = "Allow"
actions = ["codedeploy:PutLifecycleEventHookExecutionStatus"]
resources = [aws_codedeploy_deployment_group.myapp.arn]
}
}
resource "aws_iam_role_policy" "lambda_hook" {
name= "lambda-hook-policy"
role= aws_iam_role.lambda_hook.id
policy = data.aws_iam_policy_document.lambda_hook.json
}
6-7. hook timeout 設計指針
| 設定項目 | デフォルト | 推奨値 | 理由 |
|---|---|---|---|
| hook timeout(CodeDeploy 側) | 3600 秒(1時間) | 300〜600 秒(5〜10 分) | 障害時の復旧遅延を防ぐ |
| Lambda function timeout | 3 秒 | hook timeout − 30 秒 | Lambda 先タイムアウトで Failed を確実に報告する |
| HTTP リクエスト timeout | — | 5〜10 秒 | ALB ヘルスチェック interval に合わせる |
| リトライ回数 | — | 3 回 | 一時的なネットワーク揺らぎへの対応 |
# CodeDeploy の hook timeout 設定確認
aws codedeploy get-deployment-group \
--application-name myapp \
--deployment-group-name myapp-deployment-group \
--query 'deploymentGroupInfo.blueGreenDeploymentConfiguration' \
--output json
Section 7. Terraform で全体をコード化
Section 3〜6 で設計したリソース群(ALB target group × 2・listener × 2・CodeDeploy app/deployment_group・lifecycle hook Lambda)を Terraform モジュールとして統合し、terraform apply 1 回で Blue/Green パイプライン全体が立ち上がる完成形を実装します。
第1弾(Rolling Update 編)の modules/ci-cd を継承しつつ、modules/codedeploy-bluegreen を新規追加する拡張アーキテクチャです。

7-1. ディレクトリ構成とモジュール拡張方針
terraform/
├── environments/
│└── dev/
│ ├── main.tf ← codedeploy-bluegreen モジュール呼び出しを追加
│ ├── variables.tf ← deployment_config_name 等 Blue/Green 変数を追加
│ ├── terraform.tfvars
│ ├── outputs.tf
│ └── backend.tf
└── modules/
├── ecr/ ← 第1弾から変更なし
├── ecs-fargate/← deployment_controller + lifecycle.ignore_changes 追加
├── ci-cd/← CodePipeline Deploy stage を CodeDeployToECS に変更
└── codedeploy-bluegreen/ ← 第2弾で新規追加
├── main.tf ← CodeDeploy app / deployment_group / CloudWatch Alarm
├── alb.tf ← ALB target group × 2 / listener × 2 / listener_rule
├── iam.tf ← codedeploy-ecs-service-role
├── lambda_hooks.tf ← BeforeAllowTraffic / AfterAllowTraffic Lambda
├── variables.tf
└── outputs.tf
設計方針:
– modules/codedeploy-bluegreen は ALB 拡張(TG × 2・listener × 2)と CodeDeploy を一体管理
– ECS service の load_balancer は Blue TG のみ指定(Green TG は CodeDeploy が動的 attach)
– lifecycle { ignore_changes } で CodeDeploy 管理リソースの drift を回避
7-2. backend.tf / provider — Terraform 1.9 / hashicorp/aws ~> 5.0
# environments/dev/backend.tf
terraform {
required_version = "~> 1.9"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
backend "s3" {
bucket= "myorg-terraform-state"
key= "ecs-fargate-cicd2/dev/terraform.tfstate"
region= "ap-northeast-1"
dynamodb_table = "terraform-state-lock"
encrypt = true
}
}
provider "aws" {
region = var.region
default_tags {
tags = {
ManagedBy= "Terraform"
Project = var.project_name
Environment = var.environment
}
}
}
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}
7-3. tfvars 拡張 — Blue/Green 固有変数
第1弾の変数に加え、Blue/Green デプロイ制御用変数を追加します。
# environments/dev/variables.tf(Blue/Green 追加分)
variable "deployment_config_name" {
type = string
default = "CodeDeployDefault.ECSCanary10Percent5Minutes"
description = "CodeDeploy デプロイ設定(Canary/Linear/AllAtOnce)"
}
variable "termination_wait_minutes" {
type = number
default = 5
description = "本番切替後に ORIGINAL TaskSet を保持する猶予期間(分)"
}
variable "container_port" { type = number; default = 8080 }
# environments/dev/terraform.tfvars
project_name = "myapp"
environment = "dev"
region = "ap-northeast-1"
github_repo = "myorg/myapp"
container_port = 8080
desired_count= 2
deployment_config_name= "CodeDeployDefault.ECSCanary10Percent5Minutes"
termination_wait_minutes = 5
vpc_id = "vpc-0abc1234567890abc"
public_subnet_ids = ["subnet-0111aaaa", "subnet-0222bbbb"]
private_subnet_ids = ["subnet-0333cccc", "subnet-0444dddd"]
7-4. modules/codedeploy-bluegreen/alb.tf — target group × 2 + listener × 2
Blue/Green の核心は 2 本の target group と 2 本の listener です。本番 listener(port 80)は常に Blue TG を向き、テスト listener(port 8080)は REPLACEMENT TaskSet(Green)を向いています。CodeDeploy がカットオーバー時に本番 listener の向き先を swap します。
# modules/codedeploy-bluegreen/alb.tf
# Terraform 1.9.x / hashicorp/aws ~> 5.0
resource "aws_lb_target_group" "blue" {
name = "${var.project_name}-blue-tg"
port = var.container_port
protocol = "HTTP"
vpc_id= var.vpc_id
target_type = "ip"# Fargate は ip 指定必須
health_check {
path = var.health_check_path
interval= 30
timeout = 5
healthy_threshold= 2
unhealthy_threshold = 3
matcher = "200"
}
}
resource "aws_lb_target_group" "green" {
name = "${var.project_name}-green-tg"
port = var.container_port
protocol = "HTTP"
vpc_id= var.vpc_id
target_type = "ip"
health_check {
path = var.health_check_path
interval= 30
timeout = 5
healthy_threshold= 2
unhealthy_threshold = 3
matcher = "200"
}
}
# 本番 listener(port 80)— 常に Blue TG を向く
resource "aws_lb_listener" "prod" {
load_balancer_arn = var.alb_arn
port = 80
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.blue.arn
}
# CodeDeploy がカットオーバー時に向き先を Green に書き換えるため drift を無視する
lifecycle {
ignore_changes = [default_action]
}
}
# テスト listener(port 8080)— REPLACEMENT Green TG を向く
resource "aws_lb_listener" "test" {
load_balancer_arn = var.alb_arn
port = 8080
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.green.arn
}
# テスト listener も CodeDeploy が管理するため drift を無視する
lifecycle {
ignore_changes = [default_action]
}
}
resource "aws_lb_listener_rule" "api" {
listener_arn = aws_lb_listener.prod.arn
priority = 100
action {
type = "forward"
target_group_arn = aws_lb_target_group.blue.arn
}
condition {
path_pattern {
values = ["/api/*"]
}
}
lifecycle {
ignore_changes = [action]
}
}
7-5. modules/codedeploy-bluegreen/iam.tf — CodeDeploy サービスロール
# modules/codedeploy-bluegreen/iam.tf
# Terraform 1.9.x / hashicorp/aws ~> 5.0
resource "aws_iam_role" "codedeploy_ecs" {
name = "${var.project_name}-codedeploy-ecs-service-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "codedeploy.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
# AWS マネージドポリシー — ECS Blue/Green に必要な ecs:*TaskSet / elb:ModifyListener / lambda:InvokeFunction 等を包含
resource "aws_iam_role_policy_attachment" "codedeploy_ecs" {
role = aws_iam_role.codedeploy_ecs.name
policy_arn = "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS"
}
7-6. modules/codedeploy-bluegreen/main.tf — CodeDeploy app + deployment_group + Alarm
# modules/codedeploy-bluegreen/main.tf
# Terraform 1.9.x / hashicorp/aws ~> 5.0
resource "aws_codedeploy_app" "myapp" {
name = var.project_name
compute_platform = "ECS"# ECS Blue/Green では必須(EC2/Lambda とは別設定)
}
resource "aws_codedeploy_deployment_group" "myapp" {
app_name= aws_codedeploy_app.myapp.name
deployment_group_name = "${var.project_name}-deployment-group"
deployment_config_name = var.deployment_config_name
service_role_arn = aws_iam_role.codedeploy_ecs.arn
ecs_service {
cluster_name = var.ecs_cluster_name
service_name = var.ecs_service_name
}
load_balancer_info {
target_group_pair_info {
prod_traffic_route {
listener_arns = [aws_lb_listener.prod.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
}
}
}
blue_green_deployment_config {
deployment_ready_option {
action_on_timeout = "CONTINUE_DEPLOYMENT"
wait_time_in_minutes = 0
}
terminate_blue_instances_on_deployment_success {
action = "TERMINATE"
# 本番切替後に ORIGINAL TaskSet を保持する猶予期間。ロールバック用バッファ。
termination_wait_time_in_minutes = var.termination_wait_minutes
}
}
deployment_style {
deployment_option = "WITH_TRAFFIC_CONTROL"
deployment_type= "BLUE_GREEN"
}
auto_rollback_configuration {
enabled = true
events = ["DEPLOYMENT_FAILURE", "DEPLOYMENT_STOP_ON_ALARM"]
}
alarm_configuration {
alarms = [aws_cloudwatch_metric_alarm.error_5xx.alarm_name,
aws_cloudwatch_metric_alarm.unhealthy_host.alarm_name]
enabled = true
}
# 依存関係の明示:ECS service / ALB listener / IAM role が揃ってから作成
depends_on = [
var.ecs_service_id,
aws_lb_listener.prod,
aws_lb_listener.test,
aws_iam_role_policy_attachment.codedeploy_ecs,
]
}
# ── CloudWatch Alarm × 2(自動ロールバックトリガー)──────────────
resource "aws_cloudwatch_metric_alarm" "error_5xx" {
alarm_name = "${var.project_name}-deploy-5xx-count"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name= "HTTPCode_Target_5XX_Count"
namespace = "AWS/ApplicationELB"
period = 60
statistic = "Sum"
threshold = 10
alarm_description= "Blue/Green デプロイ中に 5xx エラー急増で自動ロールバック"
treat_missing_data = "notBreaching"
dimensions = {
LoadBalancer = var.alb_arn_suffix
TargetGroup = aws_lb_target_group.green.arn_suffix
}
}
resource "aws_cloudwatch_metric_alarm" "unhealthy_host" {
alarm_name = "${var.project_name}-deploy-unhealthy-host-count"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name= "UnHealthyHostCount"
namespace = "AWS/ApplicationELB"
period = 60
statistic = "Average"
threshold = 1
alarm_description= "REPLACEMENT TaskSet ヘルスチェック失敗が続く場合に自動ロールバック"
treat_missing_data = "notBreaching"
dimensions = {
LoadBalancer = var.alb_arn_suffix
TargetGroup = aws_lb_target_group.green.arn_suffix
}
}
7-7. modules/codedeploy-bluegreen/lambda_hooks.tf — lifecycle hook Lambda
§6 で実装した BeforeAllowTraffic / AfterAllowTraffic hook Lambda の Terraform 定義です。
# modules/codedeploy-bluegreen/lambda_hooks.tf
# Terraform 1.9.x / hashicorp/aws ~> 5.0
resource "aws_lambda_function" "before_allow_traffic" {
function_name = "${var.project_name}-before-allow-traffic"
filename= "${path.module}/lambda/before_allow_traffic.zip"
handler = "handler.handler"
runtime = "python3.12"
role = aws_iam_role.lambda_hook.arn
# hook timeout(300s) より 30s 短く設定して Lambda 先タイムアウトで Failed を確実に報告する
timeout = 270
environment {
variables = {
TEST_LISTENER_URL= "http://${var.alb_dns_name}:8080"
HEALTH_CHECK_PATH= var.health_check_path
HEALTH_CHECK_TIMEOUT_SECONDS = "5"
HEALTH_CHECK_RETRY_COUNT = "3"
EXPECTED_STATUS_CODE= "200"
}
}
source_code_hash = filebase64sha256("${path.module}/lambda/before_allow_traffic.zip")
}
resource "aws_lambda_function" "after_allow_traffic" {
function_name = "${var.project_name}-after-allow-traffic"
filename= "${path.module}/lambda/after_allow_traffic.zip"
handler = "handler.handler"
runtime = "python3.12"
role = aws_iam_role.lambda_hook.arn
timeout = 270
environment {
variables = {
PRODUCTION_URL = "http://${var.alb_dns_name}"
SMOKE_TEST_PATH= var.health_check_path
SMOKE_TEST_TIMEOUT_SECONDS = "10"
EXPECTED_STATUS_CODE = "200"
}
}
source_code_hash = filebase64sha256("${path.module}/lambda/after_allow_traffic.zip")
}
resource "aws_iam_role" "lambda_hook" {
name= "${var.project_name}-lambda-hook-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{ Effect = "Allow", Principal = { Service = "lambda.amazonaws.com" }, Action = "sts:AssumeRole" }]
})
}
resource "aws_iam_role_policy" "lambda_hook" {
name = "lambda-hook-policy"
role = aws_iam_role.lambda_hook.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{ Sid = "Logs", Effect = "Allow", Action = ["logs:CreateLogGroup","logs:CreateLogStream","logs:PutLogEvents"], Resource = "arn:aws:logs:*:*:*" },
# deployment_group ARN を動的参照して最小権限を実現
{ Sid = "Hook", Effect = "Allow", Action = "codedeploy:PutLifecycleEventHookExecutionStatus", Resource = aws_codedeploy_deployment_group.myapp.arn },
]
})
}
7-8. modules/ecs-fargate の変更点 — deployment_controller + lifecycle
第1弾の aws_ecs_service に 2 箇所変更を加えます。
# modules/ecs-fargate/main.tf(変更差分)
# Terraform 1.9.x / hashicorp/aws ~> 5.0
resource "aws_ecs_service" "app" {
name= var.project_name
cluster= aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
desired_count= var.desired_count
launch_type = "FARGATE"
network_configuration {
subnets = var.private_subnet_ids
security_groups = [aws_security_group.ecs_tasks.id]
assign_public_ip = false
}
# load_balancer は Blue TG のみ指定。
# Green TG は CodeDeploy が動的に attach するため、両方指定すると drift が発生する。
load_balancer {
target_group_arn = var.blue_target_group_arn
container_name= var.container_name
container_port= var.container_port
}
# CodeDeploy による Blue/Green デプロイを有効化
deployment_controller {
type = "CODE_DEPLOY"
}
# CodeDeploy がデプロイのたびに task_definition(新 revision)と load_balancer
# (Green TG への一時付け替え)を書き換えるため、Terraform の drift 検知から除外する。
# これを省略すると terraform plan のたびに差分が出続け apply で競合が発生する。
lifecycle {
ignore_changes = [task_definition, load_balancer]
}
}
deployment_controller = "CODE_DEPLOY" を設定すると、CodeDeploy はデプロイのたびに task_definition(最新 revision 番号)と load_balancer(カットオーバー時の listener 付け替え)を更新します。Terraform はこれらを「State との差分」として検知し、次回 apply 時に元に戻そうとしてデプロイと競合します。ignore_changes でこの自動修正を無効化することで、CodeDeploy と Terraform が安定して共存できます。7-9. environments/dev/main.tf — モジュール統合(one-shot apply)
# environments/dev/main.tf
# Terraform 1.9.x / hashicorp/aws ~> 5.0
module "ecr" {
source = "../../modules/ecr"
project_name = var.project_name
tags= local.common_tags
}
# ECS Fargate(CodeDeploy 対応)
module "ecs_fargate" {
source = "../../modules/ecs-fargate"
project_name = var.project_name
environment= var.environment
vpc_id = var.vpc_id
private_subnet_ids = var.private_subnet_ids
public_subnet_ids= var.public_subnet_ids
container_port= var.container_port
desired_count = var.desired_count
ecr_repository_url = module.ecr.repository_url
# Blue/Green 対応: Blue TG ARN をモジュール間で受け渡し
blue_target_group_arn = module.codedeploy_bluegreen.blue_target_group_arn
container_name = var.project_name
depends_on = [module.ecr, module.codedeploy_bluegreen]
}
# CodeDeploy Blue/Green(ALB + CodeDeploy + Lambda hook)
module "codedeploy_bluegreen" {
source = "../../modules/codedeploy-bluegreen"
project_name = var.project_name
vpc_id = var.vpc_id
alb_arn = module.ecs_fargate.alb_arn
alb_arn_suffix= module.ecs_fargate.alb_arn_suffix
alb_dns_name = module.ecs_fargate.alb_dns_name
container_port= var.container_port
ecs_cluster_name= module.ecs_fargate.cluster_name
ecs_service_name= module.ecs_fargate.service_name
ecs_service_id = module.ecs_fargate.service_id
deployment_config_name= var.deployment_config_name
termination_wait_minutes = var.termination_wait_minutes
depends_on = [module.ecs_fargate]
}
# CI/CD(CodeBuild + CodePipeline — Blue/Green deploy stage)
module "ci_cd" {
source = "../../modules/ci-cd"
project_name = var.project_name
environment= var.environment
github_repo= var.github_repo
github_branch = var.github_branch
ecr_repository_url = module.ecr.repository_url
ecr_repository_arn = module.ecr.repository_arn
# CodePipeline Deploy stage 用(CodeDeployToECS provider)
codedeploy_app_name = module.codedeploy_bluegreen.codedeploy_app_name
codedeploy_deployment_group_name = module.codedeploy_bluegreen.codedeploy_deployment_group_name
depends_on = [module.ecs_fargate, module.codedeploy_bluegreen]
}
locals {
common_tags = {
ManagedBy= "Terraform"
Project = var.project_name
Environment = var.environment
}
}
7-10. one-shot apply 手順
cd terraform/environments/dev
# State バックエンド初期化
terraform init \
-backend-config="bucket=myorg-terraform-state" \
-backend-config="key=ecs-fargate-cicd2/dev/terraform.tfstate" \
-backend-config="region=ap-northeast-1"
# 変更プレビュー
terraform plan -var-file="terraform.tfvars"
# 全リソース一括適用
terraform apply -var-file="terraform.tfvars"
Terraform が自動解決する依存順序:
1. module.ecr← ECR repository + lifecycle
2. module.ecs_fargate(ALB)← VPC / SG / ALB / ECS cluster + service
3. module.codedeploy_bluegreen ← TG × 2 / listener × 2 / CodeDeploy / Lambda hook
4. module.ci_cd ← CodeBuild / CodePipeline(deploy stage 最後)
depends_on を明示しているため、Terraform が正しい順序で apply します。
7-11. apply 後の確認
# CodeDeploy deployment group 設定確認
aws codedeploy get-deployment-group \
--application-name myapp \
--deployment-group-name myapp-deployment-group \
--query 'deploymentGroupInfo.{config:deploymentConfigName,blueGreen:blueGreenDeploymentConfiguration}' \
--output json
# ALB listener 確認(本番 80 / テスト 8080)
aws elbv2 describe-listeners \
--load-balancer-arn $(terraform output -raw alb_arn) \
--query 'Listeners[].{Port:Port,TG:DefaultActions[0].TargetGroupArn}' --output table
# State 確認
terraform state list | grep -E "codedeploy|lb_listener|lb_target_group|lambda"
7-12. 本記事で追加・変更する Terraform リソース一覧
| リソースタイプ | 名前 | 役割 |
|---|---|---|
aws_codedeploy_app | myapp | ECS Blue/Green 親アプリケーション |
aws_codedeploy_deployment_group | myapp-deployment-group | TG swap 制御・自動ロールバック |
aws_lb_target_group × 2 | myapp-blue-tg / myapp-green-tg | ORIGINAL / REPLACEMENT TaskSet |
aws_lb_listener × 2 | port 80 / port 8080 | 本番 / テスト検証 listener |
aws_lb_listener_rule | /api/* | パスベースルーティング |
aws_cloudwatch_metric_alarm × 2 | 5xx_count / unhealthy_host_count | 自動ロールバックトリガー |
aws_iam_role | codedeploy-ecs-service-role | CodeDeploy ECS 操作権限 |
aws_lambda_function × 2 | before/after-allow-traffic | lifecycle hook |
aws_iam_role | lambda-hook-role | Lambda hook 実行権限 |
第1弾から変更:
– aws_ecs_service.app: deployment_controller = CODE_DEPLOY + lifecycle.ignore_changes 追加
– module.ci-cd: CodePipeline Deploy stage を CodeDeployToECS provider に変更
section: “8-9”
cmd_id: cmd_052
target_article: “ECS×Fargate Blue/Greenデプロイ編 §8+§9”
author: worker
status: draft
timestamp: “2026-04-20”
8. deployment_config 選択 — Canary/Linear/AllAtOnce と自動ロールバック
Section 7 で aws_codedeploy_deployment_group に deployment_config_name 変数を持たせました。このセクションでは、その変数に渡す AWS 既定の 3 種の deployment_config を比較し、カスタム設定と自動ロールバック(CloudWatch Alarm 連携)の実装方法を解説します。
8-1. TrafficShiftStrategy — 3 種の概念と選択基準
dataclass 先出し契約(spec §4)で定義した TrafficShiftStrategy Enum を使い、3 種の戦略を先出しします。
# context/skills/bluegreen_deployment_contract.py(再掲:§8 で deployment_config 選択に使用)
from enum import Enum
class TrafficShiftStrategy(Enum):
"""CodeDeploy ECS deployment_config の既定値 + カスタム。"""
ALL_AT_ONCE = "CodeDeployDefault.ECSAllAtOnce"
CANARY_10_5 = "CodeDeployDefault.ECSCanary10Percent5Minutes"
LINEAR_10_EVERY_1 = "CodeDeployDefault.ECSLinear10PercentEvery1Minutes"
| 戦略 | deployment_config_name | 切替方式 | 完了時間目安 | リスク |
|---|---|---|---|---|
ALL_AT_ONCE | CodeDeployDefault.ECSAllAtOnce | 全トラフィックを一括切替 | 30 秒〜1 分 | 高(問題発生時の影響 = 100%) |
CANARY_10_5 | CodeDeployDefault.ECSCanary10Percent5Minutes | 10% 先行 → 5 分待機 → 残 90% 一括 | 5〜7 分 | 中(影響を 10% に限定してから切替) |
LINEAR_10_EVERY_1 | CodeDeployDefault.ECSLinear10PercentEvery1Minutes | 10% ずつ 1 分間隔で段階切替(10 ステップ) | 10〜12 分 | 低(段階的検証・ロールバック猶予が最大) |

選択ガイド:
– AllAtOnce: 開発環境・内部ツール・ダウンタイムが許容できる非本番環境向け。最速だが障害時の影響範囲が 100%。
– Canary(10%/5 分): 本番環境の標準推奨。10% の実トラフィックで新バージョンを 5 分間検証してから残りを切替。5xx レート上昇や unhealthy task 検知で自動ロールバックが働く時間的余裕がある。
– Linear(10%/1 分): 規制業界・金融・医療など障害コストが高い環境向け。10 分かけて段階的に検証。SLA が厳しく、ロールバック猶予を最大化したい場合に選択。
8-2. Terraform での 3 種切替 — deployment_config_name 変数
# environments/dev/terraform.tfvars(切替例)
# --- AllAtOnce(開発環境向け) ---
# deployment_config_name = "CodeDeployDefault.ECSAllAtOnce"
# --- Canary 10%/5 分(本番推奨) ---
deployment_config_name = "CodeDeployDefault.ECSCanary10Percent5Minutes"
# --- Linear 10%/1 分(高信頼性要件向け) ---
# deployment_config_name = "CodeDeployDefault.ECSLinear10PercentEvery1Minutes"
# modules/codedeploy-bluegreen/variables.tf(該当箇所)
variable "deployment_config_name" {
description = "CodeDeploy の deployment_config 名。TrafficShiftStrategy Enum の value を使用。"
type = string
default = "CodeDeployDefault.ECSCanary10Percent5Minutes"
validation {
condition = contains([
"CodeDeployDefault.ECSAllAtOnce",
"CodeDeployDefault.ECSCanary10Percent5Minutes",
"CodeDeployDefault.ECSLinear10PercentEvery1Minutes",
], var.deployment_config_name) || can(regex("^arn:aws:codedeploy:", var.deployment_config_name)) || length(var.deployment_config_name) > 0
error_message = "既定の 3 種または aws_codedeploy_deployment_config で作成したカスタム設定名を指定してください。"
}
}
8-3. カスタム deployment_config の作成 — Canary30%/10 分の例
AWS 既定の 3 種では要件を満たせない場合、aws_codedeploy_deployment_config リソースでカスタム設定を作成できます。
# modules/codedeploy-bluegreen/main.tf(カスタム deployment_config 追加例)
# Terraform 1.9.x / hashicorp/aws ~> 5.0
# カスタム例: 30% 先行 → 10 分待機 → 残 70% 一括(中規模 B2C 本番向け)
resource "aws_codedeploy_deployment_config" "canary_30_10" {
deployment_config_name = "${var.project_name}-canary30-10min"
compute_platform = "ECS"
traffic_routing_config {
type = "TimeBasedCanary"
time_based_canary {
interval= 10# 分:先行 30% が安定したと判断する待機時間
percentage = 30# %:最初に切り替えるトラフィック割合
}
}
}
# リニア例: 20% ずつ 2 分間隔で 5 ステップ切替(合計 8〜10 分)
resource "aws_codedeploy_deployment_config" "linear_20_2" {
deployment_config_name = "${var.project_name}-linear20-2min"
compute_platform = "ECS"
traffic_routing_config {
type = "TimeBasedLinear"
time_based_linear {
interval= 2 # 分:各ステップの待機時間
percentage = 20# %:各ステップで切り替えるトラフィック割合
}
}
}
カスタム設定を使用する場合は、deployment_group の deployment_config_name にカスタムリソースの名前を渡します。
# modules/codedeploy-bluegreen/main.tf(deployment_group への適用)
resource "aws_codedeploy_deployment_group" "myapp" {
# ...(省略)
deployment_config_name = aws_codedeploy_deployment_config.canary_30_10.deployment_config_name
# ...
}
8-4. auto_rollback_configuration — 自動ロールバック設定
auto_rollback_configuration ブロックで、デプロイ失敗時や CloudWatch Alarm 発火時に 自動的に旧バージョンへ切り戻す設定を行います。
# modules/codedeploy-bluegreen/main.tf(自動ロールバック設定部分)
resource "aws_codedeploy_deployment_group" "myapp" {
app_name= aws_codedeploy_app.myapp.name
deployment_group_name = "${var.project_name}-deployment-group"
deployment_config_name = var.deployment_config_name
service_role_arn = aws_iam_role.codedeploy_ecs.arn
# --- 自動ロールバック設定 ---
auto_rollback_configuration {
enabled = true
events = [
"DEPLOYMENT_FAILURE",# デプロイ処理自体の失敗(health check 不合格等)
"DEPLOYMENT_STOP_ON_ALARM" # CloudWatch Alarm 発火による停止
]
}
# --- CloudWatch Alarm 連携 ---
alarm_configuration {
alarms = [
aws_cloudwatch_metric_alarm.error_5xx.alarm_name, # "alb-5xx-rate"
aws_cloudwatch_metric_alarm.unhealthy_host.alarm_name # "ecs-task-unhealthy"
]
enabled = true
ignore_poll_alarm_failure = false # Alarm 取得失敗時はデプロイを続行しない(安全側)
}
# ...(blue_green_deployment_config / deployment_style は §7 参照)
}
events の選択肢:
| イベント | 説明 | 推奨 |
|---|---|---|
DEPLOYMENT_FAILURE | lifecycle hook の失敗・health check 不合格・タイムアウト | 必須 |
DEPLOYMENT_STOP_ON_ALARM | CloudWatch Alarm が ALARM 状態になった場合 | Alarm 連携時は必須 |
DEPLOYMENT_STOP_ON_REQUEST | ユーザーが手動でデプロイを停止した場合 | 任意(監査要件に応じて) |
8-5. CloudWatch Alarm 設定 — 5xx レートと Unhealthy Host
自動ロールバックのトリガーとなる Alarm を 2 本定義します。
# modules/codedeploy-bluegreen/main.tf(CloudWatch Alarm × 2)
# Alarm 1: ALB の 5xx エラー数(新 TG を使い始めてからのエラー急増を検知)
resource "aws_cloudwatch_metric_alarm" "error_5xx" {
alarm_name = "${var.project_name}-alb-5xx-rate"
alarm_description= "ALB 5xx errors > 10 in 1 min — CodeDeploy auto rollback trigger"
comparison_operator = "GreaterThanThreshold"
threshold = 10
evaluation_periods = 1
period = 60 # 秒(1 分)
statistic = "Sum"
namespace = "AWS/ApplicationELB"
metric_name= "HTTPCode_Target_5XX_Count"
dimensions = {
LoadBalancer = var.alb_arn_suffix # ALB ARN の末尾部分(aws_lb.main.arn_suffix)
}
treat_missing_data = "notBreaching" # データなし = 正常扱い(デプロイ直後のラグ対策)
}
# Alarm 2: ECS タスクの Unhealthy host 数(新タスク起動失敗を検知)
resource "aws_cloudwatch_metric_alarm" "unhealthy_host" {
alarm_name = "${var.project_name}-ecs-task-unhealthy"
alarm_description= "Target group unhealthy hosts > 0 — CodeDeploy auto rollback trigger"
comparison_operator = "GreaterThanThreshold"
threshold = 0
evaluation_periods = 2# 2 回連続で ALARM になった場合のみ発火(ノイズ除去)
period = 60
statistic = "Maximum"
namespace = "AWS/ApplicationELB"
metric_name= "UnHealthyHostCount"
dimensions = {
TargetGroup = var.green_target_group_arn_suffix # Green TG(新タスク用)
LoadBalancer = var.alb_arn_suffix
}
treat_missing_data = "notBreaching"
}
ポイント:
– threshold = 0 + evaluation_periods = 2 の組み合わせにより、一時的な health check 失敗(起動直後の 1 回ミス)でロールバックが誤発火しない。
– treat_missing_data = "notBreaching" は Canary 段階で Green TG へのメトリクスデータがない時間帯(トラフィック 0% の初期状態)でも Alarm が誤発火しないための設定。
8-6. ロールバック完了時間の目安
| 戦略 | 自動ロールバック完了まで | 旧タスク(ORIGINAL)の保持時間 |
|---|---|---|
AllAtOnce | 30 秒〜1 分 | termination_wait_minutes 経過後に削除(例: 10 分) |
Canary (10%/5min) | Alarm 発火から 1〜3 分 | Canary フェーズ中なら即時ロールバック可(旧タスクはまだ稼働中) |
Linear (10%/1min) | Alarm 発火から 1〜2 分 | 各ステップ間に 60 秒の猶予があるためロールバック発動が最も速やか |
# ロールバック状況の確認コマンド
aws deploy get-deployment \
--deployment-id <deployment-id> \
--query 'deploymentInfo.{status:status,rollbackInfo:rollbackInfo}' \
--output json
# デプロイ一覧の確認(最新 5 件)
aws deploy list-deployments \
--application-name <app-name> \
--deployment-group-name <group-name> \
--query 'deployments' \
--output table \
--max-items 5
ロールバック成功時は status: "Stopped" の rollbackInfo.rollbackDeploymentId に新しいデプロイ ID が付与されます。このデプロイ ID を追跡することで、旧バージョンへの切り戻しが完了したことを確認できます。
8-7. tfvars への deployment_config 設計指針まとめ
# environments/dev/terraform.tfvars(本番推奨設定例)
project_name = "myapp"
github_repo= "myorg/myapp"
environment= "production"
container_port= 8080
deployment_config_name = "CodeDeployDefault.ECSCanary10Percent5Minutes"
termination_wait_minutes = 10# 本番切替後に ORIGINAL TaskSet を 10 分保持してロールバック猶予を確保
# environments/dev/terraform.tfvars(開発環境向け高速デプロイ設定例)
project_name = "myapp"
github_repo= "myorg/myapp"
environment= "development"
container_port= 8080
deployment_config_name = "CodeDeployDefault.ECSAllAtOnce"
termination_wait_minutes = 0 # 開発環境では旧タスクを即時削除してコスト節約
9. IAM 最小権限設計 — CodeDeploy 実行 Role と PassRole 制約
Section 7 では AWSCodeDeployRoleForECS(AWS 管理ポリシー)を使い CodeDeploy サービスロールを作成しました。本セクションでは、本番運用で推奨する手書きの最小権限ポリシーへ切り替え、さらに iam:PassRole のリソース制約と CodePipeline ロールの拡張方法を解説します。

9-1. 登場するロールの整理
本記事で新規追加・拡張するロールは 3 つです。
| ロール名 | 信頼されるサービス | 用途 |
|---|---|---|
{project}-codedeploy-ecs-service-role | codedeploy.amazonaws.com | CodeDeploy が ECS/ALB/CloudWatch/Lambda を操作する実行ロール |
{project}-pipeline-service-role(第1弾から拡張) | codepipeline.amazonaws.com | CodePipeline の Deploy stage が CodeDeploy を呼び出す追加権限 |
{project}-ecs-task-execution-role(第1弾から継承) | ecs-tasks.amazonaws.com | ECS タスク起動時の ECR pull / CloudWatch Logs 書き込み |
9-2. codedeploy-ecs-service-role — AWS 管理ポリシーの問題点
Section 7 では手早く動かすために AWS 管理ポリシー AWSCodeDeployRoleForECS を使いました。
# NG 例(Section 7 の暫定実装):過剰権限のある AWS 管理ポリシー
resource "aws_iam_role_policy_attachment" "codedeploy_ecs" {
role = aws_iam_role.codedeploy_ecs.name
policy_arn = "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS"
}
AWSCodeDeployRoleForECS の問題点:
– ecs:*(全 ECS API)を "Resource": "*" で許可している
– s3:GetObject / s3:PutObject の範囲が広すぎる
– Lambda 操作が不要なのに lambda:* が含まれるケースがある
本番環境では、必要な API アクションのみを列挙した手書きポリシーに切り替えることを強く推奨します。
9-3. 最小権限版 codedeploy-ecs-service-role の Terraform 実装
# modules/codedeploy-bluegreen/iam.tf
# Terraform 1.9.x / hashicorp/aws ~> 5.0
# ── CodeDeploy ECS サービスロール(最小権限版) ──────────────────
resource "aws_iam_role" "codedeploy_ecs" {
name = "${var.project_name}-codedeploy-ecs-service-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "codedeploy.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy" "codedeploy_ecs_minimal" {
name = "${var.project_name}-codedeploy-ecs-minimal-policy"
role = aws_iam_role.codedeploy_ecs.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
# ECS: タスク定義登録 + サービス/タスクセット操作
{
Sid = "ECSDeployOperations"
Effect = "Allow"
Action = [
"ecs:DescribeServices",
"ecs:CreateTaskSet",
"ecs:UpdateServicePrimaryTaskSet",
"ecs:DeleteTaskSet",
"ecs:RegisterTaskDefinition",
"ecs:DescribeTaskDefinition"
]
Resource = "*" # ecs:DescribeServices / RegisterTaskDefinition はリソース ARN 制限が難しいため * を許容
# 注: 将来的に Condition キー(aws:ResourceTag)でプロジェクトタグ絞込を推奨
},
# ALB: トラフィック切替操作
{
Sid = "ALBTrafficControl"
Effect = "Allow"
Action = [
"elasticloadbalancing:ModifyListener",
"elasticloadbalancing:DescribeListeners",
"elasticloadbalancing:DescribeTargetGroups",
"elasticloadbalancing:DescribeTargetHealth",
"elasticloadbalancing:RegisterTargets",
"elasticloadbalancing:DeregisterTargets"
]
Resource = "*"
},
# CloudWatch: Alarm 状態確認(ロールバック判定に使用)
{
Sid = "CloudWatchAlarmRead"
Effect = "Allow"
Action = [
"cloudwatch:DescribeAlarms",
"cloudwatch:PutMetricAlarm"
]
Resource = "arn:aws:cloudwatch:${var.aws_region}:${var.aws_account_id}:alarm:${var.project_name}-*"
},
# Lambda: lifecycle hook 関数の呼び出し(hook 使用時のみ)
{
Sid = "LambdaHookInvoke"
Effect = "Allow"
Action = ["lambda:InvokeFunction"]
Resource = "arn:aws:lambda:${var.aws_region}:${var.aws_account_id}:function:${var.project_name}-*"
},
# IAM PassRole: ECS タスク実行ロールの引き渡し(ワイルドカード禁止)
{
Sid = "PassRoleToECSTask"
Effect = "Allow"
Action = ["iam:PassRole"]
Resource = [
var.ecs_task_execution_role_arn, # タスク実行ロール ARN のみ許可
var.ecs_task_role_arn # タスクロール ARN のみ許可(使用する場合)
]
Condition = {
StringEqualsIfExists = {
"iam:PassedToService" = "ecs-tasks.amazonaws.com"
}
}
}
]
})
}
iam:PassRole の重要ポイント:
– Resource に "*" を指定すると、CodeDeploy が任意の IAM ロールを ECS タスクに引き渡せる状態になり、権限昇格の踏み台になります。
– var.ecs_task_execution_role_arn のように具体的な ARN を指定して、引き渡し先を ECS タスク実行ロールのみに限定します。
– Condition の iam:PassedToService = "ecs-tasks.amazonaws.com" により、ECS タスク以外のサービスへの PassRole も防止します。
9-4. PassRole NG 例と修正手順
NG 例(絶対にやってはいけない設定):
{
"Sid": "PassRoleNG",
"Effect": "Allow",
"Action": ["iam:PassRole"],
"Resource": "*"
}
この設定の危険性:
1. CodeDeploy の実行環境が侵害されると、攻撃者が任意のロールを ECS タスクに注入できる
2. AdministratorAccess 付きロールを持つタスクを起動し、AWS アカウント全体を掌握される可能性がある
3. AWS Organizations を使っている場合、クロスアカウント攻撃の起点になりうる
修正手順:
# 1. 現在のポリシードキュメントを確認
aws iam get-role-policy \
--role-name myapp-codedeploy-ecs-service-role \
--policy-name myapp-codedeploy-ecs-minimal-policy \
--query 'PolicyDocument' \
--output json
# 2. ECS タスク実行ロールの ARN を確認
aws iam get-role \
--role-name myapp-ecs-task-execution-role \
--query 'Role.Arn' \
--output text
# => arn:aws:iam::123456789012:role/myapp-ecs-task-execution-role
# 3. terraform apply で PassRole リソース制約を適用
terraform apply -target=aws_iam_role_policy.codedeploy_ecs_minimal
9-5. variables.tf への PassRole 関連変数追加
# modules/codedeploy-bluegreen/variables.tf(PassRole 制約用変数を追加)
variable "ecs_task_execution_role_arn" {
description = "ECS タスク実行ロールの ARN。iam:PassRole のリソース制約に使用。"
type = string
# 例: "arn:aws:iam::123456789012:role/myapp-ecs-task-execution-role"
}
variable "ecs_task_role_arn" {
description = "ECS タスクロールの ARN(タスク内アプリケーションへの権限)。使用しない場合は空文字。"
type = string
default = ""
}
variable "aws_account_id" {
description = "AWSアカウント ID(IAM ARN 生成に使用)。"
type = string
# 例: "123456789012"
}
variable "aws_region" {
description = "デプロイ先リージョン。"
type = string
default = "ap-northeast-1"
}
9-6. CodePipeline サービスロールの拡張 — CodeDeploy 呼び出し権限
第1弾の CodePipeline サービスロールは ECS プロバイダーへのデプロイ権限しか持っていません。CodeDeployToECS プロバイダーを使うために、codedeploy:* の追加が必要です。
# modules/ci-cd/iam.tf(第1弾からの差分追加)
# Terraform 1.9.x / hashicorp/aws ~> 5.0
# CodePipeline サービスロールに CodeDeploy 呼び出し権限を追加するインラインポリシー
resource "aws_iam_role_policy" "pipeline_codedeploy" {
name = "${var.project_name}-pipeline-codedeploy-policy"
role = aws_iam_role.pipeline.id # 第1弾で作成済みの CodePipeline ロール
policy = jsonencode({
Version = "2012-10-17"
Statement = [
# CodeDeploy: デプロイの作成・状態確認・リビジョン取得
{
Sid = "CodeDeployDeployAccess"
Effect = "Allow"
Action = [
"codedeploy:CreateDeployment",
"codedeploy:GetDeployment",
"codedeploy:GetApplicationRevision",
"codedeploy:RegisterApplicationRevision",
"codedeploy:GetDeploymentConfig"
]
Resource = [
"arn:aws:codedeploy:${var.aws_region}:${var.aws_account_id}:application:${var.project_name}",
"arn:aws:codedeploy:${var.aws_region}:${var.aws_account_id}:deploymentgroup:${var.project_name}/*",
"arn:aws:codedeploy:${var.aws_region}:${var.aws_account_id}:deploymentconfig:*"
]
},
# ECS: タスク定義の確認(CodeDeployToECS action で内部的に使用)
{
Sid = "ECSDescribeForDeploy"
Effect = "Allow"
Action = [
"ecs:DescribeServices",
"ecs:DescribeTaskDefinition"
]
Resource = "*"
},
# IAM PassRole: CodeDeploy サービスロールを CodePipeline から渡す
{
Sid = "PassCodeDeployRole"
Effect = "Allow"
Action = ["iam:PassRole"]
Resource = var.codedeploy_service_role_arn # CodeDeploy ロール ARN のみ許可(ワイルドカード禁止)
Condition = {
StringEqualsIfExists = {
"iam:PassedToService" = "codedeploy.amazonaws.com"
}
}
}
]
})
}
# modules/ci-cd/variables.tf(拡張変数追加)
variable "codedeploy_service_role_arn" {
description = "CodeDeploy ECS サービスロールの ARN。Pipeline → CodeDeploy への PassRole 制約に使用。"
type = string
}
variable "aws_account_id" {
description = "AWS アカウント ID。"
type = string
}
variable "aws_region" {
description = "リージョン。"
type = string
default = "ap-northeast-1"
}
9-7. permission_boundary(任意適用例)
組織のガバナンスポリシーとして permission_boundary が要求される場合、次のように CodeDeploy ロールに適用します。
# modules/codedeploy-bluegreen/iam.tf(permission_boundary 追加)
resource "aws_iam_role" "codedeploy_ecs" {
name = "${var.project_name}-codedeploy-ecs-service-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "codedeploy.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
# permission_boundary: ロールに付与できる権限の上限を組織ポリシーで制限(任意)
# SCP と組み合わせて使用することで「最大でもこの範囲内」という天井を設定できる
permissions_boundary = var.iam_permissions_boundary_arn
}
# modules/codedeploy-bluegreen/variables.tf(permission_boundary 変数)
variable "iam_permissions_boundary_arn" {
description = "IAM ロールの permission boundary ARN。組織ポリシーが不要な場合は空文字。"
type = string
default = "" # デフォルト空 = boundary なし
}
permission_boundary の使用場面:
– AWS Organizations の SCP(Service Control Policy)で許可されたアクションの範囲内に全ロールを収めたい場合
– セキュリティチームが承認したポリシー ARN を全 IAM ロールに適用する運用ルールがある場合
– ロール作成権限を開発チームに委任しつつ、権限昇格リスクを防ぎたい場合
9-8. IAM 設計の全体まとめ
本記事で登場する IAM ロールの構成を整理します。
CodePipeline Service Role(pipeline-service-role)
└─ [iam:PassRole] → CodeDeploy Service Role(codedeploy-ecs-service-role)
└─ [iam:PassRole] → ECS Task Execution Role(ecs-task-execution-role)
└─ ECS タスクが ECR pull / CloudWatch Logs 書き込み
CodeDeploy Service Role(codedeploy-ecs-service-role)
├─ ecs:DescribeServices / UpdateServicePrimaryTaskSet / RegisterTaskDefinition
├─ elasticloadbalancing:ModifyListener / DescribeListeners / DescribeTargetGroups
├─ cloudwatch:DescribeAlarms(ロールバック判定)
└─ lambda:InvokeFunction(lifecycle hook 実行)
PassRole の連鎖と制約:
– CodePipeline → CodeDeploy: Resource = [codedeploy_service_role_arn] のみ
– CodeDeploy → ECS task: Resource = [ecs_task_execution_role_arn, ecs_task_role_arn] のみ
– 各 PassRole に Condition: iam:PassedToService を設定して引き渡し先サービスを明示的に制限
# IAM ポリシーの PassRole 設定確認(本番デプロイ前チェック用)
aws iam simulate-principal-policy \
--policy-source-arn "arn:aws:iam::123456789012:role/myapp-codedeploy-ecs-service-role" \
--action-names "iam:PassRole" \
--resource-arns "arn:aws:iam::123456789012:role/myapp-ecs-task-execution-role" \
--query 'EvaluationResults[0].EvalDecision' \
--output text
# => allowed
aws iam simulate-principal-policy \
--policy-source-arn "arn:aws:iam::123456789012:role/myapp-codedeploy-ecs-service-role" \
--action-names "iam:PassRole" \
--resource-arns "arn:aws:iam::123456789012:role/SomeOtherRole" \
--query 'EvaluationResults[0].EvalDecision' \
--output text
# => implicitDeny(意図どおり:許可されていないロールへの PassRole は拒否)
このチェックにより、PassRole の制約が正しく機能していることをデプロイ前に確認できます。
Section 10. 手動承認ゲートと運用 — wait time / 監視 / destroy 手順
Blue/Green デプロイは「自動ロールバック」と「手動承認ゲート」を組み合わせることで、本番トラフィック切替の安全度を最大化できます。本セクションでは termination_wait_time_in_minutes と deployment_ready_option の設定、EventBridge × SNS 通知、CloudWatch Dashboard、そしてdestroy 手順と料金試算を解説します。

10-1. termination_wait_time_in_minutes — 旧タスク終了待機
本番切替後も旧タスク群(ORIGINAL TaskSet)をデフォルト(0 分)では即時削除します。問題発覚時に旧タスクへ戻す猶予が必要な場合、10 分以上に設定します。
| 設定値 | 動作 |
|---|---|
0(デフォルト) | 本番切替直後に旧タスクを削除 |
10(本記事推奨) | 切替から 10 分間、旧タスクを保持 |
2880(最大) | 切替から 2 日間保持 |
# modules/codedeploy-bluegreen/main.tf(blue_green_deployment_config 抜粋)
blue_green_deployment_config {
deployment_ready_option {
# WAIT_AND_CONTINUE: deployment_ready_wait_minutes 経過後に手動承認が必要
action_on_timeout = "WAIT_AND_CONTINUE"
wait_time_in_minutes = var.deployment_ready_wait_minutes # 0 = 即時切替
}
green_fleet_provisioning_option {
action = "COPY_AUTO_SCALING_GROUP"
}
terminate_blue_instances_on_deployment_success {
action= "TERMINATE"
termination_wait_time_in_minutes = var.termination_wait_minutes # 推奨: 10
}
}
# environments/dev/variables.tf(追加変数)
variable "deployment_ready_wait_minutes" {
type = number
default = 0
description = "手動承認ウィンドウ(分)。0 = 即時切替。"
}
variable "termination_wait_minutes" {
type = number
default = 10
description = "本番切替後に旧タスクを保持する猶予時間(分)"
}
10-2. 手動承認ゲートの 2 方式

方式 A: CodeDeploy 側 — deployment_ready_option で wait
deployment_ready_wait_minutes > 0 の場合、新タスクへテスト listener を向けた後 N 分待機します。テスト listener(port 8080)で動作確認後、CLI または コンソールから承認します。
# テスト listener で手動検証
ALB_DNS="myapp-alb-123456789.ap-northeast-1.elb.amazonaws.com"
curl -s http://${ALB_DNS}:8080/healthz
# 問題なければ本番切替を承認(AWS CLI)
DEPLOYMENT_ID=$(aws deploy list-deployments \
--application-name myapp-codedeploy-app \
--deployment-group-name myapp-dg \
--include-only-statuses InProgress \
--query 'deployments[0]' --output text)
aws deploy continue-deployment \
--deployment-id "${DEPLOYMENT_ID}" \
--deployment-wait-type READY_WAIT
コンソール操作: CodeDeploy → Deployments → 対象デプロイ → 「Continue deployment」ボタン。
方式 B: CodePipeline 側 — Approval action stage
本番環境では Deploy ステージの前に Manual Approval ステージを挿入します。
# modules/ci-cd/main.tf(Approval ステージ追加)
stage {
name = "Approval"
action {
name = "ManualApproval"
category = "Approval"
owner = "AWS"
provider = "Manual"
version = "1"
configuration = {
NotificationArn = aws_sns_topic.deploy_notify.arn
CustomData= "本番デプロイ前の最終承認。変更内容を確認してください。"
ExternalEntityLink = "https://github.com/${var.github_repo}/compare/main"
}
}
}
| 比較項目 | 方式 A(CodeDeploy wait) | 方式 B(Pipeline Approval) |
|---|---|---|
| タイミング | デプロイ中(テスト traffic 到達後) | デプロイ開始前 |
| タイムアウト | deployment_ready_wait_minutes 分 | 最大 7 日 |
| 通知連携 | EventBridge → SNS | SNS(Pipeline 組み込み) |
| ロールバック | timeout 時に自動ロールバック | timeout 時はデプロイ失敗扱い |
| 適合シーン | ステージング / 素早い検証 | 本番 / 厳格な変更管理 |
10-3. EventBridge × SNS — デプロイイベント通知
CodeDeploy はデプロイのステート変化(START / SUCCESS / FAILURE / ROLLBACK)を EventBridge に自動送信します。
# modules/codedeploy-bluegreen/eventbridge.tf
resource "aws_sns_topic" "deploy_notify" {
name = "${var.project_name}-deploy-notify"
}
resource "aws_sns_topic_subscription" "email" {
topic_arn = aws_sns_topic.deploy_notify.arn
protocol = "email"
endpoint = var.notify_email
}
resource "aws_cloudwatch_event_rule" "codedeploy" {
name = "${var.project_name}-codedeploy-events"
event_pattern = jsonencode({
source= ["aws.codedeploy"]
detail-type = ["CodeDeploy Deployment State-change Notification"]
detail = {
application = [aws_codedeploy_app.this.name]
state = ["START", "SUCCESS", "FAILURE", "STOP", "READY"]
}
})
}
resource "aws_cloudwatch_event_target" "sns" {
rule= aws_cloudwatch_event_rule.codedeploy.name
target_id = "SendToSNS"
arn = aws_sns_topic.deploy_notify.arn
input_transformer {
input_paths = { app = "$.detail.application", state = "$.detail.state", id = "$.detail.deploymentId" }
input_template = "\"[CodeDeploy] <app> — <state> (ID: <id>)\""
}
}
resource "aws_sns_topic_policy" "deploy_notify" {
arn = aws_sns_topic.deploy_notify.arn
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "events.amazonaws.com" }
Action = "SNS:Publish"
Resource = aws_sns_topic.deploy_notify.arn
}]
})
}
10-4. destroy 手順(段階的ガイド)
ハンズオン終了後は ALB が 月 $16 発生し続けます。必ず以下の順序で削除してください。
削除順序が重要: ECS service が稼働したまま ALB を削除するとエラーになります。
# Step 1: 進行中デプロイを停止(ある場合のみ)
DEPLOYMENT_ID=$(aws deploy list-deployments \
--application-name myapp-codedeploy-app \
--deployment-group-name myapp-dg \
--include-only-statuses InProgress \
--query 'deployments[0]' --output text)
[ "$DEPLOYMENT_ID" != "None" ] && \
aws deploy stop-deployment --deployment-id "$DEPLOYMENT_ID" --auto-rollback-enabled
# Step 2: ECS タスク数を 0 に縮退してサービスを停止
aws ecs update-service --cluster myapp-cluster --service myapp-service --desired-count 0
aws ecs wait services-stable --cluster myapp-cluster --services myapp-service
# Step 3: terraform destroy で全リソース削除
cd terraform/environments/dev
terraform destroy -auto-approve
# Step 4: ECR イメージと S3 バケットを手動削除(terraform が保護するため手動対応)
IMAGE_IDS=$(aws ecr list-images --repository-name myapp --query 'imageIds[*]' --output json)
aws ecr batch-delete-image --repository-name myapp --image-ids "$IMAGE_IDS"
aws ecr delete-repository --repository-name myapp --force
BUCKET="myorg-myapp-artifacts"
aws s3 rm "s3://${BUCKET}" --recursive && aws s3 rb "s3://${BUCKET}"
# Step 5: ALB 削除確認(これが消えれば課金停止)
aws elbv2 describe-load-balancers \
--query "LoadBalancers[?contains(LoadBalancerName,'myapp')].[LoadBalancerName,State.Code]" \
--output table
10-5. Makefile — apply / destroy / deploy-status 一気通貫
ENV ?= dev
TF_DIR := terraform/environments/$(ENV)
CLUSTER ?= myapp-cluster
SERVICE ?= myapp-service
APP := myapp-codedeploy-app
DG:= myapp-dg
.PHONY: apply destroy deploy-status approval-continue
apply:
cd $(TF_DIR) && terraform init -upgrade && terraform apply -auto-approve
destroy:
@echo "WARNING: 全リソースを削除します。[y/N]"; read ans && [ "$${ans}" = "y" ] || exit 1
aws ecs update-service --cluster $(CLUSTER) --service $(SERVICE) --desired-count 0 || true
aws ecs wait services-stable --cluster $(CLUSTER) --services $(SERVICE) || true
cd $(TF_DIR) && terraform destroy -auto-approve
deploy-status:
@DEPLOYMENT_ID=$$(aws deploy list-deployments \
--application-name $(APP) --deployment-group-name $(DG) \
--include-only-statuses InProgress --query 'deployments[0]' --output text); \
[ "$$DEPLOYMENT_ID" = "None" ] && echo "進行中のデプロイはありません" || \
aws deploy get-deployment --deployment-id "$$DEPLOYMENT_ID" \
--query 'deploymentInfo.{Status:status,Config:deploymentConfigName}' --output table
approval-continue:
@DEPLOYMENT_ID=$$(aws deploy list-deployments \
--application-name $(APP) --deployment-group-name $(DG) \
--include-only-statuses InProgress --query 'deployments[0]' --output text); \
aws deploy continue-deployment --deployment-id "$$DEPLOYMENT_ID" \
--deployment-wait-type READY_WAIT
10-6. 料金試算(月額概算)
24 時間 × 30 日稼働させた場合の月額目安(ap-northeast-1):
| リソース | 月額 |
|---|---|
| ALB(Application Load Balancer) | $16.20 |
| Fargate(0.25 vCPU / 0.5 GB)× 2 タスク | $7.70 |
| ECR ストレージ(~1 GB) | $0.10 |
| CodePipeline | $1.00 |
| CodeBuild(3 min × 10 回) | $0.15 |
| CodeDeploy(ECS プラットフォーム) | $0.00(無料) |
| CloudWatch Alarm × 4 | $0.40 |
| CloudWatch Logs(~1 GB) | $0.53 |
| 合計 | $26.08/月 |
ALB の固定費 $16/月 が全体の 62% を占めます。ハンズオン後は必ず make destroy でリソースを削除してください。terraform destroy 完了後、aws elbv2 describe-load-balancers で ALB が消えていることを必ず確認してください。
- 平日昼間のみ稼働(8 hr/day × 20 日)なら月 $7 以下に抑制可能
- CodeDeploy ECS プラットフォームの使用料は無料(EC2/Lambda と異なり課金なし)
make destroyで ALB + Fargate を削除すれば ECR/CloudWatch のみ数十円/月- AWS Budgets でアラート設定すると料金超過を自動検知できます(推奨閾値: $30/月)
Section 11. まとめと次の発展
本記事では ECS/Fargate 環境に CodeDeploy × ALB Target Group Swap を組み合わせた Blue/Green デプロイを、Terraform 1.9 で一から実装しました。Rolling Update(第1弾)との違いを軸に、デプロイ設計の判断基準を体系的に身につけることを目指しました。
Section 1 の概念整理から始まり、Section 2 の構成全体像、Section 3 の Terraform 基盤構築、Section 4 の CodeDeploy リソース、Section 5 の appspec.yaml 設計、Section 6 の CodePipeline 連携、Section 7 の IAM 最小権限、Section 8 の Canary/Linear デプロイと自動ロールバック、Section 9 のデバッグガイド、Section 10 の手動承認ゲートと destroy 手順まで、本番で通用するフルスタックの知識を積み上げてきました。
このフルハンズオンを通じて、「ダウンタイムゼロ × 瞬時ロールバック」という本番グレードの要件を AWS ネイティブサービスだけで満たす方法を習得しました。
最後に身についたスキルを棚卸しし、次のステップへの道筋を示します。
11-1. 本記事で身につくスキルの棚卸
本記事を最後まで実践した方は、以下の 6 つのスキルを習得しています。
| # | スキル | 習得内容 |
|---|---|---|
| 1 | CodeDeploy ECS コンピュートプラットフォーム設計 | DeploymentGroup / TaskSet の役割分離、ECS × CodeDeploy の連携アーキテクチャ、appspec.yaml の配置と書式 |
| 2 | ALB × 本番 listener + テスト listener の Blue/Green 切替構成 | 本番 listener(ポート 443/80)と テスト listener(ポート 8080)の役割分離、TG swap のタイミング制御、target_group_info の2ファイル構成 |
| 3 | appspec.yaml lifecycle hook 実装 | BeforeAllowTraffic / AfterAllowTraffic の使い分け、Lambda 関数を hook として登録する手順、hook 失敗時のロールバック動作 |
| 4 | deployment_config の選択基準 | AllAtOnce / Canary10%5分 / Linear10%毎1分 の比較と適合シーン、カスタム deployment_config の Terraform 定義方法 |
| 5 | CloudWatch Alarm 連携による自動ロールバック | auto_rollback_configuration で Alarm ARN を指定する手順、Alarm 発火からロールバック完了までのフロー、termination_wait_time_in_minutes で旧タスク保持期間の制御 |
| 6 | Terraform 1.9 で全体を IaC 化する実装パターン | modules/codedeploy-bluegreen モジュール設計、第1弾資産(CodePipeline Source+Build / ECR / IAM)の継承と差分追加、lifecycle { ignore_changes } で CodeDeploy 管理リソースと競合しない記述 |
これらのスキルは、エンタープライズ現場で最も頻繁に問われる「無停止デプロイ設計」の核心です。本記事の Terraform コードを自社プロジェクトへ適用する際は、var.project_name / var.environment を変更するだけで再利用できます。
11-2. Rolling Update(第1弾)vs Blue/Green(本記事)比較マトリクス
第1弾と本記事の設計を 6 軸で比較します。どちらを選択するかの判断基準として活用してください。
| 比較軸 | Rolling Update(第1弾) | Blue/Green(本記事) |
|---|---|---|
| 切替方式 | 旧タスクを 1 台ずつ段階的に新タスクへ置換(一時共存) | 旧 TG と新 TG を ALB レイヤーでアトミック切替(完全分離) |
| ダウンタイム | 設計上ゼロ(ただし混在期間あり) | 実質ゼロ(listener swap は ~1 秒以内) |
| ロールバック速度 | 新タスクを再度古いイメージで置換(数分〜) | listener を旧 TG へ再 swap(~30 秒) |
| コスト | 追加リソースなし(タスク置換のみ) | 切替中は 2 × タスク数が稼働(一時倍増) |
| テスト検証 | 本番 listener に直結(テスト URL なし) | テスト listener(ポート 8080)で本番前検証可 |
| 適合シーン | 開発・ステージング / 素早いデプロイ優先 | 本番 / 無停止保証・安全性優先 |
判断の目安: SLA が厳しい本番環境、または「瞬時ロールバック」が必要なサービスには Blue/Green を選択してください。コスト最適化を優先するステージング以下の環境では Rolling Update が適しています。
実際のプロジェクトでは「dev/stg → Rolling Update、prod → Blue/Green」という使い分けが標準的なパターンです。第3弾ではこのマルチ環境設計を Terraform ワークスペースと CodePipeline ステージで実装します。
11-3. 運用で押さえておくべきポイント
本番環境で Blue/Green デプロイを継続運用する際に特に重要な 3 点をまとめます。
① デプロイ失敗時の確認コマンド
# 失敗したデプロイの詳細を取得(原因特定の第一歩)
DEPLOYMENT_ID=$(aws deploy list-deployments \
--application-name myapp-codedeploy-app \
--deployment-group-name myapp-dg \
--include-only-statuses Failed \
--query 'deployments[0]' --output text)
aws deploy get-deployment --deployment-id "$DEPLOYMENT_ID" \
--query 'deploymentInfo.{Status:status,Desc:description,ErrorInfo:errorInformation}' \
--output table
② appspec.yaml の最頻頻エラーと対処
| エラーメッセージ | 原因 | 対処 |
|---|---|---|
The deployment failed because the specified appspec does not contain a Hooks section | appspec.yaml に Hooks ブロックがない | Hooks セクション追加または空の {} を明示 |
The ARN of the target group is invalid | containerPort がタスク定義のポートと不一致 | appspec.yaml の containerPort を確認 |
The ECS service has reached a maximum number of deployments | 同時デプロイが5件を超えた | 古いデプロイを aws deploy stop-deployment で停止 |
③ CodeDeploy コンソールで確認すべきメトリクス
- Deployment duration: デプロイ所要時間のトレンド。増加傾向は ECS タスク起動遅延のサイン
- Rollback rate: ロールバック頻度。5% 超えは本番品質の問題を示唆
- Wait time (Ready): 手動承認ゲートの待機時間。
deployment_ready_wait_minutesの設定見直し基準
④ 本番デプロイ前のチェックリスト
デプロイ実行前に以下を確認すると、失敗リスクを大幅に低減できます。
# 1. 現在のサービスの desired_count が 0 でないことを確認
aws ecs describe-services --cluster myapp-cluster --services myapp-service \
--query "services[0].{Desired:desiredCount,Running:runningCount}" --output table
# 2. 最新タスク定義が正しいイメージを参照しているか確認
aws ecs describe-task-definition --task-definition myapp \
--query "taskDefinition.containerDefinitions[0].image" --output text
# 3. ALB テスト listener(ポート 8080)が設定済みか確認
aws elbv2 describe-listeners \
--load-balancer-arn $(aws elbv2 describe-load-balancers \
--query "LoadBalancers[?contains(LoadBalancerName,'myapp')].LoadBalancerArn" \
--output text) \
--query "Listeners[*].{Port:Port,Protocol:Protocol}" --output table
11-4. 関連シリーズ — 次の学習ステップ
本記事は ECS/Fargate CI/CD シリーズ の第2弾です。関連する記事・シリーズと合わせて学ぶことで、AWS Native CI/CD の全体像を把握できます。
ECS/Fargate CI/CD シリーズ(本シリーズ)
| 弾 | テーマ | 状態 |
|---|---|---|
| 第1弾 | Rolling Update 基礎編 — CodePipeline + CodeBuild + ECR + Fargate | 公開済 |
| 第2弾 | Blue/Green デプロイ編 — CodeDeploy × ALB TG swap(本記事) | 本記事 |
| 第3弾 | マルチ環境パイプライン — dev/stg/prod 手動承認ゲート編 | 近日公開 |
| 第4弾(検討中) | セキュリティ強化編 — ECR scan enhanced + Cosign 署名 + Inspector v2 | 検討中 |
関連シリーズ
- GitHub Actions 軸 CI/CD シリーズ: AWS CodePipeline ではなく GitHub Actions を CI/CD エンジンとして使うアーキテクチャを解説。ECS デプロイの
actions/deploy-to-ecsの使い方から始め、マルチ環境対応まで扱います。 - Terraform State 管理シリーズ:
terraform.tfstateのリモート管理(S3 + DynamoDB)、ワークスペース分離、state lock の仕組みを体系的に解説。本シリーズの Terraform 基盤構築の前提知識として最適です。
11-5. ハンズオン完了後のクリーンアップ確認
リソースを作成したまま放置すると ALB の固定費($16/月)が継続発生します。make destroy 実行後に以下で課金停止を確認してください。
# ALB が削除されていることを確認(0 件 = 課金停止)
aws elbv2 describe-load-balancers \
--query "LoadBalancers[?contains(LoadBalancerName,'myapp')].[LoadBalancerName,State.Code]" \
--output table
# ECS サービスが削除されていることを確認
aws ecs describe-services \
--cluster myapp-cluster \
--services myapp-service \
--query "services[0].status" --output text
# 出力: INACTIVE または "service not found"
全リソースが削除されたことを確認できれば、ハンズオンは完了です。本記事で培った Blue/Green デプロイの知識を活かして、次は第3弾のマルチ環境設計にチャレンジしてみてください。
AWS Budgets の設定を忘れずに: 今後も AWS を使い続ける場合、
$30/月のアラートを Budgets に設定しておくと意図しない課金を早期に検知できます。ALB は稼働しているだけで $16/月 かかるため、ハンズオン用環境は必ずmake destroyで削除してください。
- CodeDeploy ECS コンピュートプラットフォームの DeploymentGroup / TaskSet 設計
- ALB × 本番 listener + テスト listener の Blue/Green 切替構成
- appspec.yaml の lifecycle hook(BeforeAllowTraffic / AfterAllowTraffic 等)実装
- deployment_config の選択(Canary10%5分 / Linear10%毎1分 / AllAtOnce)
- CloudWatch Alarm 連携による自動ロールバック
- Terraform 1.9 で全体を IaC 化する実装パターン
次の発展:
- 第3弾予告: マルチ環境パイプライン — dev/stg/prod 手動承認ゲート編(近日公開)
- 第4弾予告(検討中): セキュリティ強化編 — ECR scan enhanced + Cosign 署名 + Inspector v2