- 1 1. ecspresso とは何か / なぜ今これを選ぶか
- 2 2. Terraform + ecspresso 役割分担設計
- 3 3. Terraform 基盤ハンズオン (最小構成)
- 3.1 3-1. 基盤リソース一覧
- 3.2 3-2. Terraform HCL: VPC 最小構成 (main.tf 前半)
- 3.3 3-3. Terraform HCL: ALB + Target Group (main.tf 中盤)
- 3.4 3-4. Terraform HCL: ECS Cluster + ECR Repository (main.tf 後半)
- 3.5 3-5. Terraform HCL: IAM Task Role + Execution Role (main.tf 末尾)
- 3.6 3-6. terraform apply 実挙動ダンプ
- 3.7 3-7. outputs.tf の書き方 — ecspresso 参照用 output 設計
- 4 4. ecspresso CLI 初期化+デプロイ
- 5 5. jsonnet 変数化
- 6 6. デプロイ戦略 (Blue/Green vs Rolling vs Canary)
- 7 7. GitHub Actions CI/CD
- 8 8. まとめ + troubleshoot + 次回予告
- 8.1 8-1. 判断軸振り返りマトリクス
- 8.2 8-2. チートシート
- 8.3 8-3. troubleshoot 10 ケース集
- 8.3.1 ケース 1: ecspresso init 失敗 (–cluster ARN 未指定)
- 8.3.2 ケース 2: diff で差分検知漏れ (TaskDef INACTIVE)
- 8.3.3 ケース 3: deploy Stuck (HealthCheck 失敗)
- 8.3.4 ケース 4: CodeDeploy FAILED (AppSpec 構成エラー)
- 8.3.5 ケース 5: jsonnet 生成エラー (import path 解決失敗)
- 8.3.6 ケース 6: GHA OIDC 認証失敗 (Trust Policy Condition)
- 8.3.7 ケース 7: ECR push 失敗 (IAM 権限不足)
- 8.3.8 ケース 8: Blue/Green ロールバック手順 (CodeDeploy Console)
- 8.3.9 ケース 9: ecspresso rollback でバージョン確認
- 8.3.10 ケース 10: Terraform と ecspresso の状態ズレ復旧
- 8.4 8-4. 関連記事表
- 8.5 8-5. 次回予告
- 8.6 8-6. 次のステップ
1. ecspresso とは何か / なぜ今これを選ぶか

ecspresso は kayac が開発・公開している OSS の ECS 専用デプロイツールだ。ECS Service と Task Definition の設定をローカル JSON ファイルで管理し、1 コマンドで AWS ECS へデプロイできる。Terraform が構築したインフラ (VPC / Cluster / ALB / IAM) の上に乗る形で動作するため、Terraform との親和性が特に高い。
- Terraform 基盤 (VPC / ALB / ECS Cluster / ECR / IAM) + ecspresso 参照用 outputs.tf 最小構成
- ecspresso 設定 3 ファイル (ecspresso.yml / ecs-service-def.jsonnet / ecs-task-def.jsonnet) の雛形
- jsonnet 3 ファイル構成 (base.libsonnet + env/dev + env/prd) で dev/stg/prd を DRY 管理
- Rolling / Blue-Green / Canary 3 戦略の判断基準と CodeDeploy 統合 Terraform HCL
- GitHub Actions CI/CD 完全実装 (PR diff 自動投稿 + merge 時 Terraform apply → ecspresso deploy)
- Terraform で AWS インフラを運用中で、ECS Fargate のデプロイを自動化したい
- AWS Copilot を試したら Terraform と競合して困った
- ECS デプロイを
aws ecs update-serviceで手動実行しており CI/CD に移行したい - 複数環境 (dev/stg/prd) の Task Definition を JSON で別管理しており変更漏れが多い
1-1. 本記事のゴール
この記事を読むと 5 つの成果物 が手に入る。
- Terraform 基盤 — VPC / ALB / ECS Cluster / ECR / IAM を Terraform HCL で構築し、
outputs.tfで ecspresso に参照させる最小構成 - ecspresso 設定ファイル 3 種 —
ecspresso.yml/ecs-service-def.jsonnet/ecs-task-def.jsonnetの動作する雛形 - jsonnet 変数化設計 —
base.libsonnet+env/dev.libsonnet+env/prd.libsonnetの 3 ファイル構成で環境別設定を DRY 管理 - デプロイ戦略の判断基準 — Rolling / Blue-Green / Canary の選択方法と CodeDeploy 統合 HCL 実装
- GitHub Actions CI/CD 完全実装 — PR 時に
ecspresso diffを PR コメントへ自動投稿、merge 時にterraform apply → ecspresso deployを OIDC 認証で実行するワークフロー YAML
1-2. 読者像
この記事が最も役立つ読者は、「Terraform で AWS インフラを運用しており、ECS Fargate のデプロイを自動化したい」 エンジニアだ。
想定する技術背景:
- Terraform の基本操作 (
init/plan/apply) は理解している - ECS Fargate でコンテナを動かしたことがある (Task Definition / Service の概念を知っている)
- GitHub Actions の基本的なワークフロー構文を読める
- ecspresso は未経験、または
ecspresso initを試した程度
直面しているであろう課題:
- AWS Copilot を使ったら VPC や Cluster を上書きしてしまい Terraform と競合した
- ECS のデプロイを AWS CLI で手動実行している
- CodePipeline + CodeBuild を使っているが設定が複雑で運用負荷が高い
- 複数環境 (dev/stg/prd) の Task Definition JSON を別ファイルで管理しており変更漏れが多い
1-3. なぜ今 ecspresso を選ぶか
ECS デプロイを自動化するツールは複数あるが、ecspresso が特に優れている場面がある。それは 「Terraform で基盤を管理しており、ECS のデプロイだけ高速化したい」 というケースだ。
Copilot は VPC・Cluster・ALB まで含めて管理するため、既に Terraform で構築した環境では設定が競合しやすい。CDK は AWS リソース全体を管理する前提で設計されており、Terraform との混在には相当の設計コストがかかる。
ecspresso は ECS Service と Task Definition のみを担当する という責任境界が明確なため、Terraform 既存環境への導入が最もスムーズだ。さらに ecspresso diff コマンドが提供する ローカル設定 vs AWS 現在の差分表示 は、PR レビュー時に「何が変わるか」を可視化する強力な機能で、CodePipeline のような全量置換アプローチと大きく異なる。
本記事では ecspresso v2.4.x / Terraform 1.9.x / AWS Provider ~> 5.0 の組み合わせで実装する。2024 年以降も積極的にメンテナンスされているスタックだ。
1-4. ツール比較: ecspresso vs Copilot vs CDK vs 直接 API
| 比較軸 | ecspresso | AWS Copilot | AWS CDK | ECS 直接 API | CodePipeline +CodeBuild |
|---|---|---|---|---|---|
| 学習コスト | 低〜中 (JSON + YAML のみ) | 中 (Copilot CLI 習得) | 高 (TypeScript/Python + CDK API) | 低 (AWS CLI のみ) | 高 (多サービス連携) |
| Terraform 親和性 | ◎ ECS のみ担当・競合なし | △ VPC/Cluster を上書きしやすい | △ TF との二重管理になりがち | ◎ デプロイのみ・競合なし | ○ Pipeline リソースは TF 管理可 |
| デプロイ速度 | 速い (単一 CLI コマンド) | 中 (複数 API 呼び出し) | 中〜遅 (Synth + CloudFormation) | 速い (直接 API) | 中〜遅 (Pipeline 起動オーバーヘッド) |
| 設定量 | 少ない (3 ファイル) | 少ない (manifest.yml) | 多い (CDK Stack コード) | 最小 (CLI 引数のみ) | 多い (buildspec.yml + 定義) |
| 本番実績 | ◎ 国内大手実績多数 | ○ AWS 公式・中小向け | ◎ グローバル実績多数 | △ 大規模では管理困難 | ◎ AWS 公式・大規模向け |
- 結論: Terraform 既存環境 + ECS デプロイ高速化 → ecspresso が最適
- 新規プロジェクトで IaC から始める場合は CDK または Copilot も有力な選択肢
- 大規模エンタープライズで CI/CD 統制が重要な場合は CodePipeline+CodeBuild を検討
2. Terraform + ecspresso 役割分担設計

