CircleCI から GitHub Actions を実行して ECS (Fargate) にデプロイしてみる

CircleCI から GitHub Actions を実行して ECS (Fargate) にデプロイしてみる

Clock Icon2025.03.22

はじめに

デプロイパイプラインを構築する際、タイトルのようにCIツールを敢えて複数組み合わせる機会は少ないとは思います。

しかし、実際のプロジェクトでは既存の CircleCI パイプラインが存在する、CircleCIの特定機能に現状依存している、段階的な移行が必要などといった理由から、このような状況に直面することがあると思います。

このような背景から、本稿では CircleCIとGitHub Actionsとを使ったデプロイを検証しました。

やりたいこと

  • GitHubリポジトリA でアプリ更新、CircleCI でDockerイメージをビルドしてECRにプッシュ
  • GitHubリポジトリT の GitHub Actions を使ってECSのタスク定義を更新
  • GitHubリポジトリT の GitHub Actions でTerraformを使用してECSにデプロイ

なお、本稿では、PRの前にデプロイしてしまっていますが、環境に応じて plan のみにするなどの調整をすると良いと思います。

イメージ

環境

  • node v22.14.0
  • npm 10.9.2
  • Terraform v1.11.2

前提

GitHubの設定

  • アプリケーションリポジトリ(as Aリポジトリ)
  • Terraformリポジトリ (as Tリポジトリ)
  • GitHub Actions のOIDC設定
  • 環境変数の設定

AWSの設定

  • ECRリポジトリの作成
  • ECSクラスターの作成

CircleCIの設定

  • プロジェクト・パイプラインの設定
  • CircleCI のOIDC設定
  • context/環境変数の設定
    • AWS_ACCOUNT_ID
    • AWS_REGION
    • AWS_ROLE_ARN
    • ECR_REPOSITORY_NAME
    • GITHUB_TOKEN (Fine-grained personal access token)
    • GITHUB_URL

circle_env

必要な設定

Aリポジトリ

アプリコードは、本稿の目的外なので省略します。(シンプルな Node Express です。)

.circleci/config.yml

version: 2.1

orbs:
  node: circleci/node@7.1.0
  aws-cli: circleci/aws-cli@5.2.0

jobs:
  test-node:
    executor: node/default
    steps:
      - checkout
      - node/install-packages:
          pkg-manager: npm
      - run:
          name: Run tests
          command: echo "No test specified in package.json"

  build-and-push-image:
    machine:
      image: ubuntu-2204:current
    steps:
      - checkout
      - aws-cli/setup:
           role_arn: ${AWS_ROLE_ARN}
           region: ${AWS_REGION}
      - run:
          name: Set image tag
          command: |
            echo "export IMAGE_TAG=${CIRCLE_SHA1}-$(date +%Y%m%d-%H%M%S)" >> $BASH_ENV
            source $BASH_ENV
      - run:
          name: Login to Amazon ECR
          command: aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com
      - run:
          name: Build and push Docker image
          command: |
            docker build -t ${ECR_REPOSITORY_NAME}:${IMAGE_TAG} .
            docker tag ${ECR_REPOSITORY_NAME}:${IMAGE_TAG} ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPOSITORY_NAME}:${IMAGE_TAG}
            docker push ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPOSITORY_NAME}:${IMAGE_TAG}
      - run:
          name: "Execute GitHub Actions"
          command: |
            curl -X POST \
              -H "Accept: application/vnd.github+json" \
              -H "Authorization: Bearer ${GITHUB_TOKEN}" \
              -H "X-GitHub-Api-Version: 2022-11-28" \
              ${GITHUB_URL} \
              -d "{
                \"event_type\": \"update-ecs-image\",
                \"client_payload\": {
                  \"image_tag\": \"${IMAGE_TAG}\",
                  \"sha\": \"${CIRCLE_SHA1}\"
                }
              }"

