API開発時、GitHub ActionsからECSへの自動デプロイプロセスを構築したので忘備録として記す。
(2024年6月29日に動作確認)

前提条件

  • ECSクラスター、サービス、タスク定義を作成済みであること
  • ECRリポジトリURIを取得済みであること

手順

GitHub Actionsでは大きく分けて以下のステップでワークフローを記述する。

  1. OpenId ConnectによるAWS連携
  2. AWSの一時クリデンシャルを取得する
  3. AWS ECRへログインする
  4. コンテナビルドを実行し、コンテナイメージをECRにPushする
  5. コンテナデプロイを実行し、ECSのタスク定義、サービスを更新する

1. OpenId ConnectによるAWS連携

まずは、OpenId ConnectによるAWS連携を行う必要がある。このフローを飛ばして先に進むことはできない。

OpenID Connectを使うと、長期的なアクセスキーやシークレットキーを使用する必要がなくなる。つまり、短期的なトークンを使用することで、セキュリティリスクを低減できるのだ。

GitHub ActionsとAWSを連携させるための設定

GitHubのOIDCトークンとAWSの一時クリデンシャルを交換するために必要なステップだ。

AWS CloudShellを使用して、以下のaws iam create-open-id-connect-providerコマンドを実行する。

$ aws iam create-open-id-connect-provider \
--url <https://token.actions.githubusercontent.com> \
--client-id-list sts.amazonaws.com \
--thumbprint-list 1234567890123456789012345678901234567890

このコマンドを実行することで、AWSはGitHub ActionsからのOIDCトークンを信頼し、そのトークンを使用してAWSリソースにアクセスすることができるようになる。これにより、GitHub ActionsのワークフローでAWSのリソース(例えばECRやECS)にアクセスするための一時的なクレデンシャルを取得できるようになる。

  • –url
  • –client-id-list
    • --client-id-listには、OIDCトークンを利用するシステムの識別子を設定する。この識別子は、AWSが受け入れるクライアントIDを指定するもので、GitHub Actionsの場合はsts.amazonaws.com を設定する。これにより、AWSがGitHub Actionsからのリクエストを正しく認識して処理できるようになる。
  • –thumbprint-list
    • –thumbprint-listには、OIDCプロバイダーのSSL証明書のフィンガープリント(拇印)を設定する。フィンガープリントは証明書のハッシュ値であり、AWSがOIDCプロバイダーの証明書の正当性を確認するために使用される。しかし、GitHubのOIDCプロバイダーの場合、検証はAWS側で行われる仕様になっているため40文字のダミー値を指定している。

Assume Roleポリシーを定義する

Assume Roleポリシーは、アクセス元(IAMの利用者は誰か)を定義するために使用する。この定義により、GitHub ActionsからAWSにアクセスしようとしているユーザーが、本当に信頼できるユーザーなのかを明確にできる。まずは、必要な変数を定義する。

# Assume Roleポリシーに必要なデータ
export GITHUB_REPOSITORY="${OWNER}/${REPOSITORY_NAME}"
export PROVIDER_URL=token.actions.githubusercontent.com
export AWS_ID=$(aws sts get-caller-identity --query Account --output text)
export ROLE_NAME=deploy-to-ecs-github-actions

次に、Assume Roleポリシーを定義したJSONファイルを作成する。Assume Roleポリシーを定義することで、特定のサービスやユーザー(ここではGitHub Actions)が、特定の条件下でAWSのIAMロールを一時的に引き受ける(つまり、そのロールの権限を一時的に借りる)ことができるようになる。

# assume_role_policy.jsonの作成
$ cat <<EOF > assume_role_policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Principal": {
        "Federated": "arn:aws:iam::${AWS_ID}:oidc-provider/${PROVIDER_URL}"
      },
      "Condition": {
        "StringLike": {
          "${PROVIDER_URL}:sub": "repo:${GITHUB_REPOSITORY}:*"
        }
      }
    }
  ]
}
EOF

次に、上記で定義したJSONファイルを使用してIAMロールを作成する。