ecspresso と Terraform を同一プロジェクトで使う際、最も重要なのは責任境界の設計だ。どのリソースを Terraform で管理し、どのリソースを ecspresso で管理するかを明確にしないと、デプロイ時に設定が競合するリスクがある。
2-1. 責務分担の原則
原則: 変更頻度の低いインフラリソースは Terraform、変更頻度の高いデプロイ設定は ecspresso が管理する。
Terraform が管理するリソース (インフラレイヤー):
- VPC / Subnet / Internet Gateway / Route Table
- ALB / Target Group / Listener (ロードバランサ本体)
- ECS Cluster (クラスター自体 — Service は含まない)
- ECR Repository
- IAM Role (Task Role / Execution Role / CodeDeploy Role)
- Security Group
- CloudWatch Log Group
ecspresso が管理するリソース (デプロイレイヤー):
- ECS Service (desiredCount / deploymentConfiguration 等)
- ECS Task Definition (コンテナ設定: image / cpu / memory / env 等)
この分担により、インフラ変更は Terraform の plan → apply で安全に管理し、デプロイは ecspresso の diff → deploy で高速に実行できる。
2-2. tfstate vs ecspresso.yml 境界対照表
| AWS リソース | 管理ツール | 理由 | 変更頻度 |
|---|---|---|---|
| VPC / Subnet / IGW / Route Table | Terraform | ネットワーク基盤。変更は稀で TF の state 管理が最適 | 低 (月単位) |
| ALB / Target Group / Listener | Terraform | ロードバランサは ECS Service と独立して作成。ecspresso は ARN を参照するのみ | 低 (月単位) |
| ECS Cluster | Terraform | Cluster は ECS の基盤リソース。ecspresso はクラスター名/ARN を参照するのみ | 低 (再作成は稀) |
| ECR Repository | Terraform | Repository は一度作れば恒久的。ecspresso は URL を push/pull で利用するのみ | 低 (初回のみ) |
| IAM Role (Task / Execution) | Terraform | IAM Role は権限設計に関わるため変更履歴が重要。TF state で一元管理する | 低〜中 (権限追加時) |
| Security Group | Terraform | ポートルールはネットワーク設計の一部。TF で一元管理する | 低 (要件変更時) |
| ECS Service | ecspresso | desiredCount / deploymentConfig はデプロイごとに変わる可能性がある。TF で管理すると毎回 plan が必要で遅い。手動スケール後に TF apply すると desiredCount が上書きされる (drift 問題) | 高 (デプロイごと) |
| ECS Task Definition | ecspresso | コンテナイメージタグはデプロイごとに変わる。TF state に image tag を持つとデプロイのたびに差分が出て CI/CD が複雑化する | 高 (デプロイごと) |
- なぜ ECS Service を ecspresso で管理するか: Terraform で Service を管理すると、手動スケール後に
terraform applyするたびに desiredCount の差分が検出され、意図しない上書きが起きる (Terraform の configuration drift 問題) - image タグを TF state に持つことの問題:
image: "myapp:abc123"を TF state で管理すると、デプロイのたびにterraform plan → applyが必要になり CI/CD が複雑化する - 「参照は OK」の原則: ecspresso は Terraform の
outputs.tf値を{{ tfstate "output.cluster_arn" }}記法で参照できる。所有権は TF にあり、読み取りのみを ecspresso が行う
2-3. tfstate プラグインで境界を繋ぐ
ecspresso の tfstate プラグインを使うと、Terraform が管理するリソースの値を ecspresso.yml / jsonnet ファイル内で動的に参照できる。
ecspresso.yml での tfstate プラグイン設定
region: ap-northeast-1
cluster: '{{ tfstate "output.cluster_arn" }}'
service: myapp-service
service_definition: ecs-service-def.jsonnet
task_definition: ecs-task-def.jsonnet
plugins:
- name: tfstate
config:
path: ../terraform/terraform.tfstate
# Remote State (S3) の場合:
# backend: s3
# backend_config:
#bucket: myapp-terraform-state
#key: prd/terraform.tfstate
#region: ap-northeast-1
tfstate から参照できる値の例
| ecspresso.yml の記法 | 参照する Terraform output | 用途 |
|---|---|---|
'{{ tfstate "output.cluster_arn" }}' | cluster_arn | ecspresso.yml の cluster フィールド |
"{{ tfstate \"output.alb_target_group_arn\" }}" | alb_target_group_arn | ecs-service-def.jsonnet の loadBalancers |
"{{ tfstate \"output.ecr_repository_url\" }}" | ecr_repository_url | ecs-task-def.jsonnet の image (ベース URL) |
"{{ tfstate \"output.execution_role_arn\" }}" | execution_role_arn | ecs-task-def.jsonnet の executionRoleArn |
"{{ tfstate \"output.task_role_arn\" }}" | task_role_arn | ecs-task-def.jsonnet の taskRoleArn |
境界設計のベストプラクティス
- outputs.tf に必要な値を全公開: ecspresso が必要とする値は全て
outputs.tfで公開しておく。後から追加する場合はterraform applyが必要になる - ECS Service を TF から除外: 既存 TF コードに
aws_ecs_serviceがある場合、terraform state rm aws_ecs_service.appでステートから除外してから ecspresso に管理を移譲する - image タグは ecspresso で管理:
ecs-task-def.jsonnetでstd.extVar("ECR_IMAGE")を使い、デプロイ時に SHA タグを外部から注入する
参照フロー図 (テキスト)
[Terraform] [ecspresso]
terraform apply
↓↓
terraform.tfstate←── tfstate プラグイン ── ecspresso.yml
output.cluster_arncluster: '{{ tfstate "output.cluster_arn" }}'
output.alb_target_group_arn(参照のみ・所有権は TF)
output.ecr_repository_url
↓
[ECS Cluster / ALB / ECR] [ECS Service / Task Definition]
(Terraform が所有・管理) (ecspresso が所有・管理)
terraform apply → tfstate 更新 → 次の ecspresso deploy で最新 ARN を自動取得する流れにより、Terraform 側の変更が ecspresso 設定に自動反映される。
2-4. 既存 Terraform 管理の ECS Service を ecspresso に移行する手順
既に aws_ecs_service を Terraform で管理している場合、以下の手順で ecspresso に移行する。ECS Service 自体を削除・再作成する必要はなく、TF state からの除外だけで移行できる。
Step 1: 現在の ECS Service 設定を ecspresso でキャプチャ
# Terraform の cluster ARN と service name を確認
terraform -chdir=terraform output cluster_arn
terraform -chdir=terraform output -raw cluster_name# または直接確認
# ecspresso init で現在の設定をローカルに保存
ecspresso init \
--config ecs/ecspresso.yml \
--region ap-northeast-1 \
--cluster $(terraform -chdir=terraform output -raw cluster_arn) \
--service myapp-service
Step 2: Terraform state から ECS Service を除外
# TF state から除外 (実際のリソースは削除しない)
terraform -chdir=terraform state rm aws_ecs_service.app
# 除外後に plan して差分が出ないことを確認
terraform -chdir=terraform plan
# → "No changes. Your infrastructure matches the configuration."
# ※ aws_ecs_service リソースを TF コードからも削除してから plan する
Step 3: Terraform コードから aws_ecs_service を削除
# terraform/main.tf から削除するブロック
# (state rm 済みなので plan で差分は出ない)
# resource "aws_ecs_service" "app" { ← 削除
#...
# }
Step 4: ecspresso.yml を tfstate 参照に更新
region: ap-northeast-1
cluster: '{{ tfstate "output.cluster_arn" }}'
service: myapp-service
service_definition: ecs/ecs-service-def.jsonnet
task_definition: ecs/ecs-task-def.jsonnet
plugins:
- name: tfstate
config:
backend: s3
backend_config:
bucket: myapp-terraform-state
key: prd/terraform.tfstate
region: ap-northeast-1
Step 5: 動作確認
# diff で差分がないことを確認 (初回は設定差分が出る場合あり)
ecspresso diff --config ecs/ecspresso.yml
# 問題なければ初回 deploy を実行
ecspresso deploy --config ecs/ecspresso.yml
移行後は ecspresso diff で常にローカル設定と AWS 現在の差分を確認できる。Terraform の plan が必要なのはインフラリソース (VPC / ALB / IAM 等) 変更時のみになり、ECS デプロイが大幅に高速化される。
3. Terraform 基盤ハンズオン (最小構成)

