- 1 1. この記事について
- 2 4. ECR 基礎 — repository / lifecycle / scan
- 3 5. CodeBuild 基礎 — buildspec.yml 設計
- 4 6. CodePipeline 基礎 — source / build / deploy ステージ
- 5 7. Terraform で全体をコード化
- 5.1 7-1. ディレクトリ構成とモジュール分割方針
- 5.2 7-2. backend.tf — S3 + DynamoDB state 管理
- 5.3 7-3. tfvars 設計
- 5.4 7-4. modules/ecr — ECR repository + lifecycle
- 5.5 7-5. modules/ecs-fargate — cluster + task definition + service + ALB
- 5.6 7-6. modules/ci-cd — IAM 4 ロール完成形
- 5.7 7-7. ルートモジュール — environments/dev/main.tf
- 5.8 7-8. apply 手順と初回確認
- 5.9 7-9. outputs.tf — 重要な参照値
- 5.10 Section 7 まとめ
- 6 8. IAM 最小権限設計
- 7 9. 運用 — デプロイ監視・ロールバック・ログ
- 8 10. ハンズオン実行と成果物確認
- 9 Section 11. まとめと次の発展
1. この記事について
ECS/Fargate CI/CD シリーズ
- 第1弾(本記事): CodePipeline×ECR×Fargate で作る AWS Native CI/CD 基礎編
- 第2弾(予告): ECS Blue/Green デプロイ — CodeDeploy×ALB target group swap で無停止切替
本記事は「Git push 一発で ECR へのイメージプッシュから Fargate rolling update まで全自動で走る環境」を Terraform 1.9 で構築するハンズオンです。
本記事のゴール状態
本記事を最後まで実施すると、以下のパイプラインが動作します。
Git push (main branch)
└─ CodePipeline トリガー(CodeStar Connections 経由)
├─ Source ステージ: GitHub からソース取得
├─ Build ステージ: CodeBuild
│ └─ docker build → docker push → ECR にイメージ登録
│ └─ imagedefinitions.json 生成(タスク定義更新用)
└─ Deploy ステージ: ECS rolling update
└─ 新タスク定義を Fargate で起動 → 旧タスク停止
```text
ひと言で表すと「**ローカルで `git push` するだけで、10〜15分後に新バージョンの Fargate タスクが稼働している**」状態です。Terraform コードで全リソースを管理するため、チームへの展開や環境複製も `terraform apply` 一発です。
### 全11セクションの構成
| セクション | タイトル | ゴール |
|-----------|---------|-------|
| §1(本節) | この記事について | シリーズ概要・前提知識・所要時間 |
| §2 | 業務背景 | 手動デプロイの課題・AWS Native の選択理由 |
| §3 | ECS/Fargate 基礎とゴール状態 | rolling update の仕組み・完成系アーキテクチャ |
| §4 | Terraform でインフラ構築 | VPC / ECS Cluster / ECR / IAM 定義 |
| §5 | CodeBuild buildspec 設計 | buildspec.yml・env注入・キャッシュ最適化 |
| §6 | CodePipeline パイプライン定義 | Source/Build/Deploy 3ステージ構成 |
| §7 | IAM 最小権限設計 | CodePipeline/CodeBuild/ECS 用ロール |
| §8 | デプロイ監視と失敗時ロールバック | CloudWatch Alarm + rollback 設定 |
| §9 | コスト管理とクリーンアップ | 使用リソース一覧・削除手順 |
| §10 | トラブルシューティング | よくあるエラー8パターンと対処法 |
| §11 | まとめと第2弾予告 | Blue/Green デプロイへの橋渡し |

### ハンズオン所要時間とコスト目安
| 項目 | 目安 |
|------|------|
| 初回(全リソース作成から動作確認まで) | **約 90 分** |
| 2回目以降(設定変更・再構築) | **約 30 分** |
| コスト(パイプライン稼働中・1時間あたり) | **約 $0.20** |
| コスト(月ベース・Fargate 常時起動の場合) | **約 $3〜5** |
コストの内訳は §9 で詳しく解説します。主な費用は ECR($0.10/GB/月)・CodePipeline($1/month/pipeline)・CodeBuild($0.005/min)・Fargate(実行時間課金)です。ハンズオン完了後にリソースを削除すれば課金はほぼゼロになります。
### cmd_040(GHA軸)との棲み分け
本記事は **AWS Native CI/CD 縛り**です。GitHub Actions(GHA)は一切登場しません。
| 比較軸 | 本記事(AWS Native) | cmd_040(GHA軸) |
|--------|---------------------|-----------------|
| CI/CD 基盤 | CodePipeline + CodeBuild | GitHub Actions |
| IAM 認証 | CodePipeline 専用 IAM ロール | OIDC + AssumeRole |
| 監査ログ | CloudTrail + CodePipeline 実行履歴 | GHA ログ + CloudTrail |
| AWS 外依存 | なし(CodeStar Connections のみ) | GitHub に依存 |
| 向いているケース | AWS 内完結・監査要件が厳しい環境 | GitHub 中心開発・マルチクラウド |
「GitHub Actions を既に使っているが、デプロイ部分だけ AWS Native に切り替えたい」場合も本記事のアーキテクチャは適用できます。CodeStar Connections 経由でトリガーを GitHub に持ちつつ、後続処理を CodePipeline で完結させる構成を §6 で解説します。
### ECS/Fargate 未経験の方へ
ECS や Fargate を使ったことがなくても、§3 で基礎から解説します。以下の疑問に答えてから実装に進みます。
- 「ECS とは何か」「Fargate と EC2 起動タイプの違い」
- 「rolling update はどのような仕組みで動くか」「旧タスクはいつ停止するか」
- 「タスク定義と Service の関係」「imagedefinitions.json の役割」
- 「ECR と Docker Hub の違い」「ECR のライフサイクルポリシーとは」
§3 を読み終えた時点で、本記事の全セクションを迷わず進める基礎知識が身につきます。
### 本記事で使用する主要 AWS サービス
| サービス | 役割 | 本記事での使用箇所 |
|---------|------|-----------------|
| CodePipeline | パイプライン orchestration | §6 |
| CodeBuild | docker build / push の実行環境 | §5 |
| ECR (Elastic Container Registry) | コンテナイメージのプライベートレジストリ | §4 |
| ECS (Elastic Container Service) | コンテナオーケストレーション | §3, §4 |
| Fargate | サーバーレスコンテナ実行環境 | §3, §4 |
| CodeStar Connections | GitHub との接続設定 | §6 |
| CloudWatch | ビルドログ・デプロイ監視 | §8 |
| IAM | 各サービス間の権限設定 | §7 |
これらのサービスは Terraform の `aws_` リソースとして定義します。手動で AWS コンソールを操作する箇所は「CodeStar Connections の初回承認」のみです。
### 第2弾・第3弾の予告
本記事(第1弾)では rolling update(新旧タスクを順番に入れ替える)を実装します。
- **第2弾(予告)**: ECS Blue/Green デプロイ — CodeDeploy × ALB target group swap で無停止切替。一瞬のダウンタイムも許容できない本番環境向けのアーキテクチャを構築します。
- **第3弾(予告)**: マルチ環境パイプライン — dev/stg/prod の3環境を手動承認付きで段階的にデプロイするシリーズ完結編。
本記事(第1弾)を完了すれば、第2・第3弾で扱う高度なデプロイ戦略に必要な基礎が全て揃います。まずは rolling update で「デプロイ自動化の感触」を掴んでください。
> **学習ゴール**: 本記事を完了した時点で、CodePipeline + CodeBuild + ECR + ECS Fargate の4サービスを Terraform で一括管理し、Git push から Fargate 更新まで自動化できる状態になっています。
それでは §2 で、なぜ手動デプロイを自動化する必要があるのかを業務観点から整理します。
### 前提知識
<div class="ep-box brc-gray">
<strong>前提知識(推奨)</strong>:
<ul>
<li><a href="https://www.watchittrend.com/eventbridge-vpc-lattice-fargate/">Amazon EventBridge × VPC Lattice × Fargate</a> — Fargate 起動の基礎</li>
<li><a href="https://www.watchittrend.com/ecs-stepfunctions-batch/">ECS × Step Functions バッチ処理</a> — ECS task 定義の基礎</li>
<li><a href="https://www.watchittrend.com/terraform-basics/">Terraform基礎</a> — init/plan/apply・変数</li>
<li>AWS CLI + Terraform 1.9.x / hashicorp/aws ~> 5.0 の動作環境</li>
<li>Docker の基本操作(build/tag/push)</li>
</ul>
<strong>関連シリーズ</strong>:
<ul>
<li>AWS×Terraform 複数人開発シリーズ 全3弾(cmd_040)— GitHub Actions 軸 CI/CD。本記事は AWS Native 軸で対をなす</li>
</ul>
</div>
---
## 2. 業務背景: AWS Native CI/CD が選ばれる理由
### 2.1 手動デプロイの煩雑さ
ECS/Fargate へのデプロイを手動で行う場合、以下の6ステップを毎回繰り返すことになります。
```text
手動デプロイフロー(Before):
──────────────────────────────────────────────
Step 1: docker build -t myapp:v1.2.3 . (2〜5分)
Step 2: docker tag myapp:v1.2.3 <ECR_URI>:v1.2.3(1分)
Step 3: aws ecr get-login-password | docker login(1分)
Step 4: docker push <ECR_URI>:v1.2.3(3〜10分)
Step 5: タスク定義 JSON 手動編集 + register-task-definition (2〜5分)
Step 6: aws ecs update-service --force-new-deployment (2〜3分)
──────────────────────────────────────────────
合計: 11〜25分 × デプロイ回数
```text
1日3回デプロイすれば 33〜75分。スプリント20営業日なら **660〜1500分(11〜25時間)** がデプロイ作業だけで消えます。ステップ5の「タスク定義 JSON 手動編集」は特にリスクが高く、typo によるタスク定義破損が本番停止につながるケースが実際にあります。
本記事のパイプライン構築後は `git push` のみで全ステップが自動化されます。デプロイ担当者が端末の前にいる必要すらなくなります。

### 2.2 GHA 経由 vs AWS Native の選択軸
CI/CD を整備する際、多くのチームは「GitHub Actions で AWS にデプロイする」構成(cmd_040 のアプローチ)を選びます。しかしエンタープライズ案件では、以下の理由から AWS Native を選ぶケースが増えています。
**IAM 信頼境界**
GHA 経由の構成では、GitHub OIDC プロバイダーを AWS IAM の信頼ポリシーに登録します。これは「GitHub が発行する JWT を AWS が信頼する」設定であり、GitHub のセキュリティインシデントが AWS への認証に影響するリスクを受け入れることになります。AWS Native 構成では IAM ロールの信頼先が AWS サービス(`codepipeline.amazonaws.com` / `codebuild.amazonaws.com`)のみになるため、外部 IdP への依存がゼロです。
**監査ログの一元管理**
GHA 経由だと「GHA のビルドログ」と「CloudTrail の API 呼び出しログ」が別系統になり、インシデント調査時に2つのシステムをまたいで調査が必要です。AWS Native では CodePipeline の実行履歴・CodeBuild のビルドログ・CloudTrail の API ログがすべて AWS コンソールおよび CloudWatch Logs に集約され、一元管理できます。
**GitHub 障害からの独立**
GitHub が障害になっても、AWS Native 構成では既存のパイプライン実行に影響がありません(CodeStar Connections によるトリガーは失敗しますが、手動実行は可能)。GHA 経由では GitHub 障害 = CI/CD 完全停止です。
### 2.3 エンタープライズ案件の両ニーズへの対応
「GitHub から離れたくない」チームと「AWS 内で閉じたい」チームは、同じプロジェクト内に共存することが多いです。本記事は両方の要件を Terraform の変数切替で対応できる設計にします。
| 構成パターン | ソース管理 | トリガー | 本記事での扱い |
|------------|----------|---------|--------------|
| ① GitHub + AWS Native | GitHub | CodeStar Connections | メイン(§4〜§6) |
| ② CodeCommit + AWS Native | CodeCommit | CloudWatch Events | §6 Option として説明 |
| ③ GitHub + GHA | GitHub | GitHub Actions | cmd_040 シリーズ参照 |
パターン①②の切替は `source_type = "GitHub" | "CodeCommit"` 変数1つで対応します。既存の GitHub ワークフローを残しながら「デプロイだけ AWS Native に移行する」段階的移行も §6 で解説します。
### 2.4 コスト比較: 手動運用 vs パイプライン構築
「パイプライン構築に工数をかける価値があるか」という問いに、数字で答えます。
| 項目 | 手動運用 | パイプライン構築後 |
|------|---------|----------------|
| 1回あたりデプロイ工数 | 11〜25分 | 0分(git push のみ) |
| 月30回デプロイ時の工数 | 330〜750分 | 実質0分 |
| パイプライン構築工数 | — | 初回 90分 |
| 投資回収 | — | **2〜3回のデプロイで回収** |
| ヒューマンエラーリスク | 高(手動 JSON 編集) | 低(buildspec 管理) |
| 深夜デプロイ | 担当者が必要 | 不要(自動実行) |
月30回のデプロイを想定すると、初回構築コスト(90分)は **2〜3回分のデプロイ工数**に相当します。4回目以降は純粋な削減効果です。さらに「深夜バッチ後に手動でイメージを更新する作業」がゼロになる運用上のメリットは数字以上に大きいと言えます。
次 §3 では、本パイプラインの核となる ECS と Fargate の基礎知識を整理し、rolling update の仕組みを図で解説します。ECS 経験者は §3 を読み飛ばし §4 のインフラ構築から入っても問題ありません。
### 2.5 本記事が対象とするシステム規模
本記事は以下のシステム規模を想定して設計しています。
- **チーム規模**: 2〜10名(個人開発から小チームまで)
- **デプロイ頻度**: 1日1〜5回程度
- **コンテナ数**: 1〜3サービス(マイクロサービスの入口としても適用可)
- **トラフィック**: ALB + ECS Service(最小構成は 1 task)
大規模マイクロサービス(10サービス以上)やカナリアリリースが必要な環境は第2・第3弾の内容が必要になります。本記事はまず「1パイプライン・1サービス」の基本形を完全に理解することに集中します。
## 3. ECS/Fargate とゴール状態
<div class="ep-box ep-box--info">
<strong>🔰 ECS/Fargate 初挑戦の方へ</strong><br>
「ECS って難しそう」と感じている方、大丈夫です。本章では ECS/Fargate の5つの概念(cluster / service / task definition / task / container)を順番に丁寧に解説します。<br>
<strong>「コンテナを何台、どこで、どう動かすか」を Terraform で宣言する</strong>という構造が掴めれば、あとは設定値の調整だけです。
</div>
---
### 3-1. ECS の5階層を理解する
ECS を初めて触ると概念が多くて混乱しがちです。以下の5階層を上から順番に理解すると整理できます。
```text
[cluster]
└── [service] ← "何台動かすか" の管理単位(desired_count=2 など)
└── [task] ← 実際に起動しているコンテナグループの1インスタンス
└── [container] ← task 内の1コンテナ(1つ以上)
[task definition] ← service が参照する "設計図"(CPU/メモリ/イメージ/ポート)
```text
各層の責務:
| 層 | 役割 | Terraform リソース |
|----|------|-------------------|
| **cluster** | コンテナを動かす論理的な実行環境のグループ | `aws_ecs_cluster` |
| **task definition** | コンテナの設計図(revision 管理あり) | `aws_ecs_task_definition` |
| **service** | task を desired_count 台維持・ALB と連動 | `aws_ecs_service` |
| **task** | service が起動した task definition の実体 | ― (AWS 管理) |
| **container** | task 内の1コンテナプロセス | task definition 内に定義 |
---
### 3-2. Fargate vs EC2 launch type — どちらを選ぶか
ECS には2つの起動タイプがあります。
| 項目 | **Fargate** | EC2 launch type |
|-----|------------|-----------------|
| サーバー管理 | **不要**(AWS 完全管理) | EC2 インスタンスを自分で管理 |
| 課金単位 | task の vCPU × メモリ × 秒 | EC2 インスタンス時間(アイドルも課金) |
| ENI 数制約 | task ごとに専用 ENI(インスタンス ENI 上限なし) | EC2 インスタンスの ENI 数上限に縛られる |
| 向いている用途 | 可変トラフィック・スパイク対応・小〜中規模 | 定常高負荷・GPU ワークロード・コスト最小化 |
| **本記事の選択** | **✓ Fargate** | — |
**本記事が Fargate を選ぶ理由**: サーバーパッチ管理不要・ENI 上限問題なし・ハンズオンとしての構成シンプル化。
---
### 3-3. 本記事のゴール状態(先出し)
本記事(第1弾)が完成したとき、以下の状態を実現します。
```text
[GitHub main ブランチへの push]
↓
[CodePipeline]
↓ Source ステージ
[CodeBuild] — buildspec.yml でイメージビルド → ECR push
↓ Build ステージ
[ECS Service] — imagedefinitions.json を受け取り rolling update
↓
[Fargate task × 2] ← desired_count=2
↓
[ALB] ← HTTPS / health check 通過後に traffic 向け
```text
**ゴール状態の定義**:
| 項目 | 値 |
|-----|-----|
| ECS Service desired_count | 2(マルチ AZ 配置) |
| デプロイ戦略 | rolling update(CodePipeline 標準) |
| イメージタグ | git SHA 由来(`$CODEBUILD_RESOLVED_SOURCE_VERSION` の先頭8文字) |
| ロールバック | 旧イメージタグへの手動 Service 更新(§10 で解説) |
---
### 3-4. Terraform 最小構成 — ECS cluster + Fargate service
```hcl
# terraform/modules/ecs/main.tf
# Terraform 1.9.x / hashicorp/aws ~> 5.0
# --- ECS Cluster ---
resource "aws_ecs_cluster" "main" {
name = "${var.app_name}-cluster"
setting {
name = "containerInsights"
value = "enabled"
}
tags = {
ManagedBy = "terraform"
Env = var.environment
}
}
# --- Task Definition ---
resource "aws_ecs_task_definition" "app" {
family = "${var.app_name}-task"
network_mode = "awsvpc" # Fargate 必須
requires_compatibilities = ["FARGATE"]
cpu = "256" # 0.25 vCPU
memory = "512" # 512 MB
# タスク実行ロール(ECR pull + CloudWatch Logs 書き込み)
execution_role_arn = aws_iam_role.ecs_task_execution.arn
container_definitions = jsonencode([
{
name= var.container_name
image = "${aws_ecr_repository.app.repository_url}:latest"
essential = true
portMappings = [
{
containerPort = var.container_port
protocol= "tcp"
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group"= "/ecs/${var.app_name}"
"awslogs-region" = data.aws_region.current.name
"awslogs-stream-prefix" = "ecs"
}
}
}
])
}
# --- ECS Service ---
resource "aws_ecs_service" "app" {
name= "${var.app_name}-service"
cluster= aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
desired_count= 2
launch_type = "FARGATE"
network_configuration {
subnets = var.private_subnet_ids
security_groups = [aws_security_group.ecs_task.id]
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.app.arn
container_name= var.container_name
container_port= var.container_port
}
# CodePipeline からのデプロイを妨げないよう ignore
lifecycle {
ignore_changes = [task_definition]
}
depends_on = [aws_lb_listener.https]
}
lifecycle { ignore_changes = [task_definition] } は重要です。CodePipeline が imagedefinitions.json を使って task definition を自動更新するため、Terraform は task_definition の変更を無視する必要があります。これがないと次の terraform apply でデプロイ結果が上書きされます。
3-5. ALB + target group(type=ip)の設定
Fargate は target group type = ip が必須です(EC2 用の instance タイプは使えません)。
# ALB target group
resource "aws_lb_target_group" "app" {
name = "${var.app_name}-tg"
port = var.container_port
protocol = "HTTP"
vpc_id= var.vpc_id
target_type = "ip"# ★ Fargate は ip 必須
health_check {
enabled = true
path = "/health"
healthy_threshold= 2
unhealthy_threshold = 3
timeout = 5
interval= 30
# ★ healthcheck grace period: 新 task 起動直後の誤判定を防ぐ
}
}
# ALB
resource "aws_lb" "main" {
name= "${var.app_name}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets= var.public_subnet_ids
}
# HTTPS リスナー(ACM 証明書必要)
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.main.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn= var.acm_certificate_arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}
healthcheck grace period の設計:
ECS Service の health_check_grace_period_seconds は、新しい task が起動してから ALB のヘルスチェックを無視する秒数です。アプリの起動時間(Spring Boot は30-60秒、Go は数秒)に応じて設定します。
resource "aws_ecs_service" "app" {
# ... 省略 ...
health_check_grace_period_seconds = 60 # アプリ起動時間に合わせて設定
}
grace period が短すぎると起動直後に unhealthy 判定 → task 停止 → 再起動ループが発生します。
3-6. ECS Task Execution Role(最小権限)
Fargate が ECR からイメージを pull し、CloudWatch Logs へログを書き込むために必要な IAM ロールです。
resource "aws_iam_role" "ecs_task_execution" {
name = "${var.app_name}-ecs-task-execution-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "ecs-tasks.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
# AWS マネージドポリシー(ECR pull + Logs 書き込み)
resource "aws_iam_role_policy_attachment" "ecs_execution" {
role = aws_iam_role.ecs_task_execution.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
AmazonECSTaskExecutionRolePolicy に含まれる権限:
| 権限 | 目的 |
|---|---|
ecr:GetAuthorizationToken | ECR 認証トークン取得 |
ecr:BatchGetImage / BatchCheckLayerAvailability | コンテナイメージ pull |
logs:CreateLogStream / PutLogEvents | CloudWatch Logs 書き込み |
3-7. ネットワーク設計(awsvpc mode)
Fargate は networkMode = "awsvpc" 固定で、task ごとに専用 ENI が割り当てられます。
[VPC]
├── Public Subnet (AZ-a, AZ-c)
│ └── ALB
└── Private Subnet (AZ-a, AZ-c)
├── Fargate Task (ENI-1) ← AZ-a
└── Fargate Task (ENI-2) ← AZ-c
```text
```hcl
# ECS タスク用セキュリティグループ
resource "aws_security_group" "ecs_task" {
name= "${var.app_name}-ecs-task-sg"
vpc_id = var.vpc_id
ingress {
description = "from ALB only"
from_port = var.container_port
to_port= var.container_port
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
egress {
from_port= 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"] # ECR pull / CW Logs 送信
}
}
3-8. cmd_040 との差別化
| 観点 | cmd_040 シリーズ | 本記事(cmd_050) |
|—–|—————-|—————-|
| CI/CD ツール | GitHub Actions(GHA) | AWS CodePipeline |
| デプロイ対象 | Lambda / EC2 | ECS/Fargate |
| IAM 認証 | OIDC(GitHub → AWS) | IAM Role(CodeBuild サービスロール) |
| 監査ログ | GHA ログ | CloudTrail + CodePipeline イベント |
| 向いている組織 | GitHub 中心・オープンソース文化 | AWS アカウント内完結・エンタープライズ |
どちらが「正解」かではなく、組織の IAM 信頼境界と既存ツールチェーンに合わせて選択してください。
3-9. コスト感(Fargate + ALB)
| リソース | 単価 | 月額目安(desired_count=2・0.25vCPU/512MB) |
|---|---|---|
| Fargate vCPU | $0.04048/vCPU時間 | 0.25 × 2 × 730h × $0.04048 ≒ $14.8 |
| Fargate メモリ | $0.004445/GB時間 | 0.5 × 2 × 730h × $0.004445 ≒ $3.2 |
| ALB | $0.0243/時間 + LCU 課金 | ≒ $17.7(ALB 固定費のみ) |
| ECR ストレージ | $0.10/GB・月 | 1GB ≒ $0.10 |
| 合計概算 | ≒ $36/月(小規模ハンズオン) |
ハンズオン後は desired_count=0 にするか terraform destroy でコスト停止できます。

4. ECR 基礎 — repository / lifecycle / scan
4-1. ECR とは何か — Docker Hub との違い
Amazon ECR(Elastic Container Registry)は AWS のフルマネージドな プライベートコンテナレジストリ です。
| 項目 | Docker Hub(Free) | Amazon ECR |
|---|---|---|
| プライバシー | パブリック / プライベート | プライベート専用(AWS アカウント分離) |
| 認証 | Docker Hub アカウント | IAM ロール(OIDC / AssumeRole) |
| ネットワーク | インターネット経由 | VPC エンドポイントでインターネット不要 |
| スキャン | 基本的な CVE | Inspector v2 連携(強化スキャン) |
| コスト | 無料枠あり(500MB/月) | $0.10/GB・月(最初の500MB無料) |
本記事が ECR を選ぶ理由: AWS IAM で認証するため外部サービスへの認証情報管理が不要・CodeBuild → ECR の pull は VPC 内で完結・監査が CloudTrail で統一できる。
4-2. ECR URI 構造
ECR のイメージ URI は以下の形式です:
<aws_account_id>.dkr.ecr.<region>.amazonaws.com/<repository_name>:<tag>
```text
例:
```text
123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/my-app:a1b2c3d4
```text
| 部分 | 説明 |
|-----|------|
| `123456789012` | AWS アカウント ID |
| `dkr.ecr` | ECR のサービスドメイン識別子 |
| `ap-northeast-1` | リージョン |
| `my-app` | リポジトリ名 |
| `a1b2c3d4` | イメージタグ(本記事は git SHA 先頭8文字) |
---
### 4-3. Terraform — ECR repository 作成
```hcl
# terraform/modules/ecr/main.tf
resource "aws_ecr_repository" "app" {
name = var.repository_name
image_tag_mutability = "MUTABLE" # ハンズオン簡略化(§11 で IMMUTABLE 移行案内)
image_scanning_configuration {
scan_on_push = true # push 時に自動スキャン(basic スキャン)
}
encryption_configuration {
encryption_type = "KMS" # 本番推奨
kms_key= aws_kms_key.ecr.arn
}
tags = {
ManagedBy = "terraform"
Env = var.environment
}
}
# KMS キー(ECR 暗号化用)
resource "aws_kms_key" "ecr" {
description = "ECR encryption key for ${var.repository_name}"
deletion_window_in_days = 7
enable_key_rotation = true
}
resource "aws_kms_alias" "ecr" {
name = "alias/${var.repository_name}-ecr"
target_key_id = aws_kms_key.ecr.key_id
}
MUTABLE vs IMMUTABLE タグ:
| 設定 | 意味 | 本記事の採用 |
|---|---|---|
MUTABLE | 同じタグで上書き push 可能 | ✓(ハンズオン操作シンプル化) |
IMMUTABLE | 同じタグの上書き禁止 | §11 で移行案内(本番推奨) |
4-4. ECR lifecycle policy — イメージ自動削除
CI/CD パイプラインで毎日 push すると、古いイメージがどんどん蓄積してストレージコストが増加します。lifecycle policy で自動削除を設定します。
resource "aws_ecr_lifecycle_policy" "app" {
repository = aws_ecr_repository.app.name
policy = jsonencode({
rules = [
{
# ルール1: タグなしイメージは7日で削除
rulePriority = 1
description = "Expire untagged images after 7 days"
selection = {
tagStatus= "untagged"
countType= "sinceImagePushed"
countUnit= "days"
countNumber = 7
}
action = { type = "expire" }
},
{
# ルール2: tagged イメージは最新30世代保持
rulePriority = 2
description = "Keep last 30 tagged images"
selection = {
tagStatus = "tagged"
tagPrefixList = ["v", "sha-"] # v1.0.0 / sha-a1b2c3d4 形式
countType = "imageCountMoreThan"
countNumber= 30
}
action = { type = "expire" }
}
]
})
}
lifecycle policy の評価順序: rulePriority が小さいほど優先です。untagged (=1) が先に評価され、その後 tagged (=2) が評価されます。
4-5. image scanning — basic vs enhanced
ECR には2種類のスキャンがあります。
| スキャン種別 | 対象 | 精度 | コスト |
|---|---|---|---|
| basic | OS パッケージ CVE | 標準 | 無料 |
| enhanced | OS + アプリ依存ライブラリ(Node.js, Python…) | 高精度 | Amazon Inspector v2 料金($0.11/container image・月) |
# enhanced スキャン(本番推奨・Inspector v2 有効化が前提)
resource "aws_ecr_repository" "app_enhanced" {
name = "${var.repository_name}-prod"
image_scanning_configuration {
scan_on_push = true
}
}
# Inspector v2 の ECR スキャン有効化
resource "aws_inspector2_enabler" "ecr" {
account_ids = [data.aws_caller_identity.current.account_id]
resource_types = ["ECR"]
}
本記事のハンズオン環境では basic スキャン(無料) を使用します。本番移行時に enhanced へアップグレードしてください。
4-6. ECR ログイン — docker login コマンド
CodeBuild や開発端末から ECR へ push するには、まず ECR 認証トークンを取得して docker login します。
# ECR ログイン(AWS CLI v2)
aws ecr get-login-password \
--region ap-northeast-1 \
| docker login \
--username AWS \
--password-stdin \
123456789012.dkr.ecr.ap-northeast-1.amazonaws.com
# 成功すると "Login Succeeded" と表示される
# イメージのビルド・タグ付け・push(ローカル開発時)
IMAGE_TAG=$(git rev-parse --short HEAD)
ECR_URI="123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/my-app"
docker build -t my-app:${IMAGE_TAG} .
docker tag my-app:${IMAGE_TAG} ${ECR_URI}:${IMAGE_TAG}
docker push ${ECR_URI}:${IMAGE_TAG}
# latest タグも更新(任意)
docker tag my-app:${IMAGE_TAG} ${ECR_URI}:latest
docker push ${ECR_URI}:latest
CodeBuild での自動 push は §5(buildspec.yml 設計)で詳しく解説します。ECR URI と IMAGE_TAG を環境変数として渡す設計です。
4-7. ECR VPC エンドポイント(本番推奨)
本番環境では、Fargate task が ECR からイメージを pull する際にインターネットを経由しないよう、VPC エンドポイントを設定します。
# ECR API エンドポイント(ecr:GetAuthorizationToken 等)
resource "aws_vpc_endpoint" "ecr_api" {
vpc_id = var.vpc_id
service_name = "com.amazonaws.${data.aws_region.current.name}.ecr.api"
vpc_endpoint_type= "Interface"
subnet_ids = var.private_subnet_ids
security_group_ids = [aws_security_group.vpc_endpoint.id]
private_dns_enabled = true
}
# ECR DKR エンドポイント(実際の pull 通信)
resource "aws_vpc_endpoint" "ecr_dkr" {
vpc_id = var.vpc_id
service_name = "com.amazonaws.${data.aws_region.current.name}.ecr.dkr"
vpc_endpoint_type= "Interface"
subnet_ids = var.private_subnet_ids
security_group_ids = [aws_security_group.vpc_endpoint.id]
private_dns_enabled = true
}
# S3 ゲートウェイエンドポイント(ECR レイヤーデータは S3 経由)
resource "aws_vpc_endpoint" "s3" {
vpc_id= var.vpc_id
service_name= "com.amazonaws.${data.aws_region.current.name}.s3"
vpc_endpoint_type = "Gateway"
route_table_ids= var.private_route_table_ids
}
ECR のレイヤーデータは S3 に保存されているため、ecr.api / ecr.dkr / s3 の3つのエンドポイントが必要です。
4-8. コスト感(ECR)
| 課金対象 | 単価 | 目安 |
|---|---|---|
| ストレージ | $0.10/GB・月 | 1GB = $0.10 |
| データ転送(同リージョン) | 無料 | — |
| データ転送(リージョン外・インターネット) | $0.09/GB | VPC エンドポイントで回避可 |
| basic スキャン | 無料 | — |
| enhanced スキャン | $0.11/コンテナイメージ・月 | 本番移行時に検討 |
lifecycle policy で古いイメージを自動削除すれば、ストレージコストはほぼ $1 以内に収まります。

5. CodeBuild 基礎 — buildspec.yml 設計
Section 4 で ECR リポジトリを用意しました。次は「Docker イメージをビルドして ECR にプッシュする」自動化エンジンである AWS CodeBuild を設定します。CodeBuild の動作はすべて buildspec.yml というファイルで宣言します。
5-1. CodeBuild の動作概要
CodeBuild ビルド処理フロー
ソースコード(GitHub)
│
│ Pull(Source fetch)
▼
CodeBuild ビルド環境(コンテナ)
│
├── install phase→ 環境確認・追加ツールインストール
├── pre_build phase → ECR ログイン / IMAGE_TAG 生成
├── build phase → docker build / docker tag
└── post_build phase → docker push / imagedefinitions.json 生成
│
└──→ S3 artifacts(CodePipeline への handoff)
CodeBuild はビルドごとに新しいコンテナを起動します。状態を持たないため再現性が高く、スケールアウトも容易です。
5-2. buildspec.yml 完全版
# buildspec.yml(プロジェクトルートに配置)
version: 0.2
env:
variables:
AWS_REGION: "ap-northeast-1"
CONTAINER_NAME: "app" # ECS タスク定義のコンテナ名と一致させること
parameter-store:
ECR_REPO_URI: "/cicd/ecr-repo-uri"# SSM Parameter Store から取得
phases:
install:
runtime-versions:
docker: 20
commands:
- echo "=== install phase: 環境確認 ==="
- docker --version
- aws --version
- echo "Build started on $(date)"
pre_build:
commands:
- echo "=== pre_build phase: ECR ログイン + IMAGE_TAG 生成 ==="
# ECR ログイン(リージョンの ECR エンドポイントに認証)
- aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $ECR_REPO_URI
# git commit SHA の先頭 8 桁を IMAGE_TAG として使用
- IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-8)
- echo "IMAGE_TAG = $IMAGE_TAG"
build:
commands:
- echo "=== build phase: docker build + tag ==="
# Dockerfile をビルド(BuildKit 有効化でキャッシュ効率向上)
- DOCKER_BUILDKIT=1 docker build -t $ECR_REPO_URI:$IMAGE_TAG .
# latest タグも付与(デプロイ環境での参照用)
- docker tag $ECR_REPO_URI:$IMAGE_TAG $ECR_REPO_URI:latest
post_build:
commands:
- echo "=== post_build phase: push + imagedefinitions.json 生成 ==="
# SHA タグと latest タグの両方を push
- docker push $ECR_REPO_URI:$IMAGE_TAG
- docker push $ECR_REPO_URI:latest
# CodePipeline → ECS Deploy への handoff ファイル生成
- printf '[{"name":"%s","imageUri":"%s"}]' $CONTAINER_NAME $ECR_REPO_URI:$IMAGE_TAG > imagedefinitions.json
- echo "Build completed on $(date)"
artifacts:
files:
- imagedefinitions.json # CodePipeline の Deploy ステージが読み込む
cache:
paths:
- '/root/.cache/pip*' # Python pip キャッシュ
- '/root/.npm*' # Node.js npm キャッシュ
5-3. 4 フェーズの責務分担
| フェーズ | 責務 | 失敗時の影響 |
|---|---|---|
install | ランタイム / ツール確認 | ビルド全体が abort |
pre_build | ECR 認証 / 変数設定 | build フェーズが実行されない |
build | イメージ生成 | push されない(ECR に古いイメージが残る) |
post_build | push / handoff ファイル生成 | ECS Deploy が古いイメージを参照 |
重要: post_build が失敗してもビルド自体は FAILED ではなく BUILD_FAILED として報告されます。imagedefinitions.json が生成されない場合、CodePipeline の Deploy ステージは前回のイメージを使い続けます。
5-4. git SHA 由来の IMAGE_TAG 生成
# CODEBUILD_RESOLVED_SOURCE_VERSION = git の full commit SHA(40 桁)
# cut -c 1-8 で先頭 8 桁に短縮(十分なユニーク性)
IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-8)
# 生成例
# CODEBUILD_RESOLVED_SOURCE_VERSION = a1b2c3d4e5f6789012345678901234567890abcd
# IMAGE_TAG = a1b2c3d4
SHA タグにより、どのコミットからビルドされたイメージかが一目でわかります。また latest タグと SHA タグの両方を push することで:
– latest: 開発環境での素早い参照
– SHA タグ: 本番環境での再現性確保(terraform apply でのピン留め)
の使い分けが可能になります。
5-5. imagedefinitions.json の役割
[
{
"name": "app",
"imageUri": "123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/myapp:a1b2c3d4"
}
]
このファイルが CodePipeline → ECS Deploy ステージへの handoff を担います。
CodePipeline の Artifacts フロー
CodeBuild(build stage)
│ imagedefinitions.json を S3 artifacts へ出力
▼
S3 Artifact Store
│ CodePipeline が自動取得
▼
ECS Deploy Action
│ imagedefinitions.json を読み込み
│ コンテナ名 "app" を ECS タスク定義の対応コンテナに一致させる
▼
ECS サービス更新(rolling update)
name フィールドは ECS タスク定義のコンテナ名と完全一致する必要があります。
5-6. 環境変数設計
CodeBuild プロジェクトに設定する環境変数の設計方針:
環境変数の種別と設定場所
┌──────────────────────────────────────────────────────────────┐
│ 種別 │ 設定場所 │ 例 │
├──────────────────────────────────────────────────────────────│
│ PLAINTEXT │ CodeBuild env.variables │ AWS_REGION│
│ PARAMETER_STORE │ SSM Parameter Store │ ECR_REPO_URI │
│ SECRETS_MANAGER │ Secrets Manager│ DB_PASSWORD (将来) │
└──────────────────────────────────────────────────────────────┘
| 環境変数名 | 設定場所 | 値の例 | 用途 |
|---|---|---|---|
AWS_REGION | buildspec.yml env.variables | ap-northeast-1 | ECR ログイン先リージョン |
ECR_REPO_URI | SSM Parameter Store | 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/myapp | push 先 URI |
CONTAINER_NAME | buildspec.yml env.variables | app | imagedefinitions.json のコンテナ名 |
CODEBUILD_RESOLVED_SOURCE_VERSION | CodeBuild 自動設定 | a1b2c3d4... | IMAGE_TAG 生成元 |
シークレット(DB_PASSWORD 等)は絶対に buildspec.yml にハードコードしません。
5-7. CodeBuild project Terraform
# modules/ci-cd/codebuild.tf
resource "aws_codebuild_project" "app" {
name = "${var.project_name}-build"
description= "ECS Fargate アプリの Docker ビルド + ECR push"
build_timeout = 20# 分(デフォルト 60 分・短縮でコスト削減)
service_role = aws_iam_role.codebuild.arn
artifacts {
type = "CODEPIPELINE"# CodePipeline 管理の S3 artifacts を使用
}
environment {
compute_type = "BUILD_GENERAL1_SMALL"# 3 GB / 2 vCPU
image = "aws/codebuild/amazonlinux2-x86_64-standard:5.0"
type= "LINUX_CONTAINER"
privileged_mode = true# Docker daemon 操作に必須
environment_variable {
name = "AWS_REGION"
value = var.region
type = "PLAINTEXT"
}
environment_variable {
name = "CONTAINER_NAME"
value = var.container_name
type = "PLAINTEXT"
}
environment_variable {
name = "ECR_REPO_URI"
value = "/cicd/ecr-repo-uri"
type = "PARAMETER_STORE"# SSM Parameter Store から実行時に取得
}
}
source {
type= "CODEPIPELINE"
buildspec = "buildspec.yml"# リポジトリルートの buildspec.yml を参照
}
# S3 キャッシュ(ビルド時間短縮・pip / npm のキャッシュを保持)
cache {
type = "S3"
location = "${aws_s3_bucket.artifacts.id}/cache"
}
logs_config {
cloudwatch_logs {
group_name = "/aws/codebuild/${var.project_name}"
stream_name = "build"
status= "ENABLED"
}
}
tags = local.common_tags
}
privileged_mode = true が必要な理由: CodeBuild のビルド環境自体がコンテナ内で動作するため、docker build 等の Docker 操作(Docker-in-Docker)には特権モードが必要です。
5-8. キャッシュ設計
# buildspec.yml キャッシュ設定(言語別)
# Python プロジェクトの場合
cache:
paths:
- '/root/.cache/pip*'
# Node.js プロジェクトの場合
cache:
paths:
- '/root/.npm*'
- 'node_modules*'
# Go プロジェクトの場合
cache:
paths:
- '/root/go/pkg/mod*'
キャッシュは S3 バケットに保存され、次回ビルド時に自動復元されます。node_modules のキャッシュにより npm install の時間を 60 秒 → 5 秒程度に短縮できます。

図 5: CodeBuild buildspec.yml フロー(4 フェーズ: install → pre_build → build → post_build)
Section 5 まとめ
| 要素 | 設計ポイント |
|---|---|
| buildspec.yml | version: 0.2 / 4 フェーズ分担 / リポジトリルートに配置 |
| IMAGE_TAG | git commit SHA 先頭 8 桁(CODEBUILD_RESOLVED_SOURCE_VERSION) |
| imagedefinitions.json | [{"name":"<container>","imageUri":"<ecr-uri>:<tag>"}] — Deploy への handoff |
| 環境変数 | PLAINTEXT / PARAMETER_STORE / SECRETS_MANAGER の 3 種使い分け |
| privileged_mode | Docker-in-Docker のために true 必須 |
| キャッシュ | S3 cache で pip / npm を保存し繰り返しビルドを高速化 |
6. CodePipeline 基礎 — source / build / deploy ステージ
Section 5 で CodeBuild(ビルドエンジン)を設定しました。次は「Source → Build → Deploy の一連のフローを自動化する」AWS CodePipeline を構築します。
6-1. CodePipeline の 3 ステージ概要
CodePipeline ステージ遷移
[Stage 1: Source]
GitHub リポジトリ(main ブランチへの push)
↓ CodeStar Connections 経由で pull
S3 Artifact Store(ソースコード zip)
[Stage 2: Build]
S3 から zip を取得
↓ CodeBuild に渡す
docker build + push → imagedefinitions.json を S3 artifacts へ
[Stage 3: Deploy]
S3 から imagedefinitions.json を取得
↓ ECS Deploy action
ECS サービス更新(rolling update)
6-2. artifact store S3 bucket
# modules/ci-cd/s3.tf
resource "aws_s3_bucket" "artifacts" {
bucket = "${var.project_name}-pipeline-artifacts-${data.aws_caller_identity.current.account_id}"
tags = local.common_tags
}
# パブリックアクセスブロック
resource "aws_s3_bucket_public_access_block" "artifacts" {
bucket = aws_s3_bucket.artifacts.id
block_public_acls = true
block_public_policy = true
ignore_public_acls= true
restrict_public_buckets = true
}
# サーバーサイド暗号化(AES-256)
resource "aws_s3_bucket_server_side_encryption_configuration" "artifacts" {
bucket = aws_s3_bucket.artifacts.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
# バージョニング有効化(ロールバック用)
resource "aws_s3_bucket_versioning" "artifacts" {
bucket = aws_s3_bucket.artifacts.id
versioning_configuration {
status = "Enabled"
}
}
# ライフサイクルポリシー(30 日以上の古い artifacts を削除)
resource "aws_s3_bucket_lifecycle_configuration" "artifacts" {
bucket = aws_s3_bucket.artifacts.id
rule {
id = "expire-old-artifacts"
status = "Enabled"
noncurrent_version_expiration {
noncurrent_days = 30
}
expiration {
days = 90# 90 日以上経過した最新バージョンも削除
}
}
}
6-3. aws_codestarconnections_connection — GitHub 連携
# modules/ci-cd/connections.tf
resource "aws_codestarconnections_connection" "github" {
name = "${var.project_name}-github"
provider_type = "GitHub"
tags = local.common_tags
}
重要: aws_codestarconnections_connection リソースは Terraform で作成した時点では PENDING 状態です。コンソールから OAuth 承認を手動で実施しないと AVAILABLE になりません。
OAuth 承認の手動手順:
1. AWS コンソール → CodePipeline → Settings → Connections を開く
2. 作成した接続(PENDING 状態)を選択
3. 「Update pending connection」ボタンをクリック
4. GitHub 認証画面にリダイレクト
5. 「Authorize AWS Connector for GitHub」をクリック
6. 組織へのアクセスを許可する場合は「Grant」をクリック
7. Connections 画面に戻り、ステータスが「Available」になることを確認
この手順は terraform apply 後に 1 回だけ実施します。接続は再利用可能で、複数の Pipeline から参照できます。
# 接続状態の確認コマンド
aws codestar-connections list-connections \
--provider-type GitHub \
--region ap-northeast-1 \
--query 'Connections[*].{Name:ConnectionName,Status:ConnectionStatus}'
6-4. CodePipeline 3 ステージ Terraform 完成形
# modules/ci-cd/pipeline.tf
resource "aws_codepipeline" "app" {
name = "${var.project_name}-pipeline"
role_arn = aws_iam_role.codepipeline.arn
artifact_store {
location = aws_s3_bucket.artifacts.bucket
type = "S3"
encryption_key {
id= "alias/aws/s3"# AWS マネージドキーで暗号化
type = "KMS"
}
}
# ─── Stage 1: Source ─────────────────────────────────────────
stage {
name = "Source"
action {
name = "GitHub_Source"
category= "Source"
owner= "AWS"
provider= "CodeStarSourceConnection"
version = "1"
output_artifacts = ["source_output"]
configuration = {
ConnectionArn = aws_codestarconnections_connection.github.arn
FullRepositoryId = var.github_repo# "owner/repo-name" 形式
BranchName = var.github_branch # デフォルト: "main"
OutputArtifactFormat = "CODE_ZIP"
DetectChanges = "true"# main ブランチ push で自動トリガー
}
}
}
# ─── Stage 2: Build ──────────────────────────────────────────
stage {
name = "Build"
action {
name = "CodeBuild"
category= "Build"
owner= "AWS"
provider= "CodeBuild"
version = "1"
input_artifacts = ["source_output"]
output_artifacts = ["build_output"]
configuration = {
ProjectName = aws_codebuild_project.app.name
}
}
}
# ─── Stage 3: Deploy ─────────────────────────────────────────
stage {
name = "Deploy"
action {
name= "ECS_Deploy"
category = "Deploy"
owner = "AWS"
provider = "ECS"
version= "1"
input_artifacts = ["build_output"]
configuration = {
ClusterName = var.ecs_cluster_name
ServiceName = var.ecs_service_name
FileName = "imagedefinitions.json"# S3 artifacts 内のファイル名
# DeploymentTimeout = "15" # ECS デプロイタイムアウト(分)
}
}
}
tags = local.common_tags
depends_on = [
aws_codestarconnections_connection.github,
aws_codebuild_project.app,
aws_s3_bucket.artifacts
]
}
6-5. rolling update 仕様
# ECS サービスの rolling update 設定(modules/ecs-fargate/service.tf で設定)
resource "aws_ecs_service" "app" {
name= "${var.project_name}-service"
cluster= aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
desired_count= var.desired_count
launch_type = "FARGATE"
# Rolling Update 設定
deployment_controller {
type = "ECS"# ROLLING_UPDATE(デフォルト)
}
deployment_minimum_healthy_percent = 100# デプロイ中も全タスクが健全である必要がある
deployment_maximum_percent= 200# 最大でも desired_count の 2 倍まで
health_check_grace_period_seconds = 60 # ALB ヘルスチェック開始までの猶予時間
network_configuration {
subnets = var.private_subnet_ids
security_groups = [aws_security_group.ecs_tasks.id]
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.app.arn
container_name= var.container_name# imagedefinitions.json の name と一致
container_port= var.container_port
}
lifecycle {
ignore_changes = [task_definition]# CodePipeline がタスク定義を更新するため無視
}
tags = local.common_tags
}
rolling update の動作(desired_count = 2 の例):
デプロイ前: タスク v1 × 2(healthy)
デプロイ中:
minimum_healthy_percent=100 → 少なくとも 2 タスクが healthy
maximum_percent=200→ 最大 4 タスクまで起動可
新タスク v2 を 2 つ起動 → ALB ヘルスチェック通過
旧タスク v1 を 2 つ停止
デプロイ後: タスク v2 × 2(healthy)
minimum_healthy_percent=100 により、デプロイ中もサービスのダウンタイムがゼロになります。
6-6. Pipeline trigger 設定
CodeStar Connections の DetectChanges = "true" を設定すると、GitHub の main ブランチへの push で Pipeline が自動起動します。
# Pipeline の手動実行(開発時・強制再デプロイ時)
aws codepipeline start-pipeline-execution \
--name myapp-pipeline \
--region ap-northeast-1
# Pipeline の実行状態確認
aws codepipeline get-pipeline-state \
--name myapp-pipeline \
--region ap-northeast-1 \
--query 'stageStates[*].{Stage:stageName,Status:actionStates[0].latestExecution.status}'
特定コミットのみデプロイしたい場合(DetectChanges = "false" にして手動コントロール):
# Source ステージを特定コミット SHA で開始
aws codepipeline start-pipeline-execution \
--name myapp-pipeline \
--source-revisions '[{"actionName":"GitHub_Source","revisionType":"COMMIT_ID","revisionValue":"a1b2c3d4"}]' \
--region ap-northeast-1
6-7. variables.tf
# modules/ci-cd/variables.tf
variable "project_name" {
type = string
description = "プロジェクト名(全リソース名のプレフィックス)"
}
variable "region" {
type = string
default = "ap-northeast-1"
description = "AWS リージョン"
}
variable "github_repo" {
type = string
description = "GitHub リポジトリ(owner/repo 形式)"
# 例: "myorg/myapp"
}
variable "github_branch" {
type = string
default = "main"
description = "トリガーとなる GitHub ブランチ名"
}
variable "ecs_cluster_name" {
type = string
description = "デプロイ先 ECS クラスター名"
}
variable "ecs_service_name" {
type = string
description = "デプロイ先 ECS サービス名"
}
variable "container_name" {
type = string
default = "app"
description = "ECS タスク定義のコンテナ名(imagedefinitions.json の name と一致)"
}
variable "container_port" {
type = number
default = 8080
description = "コンテナが LISTEN するポート"
}
locals {
common_tags = {
ManagedBy= "Terraform"
Project = var.project_name
Environment = terraform.workspace
}
}
6-8. outputs.tf
# modules/ci-cd/outputs.tf
output "pipeline_name" {
value = aws_codepipeline.app.name
description = "CodePipeline 名"
}
output "pipeline_arn" {
value = aws_codepipeline.app.arn
description = "CodePipeline ARN(§8 pytest fixture で参照)"
}
output "codestar_connection_arn" {
value = aws_codestarconnections_connection.github.arn
description = "CodeStar Connections ARN(GitHub OAuth 承認後に AVAILABLE になること)"
}
output "artifacts_bucket_name" {
value = aws_s3_bucket.artifacts.bucket
description = "Artifact store S3 バケット名"
}
output "codebuild_project_name" {
value = aws_codebuild_project.app.name
description = "CodeBuild プロジェクト名"
}

図 6: CodePipeline ステージ遷移図(Source → Build → Deploy)
Section 6 まとめ
| ステージ | Provider | 主な設定 |
|---|---|---|
| Source | CodeStarSourceConnection | GitHub v2 / DetectChanges=true / OAuth 承認が手動必須 |
| Build | CodeBuild | ProjectName を指定 / source_output → build_output |
| Deploy | ECS | imagedefinitions.json 経由 / rolling update |
| 要素 | 設計ポイント |
|---|---|
| Artifact Store S3 | AES-256 暗号化 / バージョニング有効 / 90 日 lifecycle |
| rolling update | minimum_healthy=100% / maximum=200% でダウンタイムゼロ |
| CodeStar Connection | Terraform で作成 → コンソールで OAuth 承認(1 回のみ) |
| lifecycle ignore | task_definition は CodePipeline が更新するため ignore_changes 設定必須 |
Section 7 では、§3〜§6 のすべてのリソース(ECR / ECS / ALB / CodeBuild / CodePipeline / IAM)を Terraform モジュールとして統合します。
7. Terraform で全体をコード化
Section 3〜6 で ECR / ECS Fargate / ALB / CodeBuild / CodePipeline の各リソースを個別に設計しました。このセクションでは、すべてを Terraform モジュールとして統合し、terraform apply 1 回でパイプライン全体が立ち上がる完成形を実装します。

図 7: Terraform リソース依存グラフ(pipeline → codebuild → ecr + ecs → alb + iam)
7-1. ディレクトリ構成とモジュール分割方針
terraform/
├── environments/
│└── dev/
│ ├── main.tf ← モジュール呼び出し(ルートモジュール)
│ ├── variables.tf
│ ├── terraform.tfvars ← 環境固有の値(git ignore 推奨)
│ ├── outputs.tf
│ └── backend.tf ← S3 + DynamoDB state 管理
└── modules/
├── ecr/ ← ECR repository + lifecycle + scan
│├── main.tf
│├── variables.tf
│└── outputs.tf
├── ecs-fargate/← ECS cluster + task def + service + ALB
│├── main.tf
│├── variables.tf
│└── outputs.tf
└── ci-cd/← CodeBuild + CodePipeline + S3 artifact + IAM
├── codebuild.tf
├── pipeline.tf
├── s3.tf
├── connections.tf
├── iam.tf
├── variables.tf
└── outputs.tf
モジュール分割の方針:
– modules/ecr: ECR のみ(CI/CD とは独立してデプロイできる)
– modules/ecs-fargate: アプリケーション実行基盤(ALB 含む)
– modules/ci-cd: パイプライン全体(CodeBuild / CodePipeline / IAM / S3)
この分割により、ECR や ECS を先に apply してイメージが存在する状態でパイプラインを構築するフェーズ分割も可能ですが、本記事では one-shot apply を前提とした depends_on 設計を採用します。
7-2. backend.tf — S3 + DynamoDB state 管理
# 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-cicd/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" {}
data "aws_caller_identity" "current" でアカウント ID を動的取得し、ハードコードを回避します。ARN 等の参照には data.aws_caller_identity.current.account_id を使います。
7-3. tfvars 設計
# environments/dev/variables.tf
variable "project_name" {
type = string
description = "プロジェクト名(全リソース名のプレフィックス)"
}
variable "environment" {
type = string
description = "環境名(dev / stg / prod)"
default = "dev"
}
variable "region" {
type = string
default = "ap-northeast-1"
description = "AWS リージョン"
}
variable "github_repo" {
type = string
description = "GitHub リポジトリ(owner/repo 形式)例: myorg/myapp"
}
variable "github_branch" {
type = string
default = "main"
description = "Pipeline トリガーとなるブランチ名"
}
variable "container_port" {
type = number
default = 8080
description = "コンテナが LISTEN するポート番号"
}
variable "desired_count" {
type = number
default = 2
description = "ECS サービスの desired task 数"
}
variable "vpc_id" {
type = string
description = "デプロイ先 VPC ID"
}
variable "public_subnet_ids" {
type = list(string)
description = "ALB を配置するパブリックサブネット ID リスト"
}
variable "private_subnet_ids" {
type = list(string)
description = "ECS タスクを配置するプライベートサブネット ID リスト"
}
# environments/dev/terraform.tfvars
project_name = "myapp"
environment= "dev"
region = "ap-northeast-1"
github_repo= "myorg/myapp"
github_branch = "main"
container_port = 8080
desired_count = 2
vpc_id = "vpc-0abc1234567890abc"
public_subnet_ids = ["subnet-0111aaaa", "subnet-0222bbbb"]
private_subnet_ids = ["subnet-0333cccc", "subnet-0444dddd"]
7-4. modules/ecr — ECR repository + lifecycle
# modules/ecr/main.tf
resource "aws_ecr_repository" "app" {
name = var.project_name
image_tag_mutability = "MUTABLE"# ハンズオン簡略化・本番では IMMUTABLE 推奨
image_scanning_configuration {
scan_on_push = true# push 時に basic スキャン(CVE 検査)
}
encryption_configuration {
encryption_type = "AES256"
}
tags = var.tags
}
resource "aws_ecr_lifecycle_policy" "app" {
repository = aws_ecr_repository.app.name
policy = jsonencode({
rules = [
{
rulePriority = 1
description = "untagged イメージを 7 日で削除"
selection = {
tagStatus= "untagged"
countType= "sinceImagePushed"
countUnit= "days"
countNumber = 7
}
action = { type = "expire" }
},
{
rulePriority = 2
description = "tagged イメージを 30 世代保持"
selection = {
tagStatus = "tagged"
tagPrefixList = ["v", "sha"]
countType = "imageCountMoreThan"
countNumber= 30
}
action = { type = "expire" }
}
]
})
}
# SSM Parameter Store に ECR URI を登録(CodeBuild が参照)
resource "aws_ssm_parameter" "ecr_repo_uri" {
name = "/cicd/ecr-repo-uri"
type = "String"
value = aws_ecr_repository.app.repository_url
tags = var.tags
}
# modules/ecr/outputs.tf
output "repository_url" {
value = aws_ecr_repository.app.repository_url
description = "ECR リポジトリ URI(<account>.dkr.ecr.<region>.amazonaws.com/<name>)"
}
output "repository_arn" {
value = aws_ecr_repository.app.arn
}
7-5. modules/ecs-fargate — cluster + task definition + service + ALB
# modules/ecs-fargate/main.tf
# ── ECS Cluster ──────────────────────────────────────────────────
resource "aws_ecs_cluster" "main" {
name = "${var.project_name}-cluster"
setting {
name = "containerInsights"
value = "enabled"# CloudWatch Container Insights(メトリクス強化)
}
tags = var.tags
}
# ── Task Definition ───────────────────────────────────────────────
resource "aws_ecs_task_definition" "app" {
family = "${var.project_name}-task"
cpu = 256 # 0.25 vCPU(ハンズオン最小構成)
memory = 512 # 512 MB
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
execution_role_arn = var.task_execution_role_arn
task_role_arn= var.task_role_arn
container_definitions = jsonencode([
{
name= var.container_name
image = "${var.ecr_repository_url}:latest"# 初回 apply 用(CodePipeline が更新)
cpu = 256
memory = 512
essential = true
portMappings = [
{
containerPort = var.container_port
protocol= "tcp"
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group"= "/ecs/${var.project_name}"
"awslogs-region" = var.region
"awslogs-stream-prefix" = "ecs"
}
}
}
])
tags = var.tags
}
resource "aws_cloudwatch_log_group" "ecs" {
name = "/ecs/${var.project_name}"
retention_in_days = 30
tags = var.tags
}
# ── ALB ───────────────────────────────────────────────────────────
resource "aws_lb" "app" {
name= "${var.project_name}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets= var.public_subnet_ids
tags = var.tags
}
resource "aws_lb_target_group" "app" {
name = "${var.project_name}-tg"
port = var.container_port
protocol = "HTTP"
vpc_id= var.vpc_id
target_type = "ip"# Fargate 必須(インスタンスではなく ENI IP を登録)
health_check {
path = "/health"
interval= 30
timeout = 5
healthy_threshold= 2
unhealthy_threshold = 3
}
tags = var.tags
}
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.app.arn
port = 80
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}
# ── Security Groups ───────────────────────────────────────────────
resource "aws_security_group" "alb" {
name= "${var.project_name}-alb-sg"
vpc_id = var.vpc_id
ingress {
from_port= 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port= 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = var.tags
}
resource "aws_security_group" "ecs_tasks" {
name= "${var.project_name}-ecs-sg"
vpc_id = var.vpc_id
ingress {
from_port = var.container_port
to_port= var.container_port
protocol = "tcp"
security_groups = [aws_security_group.alb.id]# ALB からのトラフィックのみ
}
egress {
from_port= 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = var.tags
}
# ── ECS Service ───────────────────────────────────────────────────
resource "aws_ecs_service" "app" {
name= "${var.project_name}-service"
cluster= aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
desired_count= var.desired_count
launch_type = "FARGATE"
deployment_controller {
type = "ECS"# ROLLING_UPDATE
}
deployment_minimum_healthy_percent = 100
deployment_maximum_percent= 200
health_check_grace_period_seconds = 60
network_configuration {
subnets = var.private_subnet_ids
security_groups = [aws_security_group.ecs_tasks.id]
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.app.arn
container_name= var.container_name
container_port= var.container_port
}
lifecycle {
ignore_changes = [task_definition]# CodePipeline がタスク定義を更新するため
}
depends_on = [aws_lb_listener.http]
tags = var.tags
}
7-6. modules/ci-cd — IAM 4 ロール完成形
§8 で詳解しますが、§7 の Terraform 統合に必要な 4 ロールの骨格を示します。
# modules/ci-cd/iam.tf(骨格 — §8 で詳細展開)
# 1. CodePipeline service role
resource "aws_iam_role" "codepipeline" {
name = "${var.project_name}-codepipeline-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "codepipeline.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
tags = var.tags
}
resource "aws_iam_role_policy_attachment" "codepipeline" {
role = aws_iam_role.codepipeline.name
policy_arn = aws_iam_policy.codepipeline.arn
}
resource "aws_iam_policy" "codepipeline" {
name = "${var.project_name}-codepipeline-policy"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect= "Allow"
Action= ["s3:GetObject", "s3:PutObject", "s3:GetBucketVersioning"]
Resource = ["${aws_s3_bucket.artifacts.arn}", "${aws_s3_bucket.artifacts.arn}/*"]
},
{
Effect= "Allow"
Action= ["codebuild:BatchGetBuilds", "codebuild:StartBuild"]
Resource = aws_codebuild_project.app.arn
},
{
Effect= "Allow"
Action= ["ecs:DescribeServices", "ecs:DescribeTaskDefinition", "ecs:RegisterTaskDefinition", "ecs:UpdateService"]
Resource = "*"
},
{
Effect= "Allow"
Action= ["iam:PassRole"]
Resource = [var.task_execution_role_arn, var.task_role_arn]
},
{
Effect= "Allow"
Action= ["codestar-connections:UseConnection"]
Resource = aws_codestarconnections_connection.github.arn
}
]
})
}
# 2. CodeBuild service role
resource "aws_iam_role" "codebuild" {
name = "${var.project_name}-codebuild-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "codebuild.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
tags = var.tags
}
resource "aws_iam_role_policy" "codebuild" {
name = "${var.project_name}-codebuild-policy"
role = aws_iam_role.codebuild.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect= "Allow"
Action= ["ecr:GetAuthorizationToken"]
Resource = "*"
},
{
Effect= "Allow"
Action= ["ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", "ecr:InitiateLayerUpload", "ecr:UploadLayerPart", "ecr:CompleteLayerUpload", "ecr:PutImage"]
Resource = var.ecr_repository_arn
},
{
Effect= "Allow"
Action= ["s3:GetObject", "s3:PutObject", "s3:GetObjectVersion"]
Resource = ["${aws_s3_bucket.artifacts.arn}/*"]
},
{
Effect= "Allow"
Action= ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"]
Resource = "arn:aws:logs:${var.region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/codebuild/${var.project_name}:*"
},
{
Effect= "Allow"
Action= ["ssm:GetParameter"]
Resource = "arn:aws:ssm:${var.region}:${data.aws_caller_identity.current.account_id}:parameter/cicd/*"
}
]
})
}
7-7. ルートモジュール — environments/dev/main.tf
# environments/dev/main.tf
# ── ECR モジュール ─────────────────────────────────────────────────
module "ecr" {
source = "../../modules/ecr"
project_name = var.project_name
tags= local.common_tags
}
# ── ECS Fargate + ALB モジュール ──────────────────────────────────
module "ecs_fargate" {
source = "../../modules/ecs-fargate"
project_name= var.project_name
region= var.region
vpc_id= var.vpc_id
public_subnet_ids = var.public_subnet_ids
private_subnet_ids= var.private_subnet_ids
container_name = "app"
container_port = var.container_port
desired_count = var.desired_count
ecr_repository_url= module.ecr.repository_url
task_execution_role_arn = module.cicd.task_execution_role_arn
task_role_arn = module.cicd.task_role_arn
tags = local.common_tags
depends_on = [module.ecr]
}
# ── CI/CD モジュール ────────────────────────────────────────────────
module "cicd" {
source = "../../modules/ci-cd"
project_name= var.project_name
region= var.region
github_repo = var.github_repo
github_branch = var.github_branch
container_name = "app"
container_port = var.container_port
ecr_repository_arn= module.ecr.repository_arn
ecs_cluster_name = module.ecs_fargate.cluster_name
ecs_service_name = module.ecs_fargate.service_name
tags = local.common_tags
depends_on = [module.ecr, module.ecs_fargate]
}
locals {
common_tags = {
ManagedBy= "Terraform"
Project = var.project_name
Environment = var.environment
}
}
depends_on の設計意図:
依存グラフ(one-shot apply 順序)
module.ecr
│ ECR URI を SSM Parameter Store に登録
▼
module.ecs_fargate
│ ECR URI を参照して task definition を作成
│ ALB・SG・ECS cluster / service を作成
▼
module.cicd
│ ECR ARN を CodeBuild IAM ポリシーに設定
│ ECS cluster/service 名を CodePipeline Deploy に設定
│ CodeBuild + CodePipeline + S3 artifact store を作成
▼
terraform apply 完了
7-8. apply 手順と初回確認
# 1. 初期化(provider / module 取得)
cd environments/dev
terraform init
# 2. 計画確認(リソース数を事前把握)
terraform plan -out=dev.tfplan
# 3. 適用(one-shot)
terraform apply dev.tfplan
# 適用されるリソース概数(環境依存)
# + aws_ecr_repository 1
# + aws_ecr_lifecycle_policy 1
# + aws_ssm_parameter 1
# + aws_ecs_cluster 1
# + aws_ecs_task_definition 1
# + aws_ecs_service 1
# + aws_lb 1
# + aws_lb_target_group1
# + aws_lb_listener 1
# + aws_security_group 2
# + aws_cloudwatch_log_group 1
# + aws_codebuild_project 1
# + aws_codepipeline1
# + aws_s3_bucket1
# + aws_codestar_connection 1
# + aws_iam_role 4(pipeline/build/task_exec/task)
# + aws_iam_policy N
# ─────────────────────────────
# 合計 ~25〜30 リソース
# 4. apply 後: CodeStar Connection の OAuth 承認(コンソール・1回のみ)
# AWS Console → CodePipeline → Settings → Connections
# PENDING の接続を選択 → "Update pending connection" → GitHub 認証
# 5. ECR に初期イメージを push(CodePipeline の初回実行前に必要)
ECR_URI=$(terraform output -raw ecr_repository_url)
docker build -t ${ECR_URI}:latest .
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${ECR_URI}
docker push ${ECR_URI}:latest
# 6. ALB の疎通確認
ALB_DNS=$(terraform output -raw alb_dns_name)
curl -f http://${ALB_DNS}/health && echo "OK"
7-9. outputs.tf — 重要な参照値
# environments/dev/outputs.tf
output "ecr_repository_url" {
value = module.ecr.repository_url
description = "ECR URI(docker push / CodeBuild buildspec で使用)"
}
output "alb_dns_name" {
value = module.ecs_fargate.alb_dns_name
description = "ALB の DNS 名(アプリの疎通確認先)"
}
output "pipeline_name" {
value = module.cicd.pipeline_name
description = "CodePipeline 名(§9 運用・監視で参照)"
}
output "ecs_cluster_name" {
value = module.ecs_fargate.cluster_name
description = "ECS クラスター名(aws ecs コマンド・§10 ハンズオンで参照)"
}
output "ecs_service_name" {
value = module.ecs_fargate.service_name
description = "ECS サービス名"
}
output "codestar_connection_arn" {
value = module.cicd.codestar_connection_arn
description = "CodeStar Connections ARN(OAuth 承認確認用)"
}
Section 7 まとめ
| モジュール | 主なリソース | 依存 |
|---|---|---|
modules/ecr | aws_ecr_repository / lifecycle policy / SSM Parameter | なし |
modules/ecs-fargate | cluster / task definition / service / ALB / SG | modules/ecr |
modules/ci-cd | CodeBuild / CodePipeline / S3 / IAM 4 ロール / CodeStar | modules/ecr + modules/ecs-fargate |
| 項目 | 設計ポイント |
|---|---|
| バージョン固定 | terraform ~> 1.9 / hashicorp/aws ~> 5.0 明記 |
| アカウント ID | data "aws_caller_identity" "current" — ハードコード禁止 |
| 依存関係 | depends_on で pipeline → codebuild → ecr + ecs の順序を保証 |
| one-shot apply | モジュール間の output 参照で初回から全リソースを一括構築 |
| SSM Parameter | ECR URI を /cicd/ecr-repo-uri に格納し buildspec が参照 |
| lifecycle ignore | task_definition は CodePipeline が更新するため ignore_changes 設定 |
| CodeStar 承認 | terraform apply 後にコンソールで OAuth 承認が 1 回だけ必要 |
Section 8 では、4 ロール(CodePipeline / CodeBuild / ECS task execution / ECS task)の IAM 最小権限設計を詳解します。
8. IAM 最小権限設計
ECS/Fargate × CodePipeline 構成では、4つの IAM ロールが絡み合う。「とりあえず AdministratorAccess で動かす」は最もやってはいけないアンチパターンだ。本セクションでは4ロールそれぞれの assume_role_policy と policy を最小権限で完全実装し、よくある過剰権限パターンと対比しながら解説する。

