ECS Blue/Greenデプロイ CodeDeploy×ALB TG swap 無停止切替 Terraform

目次

1. この記事について

前提知識(必須):

本シリーズの位置づけ:

  • 第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 group2 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 の設計がそのまま応用できます。

Blue/Green デプロイ全体フロー概略

本記事の全体構成

本記事は以下の 11 セクションで構成されています。

セクションタイトル概要
§1(本節)この記事についてゴール・Rolling vs Blue/Green・コスト
§2前提と全体構成図前提環境・アーキテクチャ・新規 vs 引継ぎリソース
§3CodeDeploy ECS 基礎DeploymentGroup / TaskSet / listener 設計
§4Terraform: CodeDeploy 設定aws_codedeploy_app / aws_codedeploy_deployment_group
§5Terraform: ALB Blue/Green 設定Blue TG / Green TG / テスト listener
§6Terraform: CloudWatch Alarm 設定自動ロールバックトリガー設計
§7appspec.yaml 解説TaskSet マッピング・Lifecycle hook 設定
§8buildspec.yml 更新imageDetail.json 生成・ECR push 手順
§9CodePipeline 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. 前提と全体構成図

前提環境

本記事の手順を実施するには、以下の環境が整っていることを前提としています。

ツール / リソースバージョン / 条件
Terraform1.9.x 以上
hashicorp/aws provider~> 5.0
AWS CLIv2
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_appECS コンピュートプラットフォームのアプリ定義
aws_codedeploy_deployment_groupBlue/Green 切替ルール・Lifecycle hook・ロールバック設定
aws_lb_target_group × 2Blue TG(旧バージョン)/ Green TG(新バージョン)
aws_lb_listenerport 8080 テスト listener
aws_lb_listener_ruleテスト listener のルーティング設定
aws_cloudwatch_metric_alarm × 2エラー率上昇時の自動ロールバックトリガー
appspec.yamlCodeDeploy へのタスク定義・TG マッピング指示
IAM ポリシー追加CodeDeploy サービスロールへの ECS / ALB / CloudWatch 権限

第1弾から引き継ぐリソース

以下のリソースは第1弾の Terraform コードから変更なしで引き継ぎます。

  • ECR リポジトリ: Docker イメージの保存先。タグ戦略(:latest + コミット SHA)もそのまま
  • CodeBuild プロジェクト: ビルド・ECR プッシュ処理。buildspec.ymlimageDetail.json 生成を追記するのみ
  • CodePipeline(Source + Build ステージ): GitHub 連携とビルド実行。Deploy ステージのみ差し替え
  • IAM ロール基盤: CodePipeline 実行ロール・CodeBuild サービスロール・ECS タスクロール・タスク実行ロール
  • VPC / サブネット / セキュリティグループ: ネットワーク基盤は第1弾のものをそのまま利用

Deploy ステージのみ aws_codepipelinestage ブロックを 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 のみが機能する点も注意してください。

ALB + Blue/Green TG swap タイミング図

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-PremisesEC2 インスタンス・オンプレサーバーIn-Place / Blue/Green必要(CodeDeploy Agent をインスタンスに導入)無料
LambdaLambda 関数のバージョン/エイリアスCanary / Linear / AllAtOnce不要(Lambda はサーバーレス)無料
ECS(本記事)ECS サービス + ALB Target GroupCanary / 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_configBlue/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_configurationCloudWatch Alarm 連携5xx count / unhealthy host count

ECS プラットフォームでは deployment_type = BLUE_GREENdeployment_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 フィールドは InProgressReadySucceeded の順に遷移します。ECS の instance-id はコンソールの「Deployment targets」タブから ARN 形式で確認できます。


AppSpec.yaml lifecycle hook シーケンス

3-8. §3 まとめ

概念ポイント
コンピュートプラットフォームEC2/Lambda/ECS の3種。ECS は Agent 不要・Blue/Green のみ
TaskSet(ORIGINAL/REPLACEMENT)Blue/Green デプロイ中に2セットのタスク群を同時維持
deployment_styleECS では BLUE_GREEN + WITH_TRAFFIC_CONTROL 固定
primary / test listenerport 80(本番)/ port 8080(検証)の2本構成
auto_rollbackDEPLOYMENT_FAILURESTOP_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接続チェックなど含む)
porttraffic-portタスク定義の containerPort と同じポートを自動使用
interval30タスク起動に数十秒かかるため余裕を持たせる
timeout10interval の 1/3 未満で設定
healthy_threshold22回連続成功でヘルシー判定(チャタリング防止)
unhealthy_threshold33回連続失敗でアンヘルシー判定
matcher200-2992xx 系レスポンスをヘルシーとみなす

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)80Blue TGエンドユーザーのリクエスト受付
テスト(test)8080Green 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_changestask_definition / load_balancer 必須(CodeDeploy との競合防止)
ALB access logS3 出力で監査証跡確保・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 providerECSCodeDeployToECS
Deploy に必要な artifactimagedefinitions.json × 1appspec.yaml + taskdef.json + イメージ URI × 3
artifact 生成元CodeBuild primary artifactCodeBuild 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 実装を解説する。

