NO IMAGE

ecspresso × Terraform × GHA で ECS デプロイ完全ガイド (jsonnet対応)

NO IMAGE
目次

1. ecspresso とは何か / なぜ今これを選ぶか

fig01: ecspresso vs Copilot vs CDK 比較マトリクス

ecspresso は kayac が開発・公開している OSS の ECS 専用デプロイツールだ。ECS Service と Task Definition の設定をローカル JSON ファイルで管理し、1 コマンドで AWS ECS へデプロイできる。Terraform が構築したインフラ (VPC / Cluster / ALB / IAM) の上に乗る形で動作するため、Terraform との親和性が特に高い。

この記事で手に入る 5 つの成果物

  • 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 つの成果物 が手に入る。

  1. Terraform 基盤 — VPC / ALB / ECS Cluster / ECR / IAM を Terraform HCL で構築し、outputs.tf で ecspresso に参照させる最小構成
  2. ecspresso 設定ファイル 3 種ecspresso.yml / ecs-service-def.jsonnet / ecs-task-def.jsonnet の動作する雛形
  3. jsonnet 変数化設計base.libsonnet + env/dev.libsonnet + env/prd.libsonnet の 3 ファイル構成で環境別設定を DRY 管理
  4. デプロイ戦略の判断基準 — Rolling / Blue-Green / Canary の選択方法と CodeDeploy 統合 HCL 実装
  5. 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

【ツール選択 5 軸比較】ecspresso / Copilot / CDK / ECS 直接 API / CodePipeline+CodeBuild

比較軸ecspressoAWS CopilotAWS CDKECS 直接 APICodePipeline
+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 を検討

ecspresso 公式 GitHub でスターを付ける


2. Terraform + ecspresso 役割分担設計

fig02: 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 境界対照表

【QG-2 最重要】Terraform (tfstate) と ecspresso の責任境界対照表

AWS リソース管理ツール理由変更頻度
VPC / Subnet / IGW / Route TableTerraformネットワーク基盤。変更は稀で TF の state 管理が最適低 (月単位)
ALB / Target Group / ListenerTerraformロードバランサは ECS Service と独立して作成。ecspresso は ARN を参照するのみ低 (月単位)
ECS ClusterTerraformCluster は ECS の基盤リソース。ecspresso はクラスター名/ARN を参照するのみ低 (再作成は稀)
ECR RepositoryTerraformRepository は一度作れば恒久的。ecspresso は URL を push/pull で利用するのみ低 (初回のみ)
IAM Role (Task / Execution)TerraformIAM Role は権限設計に関わるため変更履歴が重要。TF state で一元管理する低〜中 (権限追加時)
Security GroupTerraformポートルールはネットワーク設計の一部。TF で一元管理する低 (要件変更時)
ECS ServiceecspressodesiredCount / deploymentConfig はデプロイごとに変わる可能性がある。TF で管理すると毎回 plan が必要で遅い。手動スケール後に TF apply すると desiredCount が上書きされる (drift 問題)高 (デプロイごと)
ECS Task Definitionecspressoコンテナイメージタグはデプロイごとに変わる。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_arnecspresso.yml の cluster フィールド
"{{ tfstate \"output.alb_target_group_arn\" }}"alb_target_group_arnecs-service-def.jsonnet の loadBalancers
"{{ tfstate \"output.ecr_repository_url\" }}"ecr_repository_urlecs-task-def.jsonnet の image (ベース URL)
"{{ tfstate \"output.execution_role_arn\" }}"execution_role_arnecs-task-def.jsonnet の executionRoleArn
"{{ tfstate \"output.task_role_arn\" }}"task_role_arnecs-task-def.jsonnet の taskRoleArn

境界設計のベストプラクティス

  1. outputs.tf に必要な値を全公開: ecspresso が必要とする値は全て outputs.tf で公開しておく。後から追加する場合は terraform apply が必要になる
  2. ECS Service を TF から除外: 既存 TF コードに aws_ecs_service がある場合、terraform state rm aws_ecs_service.app でステートから除外してから ecspresso に管理を移譲する
  3. image タグは ecspresso で管理: ecs-task-def.jsonnetstd.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 基盤ハンズオン (最小構成)