図08: IAM ロール信頼関係図 — 4ロールの assume_role_policy と主要権限の全体像
8.1 4ロールの責務分担
| ロール名 | Principal | 主な権限 | 呼び出し元 |
|---|---|---|---|
codepipeline_service_role | codepipeline.amazonaws.com | S3 artifact R/W・CodeBuild 起動・ECS update | CodePipeline |
codebuild_service_role | codebuild.amazonaws.com | ECR push・S3 artifact write・CloudWatch Logs | CodeBuild project |
ecs_task_execution_role | ecs-tasks.amazonaws.com | ECR pull・CloudWatch Logs write | ECS agent(コンテナ起動時) |
ecs_task_role | ecs-tasks.amazonaws.com | アプリ固有 AWS API(本記事は S3 read-only 最小例) | コンテナ内アプリ |
この4ロールは呼び出し元が異なるため assume_role_policy(信頼ポリシー)の Principal がそれぞれ別のサービスになる。混同が最大の設定ミスの元なので表で整理してから実装に入る。
8.2 ロール1: CodePipeline Service Role
CodePipeline が他の AWS サービスを呼び出すためのロール。codepipeline.amazonaws.com を Principal に指定する。
Terraform 定義:
# modules/ci-cd/iam_codepipeline.tf
resource "aws_iam_role" "codepipeline_service_role" {
name = "${var.project_name}-codepipeline-service-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "TrustCodePipeline"
Effect = "Allow"
Principal = {
Service = "codepipeline.amazonaws.com"
}
Action = "sts:AssumeRole"
}
]
})
tags = local.common_tags
}
resource "aws_iam_role_policy" "codepipeline_policy" {
name = "${var.project_name}-codepipeline-policy"
role = aws_iam_role.codepipeline_service_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
# S3 artifact バケットへの読み書き
{
Sid = "S3ArtifactAccess"
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:GetObjectVersion",
"s3:PutObject",
"s3:GetBucketVersioning"
]
Resource = [
aws_s3_bucket.codepipeline_artifacts.arn,
"${aws_s3_bucket.codepipeline_artifacts.arn}/*"
]
},
# CodeBuild ジョブの起動と結果取得
{
Sid = "CodeBuildAccess"
Effect = "Allow"
Action = [
"codebuild:BatchGetBuilds",
"codebuild:StartBuild"
]
Resource = aws_codebuild_project.app_build.arn
},
# ECS サービスの更新(deploy stage)
{
Sid = "ECSDeployAccess"
Effect = "Allow"
Action = [
"ecs:DescribeServices",
"ecs:DescribeTaskDefinition",
"ecs:DescribeTasks",
"ecs:ListTasks",
"ecs:RegisterTaskDefinition",
"ecs:UpdateService"
]
Resource = "*" # ECS は ARN フィルタが難しいため * を許容
},
# ECS task definition 登録時に iam:PassRole が必要
{
Sid = "PassRoleToECS"
Effect = "Allow"
Action = "iam:PassRole"
Resource = [
aws_iam_role.ecs_task_execution_role.arn,
aws_iam_role.ecs_task_role.arn
]
Condition = {
StringEquals = {
"iam:PassedToService" = "ecs-tasks.amazonaws.com"
}
}
},
# CodeStar Connections (GitHub接続)
{
Sid = "CodeStarConnections"
Effect = "Allow"
Action = "codestar-connections:UseConnection"
Resource = aws_codestarconnections_connection.github.arn
}
]
})
}
iam:PassRole のリソース制約が重要
CodePipeline が ECS のタスク定義を更新するとき、ECS task execution role と task role を ECS に「渡す(Pass)」操作が発生する。iam:PassRole を Resource: "*" にすると、このロールを持つ誰でも任意の IAM ロールを他サービスに渡せてしまう権限昇格の温床になる。必ずリソースを2つの ECS ロール ARN に絞ること。
8.3 ロール2: CodeBuild Service Role
CodeBuild が Docker ビルド・ECR push・S3 書き込みを行うためのロール。
Terraform 定義:
# modules/ci-cd/iam_codebuild.tf
resource "aws_iam_role" "codebuild_service_role" {
name = "${var.project_name}-codebuild-service-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "TrustCodeBuild"
Effect = "Allow"
Principal = {
Service = "codebuild.amazonaws.com"
}
Action = "sts:AssumeRole"
}
]
})
tags = local.common_tags
}
resource "aws_iam_role_policy" "codebuild_policy" {
name = "${var.project_name}-codebuild-policy"
role = aws_iam_role.codebuild_service_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
# ECR ログイン + イメージ push
{
Sid = "ECRPush"
Effect = "Allow"
Action = [
"ecr:GetAuthorizationToken"
]
Resource = "*" # GetAuthorizationToken はリソース指定不可
},
{
Sid = "ECRImagePush"
Effect = "Allow"
Action = [
"ecr:BatchCheckLayerAvailability",
"ecr:CompleteLayerUpload",
"ecr:InitiateLayerUpload",
"ecr:PutImage",
"ecr:UploadLayerPart"
]
Resource = aws_ecr_repository.app.arn # 対象リポジトリに限定
},
# S3 artifact バケットへの書き込み
{
Sid = "S3ArtifactWrite"
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:PutObject"
]
Resource = "${aws_s3_bucket.codepipeline_artifacts.arn}/*"
},
# CloudWatch Logs(ビルドログ出力)
{
Sid = "CloudWatchLogs"
Effect = "Allow"
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
Resource = "arn:aws:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/codebuild/${var.project_name}*"
}
]
})
}
ecr:GetAuthorizationToken は Resource: "*" が必須
GetAuthorizationToken は特定のリポジトリではなく「ECR サービス全体への認証トークン取得」のため、AWS 仕様上 Resource: "*" しか受け付けない。これだけは * を許容するが、PutImage 等の書き込み系は必ずリポジトリ ARN に絞ること。
8.4 ロール3: ECS Task Execution Role
ECS エージェント(インフラ側)がコンテナ起動時に使うロール。ECR からイメージを pull し、CloudWatch Logs にログを書き込む。アプリ内部からは使われない点が task role との最大の違いだ。
Terraform 定義:
# modules/ecs-fargate/iam_task_execution.tf
resource "aws_iam_role" "ecs_task_execution_role" {
name = "${var.project_name}-ecs-task-execution-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "TrustECSAgent"
Effect = "Allow"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
Action = "sts:AssumeRole"
}
]
})
tags = local.common_tags
}
# AWS managed policy を使う(ECR pull + CW Logs は定型 → managed policy が安全)
resource "aws_iam_role_policy_attachment" "ecs_task_execution_managed" {
role = aws_iam_role.ecs_task_execution_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
# Secrets Manager 参照が必要な場合は追加(本記事はオプション)
resource "aws_iam_role_policy" "ecs_task_execution_secrets" {
count = var.enable_secrets_manager ? 1 : 0
name = "${var.project_name}-ecs-execution-secrets"
role = aws_iam_role.ecs_task_execution_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "SecretsManagerRead"
Effect = "Allow"
Action = [
"secretsmanager:GetSecretValue"
]
Resource = "arn:aws:secretsmanager:${var.aws_region}:${data.aws_caller_identity.current.account_id}:secret:${var.project_name}/*"
}
]
})
}
AWS managed policy AmazonECSTaskExecutionRolePolicy の中身確認(参考):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "*"
}
]
}
このポリシーは ECR pull と CW Logs の最小セットなのでそのまま使って問題ない。ただし Secrets Manager 参照は含まれないため、containerDefinitions の secrets フィールドを使う場合は追加ポリシーが必要だ。
8.5 ロール4: ECS Task Role(アプリ固有)
コンテナ内アプリが AWS API を呼ぶためのロール。Task execution role(ECS インフラ側)と混同しやすいが、こちらはアプリコードから boto3 や aws-sdk 経由で使われる。
本記事では S3 read-only の最小例を実装する。
Terraform 定義:
# modules/ecs-fargate/iam_task_role.tf
resource "aws_iam_role" "ecs_task_role" {
name = "${var.project_name}-ecs-task-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "TrustECSTask"
Effect = "Allow"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
Action = "sts:AssumeRole"
}
]
})
tags = local.common_tags
}
# アプリ固有権限(本記事: S3 read-only 最小例)
resource "aws_iam_role_policy" "ecs_task_policy" {
name = "${var.project_name}-ecs-task-policy"
role = aws_iam_role.ecs_task_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "S3ReadOnly"
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:ListBucket"
]
Resource = [
"arn:aws:s3:::${var.app_config_bucket}",
"arn:aws:s3:::${var.app_config_bucket}/*"
]
}
]
})
}
ECS タスク定義での紐付け:
resource "aws_ecs_task_definition" "app" {
family = "${var.project_name}-app"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = var.task_cpu
memory = var.task_memory
# task execution role: ECS インフラが使う(ECR pull / CW Logs)
execution_role_arn = aws_iam_role.ecs_task_execution_role.arn
# task role: アプリが使う(S3 read 等のアプリ固有権限)
task_role_arn = aws_iam_role.ecs_task_role.arn
container_definitions = jsonencode([
{
name = var.container_name
image = "${aws_ecr_repository.app.repository_url}:latest"
portMappings = [
{
containerPort = var.container_port
protocol= "tcp"
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group"= aws_cloudwatch_log_group.app.name
"awslogs-region" = var.aws_region
"awslogs-stream-prefix" = "ecs"
}
}
}
])
}
8.6 過剰権限の典型パターンとミニマム化
現場でよく見かける「動くけど危険」な設定と、正しいミニマム化の手順を対比する。
アンチパターン一覧:
| アンチパターン | 問題 | 正しい設定 |
|---|---|---|
Action: "*" + Resource: "*" | 全 AWS 権限 = Administrator 相当 | 使うアクションだけ列挙 |
iam:PassRole + Resource: "*" | 任意ロールを任意サービスに渡せる権限昇格 | リソースを ECS ロール ARN に限定 |
ecr:* + Resource: "*" | 全リポジトリへの全操作 | push は対象リポジトリ ARN のみ |
s3:* + Resource: "*" | 全バケットの全操作(他プロジェクトを含む) | バケット ARN + /key/* で限定 |
logs:* + Resource: "*" | 全ロググループへの書き込み | /aws/codebuild/{project}* に限定 |
ミニマム化の段階的手順:
# Step 1: 広めの権限で動かしてCloudTrailでアクセスログを収集(1週間)
# Step 2: IAM Access Analyzer で「実際に使われたアクション」を確認
aws accessanalyzer list-access-preview-findings \
--analyzer-arn arn:aws:access-analyzer:${REGION}:${ACCOUNT}:analyzer/ConsoleAnalyzer-* \
--access-preview-id <preview-id>
# Step 3: 使われていないアクションを削除 → terraform plan で差分確認
# Step 4: CloudTrail で再度1週間監視 → エラーなければ確定
IAM Access Analyzer の「未使用のアクション」検出を活用する
AWS Console の IAM → Access Analyzer → 「未使用のアクセス」で、過去 90 日間に使われなかった権限を自動検出できる。新規構築時は広め → Access Analyzer で絞り込み → 最小権限に固める 3 ステップが現実的な運用だ。
8.7 4ロール完全版の依存関係まとめ
§7 の Terraform 全体構成で4ロールが相互参照するため、depends_on の設計が重要だ。
# modules/ci-cd/main.tf — ロール間の参照関係
module "ecs_fargate" {
source = "../ecs-fargate"
project_name = var.project_name
# ecs_task_execution_role と ecs_task_role はここで生成
}
module "ci_cd" {
source = "../ci-cd"
project_name = var.project_name
# CodePipeline service role の iam:PassRole に ECS ロール ARN が必要
ecs_task_execution_role_arn = module.ecs_fargate.task_execution_role_arn
ecs_task_role_arn = module.ecs_fargate.task_role_arn
depends_on = [module.ecs_fargate]
}
outputs.tf でロール ARN を公開:
# modules/ecs-fargate/outputs.tf
output "task_execution_role_arn" {
value = aws_iam_role.ecs_task_execution_role.arn
description = "ECS task execution role ARN(CodePipeline の iam:PassRole に必要)"
}
output "task_role_arn" {
value = aws_iam_role.ecs_task_role.arn
description = "ECS task role ARN(アプリ固有権限)"
}
§8 まとめ — IAM 最小権限設計チェックリスト
- ✅ CodePipeline service role: S3 artifact R/W + CodeBuild 起動 + ECS update +
iam:PassRole(ECS ロール ARN 限定) - ✅ CodeBuild service role: ECR push(リポジトリ ARN 限定)+ S3 artifact write + CW Logs(プロジェクト名プレフィックス限定)
- ✅ ECS task execution role: AWS managed policy
AmazonECSTaskExecutionRolePolicy(ECR pull + CW Logs) - ✅ ECS task role: アプリ固有 AWS API のみ(本記事: S3 read-only)
- ✅
iam:PassRole: リソースを ECS ロール ARN 2本に限定・iam:PassedToService条件キー付与 - ✅ ミニマム化手順: 広め → IAM Access Analyzer → CloudTrail 確認 → 絞り込みの3ステップ
9. 運用 — デプロイ監視・ロールバック・ログ
CodePipeline + Fargate のパイプラインを本番運用するには、「デプロイが成功したか」を即座に知る仕組みと
「失敗時に安全に戻れる仕組み」の両輪が不可欠です。
本セクションでは EventBridge → SNS による通知・ECS の自動ロールバック・CloudWatch Logs Insights
による横断検索を実装します。
9-1. dataclass 先出し契約(§9 監視記述に活用)
spec §4 で先出しされた DeploymentRow を監視 Lambda / 通知フォーマットで利用します。
# context/skills/pipeline_deployment_contract.py
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
class PipelineStage(Enum):
SOURCE = "Source"
BUILD = "Build"
DEPLOY = "Deploy"
class DeploymentState(Enum):
STARTED = "Started"
SUCCEEDED = "Succeeded"
FAILED = "Failed"
CANCELED= "Canceled"
SUPERSEDED = "Superseded"
@dataclass(frozen=True)
class DeploymentRow:
execution_id: str
pipeline_name:str
stage: PipelineStage
state: DeploymentState
started_at:datetime
ended_at: datetime | None
duration_seconds: int | None
commit_sha:str | None
image_tag: str | None
error_message:str = ""
note:str = ""
この型をイベント通知 Lambda の入力として使うことで、通知メッセージのフォーマットを統一します。
9-2. EventBridge rule — Pipeline 実行ステータス監視
CodePipeline は実行ステータスが変化するたびに EventBridge へイベントを発行します。aws.codepipeline ソースの CodePipeline Pipeline Execution State Change を捕捉して SNS へ転送します。
# terraform/modules/pipeline_ops/eventbridge.tf
terraform {
required_version = ">= 1.9.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}
resource "aws_cloudwatch_event_rule" "pipeline_status" {
name = "codepipeline-execution-status"
description = "CodePipeline 実行ステータス変化を SNS へ転送"
event_pattern = jsonencode({
source = ["aws.codepipeline"]
"detail-type" = ["CodePipeline Pipeline Execution State Change"]
detail = {
state = ["SUCCEEDED", "FAILED", "CANCELED", "SUPERSEDED"]
}
})
}
resource "aws_cloudwatch_event_target" "pipeline_to_sns" {
rule= aws_cloudwatch_event_rule.pipeline_status.name
target_id = "PipelineToSns"
arn = aws_sns_topic.pipeline_ops.arn
input_transformer {
input_paths = {
pipeline = "$.detail.pipeline"
state = "$.detail.state"
exec_id = "$.detail['execution-id']"
region= "$.region"
}
input_template = "\"[CI/CD] pipeline=<pipeline> state=<state> exec_id=<exec_id> region=<region>\""
}
}
イベントパターンに detail.state フィルタを入れることで、STARTED の通知は省略し
最終結果(成功・失敗)のみ通知する設計です。開発初期で全ステータスを監視したい場合はstate フィルタを削除してください。
9-3. SNS トピック → メール / Slack / ntfy の 3 系統
SNS トピックとメール購読
# terraform/modules/pipeline_ops/sns.tf
resource "aws_sns_topic" "pipeline_ops" {
name = "pipeline-ops-notifications"
kms_master_key_id = aws_kms_key.sns.id
}
resource "aws_sns_topic_policy" "pipeline_ops" {
arn = aws_sns_topic.pipeline_ops.arn
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowEventBridge"
Effect = "Allow"
Principal = { Service = "events.amazonaws.com" }
Action= "SNS:Publish"
Resource = aws_sns_topic.pipeline_ops.arn
}
]
})
}
resource "aws_sns_topic_subscription" "email" {
topic_arn = aws_sns_topic.pipeline_ops.arn
protocol = "email"
endpoint = var.ops_email # シークレットは変数経由。terraform.tfvars に設定
}
Slack webhook(Lambda 経由)
resource "aws_sns_topic_subscription" "lambda_slack" {
topic_arn = aws_sns_topic.pipeline_ops.arn
protocol = "lambda"
endpoint = aws_lambda_function.slack_notifier.arn
}
resource "aws_lambda_permission" "sns_invoke" {
statement_id = "AllowSNSInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.slack_notifier.function_name
principal = "sns.amazonaws.com"
source_arn = aws_sns_topic.pipeline_ops.arn
}
# lambda/slack_notifier/index.py
import json
import os
import urllib.request
def handler(event: dict, context) -> None:
"""SNS 経由の CodePipeline ステータス通知を Slack へ転送する。"""
webhook_url = os.environ["SLACK_WEBHOOK_URL"] # SSM Parameter Store 参照推奨
for record in event.get("Records", []):
raw = record["Sns"]["Message"]
# SUCCEEDED は緑 / FAILED は赤でアイコンを切り替え
icon = ":white_check_mark:" if "SUCCEEDED" in raw else ":x:"
payload = json.dumps({"text": f"{icon} {raw}"}).encode()
req = urllib.request.Request(
webhook_url,
data=payload,
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req) as resp:
resp.read()
セキュリティ注記:
SLACK_WEBHOOK_URLは Lambda 環境変数に直書きせず、SSM Parameter Store(SecureString)から取得する構成を推奨します。
9-4. デプロイ失敗時のロールバック(ECS deployment_circuit_breaker)
ECS は deployment_circuit_breaker + rollback = true を設定することで、
ヘルスチェック失敗が続いた場合に自動的に前リビジョンへロールバックします。
# terraform/modules/ecs_service/main.tf(§7 の ECS service 定義に追記)
resource "aws_ecs_service" "app" {
name= "${var.app_name}-service"
cluster= var.cluster_arn
task_definition = aws_ecs_task_definition.app.arn
desired_count= var.desired_count
deployment_circuit_breaker {
enable= true
rollback = true# 失敗時に自動ロールバック
}
deployment_controller {
type = "ECS"
}
load_balancer {
target_group_arn = var.target_group_arn
container_name= var.app_name
container_port= var.container_port
}
# Rolling update 設定(同時入れ替え数の制御)
deployment_maximum_percent= 200
deployment_minimum_healthy_percent = 50
}
circuit_breaker の動作条件:
| 条件 | 動作 |
|---|---|
| 新タスクが 10 回連続で異常終了 | デプロイを FAILED とマーク |
rollback = true の場合 | 前のタスク定義リビジョンへ自動ロールバック |
rollback = false の場合 | FAILED 状態で停止(手動対応が必要) |
ロールバックが完了すると、EventBridge の CodePipeline Pipeline Execution State Change
イベントで state = "FAILED" が通知されます(circuit_breaker 起動分は別途 ECS イベントで確認)。
9-5. CloudWatch Logs Insights — build log 横断検索
CodeBuild のビルドログは CloudWatch Logs の /aws/codebuild/{project_name} グループに記録されます。
Logs Insights でフィルタリングすることで、複数ビルドを横断した障害調査が効率化されます。
# 過去 24 時間のビルドエラーを抽出(aws CLI 経由)
aws logs start-query \
--log-group-name "/aws/codebuild/app-build" \
--start-time "$(date -u -d '24 hours ago' +%s)" \
--end-time"$(date -u +%s)" \
--query-string '
fields @timestamp, @message
| filter @message like /ERROR|FAILED|error/
| sort @timestamp desc
| limit 50
' \
--region ap-northeast-1
# クエリ ID を受け取ったらポーリングして結果取得
aws logs get-query-results \
--query-id <上記コマンドで返ったqueryId> \
--region ap-northeast-1
Terraform でのダッシュボード設定:
# terraform/modules/pipeline_ops/cloudwatch_dashboard.tf
resource "aws_cloudwatch_dashboard" "pipeline" {
dashboard_name = "pipeline-deploy-dashboard"
dashboard_body = jsonencode({
widgets = [
{
type = "log"
properties = {
title= "Build Errors (24h)"
region = "ap-northeast-1"
query= "SOURCE '/aws/codebuild/app-build' | filter @message like /ERROR/ | stats count() as error_count by bin(1h)"
view = "timeSeries"
period = 3600
}
},
{
type = "metric"
properties = {
title = "ECS Running Task Count"
region = "ap-northeast-1"
metrics = [
["ECS/ContainerInsights", "RunningTaskCount",
"ClusterName", "app-cluster", "ServiceName", "app-service"]
]
period = 60
stat= "Average"
}
}
]
})
}
9-6. 運用 KPI — デプロイパフォーマンス指標
以下の 4 KPI を月次で記録し、CI/CD パイプラインの健全性を継続的に把握します。
| KPI | 計測方法 | 目標値 |
|---|---|---|
| デプロイ頻度 | CodePipeline の SUCCEEDED 実行数 / 月 | ≥ 10 回/月(週 2-3 回以上) |
| 平均デプロイ時間 | duration_seconds(DeploymentRow) の平均 | ≤ 7 分 |
| デプロイ失敗率 | FAILED / 全実行数 × 100 | ≤ 5% |
| 平均復旧時間(MTTR) | FAILED → 次の SUCCEEDED までの経過時間 | ≤ 30 分 |
KPI 集計スクリプト骨子:
# scripts/deploy_kpi.py
from __future__ import annotations
import boto3
from datetime import datetime, timezone
def collect_monthly_kpi(
pipeline_name: str,
year: int,
month: int,
region: str = "ap-northeast-1",
) -> dict:
"""指定年月の CodePipeline デプロイ KPI を集計する。"""
client = boto3.client("codepipeline", region_name=region)
start = datetime(year, month, 1, tzinfo=timezone.utc)
end= datetime(year, month + 1, 1, tzinfo=timezone.utc) if month < 12 \
else datetime(year + 1, 1, 1, tzinfo=timezone.utc)
executions: list[dict] = []
paginator = client.get_paginator("list_pipeline_executions")
for page in paginator.paginate(pipelineName=pipeline_name):
for ex in page["pipelineExecutionSummaries"]:
if start <= ex["startTime"] < end:
executions.append(ex)
total = len(executions)
success = sum(1 for e in executions if e["status"] == "Succeeded")
failed= sum(1 for e in executions if e["status"] == "Failed")
durations = [
int((e["lastUpdateTime"] - e["startTime"]).total_seconds())
for e in executions
if e["status"] == "Succeeded" and "lastUpdateTime" in e
]
avg_duration = sum(durations) // len(durations) if durations else 0
return {
"year": year, "month": month,
"total": total, "success": success, "failed": failed,
"success_rate_pct": round(100.0 * success / total, 1) if total else 0.0,
"avg_duration_seconds": avg_duration,
}
10. ハンズオン実行と成果物確認
本セクションでは、これまでのセクションで作成した Terraform リソースを実際に適用し、
初回 Git push → パイプライン → ECR push → Fargate rolling update → ALB 疎通確認
までを一気通貫で実行します。
10-1. 前提条件チェック
作業開始前に以下を確認してください。
# ツールバージョン確認
terraform version# 1.9.x 以上
aws --version # aws-cli/2.x 以上
docker version# Docker Desktop または Docker Engine 25.x 以上
git --version # 2.x 以上
# AWS 認証確認
aws sts get-caller-identity --region ap-northeast-1
# 出力例: { "Account": "123456789012", "Arn": "arn:aws:iam::123456789012:..." }
Q3 対応(Fargate 未経験者へ): §3 の VPC・ECS クラスター・ECR リポジトリまでは
CodePipeline なしでも動作確認できます。パイプライン構築(§7)は §3 完了後に進めてください。
10-2. Makefile — エンドツーエンド実行手順
プロジェクトルートの Makefile で全工程を管理します。
# Makefile
APP:= myapp
AWS_REGION := ap-northeast-1
TF_DIR:= terraform
ECR_URI := 123456789012.dkr.ecr.$(AWS_REGION).amazonaws.com/$(APP)
.PHONY: init apply plan first-push verify destroy clean
## Terraform 初期化
init:
(cd $(TF_DIR) && terraform init -input=false)
## Terraform plan(変更内容の確認)
plan:
(cd $(TF_DIR) && terraform plan -input=false)
## Terraform apply(リソース作成)
apply:
(cd $(TF_DIR) && terraform apply -auto-approve -input=false)
## 初回アプリコードを Git push → パイプライン起動
first-push:
git add .
git commit -m "feat: initial application deployment"
git push origin main
## パイプライン完了後に ALB 疎通確認
verify:
@ALB_DNS=$$(cd $(TF_DIR) && terraform output -raw alb_dns_name); \
echo "ALB DNS: $$ALB_DNS"; \
curl -sf "http://$$ALB_DNS/health" && echo " → OK" || echo " → FAIL"
## Terraform リソース削除(課金停止)
destroy:
(cd $(TF_DIR) && terraform destroy -auto-approve -input=false)
## ECR イメージ全削除(destroy 後に実行)
clean-ecr:
aws ecr list-images --repository-name $(APP) \
--region $(AWS_REGION) \
--query 'imageIds[*]' --output json \
| xargs -I{} aws ecr batch-delete-image \
--repository-name $(APP) --region $(AWS_REGION) \
--image-ids '{}'
10-3. 実行時系列(5〜7 分の全体フロー)
# Step 1: 初期化・インフラ構築(初回のみ。約 5 分)
make init && make apply
# Step 2: 初回デプロイ(git push でパイプライン起動)
make first-push
パイプライン起動後の時系列:
0:00 git push origin main
└→ CodeCommit がトリガー → CodePipeline Source ステージ開始
0:05 CodePipeline Build ステージ開始
└→ CodeBuild: docker build → docker push ECR
(buildspec.yml 実行 / 通常 2-3 分)
3:30 CodePipeline Deploy ステージ開始
└→ ECS サービス更新: 新タスク定義でローリングアップデート
(新タスク起動 30 秒 + ヘルスチェック待機 90 秒 = 約 2 分)
5:30 デプロイ完了
└→ EventBridge → SNS → メール / Slack 通知
「[CI/CD] pipeline=myapp-pipeline state=SUCCEEDED ...」
# Step 3: 疎通確認
make verify
# 出力例:
# ALB DNS: myapp-alb-123456789.ap-northeast-1.elb.amazonaws.com
# {"status":"ok","version":"1.0.0"} → OK
10-4. pipeline 実行ログサンプル
CodeBuild ビルドログ(正常系):
[Container] 2026/04/19 04:00:00 Running command echo "Build started"
Build started
[Container] 2026/04/19 04:00:01 Running command $(aws ecr get-login-password ...) | docker login ...
Login Succeeded
[Container] 2026/04/19 04:00:05 Running command docker build -t $ECR_URI:$IMAGE_TAG .
Step 1/8 : FROM python:3.12-slim
...
Successfully built a1b2c3d4e5f6
Successfully tagged 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/myapp:git-abc1234
[Container] 2026/04/19 04:02:30 Running command docker push $ECR_URI:$IMAGE_TAG
The push refers to repository [123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/myapp]
...
[Container] 2026/04/19 04:03:00 Phase complete: POST_BUILD State: SUCCEEDED
ECS サービスイベントログ(ローリングアップデート):
2026-04-19T04:03:30 service myapp-service has started 1 tasks: task abc12345...
2026-04-19T04:04:00 service myapp-service registered 1 targets in target-group myapp-tg
2026-04-19T04:05:00 service myapp-service has stopped 1 running tasks: task def67890...
2026-04-19T04:05:30 service myapp-service reached a steady state.
10-5. トラブルシュート — 4 大パターン
① CodePipeline が Source ステージで FAILED
# CodeCommit への push 権限確認
aws codecommit get-repository --repository-name myapp-repo --region ap-northeast-1
# 対処: CodePipeline の IAM role に codecommit:GetBranch / GetCommit 権限を追加
② CodeBuild が buildspec 構文エラーで FAILED
# ローカルで buildspec.yml の YAML 構文チェック
pip install yamllint
yamllint buildspec.yml
# よくある原因:
# - インデント(タブ NG・スペース必須)
# - phases: の下の build: が欠落
# - commands: リストの - (ハイフン + スペース)が抜けている
③ ECS タスクが起動直後に停止する
# ECS タスクの停止理由を確認
aws ecs describe-tasks \
--cluster app-cluster \
--tasks <task_id> \
--region ap-northeast-1 \
--query 'tasks[0].stoppedReason'
# よくある原因と対処:
# "CannotPullContainerError" → ECR pull 権限不足(task execution role に ecr:GetAuthorizationToken 追加)
# "OutOfMemoryError"→ タスク定義の memory を増加
# "Essential container exited" → アプリの起動エラー(CloudWatch Logs でアプリログ確認)
④ ALB ヘルスチェックが UNHEALTHY
# ALB ターゲットグループの状態確認
aws elbv2 describe-target-health \
--target-group-arn <target_group_arn> \
--region ap-northeast-1
# よくある原因と対処:
# "Target.FailedHealthChecks" → /health エンドポイントが HTTP 200 を返していない
# "Target.NotInUse" → ECS サービスがターゲットグループに登録されていない
# SG の outbound ルール不足→ ECS タスクの SG に ALB SG からのインバウンドを許可
10-6. クリーンアップ手順(課金停止)
ハンズオン完了後は以下の順でリソースを削除してください。
# Step 1: ECS サービスのタスク数を 0 に縮退(terraform destroy 前に実行推奨)
aws ecs update-service \
--cluster app-cluster \
--service myapp-service \
--desired-count 0 \
--region ap-northeast-1
# Step 2: ECR リポジトリ内のイメージ削除(ECR は空でないと destroy できない)
make clean-ecr
# Step 3: Terraform リソース全削除
make destroy
# Step 4: ローカルの terraform state をクリア(任意)
rm -rf terraform/.terraform terraform/terraform.tfstate*
課金注意点: ALB は稼働時間課金のため、ハンズオン後は速やかに
make destroyを実行してください。
ECS Fargate はタスク稼働時間課金です(タスク数を 0 にすれば課金停止)。
ECR はストレージ課金(500 MB まで無料)のためmake clean-ecrも実施してください。
Section 9・10 執筆完了。 EventBridge 監視・ECS circuit_breaker ロールバック・CloudWatch Logs Insights・Makefile ハンズオン・4 大トラブルシュートパターン・クリーンアップ手順を網羅。
Section 11. まとめと次の発展
本記事では、ECS/Fargate アプリを AWS Native CI/CD パイプラインで安全にデプロイする基礎を構築しました。GitHub へのコードプッシュから CodePipeline 起動 → CodeBuild → ECR イメージプッシュ → Fargate rolling update まで、Terraform 1.9 で一貫して管理する実装が手元に揃いました。
11-1. 本記事で身につくスキルの棚卸
本記事のハンズオンを完走した読者は、以下の6要素が実装レベルで身についています。
| スキル | 本記事での習得内容 |
|---|---|
| CodePipeline | GitHub ソース → CodeBuild → ECS デプロイの3ステージ構成・Terraform 管理 |
| CodeBuild | buildspec.yml 構造・ECR ログイン・docker build/push・環境変数注入 |
| ECR | リポジトリ作成・ライフサイクルポリシー・イメージタグ戦略(SHA + latest) |
| ECS/Fargate | タスク定義・サービス rolling update・desired count・ヘルスチェック設定 |
| IAM | CodePipeline/CodeBuild/ECS タスクロール最小権限設計・AssumeRole 信頼関係 |
| Terraform | ECS/ECR/CodePipeline リソースの IaC 化・モジュール分割・state 管理 |
これらは AWS Native CI/CD の骨格となる知識です。GitHub Actions 軸(cmd_040 シリーズ)と組み合わせることで、「AWS 側で全て完結させる」か「GitHub 上で制御する」かを選択できる幅が広がります。
11-2. 本記事で意図的に踏み込まなかった領域
| 領域 | 理由 | 参照先 |
|---|---|---|
| Blue/Green デプロイ(CodeDeploy+ALB TG swap) | 基礎完成後の第2弾テーマとして温存 | 第2弾(近日公開) |
| マルチ環境パイプライン(dev/stg/prod + 承認ゲート) | 第3弾候補・ハンズオンコスト高 | 第3弾(予定) |
| セキュリティスキャン統合(Trivy/Snyk/ECR Basic Scanning) | 運用品質向上テーマ・別記事候補 | AWS 公式ドキュメント |
| GitHub Actions 軸の CI/CD | AWS Native 軸との棲み分け | GitHub Actions × OIDC CI/CD |
11-3. 次の発展ルート
ルート A: ダウンタイムゼロを実現したい(Blue/Green デプロイ)
本記事の rolling update は minimum_healthy_percent=50 の設定で旧タスクと新タスクが一時共存します。完全無停止(ゼロダウンタイム)を実現するには CodeDeploy + ALB Target Group スワップによる Blue/Green デプロイが次のステップです。本シリーズ第2弾で取り上げます。
ルート B: GitHub Actions 軸も学びたい
本記事の CodePipeline 軸と対をなす GitHub Actions × OIDC 軸は cmd_040 複数人開発シリーズ で解説しています。AWS CodeStar Connections(GitHub 連携)を使う本記事と比べ、OIDC による一時認証情報活用やコスト構造が異なります。両者を知ることで、プロジェクト要件に合った CI/CD を選択できるようになります。
11-4. おわりに
本記事で構築した「GitHub → CodePipeline → CodeBuild → ECR → Fargate rolling update」は、AWS Native CI/CD の最もシンプルかつ拡張性の高い骨格です。
ハンズオンを終えた今、手元には以下が揃っています:
– Terraform で管理された再現可能なパイプライン一式
– buildspec.yml によるコンテナビルド・プッシュの自動化
– Fargate rolling update による安全なデプロイフロー
– ALB ヘルスチェックによるデプロイ成否の自動検証
次の第2弾では Blue/Green デプロイを実装し、本記事の rolling update から一歩進んだ無停止デプロイを実現します。
- 第2弾予告: ECS Blue/Green デプロイ — CodeDeploy×ALB target group swap で無停止切替(近日公開)
- AWS×Terraform 複数人開発シリーズ(GitHub Actions 軸) — GHA × OIDC で PR 駆動 CI/CD