workflows:
  build-test-deploy:
    jobs:
      - test-node
      - build-and-push-image:
          requires:
            - test-node
          context: aws

ここでの肝は、 create a repository dispatch event のAPIを叩くことだけです。

下記例に沿って実行します。

curl -L \
  -X POST \
  -H "Accept: application/vnd.github+json" \
  -H "Authorization: Bearer <YOUR-TOKEN>" \
  -H "X-GitHub-Api-Version: 2022-11-28" \
  https://api.github.com/repos/OWNER/REPO/dispatches \
  -d '{"event_type":"on-demand-test","client_payload":{"unit":false,"integration":true}}'

Tリポジトリ

ECSを構築するTFのコードは、本稿の目的外なので関連箇所以外は省略します。

locals.tf

locals {
  container_name = "app"
  container_port = 3000
  image_tag     = "hogehoge"
  ecr_repository_name = "sample-express-app"
}

task_definition.json

[
    {
      "name": "${container_name}",
      "image": "${account_id}.dkr.ecr.${region}.amazonaws.com/${ecr_repository_name}:${image_tag}",
      "portMappings": [
        {
          "containerPort": ${container_port},
          "protocol": "tcp"
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/app",
          "awslogs-region": "${region}",
          "awslogs-stream-prefix": "ecs"
        }
      }
    }
  ]

.github/workflows/update-ecs.yml

name: "Update ECS Task Definition"
on:
  repository_dispatch:
    types: [update-ecs-image]
env:
  IMAGE_TAG: ${{ github.event.client_payload.image_tag }}
  SHA: ${{ github.event.client_payload.sha }}
  BRANCH_NAME: feature/update_ecs_image_${{ github.event.client_payload.sha }}

jobs:
  update-ecs:
    name: "Dispatch"
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: write
      pull-requests: write

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          ref: main

      - name: Update Image Tag in locals.tf
        run: |
          # locals.tfのパスを指定
          LOCALS_FILE="${GITHUB_WORKSPACE}/locals.tf"

          # image_tagの値を更新
          sed -i "s/image_tag *= *\".*\"/image_tag = \"$IMAGE_TAG\"/" "$LOCALS_FILE"

          echo "=== After Update ==="
          cat "$LOCALS_FILE"

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-region: "ap-northeast-1"
          role-to-assume: "arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-for-ecs-deploy-role"

      - name: Terraform Init
        run: terraform init

      - name: Terraform Plan
        run: terraform plan -out=tfplan

	  # 環境に応じてplanのみにするなど検討
      - name: Terraform Apply
        run: terraform apply tfplan

      - name: Create Pull Request
        uses: peter-evans/create-pull-request@v7
        with:
          title: "Update ECS Task"
          commit-message: Update ECS Task Definition Image Tag
          body: |
            Updates the ECS task definition with new image tag:
            - Tag: ${{ env.IMAGE_TAG }}
            - Auto-generated by [create-pull-request][1]

            [1]: https://github.com/peter-evans/create-pull-request
          branch: ${{ env.BRANCH_NAME }}
          base: main
          labels: |
            automated pr
            ecs-update

Webhook でリクエストされたクライアントペイロードを受け取って GitHub Actions のworkflow を実行します。

下記例に沿って記述します。

on:
  repository_dispatch:
    types: [test_result]

jobs:
  run_if_failure:
    if: ${{ !github.event.client_payload.passed }}
    runs-on: ubuntu-latest
    steps:
      - env:
          MESSAGE: ${{ github.event.client_payload.message }}
        run: echo $MESSAGE

本稿では、PRを出すのに、Create Pull Request Actions を利用しました。

実行してみる

CircleCI Pipeline

circli_ci2

GHA workflows

gha_workflow

GitHub PR

pr

ECS タスク

ecs

複数環境におけるデプロイフローの例

続けて、下図のようなデプロイフローで対応してみます。