fig03: Terraform 基盤構成図

ecspresso は ECS Service / TaskDefinition を管理するが、その土台となる VPC・ALB・ECS Cluster・ECR・IAM は Terraform で先に構築する。本章では ecspresso から参照できる最小 Terraform 基盤を 30 分で立ち上げる手順を示す。

3-1. 基盤リソース一覧

ecspresso が依存する Terraform 管理リソースを以下に整理する。

リソースTerraform リソース型ecspresso での利用目的
VPCaws_vpcネットワーク基盤
Public Subnet × 2aws_subnetALB 配置 (AZ 冗長)
Private Subnet × 2aws_subnetECS Fargate タスク配置
Internet Gatewayaws_internet_gatewayPublic Subnet の外部通信
Route Table (Public)aws_route_tablePublic Subnet → IGW ルート
ALBaws_lbFargate へのトラフィック転送
Target Groupaws_lb_target_groupalb_target_group_arn として参照
ALB Listeneraws_lb_listenerHTTP:80 → Target Group 転送
ECS Clusteraws_ecs_clustercluster_name として参照
ECR Repositoryaws_ecr_repositoryコンテナイメージ保管先
IAM Task Roleaws_iam_roleタスク実行時の AWS API 権限
IAM Execution Roleaws_iam_roleタスク起動・ECR pull 権限
Security Group (ALB)aws_security_groupHTTP 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.ymlplugins.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
outputs.tf 設計のポイント

  • 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)
--regionAWS リージョン
--clusterECS クラスター名または完全 ARN
--serviceECS サービス名

実行すると ECS:DescribeServicesECS: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_definitionECS Service 定義 JSON のパス (.jsonnet も指定可)
task_definitionECS Task Definition JSON のパス
timeoutデプロイ完了待機のタイムアウト (デフォルト 5m)
plugins.tfstateterraform.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 diffAWS API から現在の ECS 設定を取得し、ローカル JSON と差分を表示するコマンドだ。Terraform の terraform plan に相当する。デプロイ前に必ず実行する習慣をつける。

# デプロイ前の差分確認
ecspresso diff --config ecspresso.yml

内部動作フロー:

  1. ECS:DescribeServices でクラウド上のサービス定義を取得
  2. ECS:DescribeTaskDefinition で最新タスク定義を取得
  3. ローカルの ecs-service-def.json / ecs-task-def.json と JSON 差分を生成
  4. unified diff 形式で標準出力に表示
  5. 差分なし → 終了コード 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

デプロイの実行フロー:

  1. ecs-task-def.json を元に RegisterTaskDefinition で新リビジョンを登録 (myapp:15myapp:16)
  2. UpdateService で新リビジョンを指定してローリング更新を開始
  3. 新タスク起動 → ALB ヘルスチェック通過 → 旧タスクのドレイン開始 → STOPPED
  4. DescribeServices を定期ポーリングして runningCount == desiredCount を確認
  5. 安定確認後に終了コード 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 CLI 5 選

  • 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.jsonecs-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.ymlservice_definitiontask_definition フィールドに .jsonnet ファイルを指定するだけで有効になる。ecspresso バイナリに jsonnet エンジンが内包されているため追加インストールは不要だ。

jsonnet のコンパイル (jsonnet → json) は ecspresso が内部で自動実行する。ユーザーは jsonnet を書いてデプロイするだけでよい。

fig04: 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+importstd.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' },
}
QG-3: base + env 上書き設計の 5 原則

  • 共通設定は 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.ymlservice_definitiontask_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 の出力を比較し cpumemorydesiredCountlogGroup が正しく切り替わっていることを確認する。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 公式サイト (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 戦略すべてに対応しており、設定ひとつで切り替えられる。

fig05: デプロイ戦略 3 択判定フロー

6-1. 3 戦略の比較

【デプロイ戦略 3 択比較】Rolling / Blue-Green / Canary

比較軸Rolling UpdateBlue/GreenCanary
デプロイ基盤ECS 標準 (ネイティブ)CodeDeploy + ECSCodeDeploy + 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"
 }
  }
}