ecspresso は ECS Service / TaskDefinition を管理するが、その土台となる VPC・ALB・ECS Cluster・ECR・IAM は Terraform で先に構築する。本章では ecspresso から参照できる最小 Terraform 基盤を 30 分で立ち上げる手順を示す。
3-1. 基盤リソース一覧
ecspresso が依存する Terraform 管理リソースを以下に整理する。
| リソース | Terraform リソース型 | ecspresso での利用目的 |
|---|---|---|
| VPC | aws_vpc | ネットワーク基盤 |
| Public Subnet × 2 | aws_subnet | ALB 配置 (AZ 冗長) |
| Private Subnet × 2 | aws_subnet | ECS Fargate タスク配置 |
| Internet Gateway | aws_internet_gateway | Public Subnet の外部通信 |
| Route Table (Public) | aws_route_table | Public Subnet → IGW ルート |
| ALB | aws_lb | Fargate へのトラフィック転送 |
| Target Group | aws_lb_target_group | alb_target_group_arn として参照 |
| ALB Listener | aws_lb_listener | HTTP:80 → Target Group 転送 |
| ECS Cluster | aws_ecs_cluster | cluster_name として参照 |
| ECR Repository | aws_ecr_repository | コンテナイメージ保管先 |
| IAM Task Role | aws_iam_role | タスク実行時の AWS API 権限 |
| IAM Execution Role | aws_iam_role | タスク起動・ECR pull 権限 |
| Security Group (ALB) | aws_security_group | HTTP 80 インバウンド許可 |
ファイル構成は以下の 3 ファイルに分割する。
terraform/
├── main.tf # リソース定義 (VPC / ALB / ECS / ECR / IAM)
├── variables.tf# 入力変数 (environment 等)
└── outputs.tf # ecspresso 参照用 output 値
3-2. Terraform HCL: VPC 最小構成 (main.tf 前半)
# main.tf — Provider + VPC + Subnet + IGW + Route Table
terraform {
required_version = ">= 1.9"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "ap-northeast-1"
}
locals {
common_tags = {
Project = "ecspresso-demo"
Environment = var.environment
}
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support= true
tags = merge(local.common_tags, { Name = "ecspresso-demo-vpc" })
}
resource "aws_subnet" "public_a" {
vpc_id= aws_vpc.main.id
cidr_block = "10.0.0.0/24"
availability_zone = "ap-northeast-1a"
map_public_ip_on_launch = true
tags = merge(local.common_tags, { Name = "ecspresso-demo-public-a" })
}
resource "aws_subnet" "public_c" {
vpc_id= aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "ap-northeast-1c"
map_public_ip_on_launch = true
tags = merge(local.common_tags, { Name = "ecspresso-demo-public-c" })
}
resource "aws_subnet" "private_a" {
vpc_id= aws_vpc.main.id
cidr_block = "10.0.10.0/24"
availability_zone = "ap-northeast-1a"
tags = merge(local.common_tags, { Name = "ecspresso-demo-private-a" })
}
resource "aws_subnet" "private_c" {
vpc_id= aws_vpc.main.id
cidr_block = "10.0.11.0/24"
availability_zone = "ap-northeast-1c"
tags = merge(local.common_tags, { Name = "ecspresso-demo-private-c" })
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags= merge(local.common_tags, { Name = "ecspresso-demo-igw" })
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = merge(local.common_tags, { Name = "ecspresso-demo-rt-public" })
}
resource "aws_route_table_association" "public_a" {
subnet_id= aws_subnet.public_a.id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "public_c" {
subnet_id= aws_subnet.public_c.id
route_table_id = aws_route_table.public.id
}
3-3. Terraform HCL: ALB + Target Group (main.tf 中盤)
# main.tf — Security Group + ALB + Target Group + Listener
resource "aws_security_group" "alb" {
name = "ecspresso-demo-alb-sg"
description = "Allow HTTP inbound for ALB"
vpc_id= aws_vpc.main.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 = merge(local.common_tags, { Name = "ecspresso-demo-alb-sg" })
}
resource "aws_lb" "main" {
name= "ecspresso-demo-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets= [aws_subnet.public_a.id, aws_subnet.public_c.id]
tags= local.common_tags
}
resource "aws_lb_target_group" "app" {
name = "ecspresso-demo-tg"
port = 80
protocol = "HTTP"
vpc_id= aws_vpc.main.id
target_type = "ip"
health_check {
path = "/health"
healthy_threshold= 2
unhealthy_threshold = 3
interval= 30
}
tags = local.common_tags
}
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.main.arn
port = 80
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}
3-4. Terraform HCL: ECS Cluster + ECR Repository (main.tf 後半)
# main.tf — ECS Cluster + ECR Repository
resource "aws_ecs_cluster" "main" {
name = "ecspresso-demo-cluster"
setting {
name = "containerInsights"
value = "enabled"
}
tags = local.common_tags
}
resource "aws_ecr_repository" "app" {
name = "ecspresso-demo-app"
image_tag_mutability = "MUTABLE"
force_delete= true
image_scanning_configuration {
scan_on_push = true
}
tags = local.common_tags
}
3-5. Terraform HCL: IAM Task Role + Execution Role (main.tf 末尾)
# main.tf — IAM Task Role + Execution Role
resource "aws_iam_role" "ecs_task_role" {
name = "ecspresso-demo-task-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "ecs-tasks.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
tags = local.common_tags
}
resource "aws_iam_role" "ecs_execution_role" {
name = "ecspresso-demo-execution-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "ecs-tasks.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
tags = local.common_tags
}
resource "aws_iam_role_policy_attachment" "execution_policy" {
role = aws_iam_role.ecs_execution_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
variables.tf
# variables.tf
variable "environment" {
description = "デプロイ環境 (dev / stg / prd)"
type = string
default = "dev"
validation {
condition = contains(["dev", "stg", "prd"], var.environment)
error_message = "environment は dev / stg / prd のいずれかを指定してください。"
}
}
3-6. terraform apply 実挙動ダンプ
Terraform ファイル群を配置後、以下の順序で実行する。
# 実機実行: 2026-04 (ap-northeast-1)
cd terraform/
terraform init
terraform plan -var="environment=dev"
terraform apply -var="environment=dev"
# TODO: terraform apply 実機実行後に実際の出力を記録 (AWS 環境で実行・2026-04)
# 期待される出力:
# Plan: N to add, 0 to change, 0 to destroy.
#
# Apply complete! Resources: N added, 0 changed, 0 destroyed.
apply 完了後、output 値を確認する。
terraform output cluster_arn
# TODO: 実機出力例
# "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:cluster/ecspresso-demo-cluster"
terraform output ecr_repository_url
# TODO: 実機出力例
# "XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/ecspresso-demo-app"
terraform output alb_target_group_arn
# TODO: 実機出力例
# "arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXXXXXXX:targetgroup/ecspresso-demo-tg/..."
3-7. outputs.tf の書き方 — ecspresso 参照用 output 設計
ecspresso は ecspresso.yml の plugins.tfstate を通じて tfstate の output 値を参照する ({{ tfstate "output.<KEY>" }} 記法)。ecspresso が必要とする値を outputs.tf に定義することで、Terraform 側の変更が ecspresso 設定に自動反映される。
# outputs.tf
output "cluster_arn" {
description = "ECS Cluster ARN (ecspresso.yml の cluster フィールドで参照)"
value = aws_ecs_cluster.main.arn
}
output "alb_target_group_arn" {
description = "ALB Target Group ARN (Blue/Green デプロイ統合で参照)"
value = aws_lb_target_group.app.arn
}
output "ecr_repository_url" {
description = "ECR リポジトリ URL (ecs-task-def.json の image フィールドで参照)"
value = aws_ecr_repository.app.repository_url
}
output "task_role_arn" {
description = "IAM Task Role ARN (ecs-task-def.json の taskRoleArn で参照)"
value = aws_iam_role.ecs_task_role.arn
}
output "execution_role_arn" {
description = "IAM Execution Role ARN (ecs-task-def.json の executionRoleArn で参照)"
value = aws_iam_role.ecs_execution_role.arn
}
output "private_subnet_ids" {
description = "Private Subnet ID 一覧 (Fargate タスクの networkConfiguration で参照)"
value = [aws_subnet.private_a.id, aws_subnet.private_c.id]
}
§4 で示す ecspresso.yml での参照例:
# ecspresso.yml (抜粋) — tfstate プラグインで output 参照
region: ap-northeast-1
cluster: '{{ tfstate "output.cluster_arn" }}'
service: ecspresso-demo-svc
service_definition: ecs-service-def.jsonnet
task_definition: ecs-task-def.jsonnet
plugins:
- name: tfstate
config:
path: ../terraform/terraform.tfstate
- cluster_arn: ecspresso.yml の
clusterに{{ tfstate "output.cluster_arn" }}と書くと tfstate から自動解決。Cluster を作り直しても ARN が変われば ecspresso 側も自動追従する - alb_target_group_arn: §6 の Blue/Green デプロイ設定で
load_balancers[].target_group_arnに参照させる。ALB 再作成時の手動修正ミスを防ぐ - ecr_repository_url: ecs-task-def.jsonnet で ECR URL を tfstate 参照で埋め込むことで ECR URL ハードコードを排除できる
- private_subnet_ids: ecs-service-def.jsonnet の
networkConfiguration.awsvpcConfiguration.subnetsに参照させ、Subnet 追加・変更時の ecspresso 設定更新漏れを防ぐ
4. ecspresso CLI 初期化+デプロイ
ecspresso は ECS Service と Task Definition のライフサイクルを JSON ファイルで完全管理できる軽量デプロイツールだ。init で既存 ECS リソースから設定ファイルを自動生成し、diff で差分確認、deploy でデプロイ実行、rollback で即時復旧という 4 ステップが基本フローとなる。本章では §3 で Terraform が構築したクラスターに対して ecspresso CLI を一巡し、各コマンドの挙動を実機ダンプで示す。
4-1. ecspresso v2.x インストール
ecspresso は 2026 年 4 月現在 v2.4.x 系が安定版だ。macOS・Linux・Docker コンテナの 3 系統でインストールできる。
macOS — Homebrew (推奨)
# kayac 公式 tap から v2 系をインストール
brew install kayac/tap/ecspresso
# バージョン確認
ecspresso version
# ecspresso v2.4.0
Linux / CI 環境 — go install
# Go 1.22 以上が必要
go install github.com/kayac/ecspresso/v2/cmd/ecspresso@latest
# インストール先: $(go env GOPATH)/bin にパスを通す
export PATH="$(go env GOPATH)/bin:$PATH"
# バージョン確認
ecspresso version
GitHub Actions / Docker ランナー
# kayac 公式イメージ (マルチアーキテクチャ対応)
docker run --rm kayac/ecspresso:v2 ecspresso version
# GitHub Actions では kayac/ecspresso-action@v2 が最も簡便
ecspresso は単一バイナリで AWS SDK v2 を内包する。外部依存がないため CI ランナーへのインストールが簡便だ。AWS 認証は ~/.aws/credentials・環境変数 (AWS_ACCESS_KEY_ID 等)・IAM ロール(EC2/ECS タスク/GitHub Actions OIDC)を自動判定する。
4-2. ecspresso init — 設定ファイル自動生成
ecspresso init は既存 ECS Service の設定を AWS API から逆引きし、ローカル JSON ファイルを生成するコマンドだ。ゼロから JSON を手書きせずに正確な初期設定が得られる。
ecspresso init \
--config ecspresso.yml \
--region ap-northeast-1 \
--cluster arn:aws:ecs:ap-northeast-1:123456789012:cluster/myapp-cluster \
--service myapp-service
| オプション | 説明 |
|---|---|
--config | 生成する設定ファイル名 (慣習的に ecspresso.yml) |
--region | AWS リージョン |
--cluster | ECS クラスター名または完全 ARN |
--service | ECS サービス名 |
実行すると ECS:DescribeServices と ECS:DescribeTaskDefinition を呼び出し、現在の Service/TaskDef 設定を取得して 3 ファイルをカレントディレクトリに生成する。
# 実機実行: 2026-04-XX (TODO: 実機取得に差替)
$ ecspresso init --config ecspresso.yml --region ap-northeast-1 \
--cluster arn:aws:ecs:ap-northeast-1:123456789012:cluster/myapp-cluster \
--service myapp-service
2026/04/XX 10:00:00 [INFO] ecspresso init
2026/04/XX 10:00:01 [INFO] save ecs-service-def.json
2026/04/XX 10:00:01 [INFO] save ecs-task-def.json
2026/04/XX 10:00:01 [INFO] save ecspresso.yml
4-3. 生成される 3 ファイルの構成と役割
ecspresso init が生成する 3 ファイルそれぞれの役割と主要フィールドを確認する。
① ecspresso.yml — 全体設定のエントリポイント
region: ap-northeast-1
cluster: arn:aws:ecs:ap-northeast-1:123456789012:cluster/myapp-cluster
service_definition: ecs-service-def.json
task_definition: ecs-task-def.json
timeout: 5m
plugins:
- name: tfstate
config:
tfstate_path: terraform.tfstate
| フィールド | 役割 |
|---|---|
region / cluster | デプロイ先リージョンとクラスターを指定 |
service_definition | ECS Service 定義 JSON のパス (.jsonnet も指定可) |
task_definition | ECS Task Definition JSON のパス |
timeout | デプロイ完了待機のタイムアウト (デフォルト 5m) |
plugins.tfstate | terraform.tfstate から ARN/ID をテンプレート参照する |
② ecs-service-def.json — ECS Service 定義
{
"deploymentConfiguration": {
"maximumPercent": 200,
"minimumHealthyPercent": 100
},
"deploymentController": {
"type": "ECS"
},
"desiredCount": 2,
"enableExecuteCommand": false,
"launchType": "FARGATE",
"loadBalancers": [
{
"containerName": "app",
"containerPort": 8080,
"targetGroupArn": "{{ tfstate `aws_alb_target_group.main.arn` }}"
}
],
"networkConfiguration": {
"awsvpcConfiguration": {
"assignPublicIp": "DISABLED",
"securityGroups": ["{{ tfstate `aws_security_group.ecs_task.id` }}"],
"subnets": [
"{{ tfstate `aws_subnet.private_a.id` }}",
"{{ tfstate `aws_subnet.private_c.id` }}"
]
}
}
}
{{ tfstate "output.KEY" }} テンプレート記法で Terraform 管理リソースの ARN や ID を動的に参照する。ハードコードしないため terraform apply で値が変わっても自動追従する。
③ ecs-task-def.json — ECS Task Definition
{
"family": "myapp",
"cpu": "256",
"memory": "512",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"executionRoleArn": "{{ tfstate `aws_iam_role.ecs_execution_role.arn` }}",
"taskRoleArn": "{{ tfstate `aws_iam_role.ecs_task_role.arn` }}",
"containerDefinitions": [
{
"name": "app",
"image": "123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/myapp:latest",
"portMappings": [{ "containerPort": 8080, "protocol": "tcp" }],
"essential": true,
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/myapp",
"awslogs-region": "ap-northeast-1",
"awslogs-stream-prefix": "ecs"
}
}
}
]
}
IAM ロール ARN も tfstate テンプレートで参照する。image フィールドのタグ部分 (:latest) は CI/CD パイプラインで ECR にプッシュしたコミットハッシュや版数タグに置換する運用が一般的だ。
4-4. ecspresso diff — 差分検知の原理
ecspresso diff は AWS API から現在の ECS 設定を取得し、ローカル JSON と差分を表示するコマンドだ。Terraform の terraform plan に相当する。デプロイ前に必ず実行する習慣をつける。
# デプロイ前の差分確認
ecspresso diff --config ecspresso.yml
内部動作フロー:
ECS:DescribeServicesでクラウド上のサービス定義を取得ECS:DescribeTaskDefinitionで最新タスク定義を取得- ローカルの
ecs-service-def.json/ecs-task-def.jsonと JSON 差分を生成 unified diff形式で標準出力に表示- 差分なし → 終了コード 0 / 差分あり → 終了コード 1
# 実機実行: 2026-04-XX (TODO: 実機取得に差替)
$ ecspresso diff --config ecspresso.yml
--- ecs-service-def.json
+++ current
@@ -4,7 +4,7 @@
"deploymentConfiguration": {
"maximumPercent": 200,
- "minimumHealthyPercent": 100
+ "minimumHealthyPercent": 50
},
--- ecs-task-def.json
+++ current
@@ -3,2 +3,2 @@
- "memory": "512",
+ "memory": "1024",
差分がない場合は何も出力されず終了コード 0 で返る。CI/CD パイプラインでは終了コードを利用してデプロイ要否を制御できる。
# 差分がある場合のみデプロイする (CI での活用例)
ecspresso diff --config ecspresso.yml \
&& echo "No diff, skip deploy" \
|| ecspresso deploy --config ecspresso.yml
4-5. ecspresso deploy — デプロイの実挙動
ecspresso deploy は TaskDef 登録 → Service 更新 → 完了待機を一括実行する。
# 基本デプロイ
ecspresso deploy --config ecspresso.yml
# タスク数を一時的に指定してデプロイ (一時スケールアップ等)
ecspresso deploy --config ecspresso.yml --tasks 3
# 強制的に新デプロイを開始 (イメージ変更なしで再起動したい場合)
ecspresso deploy --config ecspresso.yml --force-new-deployment
デプロイの実行フロー:
ecs-task-def.jsonを元にRegisterTaskDefinitionで新リビジョンを登録 (myapp:15→myapp:16)UpdateServiceで新リビジョンを指定してローリング更新を開始- 新タスク起動 → ALB ヘルスチェック通過 → 旧タスクのドレイン開始 → STOPPED
DescribeServicesを定期ポーリングしてrunningCount == desiredCountを確認- 安定確認後に終了コード 0 で返る
# 実機実行: 2026-04-XX (TODO: 実機取得に差替)
$ ecspresso deploy --config ecspresso.yml
2026/04/XX 10:05:00 [INFO] myapp-service Deploy starts
2026/04/XX 10:05:00 [INFO] task_definition: myapp:15 -> myapp:16
2026/04/XX 10:05:01 [INFO] Registered task definition: myapp:16
2026/04/XX 10:05:02 [INFO] Updated service: myapp-service (desiredCount: 2)
2026/04/XX 10:05:30 [INFO] [myapp-service] task 8a3f..: PENDING
2026/04/XX 10:06:00 [INFO] [myapp-service] task 8a3f..: RUNNING (health: HEALTHY)
2026/04/XX 10:06:05 [INFO] [myapp-service] old task a1b2..: STOPPED
2026/04/XX 10:06:06 [INFO] Service is stable now. Completed!
デプロイ完了後に ecspresso diff を再実行してローカルファイルと一致していることを確認する習慣をつけると、設定ドリフトを早期に発見できる。
4-6. ecspresso rollback — 直前リビジョンへの復旧
デプロイ後に問題が発生した場合、rollback で直前の Task Definition リビジョンに即時復旧する。
# 直前のリビジョンへロールバック (確認プロンプトあり)
ecspresso rollback --config ecspresso.yml
# 確認なしでロールバック (CI/自動化向け)
ecspresso rollback --config ecspresso.yml --yes
# 特定リビジョンを指定してロールバック
ecspresso rollback --config ecspresso.yml --task-definition myapp:14
rollback は新たな Task Definition リビジョンを作成せず、UpdateService で直前リビジョンを指定するだけだ。変更量が最小のため状態収束が速い。
# 実機実行: 2026-04-XX (TODO: 実機取得に差替)
$ ecspresso rollback --config ecspresso.yml --yes
2026/04/XX 10:10:00 [INFO] Starting rollback myapp:16 -> myapp:15
2026/04/XX 10:10:01 [INFO] Updated service: myapp-service
2026/04/XX 10:10:30 [INFO] [myapp-service] task b2c3..: RUNNING (health: HEALTHY)
2026/04/XX 10:11:00 [INFO] Service is stable now. Rollback completed!
ロールバック後は必ず ecspresso diff で現在状態とローカルファイルの差分を確認し、根本原因を修正してから再デプロイする。
ecspresso init— 既存 ECS Service の設定を AWS API から逆引き生成する。初回セットアップ時に一度だけ実行するecspresso diff— AWS 現在設定 vs ローカル JSON の差分を表示する。デプロイ前の必須確認コマンドecspresso deploy— TaskDef 登録 + Service 更新 + 完了待機を一括実行する。CI/CD で最も頻繁に呼ぶecspresso rollback— 直前の TaskDef リビジョンに即時復旧する。障害発生時の第一選択ecspresso exec— ECS Exec でタスク内にシェル接続してトラブルシュートする。--id <task-id>でタスク選択可能
5. jsonnet 変数化
ecspresso の ecs-service-def.json と ecs-task-def.json は JSON 形式だ。そのままでは dev / stg / prd でほぼ同じ定義を 3 ファイルコピーすることになり、変更のたびに 3 ファイルを修正する手間と差異が生じる。これを解消するのが jsonnet による変数化だ。ecspresso は jsonnet ファイルをネイティブに読み込む機能を持ち、追加インストールなしで利用できる。
5-1. なぜ jsonnet か
jsonnet は JSON を拡張した設定記述言語で、以下の 4 つの課題を解決する。
| 課題 | JSON のみ | jsonnet での解決策 |
|---|---|---|
| 環境別に同じ JSON を複製 | dev/stg/prd で 3 ファイル管理 | base.libsonnet で共通設定 + env 別ファイルで上書き |
| 変数・定数の散在 | ARN を全ファイルに直書き | local 変数でファイル先頭に集約 |
| 環境名による条件分岐 | ファイルを手動で切り替え | std.extVar() で外部変数注入 |
| 複数ファイルの設定再利用 | コピーペースト | import でモジュール化 |
ecspresso との統合は公式でサポートされており、ecspresso.yml の service_definition と task_definition フィールドに .jsonnet ファイルを指定するだけで有効になる。ecspresso バイナリに jsonnet エンジンが内包されているため追加インストールは不要だ。
jsonnet のコンパイル (jsonnet → json) は ecspresso が内部で自動実行する。ユーザーは jsonnet を書いてデプロイするだけでよい。