# IAMロールの作成
$ aws iam create-role \
--role-name $ROLE_NAME \
--assume-role-policy-document file://assume_role_policy.json

新しく作成されたIAMロールは、指定されたAssume Roleポリシーに基づいて、GitHub Actionsがロールを引き受けることを許可する。このロールを使用して、GitHub Actionsは一時的なAWS認証情報を取得し、AWSリソースにアクセスすることができるようになる。

IAMポリシーのアタッチ

あと少しで完了する。

ここでは、ECRにDockerイメージをデプロイするためにAmazonEC2ContainerRegistryPowerUserというポリシーをアタッチしている。

# IAMポリシーのアタッチ
$ aws iam attach-role-policy \
--role-name $ROLE_NAME \
--policy-arn arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser

AmazonEC2ContainerRegistryPowerUserポリシーをIAMロールにアタッチすることで、GitHub ActionsのワークフローからECRに対する操作が可能になる。これにより、CI/CDパイプラインを通じて自動的にDockerイメージのビルド、プッシュ、デプロイが行える。

これにて、OpenId ConnectによるAWS連携が終了だ。ここから、ワークフローを実装していく。

ワークフローの全体像

# デプロイワークフロー

# 1. ソースコードのチェックアウト
# 2. AWSの一時クリデンシャルを取得
# 3. コンテナビルドアクションを実行し、コンテナイメージをPush
# 4. コンテナデプロイアクションを実行し、コンテナを入れ替え

name: Deploy
on:
  pull_request:
    branches:
      - main
    types:
      - closed
env:
  ROLE_ARN: arn:aws:iam::${{ secrets.AWS_ID }}:role/${{ secrets.ROLE_NAME }}
  SESSION_NAME: gh-oidc-${{ github.run_id }}-${{ github.run_attempt }}
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    steps:
      # ソースコードのチェックアウト
      - uses: actions/checkout@v4

      # 2. AWSの一時クリデンシャルを取得する
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ env.ROLE_ARN }}
          role-session-name: ${{ env.SESSION_NAME }}
          aws-region: ap-northeast-1

      # 3. AWS ECRへログインする
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      # 4. コンテナビルドアクションを実行し、ECRにPushする
      - name: Build, tag, and push image to Amazon ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          # ビルド
          docker build --platform linux/amd64 -t ${{ vars.ECR_REPOSITORY_URI }}:$IMAGE_TAG .
          # ECRにPUSH
          docker push ${{ vars.ECR_REPOSITORY_URI }}:$IMAGE_TAG

      # 5. コンテナデプロイアクションを実行し、ECSのタスク定義、サービスを更新する
      - name: Deploy to ECS
        uses: ./.github/actions/container-deploy/
        with:
          ecs-cluster: ${{ vars.ECS_CLUSTER_NAME }}
          ecs-service: ${{ vars.ECS_SERVICE_NAME }}
          task-definition: ${{ vars.TASK_DEFINITION_NAME }}
          container-name: ${{ vars.CONTAINER_NAME }}
          container-image: ${{ steps.build-image.outputs.image }}

※環境変数

以下の環境変数は、GitHub Variablesに保存する必要があるが、

ECS_CLUSTER_NAMEECS_SERVICE_NAMETASK_DEFINITION_NAMECONTAINER_NAME

それぞれ、次のコマンドで取得できる。取得したらVariablesとしてセットする。

# ECSクラスター名を取得
aws ecs list-clusters --output text \\
--query "clusterArns[?contains(@, '${APP_NAME}-${ENV_NAME}')]" \\
| cut -d/ -f2

# ECSサービス名を取得
aws ecs list-services --cluster "${ECSクラスター名}" --output text \\
--query "serviceArns[?contains(@, '${APP_NAME}-${ENV_NAME}')]" \\
| cut -d/ -f3

# タスク定義名を取得
aws ecs list-task-definitions --status ACTIVE --sort DESC --output text \\
--query "taskDefinitionArns[?contains(@, '${APP_NAME}-${ENV_NAME}')]" \\
| cut -d/ -f2 | cut -d: -f1