以下のような要件とします。

  • アプリの main ブランチへの merge は terraform apply でデプロイされる
    • インフラリポジトリの main から派生されたブランチは自動で merge される
  • アプリのタグリリースでは terraform plan される
    • 指定したインフラリポジトリのブランチから派生されたブランチで自動 PR できる

① 〜 ⑨ は簡単な流れの順を示しています。

スクリーンショット 2025-03-29 19.54.23

(アプリ) CircleCIのTrigger

タグリリースで発火できるように Trigger 設定を追加します。

circleci_trigger

また、Why do I see "No workflow" when I push a commit or a tag – CircleCI Support Center で記載されている通り、 config.yml にも tag filter を追記します。

Terraform で apply するか plan するかの判別のために、以下のようにタグプッシュか main ブランチへのマージかの is_deploy 、およびインフラで利用するブランチ branch をペイロードに追加させます。

...省略
            if [[ -n "${CIRCLE_TAG}" ]]; then
              echo "export IS_DEPLOY=false" >> $BASH_ENV
            else
              echo "export IS_DEPLOY=true" >> $BASH_ENV
            fi

            source $BASH_ENV
 ...省略
            curl -X POST \
              -H "Accept: application/vnd.github+json" \
              -H "Authorization: Bearer ${GITHUB_TOKEN}" \
              -H "X-GitHub-Api-Version: 2022-11-28" \
              ${GITHUB_URL} \
              -d "{
                \"event_type\": \"update-ecs-image\",
                \"client_payload\": {
                  \"image_tag\": \"${IMAGE_TAG}\",
                  \"sha\": \"${CIRCLE_SHA1}\",
                  \"branch\": \"${INFRA_TARGET_BRANCH}\",
                  \"is_deploy\": \"${IS_DEPLOY}\"
                }
              }"

上記を踏まえて .circleci/config.yml は以下のようになりました。

version: 2.1

orbs:
  node: circleci/node@7.1.0
  aws-cli: circleci/aws-cli@5.2.0

jobs:
  test-node:
    executor: node/default
    steps:
      - checkout
      - node/install-packages:
          pkg-manager: npm
      - run:
          name: Run tests
          command: echo "No test specified in package.json"

  build-and-push-image:
    machine:
      image: ubuntu-2204:current
    steps:
      - checkout
      - aws-cli/setup:
          role_arn: ${AWS_ROLE_ARN}
          region: ${AWS_REGION}
      - run:
          name: Set image tag
          command: |
            echo "export IMAGE_TAG=${CIRCLE_SHA1}-$(date +%Y%m%d-%H%M%S)" >> $BASH_ENV

            # タグプッシュかmainブランチかでDEPLOYかどうか判断
            if [[ -n "${CIRCLE_TAG}" ]]; then
              echo "export IS_DEPLOY=false" >> $BASH_ENV
            else
              echo "export IS_DEPLOY=true" >> $BASH_ENV
            fi

            source $BASH_ENV

      - run:
          name: Login to Amazon ECR
          command: aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com
      - run:
          name: Build and push Docker image
          command: |
            docker build -t ${ECR_REPOSITORY_NAME}:${IMAGE_TAG} .
            docker tag ${ECR_REPOSITORY_NAME}:${IMAGE_TAG} ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPOSITORY_NAME}:${IMAGE_TAG}
            docker push ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${ECR_REPOSITORY_NAME}:${IMAGE_TAG}
      - run:
          name: "Execute GitHub Actions"
          environment:
            # default main ブランチ
            INFRA_TARGET_BRANCH: "main"
          command: |
            curl -X POST \
              -H "Accept: application/vnd.github+json" \
              -H "Authorization: Bearer ${GITHUB_TOKEN}" \
              -H "X-GitHub-Api-Version: 2022-11-28" \
              ${GITHUB_URL} \
              -d "{
                \"event_type\": \"update-ecs-image\",
                \"client_payload\": {
                  \"image_tag\": \"${IMAGE_TAG}\",
                  \"sha\": \"${CIRCLE_SHA1}\",
                  \"branch\": \"${INFRA_TARGET_BRANCH}\",
                  \"is_deploy\": \"${IS_DEPLOY}\"
                }
              }"

