BEMAロゴ

エンジニアの
成長を支援する
技術メディア

すべてを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を利用していてリソース管理にやりづらさを感じている方の参考になれば幸いです。

この記事が役に立ったと思ったら、
ぜひ「いいね」とシェアをお願いします!

リンクをコピーXでシェアするfacebookでシェアする

この記事を書いた人

ykatsuno
ykatsuno
2020年に大手SIerに新卒入社。AI精度改善やWebアプリ開発/運用などに従事。2023年にメンバーズに入社後はクライアント先でSREとしてプロダクトの信頼性向上に向けた取り組みを行っている。
詳しく見る
ページトップへ戻る