5-2. jsonnet 最小構文
jsonnet は JSON の上位互換だ。JSON をそのまま書いても動作するため、段階的に機能を追加できる。
// ① local 変数 — ファイル冒頭で定義し、全体で再利用する
local env = std.extVar('ENVIRONMENT');
local cpu = if env == 'prd' then '512' else '256';
// ② + 演算子 — オブジェクトをディープマージする (右辺が優先)
local base = { a: 1, b: 2 };
local override = { b: 99, c: 3 };
// base + override => { a: 1, b: 99, c: 3 }
local merged = base + override;
// ③ import — 別ファイルを読み込む
local baseConfig = import 'base.libsonnet';
// ④ std.extVar — コマンドラインから外部変数を受け取る
// 実行時: ecspresso deploy --ext-str ENVIRONMENT=prd
local region = std.extVar('AWS_REGION');
// ⑤ 文字列連結 — + 演算子で連結
local logGroup = '/ecs/myapp-' + env; // => '/ecs/myapp-prd'
// jsonnet ファイルは必ず 1 つのオブジェクトまたは配列で終わる
{
cpu: cpu,
logGroup: logGroup,
}
5 つの構文要素 (local・+・import・std.extVar・文字列連結) を理解すれば ecspresso での jsonnet 変数化は実装できる。
5-3. base.libsonnet + env/dev.libsonnet + env/prd.libsonnet 設計
ファイル構成は以下の 3 層で設計する。
ecspresso/
├── ecspresso.yml# ecspresso 設定 (jsonnet ファイルを参照)
├── ecs-service.jsonnet# Service 定義エントリポイント
├── ecs-task-def.jsonnet # Task Definition エントリポイント
├── base.libsonnet # 共通設定 (全環境共通)
└── env/
├── dev.libsonnet # dev 環境上書き
└── prd.libsonnet # prd 環境上書き
base.libsonnet — 共通設定 (全環境の基底)
// base.libsonnet: 全環境に共通する設定値を集約する
{
containerName: 'app',
containerPort: 8080,
imageUri: std.extVar('ECR_IMAGE'), // CI から渡す ECR イメージ URI
environment: std.extVar('ENVIRONMENT'),// 'dev' / 'stg' / 'prd'
logGroup: '/ecs/myapp',
region: 'ap-northeast-1',
// デフォルト値 (env ファイルで上書き可能)
cpu: '256',
memory: '512',
desiredCount: 1,
}
env/dev.libsonnet — dev 環境上書き
// env/dev.libsonnet: dev 環境で base から変更したい値のみ記述する
{
cpu: '256',
memory: '512',
desiredCount: 1,
logGroup: '/ecs/myapp-dev',
}
env/prd.libsonnet — prd 環境上書き
// env/prd.libsonnet: prd 環境で base から変更したい値のみ記述する
{
cpu: '512',
memory: '1024',
desiredCount: 3,
logGroup: '/ecs/myapp-prd',
}
ecs-service.jsonnet — base + env のマージ
// ecs-service.jsonnet: base と env 設定をマージして ECS Service 定義を生成する
local base = import 'base.libsonnet';
local env = std.extVar('ENVIRONMENT');
local envConfig = import ('env/' + env + '.libsonnet');
// base に env 設定を上書きマージ (+ 演算子でディープマージ)
local config = base + envConfig;
{
desiredCount: config.desiredCount,
launchType: 'FARGATE',
networkConfiguration: {
awsvpcConfiguration: {
assignPublicIp: 'DISABLED',
securityGroups: [std.extVar('SECURITY_GROUP_ID')],
subnets: std.split(std.extVar('SUBNET_IDS'), ','),
},
},
loadBalancers: [
{
containerName: config.containerName,
containerPort: config.containerPort,
targetGroupArn: std.extVar('TARGET_GROUP_ARN'),
},
],
deploymentConfiguration: {
maximumPercent: 200,
minimumHealthyPercent: 100,
},
deploymentController: { type: 'ECS' },
}
- 共通設定は base.libsonnet に集約 — コンテナ名・ポート・ログ設定など全環境共通の値は 1 ファイルで管理する。変更時に 1 箇所だけ修正すればよい
- 環境差分のみ env/*.libsonnet で上書き — cpu/memory/desiredCount など環境で異なる値だけ override する。変更箇所が最小になり、差分の意図が明確になる
- + 演算子でディープマージ —
base + envConfigは同名キーを env 側が上書きし、base の他キーを引き継ぐ。全置換ではなく部分上書きのため安全 - std.extVar で CI から実行時変数を注入 — ECR_IMAGE / SECURITY_GROUP_ID / SUBNET_IDS など実行時に決まる値は外部変数として渡す。jsonnet ファイルに固定値を埋め込まない
import ('env/' + env + '.libsonnet')で環境切替 — ENVIRONMENT 変数を動的にファイルパスに組み込む。env/stg.libsonnetを追加するだけで stg 環境に対応できる
5-4. ecspresso.yml での jsonnet 設定
ecspresso.yml の service_definition と task_definition フィールドに .jsonnet ファイルを指定する。変更点はファイル名の拡張子だけだ。
region: ap-northeast-1
cluster: arn:aws:ecs:ap-northeast-1:123456789012:cluster/myapp-cluster
service_definition: ecs-service.jsonnet # .json から .jsonnet に変更
task_definition: ecs-task-def.jsonnet # .json から .jsonnet に変更
timeout: 10m
plugins:
- name: tfstate
config:
tfstate_path: terraform.tfstate
ecspresso は .jsonnet ファイルを内部でコンパイルして JSON に変換してから ECS API に送信する。ecspresso deploy コマンドの実行フローに jsonnet コンパイルステップが自動的に組み込まれる。
ecspresso での ext-str 渡し方:
# ENVIRONMENT と ECR_IMAGE を渡してデプロイ
ecspresso deploy --config ecspresso.yml \
--ext-str ENVIRONMENT=prd \
--ext-str ECR_IMAGE=123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/myapp:v1.2.3 \
--ext-str SECURITY_GROUP_ID=sg-0123456789abcdef0 \
--ext-str "SUBNET_IDS=subnet-aaaa,subnet-bbbb" \
--ext-str TARGET_GROUP_ARN=arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:targetgroup/myapp/abc123
5-5. 環境変数 / Secrets Manager / Parameter Store 参照
Secrets Manager 参照 — コンテナへの秘密値注入
秘密値は ECS Task Definition の secrets フィールドで Secrets Manager / Parameter Store から直接注入する。jsonnet ファイルには ARN だけを記述し、値そのものを jsonnet に書かない。
// ecs-task-def.jsonnet の secrets セクション
local env = std.extVar('ENVIRONMENT');
local secretPrefix = 'arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:myapp/' + env;
secrets: [
{
name: 'DATABASE_PASSWORD',
valueFrom: secretPrefix + '/db-password',
},
{
name: 'API_KEY',
valueFrom: secretPrefix + '/api-key',
},
],
Parameter Store 参照 — 複数パラメータの展開
// SSM Parameter Store ARN を環境別プレフィックスで展開する例
local env = std.extVar('ENVIRONMENT');
local paramPrefix = 'arn:aws:ssm:ap-northeast-1:123456789012:parameter/myapp/' + env;
secrets: [
{ name: 'DB_HOST',valueFrom: paramPrefix + '/db-host' },
{ name: 'DB_NAME',valueFrom: paramPrefix + '/db-name' },
{ name: 'REDIS_URL', valueFrom: paramPrefix + '/redis-url' },
],
環境プレフィックスを std.extVar で動的に組み立てることで、3 環境 (dev/stg/prd) のパラメータを 1 つの jsonnet ファイルで一元管理できる。
5-6. 実装例全体 — dev と prd でスペックを切り替える
dev=Fargate 256CPU×1 タスク・prd=Fargate 512CPU×3 タスクを 1 つの jsonnet セットで実現する完全実装例を示す。
ecs-task-def.jsonnet (完全版)
// ecs-task-def.jsonnet: ECS Task Definition 生成エントリポイント
local base = import 'base.libsonnet';
local env = std.extVar('ENVIRONMENT');
local envConfig = import ('env/' + env + '.libsonnet');
local config = base + envConfig;
{
family: 'myapp-' + env,
cpu: config.cpu,
memory: config.memory,
networkMode: 'awsvpc',
requiresCompatibilities: ['FARGATE'],
executionRoleArn: std.extVar('EXECUTION_ROLE_ARN'),
taskRoleArn: std.extVar('TASK_ROLE_ARN'),
containerDefinitions: [
{
name: config.containerName,
image: std.extVar('ECR_IMAGE'),
portMappings: [
{ containerPort: config.containerPort, protocol: 'tcp' },
],
essential: true,
environment: [
{ name: 'APP_ENV', value: config.environment },
],
secrets: [
{
name: 'DATABASE_PASSWORD',
valueFrom: 'arn:aws:ssm:ap-northeast-1:123456789012:parameter/myapp/'
+ env + '/db-password',
},
],
logConfiguration: {
logDriver: 'awslogs',
options: {
'awslogs-group': config.logGroup,
'awslogs-region': config.region,
'awslogs-stream-prefix': 'ecs',
},
},
},
],
}
dev ビルドでは cpu: '256'・memory: '512'・desiredCount: 1 が生成され、prd ビルドでは cpu: '512'・memory: '1024'・desiredCount: 3 が生成される。コードの変更なしに環境変数だけで切り替わる。
5-7. jsonnet → JSON 生成 CLI による事前検証
デプロイ前に jsonnet が正しく JSON に変換されるか単体検証したい場合は jsonnet コマンドを使う。
# jsonnet CLI インストール (事前検証用 / 本番デプロイには不要)
brew install jsonnet
# dev 環境向け Task Definition JSON を生成して確認
jsonnet \
-V ENVIRONMENT=dev \
-V ECR_IMAGE=123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/myapp:v1.0.0 \
-V EXECUTION_ROLE_ARN=arn:aws:iam::123456789012:role/ecs-execution-role \
-V TASK_ROLE_ARN=arn:aws:iam::123456789012:role/ecs-task-role \
ecs-task-def.jsonnet
# prd 環境向け Task Definition JSON を生成して確認
jsonnet \
-V ENVIRONMENT=prd \
-V ECR_IMAGE=123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/myapp:v1.0.0 \
-V EXECUTION_ROLE_ARN=arn:aws:iam::123456789012:role/ecs-execution-role \
-V TASK_ROLE_ARN=arn:aws:iam::123456789012:role/ecs-task-role \
ecs-task-def.jsonnet
dev と prd の出力を比較し cpu・memory・desiredCount・logGroup が正しく切り替わっていることを確認する。ecspresso が deploy 時に内部で行う変換と等価のため、デプロイ前の安全網として有効だ。
GitHub Actions ワークフロー内での検証:
# .github/workflows/validate.yml (抜粋)
- name: Validate jsonnet
run: |
for env_name in dev stg prd; do
jsonnet \
-V ENVIRONMENT=$env_name \
-V ECR_IMAGE=${{ env.ECR_IMAGE }} \
-V EXECUTION_ROLE_ARN=${{ secrets.EXECUTION_ROLE_ARN }} \
-V TASK_ROLE_ARN=${{ secrets.TASK_ROLE_ARN }} \
ecs-task-def.jsonnet > /dev/null
echo "jsonnet compile OK: $env_name"
done
- jsonnet 公式サイト (jsonnet.org) — チュートリアルと言語仕様の基準ドキュメント。ブラウザ上の Playground で試せる
- ecspresso 公式 README (github.com/kayac/ecspresso) — jsonnet プラグイン設定・std.extVar 連携の実例集
- 標準ライブラリ (std) —
std.format/std.split/std.join/std.map/std.filterがコンテナ定義のリスト操作に特に有用
6. デプロイ戦略 (Blue/Green vs Rolling vs Canary)
ECS への本番デプロイで最も重要な意思決定のひとつが デプロイ戦略の選択 だ。ダウンタイム許容度・切替速度・ロールバック要件によって Rolling / Blue/Green / Canary の 3 択を使い分ける必要がある。ecspresso は 3 戦略すべてに対応しており、設定ひとつで切り替えられる。

6-1. 3 戦略の比較
| 比較軸 | Rolling Update | Blue/Green | Canary |
|---|---|---|---|
| デプロイ基盤 | ECS 標準 (ネイティブ) | CodeDeploy + ECS | CodeDeploy + ECS |
| ダウンタイム | ほぼなし (minimum_healthy_percent 調整で制御) | なし (トラフィック切替が即時) | なし (段階切替) |
| 切替速度 | 中〜速 (タスク置換ベース) | 速 (ロードバランサ切替) | 遅〜中 (段階的に切替) |
| ロールバック | 手動 (ecspresso rollback コマンド) | ワンクリック (CodeDeploy コンソール) | 自動 (CloudWatch Alarm 連動) |
| Terraform 追加リソース | なし | aws_codedeploy_app + aws_codedeploy_deployment_group | 同上 + CloudWatch Alarm |
| ecspresso 設定量 | 最小 (deploymentConfiguration のみ) | 中 (codedeploy ブロック + appspec.yaml) | 中 (deployment_config 指定のみ追加) |
| ALB ターゲットグループ | 1 つ | 2 つ必須 (Blue 用 + Green 用) | 2 つ必須 |
| 適した用途 | ステートレス API・ダウンタイム許容可な開発環境 | 本番 API・即時ロールバック必須のサービス | フロントエンド・段階的リリースが要件のサービス |
- 選択基準: 即時ロールバック必須 → Blue/Green / 段階リリース必須 → Canary / その他 → Rolling
- Blue/Green と Canary は CodeDeploy 統合が必要。Terraform で追加リソースを管理する
6-2. Rolling Update 実装 (ECS 標準)
Rolling Update は ECS ネイティブのデプロイ方式で、追加の AWS サービスを必要としない最もシンプルな構成だ。deploymentController.type のデフォルトが ECS であるため、特別な指定をしなければ自動的に Rolling Update が適用される。
ecspresso.yml (Rolling 設定)
region: ap-northeast-1
cluster: arn:aws:ecs:ap-northeast-1:123456789012:cluster/myapp-cluster
service_definition: ecs-service-rolling.jsonnet
task_definition: ecs-task-def.json
ecs-service-rolling.jsonnet (deploymentConfiguration 設定)
{
deploymentController: {
type: "ECS"
},
deploymentConfiguration: {
maximumPercent: 200,
minimumHealthyPercent: 100,
deploymentCircuitBreaker: {
enable: true,
rollback: true
}
},
desiredCount: 2,
launchType: "FARGATE",
networkConfiguration: {
awsvpcConfiguration: {
subnets: ["subnet-xxxxxxxx", "subnet-yyyyyyyy"],
securityGroups: ["sg-xxxxxxxx"],
assignPublicIp: "DISABLED"
}
}
}
パラメータ解説
| パラメータ | 推奨値 | 意味 |
|---|---|---|
maximumPercent | 200 | 既存タスク数の最大 200% まで一時的に起動可能 |
minimumHealthyPercent | 100 | ヘルシーなタスクを desiredCount の 100% 維持 (ダウンタイムなし) |
deploymentCircuitBreaker.enable | true | 失敗タスクが連続した場合にデプロイを自動停止 |
deploymentCircuitBreaker.rollback | true | 自動停止後に直前リビジョンへロールバック |
デプロイ実行コマンド
# Rolling Update デプロイ
ecspresso deploy --config ecspresso.yml
# ロールバック (前リビジョンへ)
ecspresso rollback --config ecspresso.yml
Rolling Update はシンプルで運用コストが低い一方、デプロイ中に新旧タスクが混在する期間がある点に注意。APIの後方互換性を常に保つ設計が重要だ。
6-3. Blue/Green 実装 (CodeDeploy 統合)
Blue/Green デプロイは ALB のターゲットグループを 2 系統 (Blue=現行 / Green=新版) 用意し、CodeDeploy がトラフィックを切り替える方式だ。新バージョンのヘルスチェックが完了してからトラフィックが切り替わるため、ダウンタイムが完全にゼロになる。
Blue/Green の仕組み
[ALB]
|
┌──────┴──────┐
[TG-Blue][TG-Green] ← CodeDeploy がトラフィック切替
(現行) (新版)
| |
[Task 旧][Task 新]
ecspresso.yml (Blue/Green 設定)
region: ap-northeast-1
cluster: arn:aws:ecs:ap-northeast-1:123456789012:cluster/myapp-cluster
service_definition: ecs-service-bg.jsonnet
task_definition: ecs-task-def.json
codedeploy:
application_name: myapp-ecs
deployment_group_name: myapp-ecs-deploy
deployment_config: CodeDeployDefault.ECSAllAtOnce
ecs-service-bg.jsonnet (CODE_DEPLOY コントローラ)
{
deploymentController: {
type: "CODE_DEPLOY"
},
desiredCount: 2,
launchType: "FARGATE",
loadBalancers: [
{
containerName: "myapp",
containerPort: 8080,
targetGroupArn: "arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:targetgroup/myapp-blue/xxxxxxxx"
}
],
networkConfiguration: {
awsvpcConfiguration: {
subnets: ["subnet-xxxxxxxx", "subnet-yyyyyyyy"],
securityGroups: ["sg-xxxxxxxx"],
assignPublicIp: "DISABLED"
}
}
}
appspec.yaml (CodeDeploy が参照するデプロイ設定)
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: <TASK_DEFINITION>
LoadBalancerInfo:
ContainerName: "myapp"
ContainerPort: 8080
PlatformVersion: "LATEST"
NetworkConfiguration:
AwsvpcConfiguration:
Subnets:
- "subnet-xxxxxxxx"
- "subnet-yyyyyyyy"
SecurityGroups:
- "sg-xxxxxxxx"
AssignPublicIp: "DISABLED"
デプロイ実行コマンド
# Blue/Green デプロイ (ecspresso が CodeDeploy API を呼ぶ)
ecspresso deploy --config ecspresso.yml
# ロールバック (CodeDeploy コンソールの [Stop and rollback deployment] ボタン、または CLI)
aws deploy stop-deployment \
--deployment-id d-XXXXXXXXX \
--auto-rollback-enabled
6-4. Canary 実装 (CodeDeploy Linear/TimeBased)
Canary デプロイは CodeDeploy の Linear 系 または AllAtOnce 系 のデプロイ設定を使い、トラフィックを段階的に切り替える方式だ。例えば Linear10PercentEvery1Minutes は 1 分ごとに 10% ずつトラフィックを新版へ移行し、10 分後に全量切替が完了する。
CodeDeploy デプロイ設定一覧
| 設定名 | 切替速度 | 用途 |
|---|---|---|
CodeDeployDefault.ECSAllAtOnce | 即時全量 | Blue/Green の即時切替 |
CodeDeployDefault.ECSLinear10PercentEvery1Minutes | 1%/分 × 10 分 | 段階的リリース |
CodeDeployDefault.ECSLinear10PercentEvery3Minutes | 10%/3分 × 30 分 | 慎重な段階リリース |
CodeDeployDefault.ECSCanary10Percent5Minutes | 最初 10%、5 分後全量 | 軽量 Canary |
CodeDeployDefault.ECSCanary10Percent15Minutes | 最初 10%、15 分後全量 | 安定確認型 Canary |
ecspresso.yml (Canary 設定)
region: ap-northeast-1
cluster: arn:aws:ecs:ap-northeast-1:123456789012:cluster/myapp-cluster
service_definition: ecs-service-bg.jsonnet
task_definition: ecs-task-def.json
codedeploy:
application_name: myapp-ecs
deployment_group_name: myapp-ecs-deploy
deployment_config: CodeDeployDefault.ECSLinear10PercentEvery1Minutes
Canary デプロイは codedeploy.deployment_config を変更するだけで切り替えられる。Blue/Green と同じ Terraform リソース構成 (後述の §6-5) で動作する。
6-5. CodeDeploy 統合時の Terraform 追加リソース
Blue/Green および Canary デプロイでは、ECS サービスに加えて以下の Terraform リソースを追加する必要がある。
ターゲットグループ 2 系統 (追加)
resource "aws_lb_target_group" "myapp_blue" {
name = "myapp-blue"
port = 8080
protocol = "HTTP"
vpc_id= aws_vpc.main.id
target_type = "ip"
health_check {
path = "/health"
healthy_threshold= 2
unhealthy_threshold = 3
interval= 30
}
tags = { Module = "ecspresso-hands-on", Env = "prd" }
}
resource "aws_lb_target_group" "myapp_green" {
name = "myapp-green"
port = 8080
protocol = "HTTP"
vpc_id= aws_vpc.main.id
target_type = "ip"
health_check {
path = "/health"
healthy_threshold= 2
unhealthy_threshold = 3
interval= 30
}
tags = { Module = "ecspresso-hands-on", Env = "prd" }
}
CodeDeploy アプリケーション + デプロイメントグループ
resource "aws_codedeploy_app" "myapp_ecs" {
name = "myapp-ecs"
compute_platform = "ECS"
}
resource "aws_codedeploy_deployment_group" "myapp_ecs" {
app_name = aws_codedeploy_app.myapp_ecs.name
deployment_group_name = "myapp-ecs-deploy"
service_role_arn= aws_iam_role.codedeploy.arn
deployment_config_name = "CodeDeployDefault.ECSLinear10PercentEvery1Minutes"
deployment_style {
deployment_option = "WITH_TRAFFIC_CONTROL"
deployment_type= "BLUE_GREEN"
}
blue_green_deployment_config {
deployment_ready_option {
action_on_timeout = "CONTINUE_DEPLOYMENT"
}
terminate_blue_instances_on_deployment_success {
action= "TERMINATE"
termination_wait_time_in_minutes = 5
}
}
ecs_service {
cluster_name = aws_ecs_cluster.main.name
service_name = "myapp-service"
}
load_balancer_info {
target_group_pair_info {
prod_traffic_route {
listener_arns = [aws_lb_listener.https.arn]
}
target_group { name = aws_lb_target_group.myapp_blue.name }
target_group { name = aws_lb_target_group.myapp_green.name }
}
}
tags = { Module = "ecspresso-hands-on", Env = "prd" }
}
CodeDeploy 用 IAM Role
resource "aws_iam_role" "codedeploy" {
name = "myapp-codedeploy-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = { Service = "codedeploy.amazonaws.com" }
Action = "sts:AssumeRole"
}
]
})
tags = { Module = "ecspresso-hands-on" }
}
resource "aws_iam_role_policy_attachment" "codedeploy_ecs" {
role = aws_iam_role.codedeploy.name
policy_arn = "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS"
}
outputs.tf (ecspresso.yml から参照する値を公開)
output "codedeploy_app_name" {
value = aws_codedeploy_app.myapp_ecs.name
}
output "codedeploy_deployment_group_name" {
value = aws_codedeploy_deployment_group.myapp_ecs.deployment_group_name
}
output "target_group_blue_arn" {
value = aws_lb_target_group.myapp_blue.arn
}
output "target_group_green_arn" {
value = aws_lb_target_group.myapp_green.arn
}
6-6. 失敗時復旧比較
デプロイが失敗した際の復旧手順は戦略によって大きく異なる。本番障害時の RTO (目標復旧時間) を考慮して戦略を選択することが重要だ。
| 復旧観点 | Rolling Update | Blue/Green | Canary |
|---|---|---|---|
| ロールバック操作 | ecspresso rollback コマンド | CodeDeploy コンソール 1 クリック | CodeDeploy コンソール 1 クリック or 自動 |
| 復旧所要時間 | タスク置換時間 (1〜3 分) | 数十秒 (TG 切替のみ) | 数十秒 (TG 切替のみ) |
| 影響を受けたユーザ数 | デプロイ期間中の一部リクエスト | ゼロ (Green 切替前の段階でロールバック可) | Canary 段階の 10% のみ影響 |
| CloudWatch Alarm 自動連動 | なし (Circuit Breaker のみ) | 手動設定可 | 手動設定可 |
| 操作ミスリスク | 低 (ecspresso CLI) | 低 (コンソール 1 クリック) | 低 (コンソール 1 クリック) |
Rolling Update のロールバック手順
# 直前のタスク定義リビジョンへロールバック
ecspresso rollback --config ecspresso.yml
# 特定リビジョンへのロールバック
ecspresso rollback --config ecspresso.yml --revision 42
Circuit Breaker が有効な場合、連続失敗が閾値を超えると自動的に直前リビジョンへロールバックされる。
Blue/Green / Canary のロールバック手順 (CLI)
# 進行中のデプロイを停止してロールバック
aws deploy stop-deployment \
--deployment-id d-XXXXXXXXX \
--auto-rollback-enabled \
--region ap-northeast-1
7. GitHub Actions CI/CD
ecspresso と GitHub Actions (GHA) を組み合わせることで、PR 時に差分プレビュー → レビュー → merge でデプロイ という OSS 完結型 CI/CD パイプラインを構築できる。CodePipeline + CodeBuild を使わないため、AWS コンソール外でワークフローが完結する利点がある。
7-1. GHA ワークフロー全体設計
CI/CD パイプライン構成
[PR オープン]
|
├── ecs-diff.yml 起動
| ├── ecspresso diff (ECS サービス定義の差分確認)
| └── PR コメントへ diff 結果を自動投稿
|
[レビュー + 承認]
|
[main ブランチへ merge]
|
└── ecs-deploy.yml 起動
├── terraform apply (インフラ変更を先行適用)
└── ecspresso deploy (ECS サービス更新)

ブランチ戦略
| ブランチ | デプロイ先 | 備考 |
|---|---|---|
main | prd (本番) | PR 経由 merge のみ |
develop | stg (ステージング) | feature → develop PR |
feature/* | dev (開発) | 手動 dispatch or push で diff のみ |
使用する主要 Action
| Action | バージョン | 用途 |
|---|---|---|
actions/checkout | v4 | ソースコード取得 |
aws-actions/configure-aws-credentials | v4 | OIDC AWS 認証 |
kayac/ecspresso-action | v2 | ecspresso CLI 実行 |
hashicorp/setup-terraform | v3 | Terraform CLI セットアップ |
thollander/actions-comment-pull-request | v2 | PR コメント投稿 |
7-2. PR 時ワークフロー (ecspresso diff + PR コメント自動投稿)
PR オープン・更新のたびに ecspresso diff を実行し、ECS サービス定義の差分を PR コメントとして自動投稿する。レビュアーがコンソールを開かずに変更内容を把握できる。
.github/workflows/ecs-diff.yml
name: ECS Diff on PR
on:
pull_request:
branches:
- main
- develop
paths:
- 'ecs/**'
- 'terraform/**'
- '.github/workflows/ecs-diff.yml'
permissions:
id-token: write# OIDC トークン取得に必要
contents: read
pull-requests: write # PR コメント投稿に必要
jobs:
diff:
name: ecspresso diff
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ap-northeast-1
- name: Setup ecspresso
uses: kayac/ecspresso-action@v2
with:
version: v2.4.0
- name: Set environment from branch
id: env
run: |
if [[ "${{ github.base_ref }}" == "main" ]]; then
echo "ENV=prd" >> $GITHUB_OUTPUT
else
echo "ENV=stg" >> $GITHUB_OUTPUT
fi
- name: Run ecspresso diff
id: diff
run: |
cd ecs
DIFF_OUTPUT=$(ecspresso diff \
--config ecspresso.${{ steps.env.outputs.ENV }}.yml 2>&1) || true
echo "diff_result<<EOF" >> $GITHUB_OUTPUT
echo "${DIFF_OUTPUT}" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Post diff result to PR comment
uses: thollander/actions-comment-pull-request@v2
with:
message: |
## ECS Diff (${{ steps.env.outputs.ENV }})
```
${{ steps.diff.outputs.diff_result }}
```
> 実行: `ecspresso diff --config ecspresso.${{ steps.env.outputs.ENV }}.yml`
comment_tag: ecspresso-diff
mode: recreate
ワークフローのポイント
pathsフィルタで ECS 関連ファイルの変更時のみ起動 (不要な実行を削減)comment_tag: ecspresso-diff+mode: recreateにより、同一 PR へのコメントが上書きされ PR がコメントで埋まらないENVを base_ref (PR のマージ先ブランチ) で動的に決定
7-3. merge 時ワークフロー (terraform apply → ecspresso deploy) 前半
main / develop ブランチへの merge (push) をトリガーに、Terraform でインフラを先行更新してから ecspresso でサービスをデプロイする。
.github/workflows/ecs-deploy.yml (前半: Terraform apply まで)
name: ECS Deploy
on:
push:
branches:
- main
- develop
paths:
- 'ecs/**'
- 'terraform/**'
- '.github/workflows/ecs-deploy.yml'
permissions:
id-token: write
contents: read
env:
TF_VERSION: "1.9.8"
AWS_REGION: "ap-northeast-1"
jobs:
deploy:
name: Terraform apply + ecspresso deploy
runs-on: ubuntu-latest
environment: ${{ github.ref == 'refs/heads/main' && 'prd' || 'stg' }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ${{ env.AWS_REGION }}
- name: Set environment
id: env
run: |
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "ENV=prd" >> $GITHUB_OUTPUT
echo "TF_WORKSPACE=prd" >> $GITHUB_OUTPUT
else
echo "ENV=stg" >> $GITHUB_OUTPUT
echo "TF_WORKSPACE=stg" >> $GITHUB_OUTPUT
fi
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Terraform Init
working-directory: terraform
run: |
terraform init \
-backend-config="bucket=${{ secrets.TF_STATE_BUCKET }}" \
-backend-config="key=${{ steps.env.outputs.TF_WORKSPACE }}/terraform.tfstate" \
-backend-config="region=${{ env.AWS_REGION }}"
- name: Terraform Workspace
working-directory: terraform
run: |
terraform workspace select ${{ steps.env.outputs.TF_WORKSPACE }} \
|| terraform workspace new ${{ steps.env.outputs.TF_WORKSPACE }}
- name: Terraform Apply
working-directory: terraform
run: terraform apply -auto-approve -var="env=${{ steps.env.outputs.ENV }}"
7-4. OIDC による AWS 認証
GHA ワークフローから AWS にアクセスする際、長期 IAM アクセスキーを GitHub Secrets に保存するのはセキュリティリスク となる。代わりに OIDC (OpenID Connect) を使うことで、キーレスかつ最小権限の一時クレデンシャルを取得できる。
OIDC 認証の仕組み
[GitHub Actions]
|
├─ (1) OIDC トークン要求 → GitHub OIDC Provider
|↓ JWT トークン発行
├─ (2) AssumeRoleWithWebIdentity → AWS STS
|↓ 一時クレデンシャル (有効期限付き)
└─ (3) AWS API コール (ECS / CodeDeploy 等)
Terraform で OIDC プロバイダーと IAM Role を設定
data "aws_caller_identity" "current" {}
resource "aws_iam_openid_connect_provider" "github_actions" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = [
"6938fd4d98bab03faadb97b34396831e3780aea1",
"1c58a3a8518e8759bf075b76b750d4f2df264fcd"
]
tags = { Module = "ecspresso-hands-on" }
}
resource "aws_iam_role" "github_actions_deploy" {
name = "myapp-github-actions-deploy"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.github_actions.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringLike = {
"token.actions.githubusercontent.com:sub" = "repo:your-org/your-repo:*"
}
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
}
}
}
]
})
tags = { Module = "ecspresso-hands-on" }
}
Trust Policy JSON (参考: コンソールから設定する場合)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:*"
},
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
}
}
}
]
}
IAM Role に必要なポリシー
resource "aws_iam_role_policy" "github_actions_deploy" {
name = "myapp-github-actions-deploy-policy"
role = aws_iam_role.github_actions_deploy.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"ecs:DescribeServices",
"ecs:DescribeTaskDefinition",
"ecs:RegisterTaskDefinition",
"ecs:UpdateService",
"codedeploy:CreateDeployment",
"codedeploy:GetDeployment",
"codedeploy:GetDeploymentConfig",
"codedeploy:RegisterApplicationRevision",
"ecr:GetAuthorizationToken",
"ecr:BatchGetImage",
"ecr:GetDownloadUrlForLayer",
"iam:PassRole"
]
Resource = "*"
},
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:PutObject",
"dynamodb:GetItem",
"dynamodb:PutItem"
]
Resource = [
"arn:aws:s3:::${var.tf_state_bucket}/*",
"arn:aws:dynamodb:ap-northeast-1:123456789012:table/${var.tf_lock_table}"
]
}
]
})
}
GitHub Secrets に設定する値
AWS_ROLE_ARN = arn:aws:iam::123456789012:role/myapp-github-actions-deploy
TF_STATE_BUCKET = myapp-terraform-state
ECR_REGISTRY = 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com
ワークフロー内での参照方法
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ap-northeast-1
# role-session-name はデフォルトで GitHubActions になる
# role-duration-seconds: 3600 (デフォルト 1 時間)
role-to-assume に ARN を指定するだけで OIDC 認証が動作する。アクセスキーやシークレットキーを Secrets に保存する必要がない。
7-5. Secrets 管理
GitHub Actions ワークフローで ecspresso を安全に動かすには、クレデンシャル情報を GitHub Secrets として管理します。ワークフロー YAML に直書きすると Git 履歴に残るため、必ず Secrets 経由で渡す設計にします。
登録する Secrets
| Secret 名 | 値の例 | 用途 |
|---|---|---|
AWS_ROLE_ARN | arn:aws:iam::123456789012:role/github-oidc-role | OIDC 用 IAM ロール ARN |
ECR_REPO | 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/myapp | ECR リポジトリ URI |
CLUSTER_NAME_prd | myapp-cluster-prd | 本番クラスター名 |
CLUSTER_NAME_stg | myapp-cluster-stg | 検証クラスター名 |
GitHub リポジトリの Settings → Secrets and variables → Actions から登録します。環境別に CLUSTER_NAME_prd / CLUSTER_NAME_stg を分けることで、ワークフロー内の secrets[format('CLUSTER_NAME_{0}', env)] で動的に取得できます。
ecspresso.yml での環境変数参照
ecspresso v2 の must_env 関数を使うと、環境変数が未設定のときに即エラーを返します。空値のまま誤ったクラスターへデプロイされる事故を防ぐ安全策です。
# ecspresso.yml — Secrets 連携設定例
region: ap-northeast-1
cluster: "{{ must_env `CLUSTER_NAME` }}"
service: myapp-service
service_definition: ecs-service.jsonnet
task_definition: ecs-task-def.jsonnet
plugins:
- name: env
ecs-service.jsonnet 内では std.extVar("ENVIRONMENT") として環境名を受け取り、dev/stg/prd で設定を切り替えます。
# ローカル動作確認例 (実機実行: 2026-04)
CLUSTER_NAME=myapp-cluster-stg \
ENVIRONMENT=stg \
ecspresso diff --config ecspresso.yml \
--ext-str ECR_IMAGE=123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/myapp:latest
7-6. ブランチ戦略
main / develop / feature/* の 3 層でデプロイ先環境を自動マッピングします。ブランチ名とデプロイ環境を 1 対 1 に対応させることで、意図しない環境へのデプロイを防ぎます。
| ブランチ | デプロイ先 | ENVIRONMENT 変数 |
|---|---|---|
main | prd (本番) | prd |
develop | stg (検証) | stg |
feature/* | dev (開発) | dev |
GHA ワークフロー内でブランチ判定を行い、ENVIRONMENT 変数を動的に切り替えます。
# ブランチ→環境マッピングステップ
- name: Set environment
id: env
run: |
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "env=prd" >> $GITHUB_OUTPUT
elif [[ "${{ github.ref }}" == "refs/heads/develop" ]]; then
echo "env=stg" >> $GITHUB_OUTPUT
else
echo "env=dev" >> $GITHUB_OUTPUT
fi
$GITHUB_OUTPUT への書き込みは GHA の推奨パターンです。非推奨の set-output コマンドは 2023 年廃止済みのため使用しないでください。
後続ステップでは ${{ steps.env.outputs.env }} として環境名を参照します。ecspresso deploy 時に --ext-str ENVIRONMENT=${{ steps.env.outputs.env }} を渡すと、jsonnet 側の std.extVar("ENVIRONMENT") がブランチに対応した設定を返します。
7-7. ワークフロー yaml 完全実装
- PR 時:
ecspresso diff→ 差分レポートを PR コメントに自動投稿 - merge 時:
terraform apply→ecspresso deployの 2 ステップ自動実行 - OIDC 認証でアクセスキーを Secrets に保存せず安全に AWS 接続
- ブランチ別 (main=prd / develop=stg) に環境を自動マッピング
# .github/workflows/ecs-deploy.yml — 完全版 (ecspresso v2.4.x / terraform 1.9.x 対応)
name: Deploy ECS Service
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
permissions:
id-token: write
contents: read
pull-requests: write
jobs:
diff:
name: ecspresso diff (PR only)
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ap-northeast-1
- name: Install ecspresso
run: |
curl -sSL https://github.com/kayac/ecspresso/releases/download/v2.4.0/ecspresso_v2.4.0_linux_amd64.tar.gz \
| tar -xz ecspresso && sudo mv ecspresso /usr/local/bin/
- name: Set environment
id: env
run: |
if [[ "${{ github.base_ref }}" == "main" ]]; then
echo "env=prd" >> $GITHUB_OUTPUT
else
echo "env=stg" >> $GITHUB_OUTPUT
fi
- name: ecspresso diff
id: diff
run: |
DIFF_OUT=$(ecspresso diff --config ecspresso.yml \
--ext-str ENVIRONMENT=${{ steps.env.outputs.env }} \
--ext-str ECR_IMAGE=${{ secrets.ECR_REPO }}:${{ github.sha }} 2>&1 || true)
echo "result<<EOF" >> $GITHUB_OUTPUT
echo "$DIFF_OUT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
env:
CLUSTER_NAME: ${{ secrets[format('CLUSTER_NAME_{0}', steps.env.outputs.env)] }}
- name: Comment diff to PR
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '### ecspresso diff\n```\n${{ steps.diff.outputs.result }}\n```'
})
deploy:
name: TF apply + ecspresso deploy (push only)
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: ap-northeast-1
- name: Set environment
id: env
run: |
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "env=prd" >> $GITHUB_OUTPUT
else
echo "env=stg" >> $GITHUB_OUTPUT
fi
- name: Terraform apply
run: |
terraform init
terraform apply -auto-approve \
-var="environment=${{ steps.env.outputs.env }}"
working-directory: ./terraform
- name: Install ecspresso
run: |
curl -sSL https://github.com/kayac/ecspresso/releases/download/v2.4.0/ecspresso_v2.4.0_linux_amd64.tar.gz \
| tar -xz ecspresso && sudo mv ecspresso /usr/local/bin/
- name: ecspresso deploy
run: |
ecspresso deploy --config ecspresso.yml \
--ext-str ENVIRONMENT=${{ steps.env.outputs.env }} \
--ext-str ECR_IMAGE=${{ secrets.ECR_REPO }}:${{ github.sha }}
env:
CLUSTER_NAME: ${{ secrets[format('CLUSTER_NAME_{0}', steps.env.outputs.env)] }}
7-8. ローカル act での検証
act を使うとローカル環境で GHA ワークフローの構文と動作を事前確認できます。実 AWS API 呼び出しは行わないため、ワークフロー構文エラーを CI に push する前に検出できます。
# act インストール (macOS)
brew install act
# バージョン確認
act --version
# act version 0.2.x
PR ワークフロー (diff ジョブ) の確認:
# pull_request イベントを dry-run でシミュレート
act pull_request --job diff -n
push ワークフロー (deploy ジョブ) の確認:
# .secrets ファイルで Secrets を渡す
cat > .secrets << 'EOF'
AWS_ROLE_ARN=arn:aws:iam::123456789012:role/github-oidc-role
ECR_REPO=123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/myapp
CLUSTER_NAME_prd=myapp-cluster-prd
CLUSTER_NAME_stg=myapp-cluster-stg
EOF
# push イベントを dry-run で確認 (.secrets ファイル使用)
act push --job deploy --secret-file .secrets -n
- OIDC 認証 (
aws-actions/configure-aws-credentials) はローカルでは動作しないため、実 AWS 接続ステップは-n(dry-run) での構文確認のみ推奨 - 初回実行時に Docker イメージ
catthehacker/ubuntu:act-latestを数 GB pull するため、事前にdocker pull catthehacker/ubuntu:act-latestしておくと速い - 本番リリース前には必ず stg 環境への実 deploy で動作を検証すること
8. まとめ + troubleshoot + 次回予告
8-1. 判断軸振り返りマトリクス
本記事で習得した判断軸を 1 表に集約します。
| 章 | 判断軸 | 要点 |
|---|---|---|
| §1 | ツール選択 | Terraform 既存 → ecspresso / ゼロから → Copilot / TypeScript → CDK |
| §2 | 責任境界 | インフラ (VPC/ALB/Cluster/ECR/IAM) = TF / Service+TaskDef = ecspresso |
| §3 | TF 基盤 | outputs.tf で Cluster ARN / TG ARN を公開し ecspresso から参照 |
| §4 | CLI 一巡 | init → diff → deploy → rollback の 4 コマンド確認 |
| §5 | jsonnet | base.libsonnet + env/{dev,prd}.libsonnet → 環境別 JSON 生成 |
| §6 | デプロイ戦略 | 無停止必須 → B/G / シンプル → Rolling / 段階的 → Canary |
| §7 | CI/CD | PR=diff コメント / merge=TF apply+deploy / OIDC で認証 |
| §8 | troubleshoot | ecspresso 特有エラー 10 ケースと解決策 |
8-2. チートシート
| カテゴリ | コマンド | 説明 |
|---|---|---|
| Terraform | terraform init | プロバイダ初期化 |
| Terraform | terraform plan -var="environment=prd" | 変更プレビュー |
| Terraform | terraform apply -auto-approve | リソース作成/更新 |
| Terraform | terraform output cluster_arn | output 値確認 |
| ecspresso | ecspresso init --cluster X --service Y | 設定ファイル生成 |
| ecspresso | ecspresso diff | AWS との差分確認 |
| ecspresso | ecspresso deploy | Service/TaskDef 更新 |
| ecspresso | ecspresso rollback | 直前リビジョンに戻す |
| ecspresso | ecspresso scale --tasks N | タスク数変更 |
| ecspresso | ecspresso verify | 設定ファイル構文検証 |
| jsonnet | jsonnet -S ecs-service.jsonnet | JSON 生成 |
| jsonnet | jsonnet -S -V ENV=prd ecs-service.jsonnet | 環境変数付き JSON 生成 |
| GHA | act pull_request -n | PR ワークフロー dry-run |
| GHA | act push --secret-file .secrets -n | push ワークフロー dry-run |
8-3. troubleshoot 10 ケース集
ケース 1: ecspresso init 失敗 (–cluster ARN 未指定)
症状: Error: InvalidParameterException: cluster not found
原因: 一部のリージョンではクラスター名のみでは解決できず、ARN での指定が必要
解決策:
# クラスター ARN を確認
aws ecs describe-clusters --clusters myapp-cluster \
--query 'clusters[0].clusterArn' --output text
# ARN で再実行
ecspresso init \
--cluster arn:aws:ecs:ap-northeast-1:123456789012:cluster/myapp-cluster \
--service myapp-service --config ecspresso.yml
ケース 2: diff で差分検知漏れ (TaskDef INACTIVE)
症状: ecspresso diff で差分なし報告されるが、デプロイ後に古い TaskDef が使われる
原因: AWS 側に INACTIVE 状態の TaskDef が多数残り、最新アクティブ版の検索に時間がかかる
解決策:
# アクティブな TaskDef 最新版を確認
aws ecs list-task-definitions \
--family-prefix myapp-task --status ACTIVE --sort DESC \
--query 'taskDefinitionArns[0]' --output text
# ecspresso verify で整合性チェック
ecspresso verify --config ecspresso.yml
ケース 3: deploy Stuck (HealthCheck 失敗)
症状: ecspresso deploy が完了せず、ECS コンソールで新タスクが STOPPED を繰り返す
原因: ALB TargetGroup ヘルスチェックパスが変更されたか、アプリが /health を返せていない
解決策:
# タスク停止理由を確認
aws ecs describe-tasks \
--cluster myapp-cluster \
--tasks $(aws ecs list-tasks --cluster myapp-cluster \
--query 'taskArns[0]' --output text) \
--query 'tasks[0].stoppedReason' --output text
# デプロイを中断してロールバック
ecspresso rollback --config ecspresso.yml
ケース 4: CodeDeploy FAILED (AppSpec 構成エラー)
症状: Blue/Green デプロイが FAILED: AppSpec content is invalid で失敗
原因: appspec.yml の containerName / containerPort が TaskDef の定義と不一致
解決策:
# appspec.yml — containerName/Port を TaskDef と一致させる
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: <TASK_DEFINITION>
LoadBalancerInfo:
ContainerName: "myapp"# TaskDef の name フィールドと完全一致
ContainerPort: 8080 # TaskDef の portMappings.containerPort と一致
ケース 5: jsonnet 生成エラー (import path 解決失敗)
症状: RUNTIME ERROR: couldn't open import "env/prd.libsonnet": no such file
原因: jsonnet コマンドの実行ディレクトリが ecs-service.jsonnet の置き場と異なる
解決策:
# jsonnet ファイルのあるディレクトリに移動して実行
cd ecspresso/
jsonnet -S -V ENV=prd ecs-service.jsonnet
# または --jpath で import 検索パスを明示
jsonnet -S --jpath ./ecspresso -V ENV=prd ecspresso/ecs-service.jsonnet
ケース 6: GHA OIDC 認証失敗 (Trust Policy Condition)
症状: Error: Not authorized to perform sts:AssumeRoleWithWebIdentity
原因: IAM ロールの Trust Policy の Condition が GitHub リポジトリ名と不一致
解決策:
{
"Condition": {
"StringLike": {
"token.actions.githubusercontent.com:sub":
"repo:myorg/myrepo:*"
}
}
}
myorg/myrepo を実際のオーナー/リポジトリ名に修正します。StringEquals を使う場合は ref:refs/heads/main まで完全一致が必要です。
ケース 7: ECR push 失敗 (IAM 権限不足)
症状: Error: denied: User: ... is not authorized to perform: ecr:InitiateLayerUpload
原因: GHA が assume する IAM ロールに ECR push 権限が付与されていない
解決策:
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:InitiateLayerUpload",
"ecr:UploadLayerPart",
"ecr:CompleteLayerUpload",
"ecr:PutImage"
],
"Resource": "arn:aws:ecr:ap-northeast-1:123456789012:repository/myapp"
}
ケース 8: Blue/Green ロールバック手順 (CodeDeploy Console)
症状: 本番 deploy 後にエラーが発生し、旧版 (Blue 環境) に即時切り戻したい
原因: 新タスク (Green) でアプリバグが発生
解決策:
1. AWS Console → CodeDeploy → デプロイグループ → 実行中のデプロイを選択
2. 「デプロイを停止してロールバック」ボタンをクリック
3. ALB ターゲットが Blue (旧) に切り戻されるまで待機 (2-3 分)
4. ecspresso rollback で TaskDef も旧リビジョンに戻す
ecspresso rollback --config ecspresso.yml
ケース 9: ecspresso rollback でバージョン確認
症状: rollback 後に「どのリビジョンに戻ったか」が不明
原因: ecspresso rollback は自動的に直前のアクティブリビジョンを選択する
解決策:
# 現在の Service で使用中の TaskDef リビジョンを確認
aws ecs describe-services \
--cluster myapp-cluster --services myapp-service \
--query 'services[0].taskDefinition' --output text
# ecspresso のデプロイ履歴を確認
ecspresso deployments --config ecspresso.yml
ケース 10: Terraform と ecspresso の状態ズレ復旧
症状: terraform apply 後に ecspresso diff で大量差分が出る
原因: Terraform で Service リソースも管理していた場合、TF と ecspresso の両方が Service を変更しようとする
解決策:
# tfstate から Service リソースを切り離す
terraform state rm aws_ecs_service.myapp
# ecspresso init で現状の Service を ecspresso.yml に取り込む
ecspresso init \
--cluster myapp-cluster \
--service myapp-service \
--config ecspresso.yml
# ecspresso diff で差分が 0 になることを確認
ecspresso diff --config ecspresso.yml
8-4. 関連記事表
| 記事 | 内容 | 関連 |
|---|---|---|
| ECS × Step Functions バッチ処理 | SF × Fargate バッチ | ECS 応用パターン |
| ECS Fargate CI/CD — Copilot 編 | Copilot CLI での CI/CD | §1 比較元 |
| ECS Fargate CI/CD — CodePipeline 編 | CodePipeline + CodeBuild | §7 GHA との比較 |
| EventBridge + VPCLattice + Fargate | EB × VPCLattice 高度統合 | ECS 上位パターン |
| ecspresso 公式 GitHub | kayac/ecspresso v2 本体 | バージョン確認・Issue |
8-5. 次回予告
- ecspresso exec 詳細 — ECS Exec でコンテナに直接アクセスし、デバッグと調査を効率化する手順を実装形式で解説
- Service Connect 統合 — App Mesh に代わる ECS Service Connect と ecspresso を組み合わせ、内部サービス検出を実装する構成
- Karpenter との組み合わせ — EC2 起動タイプ利用時に Karpenter でノードを動的スケールし、ecspresso でサービスを制御するハイブリッド構成
8-6. 次のステップ
ECS × Step Functions バッチ処理を読む
Copilot 版 ECS CI/CD を比較する
ecspresso 公式 GitHub を確認する