workflows:
  build-test-deploy:
    jobs:
      - test-node:
          filters:
            tags:
              only: /.*/
      - build-and-push-image:
          requires:
            - test-node
          context: aws
          filters:
            tags:
              only: /.*/

(インフラ) 自動マージについて

peter-evans/create-pull-request を利用しているときの注意点として GitHub Actionsで作成したプルリクエストが他のActionsをトリガーしない(on:pull_request) というものがありました。

Authenticating with GitHub App generated tokens
紹介されている通り、GitHub Apps で対応します。

1

APP_IDAPP_PRIVATE_KEY は GitHub Actions の secret に設定します。

2

なお、 GitHub Apps と secrets.GITHUB_TOKEN とを併用することによって、同じユーザー(GITHUB_TOKEN を含む)が作成した PR を自身で approve することを防ぐ Pull request authors can’t approve their own pull request を避けることができます。

上記を踏まえて、インフラの GHA は以下のような構成になりました。

.github
├── actions
│   └── terraform-execute
│       └── action.yml
└── workflows
    ├── branches-for-env.yml
    ├── fire-update-ecs-workflow.yml
    └── update-common-process.yml

それぞれの役割については箇条書きで説明しておきます。

.github/workflows/fire-update-ecs-workflow.yml

  • エントリーポイントとなるワークフロー
  • CircleCIからのrepository_dispatchイベントを受け取る
  • is_deployの値に応じて、デプロイ(true)または計画(false)を分岐
  • 必要な権限(permissions)を設定
  • branches-for-env.yml を呼び出す
name: "Fire Update ECS Workflow"
on:
  repository_dispatch:
    types: [update-ecs-image]

permissions:
  id-token: write
  contents: write
  pull-requests: write

jobs:
  deploy:
    if: github.event.client_payload.is_deploy == 'true'
    uses: ./.github/workflows/branches-for-env.yml
    with:
      is_deploy: true
    secrets: inherit

  plan:
    if: github.event.client_payload.is_deploy == 'false'
    uses: ./.github/workflows/branches-for-env.yml
    with:
      is_deploy: false
    secrets: inherit

.github/workflows/branches-for-env.yml

  • 環境(dev/stg/prd)ごとの処理を分岐
  • is_deploy に基づいて実行環境を決定:
    • true: devのみ実行
    • false: stgとprdを実行(prdはstg完了後)
  • 各環境のAWSアカウントIDを variables から渡す
  • update-common-process.yml を呼び出す
name: "Branches For Env"
on:
  workflow_call:
    inputs:
      is_deploy:
        required: true
        type: boolean

jobs:
  dev:
    if: inputs.is_deploy
    uses: ./.github/workflows/update-common-process.yml
    with:
      aws_account_id: ${{ vars.AWS_DEV_ACCOUNT_ID }}
      is_deploy: true
    secrets: inherit

  stg:
    if: "!inputs.is_deploy"
    uses: ./.github/workflows/update-common-process.yml
    with:
      aws_account_id: ${{ vars.AWS_STG_ACCOUNT_ID }}
      is_deploy: false
    secrets: inherit

  prd:
    if: "!inputs.is_deploy"
    needs: stg
    uses: ./.github/workflows/update-common-process.yml
    with:
      aws_account_id: ${{ vars.AWS_PRD_ACCOUNT_ID }}
      is_deploy: false
    secrets: inherit

.github/workflows/update-common-process.yml

  • ECS タスク更新処理を実行
  • GitHub App トークンの生成
  • イメージタグの更新
  • Terraform 実行(terraform-execute action を使用)
  • GitHub App トークンによる自動 PR 作成
  • GitHub トークンによる自動マージ(dev環境の場合)
  • 環境変数の設定と管理
