すべてをTerraformで管理しない。ecspressoを組み合わせたECS運用構成の検証
はじめに
インフラとアプリケーションの開発・運用で、チームや変更サイクルの違いによる「管理の境界線」に悩んでいませんか?
サービスが成長し、インフラチームとアプリケーションチームが分かれると、インフラは変更頻度が低い一方で、アプリは変更頻度が高いため、IaC(Infrastructure as Code)管理の線引きが曖昧になりがちです。
本記事では、この課題を解決するため「すべてを Terraform で管理しない」構成を検証してみました。
具体的には、静的なインフラ基盤を Terraform で管理し、変更頻度の高いECSサービスやタスク定義は、GitHub Actions と ecspresso で管理する CI/CD 構成を紹介します。
ecspressoとは、AWS ECS デプロイに特化した OSS ツールで、YAML等を用いたタスク定義の管理やロールバック機能を備えています。
デプロイ前に差分を確認するdiffや設定検証ができるため、安全なCI/CDパイプラインを構築できます。
システム構成
検証で作成したシステム構成は、主に以下のようになります。
システム構成図
Terraformとecspressoの役割分担
インフラとアプリの境界を明確にするため「すべてを Terraform で管理しない」のが今回のポイントです。
Terraform
VPC、ALB、ECR、ECSクラスター、IAMロールなどを管理
一度構築するとあまり変更が入らない「静的な基盤」
ecspresso
ECS サービス、ECS タスク定義を管理
高速に試行錯誤したい「アプリケーションの一部」
今回作成したコードは1つのリポジトリで管理していましたが、下記のようにディレクトリを分けてインフラとアプリの境界を明確にしました。
.
├── README.md
├── backend
│ ├── Dockerfile
│ ├── ecs
│ │ ├── config.yaml
│ │ ├── ecs-service-def.json
│ │ └── ecs-task-def.json
│ ├── go.mod
│ └── main.go
└── terraform
├── main.tf
├── modules
│ ├── ecr
│ │ └── ecr.tf
│ ├── ecs
│ │ ├── ecs.tf
│ │ └── variables.tf
│ ├── network
│ │ ├── network.tf
│ │ └── variables.tf
│ └── security
│ ├── iam.tf
│ ├── security_group.tf
│ └── variables.tf
└── provider.tf構築の流れ
1. Terraform によるインフラ構築
まずは Terraform で必要なインフラ構築を行います。 Terraform で構築する主なリソースは下記になります。
インフラ基盤・ネットワーク
VPC, Subnet, Internet Gateway / NAT Gateway, Route Table
コンテナ実行基盤
ECR, ECS Cluster, ALB, Target Group
セキュリティ・権限管理
IAM Role, Security Group
今回の検証で特にポイントと感じたものは2点あります。
① コンテナ実行基盤としては ECS クラスターやALBに留める
先述したように、ECS サービスや ECSタスク定義は変更頻度が高いものであり、またアプリケーションのデプロイサイクルと関連付けられるもののため、Terraform ではなく ecspresso での管理とします。
② CI/CD 実現のためのIAMロールを作成する
GitHub Actions による CI/CD を実現するために必要なIAMロールを作成します。
IAMロールに紐づけるポリシーとしては3つあります。
GitHub から AWS を操作するための OIDC のポリシー
ECR へビルドしたイメージを push するためのポリシー
ECS サービスやタスク定義を更新するためのポリシー
今回は GitHub Actions から AWS を操作するにあたって OIDC を利用します。
OIDC を利用することで、アクセスキー(シークレット)を GitHub 側に直接保存・管理しなくて済むため、漏洩リスクが減りよりセキュリティの高い運用が可能になります。
OIDC のポリシーではセキュリティ上、「特定のリポジトリ・ブランチ」に対してのみに絞り込むのが重要です。
また別途 GitHub OIDC プロバイダーを IAM に追加する必要がありますが、検証したAWSアカウントでは既に追加済みでした。
OIDC プロバイダーを IAM に追加する際は下記を参考にしてみてください。
これらのポリシー及びロールについては、具体的には下記のようなコードで作成しました。
# iam.tfの一部
data "aws_iam_policy_document" "github_actions_assume_role" {
statement {
actions = ["sts:AssumeRoleWithWebIdentity"]
effect = "Allow"
principals {
type = "Federated"
identifiers = ["arn:aws:iam::AWSアカウントID:oidc-provider/token.actions.githubusercontent.com"]
}
# 宛先(Audience)のチェック
condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:aud"
values = ["sts.amazonaws.com"]
}
# ★ここで「特定のリポジトリ・ブランチ」に絞り込む
condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:sub"
values = ["repo:GitHubアカウント名/リポジトリ名:ref:refs/heads/main"]
}
}
}
resource "aws_iam_role" "github_actions" {
name = "${var.app_name}-github-actions-deploy-role"
assume_role_policy = data.aws_iam_policy_document.github_actions_assume_role.json
}
data "aws_iam_policy_document" "ecr_push_limited" {
statement {
actions = ["ecr:GetAuthorizationToken"]
resources = ["*"]
}
statement {
actions = [
"ecr:CompleteLayerUpload",
"ecr:UploadLayerPart",
"ecr:InitiateLayerUpload",
"ecr:BatchCheckLayerAvailability",
"ecr:PutImage",
"ecr:BatchGetImage"
]
resources = [var.ecr_repository_arn]
}
}
resource "aws_iam_policy" "ecr_push_policy" {
name = "${var.app_name}-ecr-push-policy"
policy = data.aws_iam_policy_document.ecr_push_limited.json
}
resource "aws_iam_role_policy_attachment" "attach_ecr_push" {
role = aws_iam_role.github_actions.name
policy_arn = aws_iam_policy.ecr_push_policy.arn
}
data "aws_iam_policy_document" "ecspresso_deploy" {
statement {
effect = "Allow"
actions = [
"ecs:RegisterTaskDefinition",
"ecs:UpdateService",
"ecs:DescribeServices",
"ecs:DescribeTaskDefinition",
"ecs:DescribeTasks",
"ecs:ListTasks",
"ecs:TagResource",
"ecs:DescribeServiceDeployments"
]
resources = ["*"]
}
statement {
effect = "Allow"
actions = [
"application-autoscaling:DescribeScalableTargets",
"application-autoscaling:DescribeScalingPolicies"
]
resources = ["*"]
}
statement {
effect = "Allow"
actions = [
"s3:GetObject",
"s3:ListBucket"
]
resources = [
"arn:aws:s3:::バケット名",
"arn:aws:s3:::バケット名/*"
]
}
statement {
effect = "Allow"
actions = ["iam:PassRole"]
resources = [
aws_iam_role.ecs_execution_role.arn,
aws_iam_role.ecs_task_role.arn
]
condition {
test = "StringEquals"
variable = "iam:PassedToService"
values = ["ecs-tasks.amazonaws.com"]
}
}
}
resource "aws_iam_policy" "ecspresso_deploy_policy" {
name = "${var.app_name}-ecspresso-deploy-policy"
policy = data.aws_iam_policy_document.ecspresso_deploy.json
}
resource "aws_iam_role_policy_attachment" "attach_ecspresso_deploy" {
role = aws_iam_role.github_actions.name
policy_arn = aws_iam_policy.ecspresso_deploy_policy.arn
}
2. GitHub Actions によるCI/CD パイプライン構築
続いて GitHub Actions による CI/CD パイプライン構築を行います。
まずは、GitHub Actions からの AWS 操作を行うため、対象となるリポジトリの設定から Secrets として作成した IAM ロール の ARN を登録します。
GitHub Secrets設定画面のスクリーンショット
次に workflow の定義を行います。
今回は「イメージのビルドとプッシュ」から「 ecspresso による ECS サービス/タスク定義の更新」まで1つの workflow としています。
# deploy.yaml
name: Deploy to AWS ECS
on:
push:
branches: [ "main" ]
paths:
- 'backend/'
- '.github/workflows/'
env:
AWS_REGION: ap-northeast-1
ECR_REPOSITORY: ECRリポジトリ名
ECS_CLUSTER: ECSクラスター名
ECS_SERVICE: ECSサービス名
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ${{ env.AWS_REGION }}
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Build, tag, and push image to Amazon ECR
id: build-image
working-directory: ./backend
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
- name: Install ecspresso
uses: kayac/ecspresso@v2
with:
version: v2.7.0
- name: Deploy to Amazon ECS
working-directory: ./backend/ecs
env:
IMAGE_TAG: ${{ steps.build-image.outputs.image_tag }}
run: |
ecspresso deploy --config config.yaml
3. 動作確認
最後に動作確認です。
アプリケーションコードを修正しプッシュすると GitHub Actions のワークフローが実行され、
イメージのビルド
ECR へイメージをプッシュ
ecspresso によるデプロイ
が動き、更新後のイメージでECSにデプロイされていることを確認できました。
図3-1: GitHub Actionsのワークフローが正常終了した様子
図3-2: ECRに新しいタグ(Gitのコミットハッシュ)でイメージがプッシュされた状態
図3-3: ECS サービスに新しいイメージでコンテナが起動した様子
まとめと感想
上記のような流れでインフラ構築を Terraform で行いつつ、アプリの CI/CD は GitHub Actions で ecspresso を動かすことができ、変更サイクルの異なるリソースの管理を分離することができました。
これまであまりコンテナベースのアプリケーションのCI/CDを組んだことがなかったので、「どういう流れになるのか」「どういったリソースが必要になるのか」など、学びが多い検証になったなと思います。
また初めて ecspresso を調べて触ってみたのですが、体験としてはとても良いものでした。
ECSを利用していてリソース管理にやりづらさを感じている方の参考になれば幸いです。
この記事を書いた人
What is BEMA!?
Be Engineer, More Agile