パラメータ解説

パラメータ推奨値意味
maximumPercent200既存タスク数の最大 200% まで一時的に起動可能
minimumHealthyPercent100ヘルシーなタスクを desiredCount の 100% 維持 (ダウンタイムなし)
deploymentCircuitBreaker.enabletrue失敗タスクが連続した場合にデプロイを自動停止
deploymentCircuitBreaker.rollbacktrue自動停止後に直前リビジョンへロールバック

デプロイ実行コマンド

# 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.ECSLinear10PercentEvery1Minutes1%/分 × 10 分段階的リリース
CodeDeployDefault.ECSLinear10PercentEvery3Minutes10%/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 UpdateBlue/GreenCanary
ロールバック操作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 サービス更新)

fig06: GHA CI/CD パイプライン全体図

ブランチ戦略

ブランチデプロイ先備考
mainprd (本番)PR 経由 merge のみ
developstg (ステージング)feature → develop PR
feature/*dev (開発)手動 dispatch or push で diff のみ

使用する主要 Action

Actionバージョン用途
actions/checkoutv4ソースコード取得
aws-actions/configure-aws-credentialsv4OIDC AWS 認証
kayac/ecspresso-actionv2ecspresso CLI 実行
hashicorp/setup-terraformv3Terraform CLI セットアップ
thollander/actions-comment-pull-requestv2PR コメント投稿

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_ARNarn:aws:iam::123456789012:role/github-oidc-roleOIDC 用 IAM ロール ARN
ECR_REPO123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/myappECR リポジトリ URI
CLUSTER_NAME_prdmyapp-cluster-prd本番クラスター名
CLUSTER_NAME_stgmyapp-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 変数
mainprd (本番)prd
developstg (検証)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 完全実装

QG-5: GHA デプロイワークフロー完全実装

  • PR 時: ecspresso diff → 差分レポートを PR コメントに自動投稿
  • merge 時: terraform applyecspresso 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
act 使用上の注意

  • 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
§3TF 基盤outputs.tf で Cluster ARN / TG ARN を公開し ecspresso から参照
§4CLI 一巡init → diff → deploy → rollback の 4 コマンド確認
§5jsonnetbase.libsonnet + env/{dev,prd}.libsonnet → 環境別 JSON 生成
§6デプロイ戦略無停止必須 → B/G / シンプル → Rolling / 段階的 → Canary
§7CI/CDPR=diff コメント / merge=TF apply+deploy / OIDC で認証
§8troubleshootecspresso 特有エラー 10 ケースと解決策

8-2. チートシート

カテゴリコマンド説明
Terraformterraform initプロバイダ初期化
Terraformterraform plan -var="environment=prd"変更プレビュー
Terraformterraform apply -auto-approveリソース作成/更新
Terraformterraform output cluster_arnoutput 値確認
ecspressoecspresso init --cluster X --service Y設定ファイル生成
ecspressoecspresso diffAWS との差分確認
ecspressoecspresso deployService/TaskDef 更新
ecspressoecspresso rollback直前リビジョンに戻す
ecspressoecspresso scale --tasks Nタスク数変更
ecspressoecspresso verify設定ファイル構文検証
jsonnetjsonnet -S ecs-service.jsonnetJSON 生成
jsonnetjsonnet -S -V ENV=prd ecs-service.jsonnet環境変数付き JSON 生成
GHAact pull_request -nPR ワークフロー dry-run
GHAact push --secret-file .secrets -npush ワークフロー 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.ymlcontainerName / 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 + FargateEB × VPCLattice 高度統合ECS 上位パターン
ecspresso 公式 GitHubkayac/ecspresso v2 本体バージョン確認・Issue

8-5. 次回予告

次回予告: ecspresso 深掘りシリーズ候補

  • 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 を確認する