name: "ECS Common Process"
on:
  workflow_call:
    inputs:
      aws_account_id:
        required: true
        type: string
      is_deploy:
        required: true
        type: boolean
    secrets:
      APP_ID:
        required: true
      APP_PRIVATE_KEY:
        required: true

env:
  IMAGE_TAG: ${{ github.event.client_payload.image_tag }}
  SHA: ${{ github.event.client_payload.sha }}
  BRANCH_NAME: feature/update_ecs_image_${{ github.event.client_payload.sha }}
  TARGET_BRANCH: ${{ github.event.client_payload.branch }}

jobs:
  update:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/create-github-app-token@v1
        id: generate-token
        with:
          app-id: ${{ secrets.APP_ID }}
          private-key: ${{ secrets.APP_PRIVATE_KEY }}

      - name: Checkout
        uses: actions/checkout@v4
        with:
          ref: ${{ env.TARGET_BRANCH }}

      - name: Update Image Tag in locals.tf
        run: |
          LOCALS_FILE="${GITHUB_WORKSPACE}/locals.tf"
          sed -i "s/image_tag *= *\".*\"/image_tag = \"${{ env.IMAGE_TAG }}\"/" "$LOCALS_FILE"
          echo "=== After Update ==="
          cat "$LOCALS_FILE"

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3

      - name: Execute Terraform
        uses: ./.github/actions/terraform-execute
        with:
          aws_account_id: ${{ inputs.aws_account_id }}
          is_deploy: ${{ inputs.is_deploy }}

      - name: Create Pull Request
        id: cpr
        uses: peter-evans/create-pull-request@v7
        with:
          token: ${{ steps.generate-token.outputs.token }}
          title: "Update ECS Task"
          body: |
            Updates the ECS task definition with new image tag:
            - Tag: ${{ env.IMAGE_TAG }}
            - Auto-generated by [create-pull-request][1]

            [1]: https://github.com/peter-evans/create-pull-request
          branch: ${{ env.BRANCH_NAME }}
          base: ${{ env.TARGET_BRANCH }}
          labels: |
            automated pr
            ecs-update

      - name: Auto Approve and Merge PR
        if: inputs.is_deploy && steps.cpr.outputs.pull-request-number
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          echo "Processing PR #${{ steps.cpr.outputs.pull-request-number }}"

           # PRを承認(PR番号を指定)
          gh pr review ${{ steps.cpr.outputs.pull-request-number }} --approve

          # PRをマージ(PR番号を指定)
          gh pr merge ${{ steps.cpr.outputs.pull-request-number }} --merge --auto --delete-branch

.github/actions/terraform-execute/action.yml

  • Terraform の共通処理をカスタムアクションとして実装
name: "Terraform Execute"
description: "Execute Terraform commands"
inputs:
  aws_account_id:
    description: "AWS Account ID for assuming role"
    required: true
  is_deploy:
    description: "Whether to deploy or not"
    required: true

runs:
  using: "composite"
  steps:
    - name: Configure AWS Credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-region: "ap-northeast-1"
        role-to-assume: "arn:aws:iam::${{ inputs.aws_account_id }}:role/github-actions-for-ecs-deploy-role"

    - name: Terraform Init
      shell: bash
      run: terraform init

    - name: Terraform Plan
      shell: bash
      run: terraform plan -out=tfplan

    - name: Terraform Apply
      if: inputs.is_deploy == 'true'
      shell: bash
      run: terraform apply tfplan

さいごに

本記事では、CircleCI と GitHub Actions を組み合わせたECS (Fargate) へのデプロイパイプラインを構築してみました。

なお、ロールバックについては検討の上、準備しておく必要があると考えます。

完全自動のロールバックなのか、タグベースの手動ロールバックなのか、チームの開発事情に応じた適切な方法を選択して、明確な手順とフローを共有しておくことが重要だと考えます。

参考

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.