AppSpec.yaml lifecycle hook シーケンス


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 名フェーズ実行タイミング主な用途重要度
BeforeInstallORIGINAL → REPLACEMENT新タスク ECS 登録DBスキーマ変更・フィーチャーフラグ有効化
AfterInstallREPLACEMENT 起動後新タスク起動・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 経由で DeploymentIdLifecycleEventHookExecutionId を受け取る
– 処理後に必ず 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 timeout3 秒hook timeout − 30 秒Lambda 先タイムアウトで Failed を確実に報告する
HTTP リクエスト timeout5〜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 を新規追加する拡張アーキテクチャです。

Terraform リソース依存関係図


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_balancerBlue 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]
  }
}
⚠️ ignore_changes が必要な理由:
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_appmyappECS Blue/Green 親アプリケーション
aws_codedeploy_deployment_groupmyapp-deployment-groupTG swap 制御・自動ロールバック
aws_lb_target_group × 2myapp-blue-tg / myapp-green-tgORIGINAL / REPLACEMENT TaskSet
aws_lb_listener × 2port 80 / port 8080本番 / テスト検証 listener
aws_lb_listener_rule/api/*パスベースルーティング
aws_cloudwatch_metric_alarm × 25xx_count / unhealthy_host_count自動ロールバックトリガー
aws_iam_rolecodedeploy-ecs-service-roleCodeDeploy ECS 操作権限
aws_lambda_function × 2before/after-allow-trafficlifecycle hook
aws_iam_rolelambda-hook-roleLambda 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_groupdeployment_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_ONCECodeDeployDefault.ECSAllAtOnce全トラフィックを一括切替30 秒〜1 分高(問題発生時の影響 = 100%)
CANARY_10_5CodeDeployDefault.ECSCanary10Percent5Minutes10% 先行 → 5 分待機 → 残 90% 一括5〜7 分中(影響を 10% に限定してから切替)
LINEAR_10_EVERY_1CodeDeployDefault.ECSLinear10PercentEvery1Minutes10% ずつ 1 分間隔で段階切替(10 ステップ)10〜12 分低(段階的検証・ロールバック猶予が最大)

Canary/Linear/AllAtOnce トラフィック推移グラフ

選択ガイド:
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_groupdeployment_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_FAILURElifecycle hook の失敗・health check 不合格・タイムアウト必須
DEPLOYMENT_STOP_ON_ALARMCloudWatch 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)の保持時間
AllAtOnce30 秒〜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 ロールの拡張方法を解説します。

IAM ロール信頼関係図


9-1. 登場するロールの整理

本記事で新規追加・拡張するロールは 3 つです。

ロール名信頼されるサービス用途
{project}-codedeploy-ecs-service-rolecodedeploy.amazonaws.comCodeDeploy が ECS/ALB/CloudWatch/Lambda を操作する実行ロール
{project}-pipeline-service-role(第1弾から拡張)codepipeline.amazonaws.comCodePipeline の Deploy stage が CodeDeploy を呼び出す追加権限
{project}-ecs-task-execution-role(第1弾から継承)ecs-tasks.amazonaws.comECS タスク起動時の 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 タスク実行ロールのみに限定します。
Conditioniam: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_minutesdeployment_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 → SNSSNS(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 つのスキルを習得しています。

#スキル習得内容
1CodeDeploy ECS コンピュートプラットフォーム設計DeploymentGroup / TaskSet の役割分離、ECS × CodeDeploy の連携アーキテクチャ、appspec.yaml の配置と書式
2ALB × 本番 listener + テスト listener の Blue/Green 切替構成本番 listener(ポート 443/80)と テスト listener(ポート 8080)の役割分離、TG swap のタイミング制御、target_group_info の2ファイル構成
3appspec.yaml lifecycle hook 実装BeforeAllowTraffic / AfterAllowTraffic の使い分け、Lambda 関数を hook として登録する手順、hook 失敗時のロールバック動作
4deployment_config の選択基準AllAtOnce / Canary10%5分 / Linear10%毎1分 の比較と適合シーン、カスタム deployment_config の Terraform 定義方法
5CloudWatch Alarm 連携による自動ロールバックauto_rollback_configuration で Alarm ARN を指定する手順、Alarm 発火からロールバック完了までのフロー、termination_wait_time_in_minutes で旧タスク保持期間の制御
6Terraform 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 sectionappspec.yaml に Hooks ブロックがないHooks セクション追加または空の {} を明示
The ARN of the target group is invalidcontainerPort がタスク定義のポートと不一致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