# ECRリポジトリ名を取得
aws ecr describe-repositories --output text \\
--query "repositories[?contains(repositoryUri, '$APP_NAME')].repositoryUri"

# コンテナ名を取得
# 以下のコマンドを実行し taskDefinition > containerDefinitions > nameを参照
aws ecs describe-task-definition --task-definition ${取得したタスク定義名}

secrets.AWS_ID = AWSアカウントID secrets.ROLE_NAMEについては、上記で作成したロール名(deploy-to-ecs-github-actions)を環境変数としてGitHub Secretsにセットする。

2. AWSの一時クリデンシャルを取得

以下のワークフローで、AWSの一時クリデンシャルの取得を実行している。

env:
  ROLE_ARN: arn:aws:iam::${{ secrets.AWS_ID }}:role/${{ secrets.ROLE_NAME }}
  SESSION_NAME: gh-oidc-${{ github.run_id }}-${{ github.run_attempt }}
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    steps:
      # ソースコードのチェックアウト
      - uses: actions/checkout@v4

      # 1. AWSの一時クリデンシャルを取得する
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ env.ROLE_ARN }}
          role-session-name: ${{ env.SESSION_NAME }}
          aws-region: ap-northeast-1

3. AWS ECRへログインする

OpenId ConnectによるAWS連携が正常に行えている場合、以下のスクリプトでECRにログインできる。

# 3. AWS ECRへログインする
- name: Login to Amazon ECR
  id: login-ecr
  uses: aws-actions/amazon-ecr-login@v2

4. コンテナビルドを実行し、コンテナイメージをECRにPushする

# 4. コンテナビルドアクションを実行し、ECRにPushする
- name: Build, tag, and push image to Amazon ECR
  id: build-image
  env:
    ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
    IMAGE_TAG: ${{ github.sha }}
  run: |
    # ビルド
    docker build --platform linux/amd64 -t ${{ vars.ECR_REPOSITORY_URI }}:$IMAGE_TAG .
    # ECRにPUSH
    docker push ${{ vars.ECR_REPOSITORY_URI }}:$IMAGE_TAG

5. コンテナデプロイを実行し、ECSのタスク定義、サービスを更新する

こちらは、モジュール化したため別ファイルで定義している。

./.github/actions/container-deploy/action.yml

# コンテナデプロイアクション

# 1. 現在のタスク定義を取得
# 2. タスク定義のimage部分を新しいコンテナイメージに書き換え
# 3. 新しいタスク定義でECSサービスを更新し、コンテナを入れ替え

name: Container Deploy
description: ECSサービスを更新し、コンテナをデプロイする。
inputs:
  ecs-cluster:
    required: true
    description: ECSクラスター
  ecs-service:
    required: true
    description: ECSサービス
  task-definition:
    required: true
    description: タスク定義
  container-name:
    required: true
    description: コンテナ名
  container-image:
    required: true
    description: コンテナイメージ
runs:
  using: composite
  steps:
    # 1. 現在のタスク定義を取得
    - run: | # 次のステップで使用するため、取得したタスク定義をファイルへ保存する
        aws ecs describe-task-definition --task-definition "${TASK_DEFINITION}" \\
        --query taskDefinition --output json > "${RUNNER_TEMP}/task-def.json"
      env:
        TASK_DEFINITION: ${{ inputs.task-definition }}
      shell: bash

    # 2. タスク定義のimage部分を新しいコンテナイメージに書き換え
    - name: Fill in the new image ID in the Amazon ECS task definition
      id: task-def
      uses: aws-actions/amazon-ecs-render-task-definition@v1
      with:
        task-definition: ${{ runner.temp }}/task-def.json
        container-name: ${{ inputs.container-name }}
        image: ${{ inputs.container-image }}

    # 3. ECSサービスの更新
    - name: Deploy Amazon ECS task definition
      uses: aws-actions/amazon-ecs-deploy-task-definition@v1
      with:
        cluster: ${{ inputs.ecs-cluster }}
        service: ${{ inputs.ecs-service }}
        task-definition: ${{ steps.task-def.outputs.task-definition }}

カテゴリー: aws