【GitHub Actions】AWS 公式のアクションを使わないで Fargate の B/G Deploy 機構を用意してみる

こんにちは。お久しぶりです。
今年の春に受験した IPA のネットワークスペシャリスト試験に合格しているのを確認し、ホっと胸をなで下ろしている今日この頃です。

さて、ちょうど AWS が Fargate ネイティブの Blue/Green デプロイを公式サポートしたところではありますが、普段 CodeDeploy と AWS 公式の GitHub Actions を使って組んでいるデプロイ機構に関して、あえて公式のアクション ( amazon-ecs-deploy-task-definition 等 ) を使わずに実装してみようと思います。
※ シェルと CLI で頑張る?という謎な縛りで実装する備忘録になります。いったい何の役に立つのやら。

構成

以下の図のように GitHub Actions でコンテナイメージのビルド、及び ECR への PUSH と、タスク定義登録、CodeDeploy のキックを行います。
今回は主に GitHub Actions の WorkFlow の処理に着目してみます。

詳細な処理のシーケンスは以下の通りです。
① ビルドしたコンテナイメージを ECR に PUSH
② タスク定義を編集し、新規登録
③ AppSpec の内容を更新し、CodeDeploy を起動
④ Green のターゲットグループ内で新イメージを元に ECS タスクが起動
⑤ ALB からの同線を Green に切り替え

Actions ( steps )

⓪ 事前処理

リポジトリのコードをチェックアウトし、IAM Role から一時的な認証情報を取得します。
ECR へのログインも済ませておきます。
※ ココだけ AWS 公式のアクションを使います。( 早速のレギュ違反 )

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:

      - name: Checkout
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4.0.2
        with:
          role-to-assume: ${{ inputs.role-arn }}
          aws-region: ${{ inputs.region }}

      - name: Login to Amazon ECR
        uses: aws-actions/amazon-ecr-login@v2.0.1

① ビルドしたコンテナイメージを ECR に PUSH

Docker のコンテナイメージをビルドし、ビルドしたイメージへタグを付与、そして ECR へ PUSH します。

      - name: Build and push the image
        env:
          IMAGE_URI: ${{ inputs.image-uri }}
          IMAGE_TAG: ${{ inputs.github-sha }}
        run: |
          docker build -f ./Dockerfile -t src_image .
          docker tag src_image ${IMAGE_URI}:${IMAGE_TAG}
          docker push ${IMAGE_URI}:${IMAGE_TAG}

※ タグ ( IMAGE_TAG ) には コミット SHA を使用する想定です。

on:
  workflow_call:
    inputs:
      github-sha:
        required: false
        default: ${{ github.sha }}
        type: string

② タスク定義を編集し、新規登録

① で PUSH したイメージでタスクが起動するように、タスク定義の内容を更新し、AWS へ登録します。
ここで登録したタスク定義の ARN は後続の処理で使う想定です。

      - name: Edit taskdefinition
        env:
          IMAGE_URI: ${{ inputs.image-uri }}
          IMAGE_TAG: ${{ inputs.github-sha }}
          TASK_DEF_FILE: ${{ inputs.task-def-file }}
        run: |
          sed -i "s#<IMAGE_TAG>#${IMAGE_TAG}#g" ${TASK_DEF_FILE}
          sed -i "s#<IMAGE_URI>#${IMAGE_URI}#g" ${TASK_DEF_FILE}
          cat ${TASK_DEF_FILE}

      - name: Registe taskdefinition
        id: register-task
        env:
          IMAGE_URI: ${{ inputs.image-uri }}
          IMAGE_TAG: ${{ inputs.github-sha }}
          TASK_DEF_FILE: ${{ inputs.task-def-file }}
        run: |
          TASK_DEF_ARN=$(aws ecs register-task-definition \
            --cli-input-json file://${TASK_DEF_FILE} \
            --query 'taskDefinition.taskDefinitionArn' --output text)
          echo "task_definition_arn=${TASK_DEF_ARN}" >> $GITHUB_OUTPUT

③ AppSpec の内容を更新し、CodeDeploy を起動

AppSpec テンプレートファイル ( ※① ) にある プレースホルダを、登録したタスク定義の ARN に置き換えます。
置き換えた結果を appspec-tmp.yml という一時的なファイルとして保存します。

CodeDeploy に渡すリビジョン JSON 構築のため、先のステップで作った appspec-tmp.yml を jq コマンドで JSON 文字列へ変換しつつ appSpecContent の値として埋め込みます。

最後に aws deploy create-deployment を実行し、 CodeDeploy による ECS デプロイを開始します。

      - name: Replace task definition ARN in AppSpec
        env:
          APPSPEC_FILE: ${{ inputs.appspec-file }}
        run: |
          sed "s|<TASK_DEFINITION>|${{ steps.register-task.outputs.task_definition_arn }}|g" ${APPSPEC_FILE} > ./appspec-tmp.yml

      - name: Create CodeDeploy revision JSON
        id: revision-json
        run: |
          APPSPEC=$(cat ./appspec-tmp.yml | jq -Rs .)
          REVISION='{"revisionType":"AppSpecContent","appSpecContent":{"content":'${APPSPEC}'}}'
          echo "revision_json=${REVISION}" >> $GITHUB_OUTPUT

      - name: Create deployment
        id: create-deployment
        env:
          CODEDEPLOY_APP_NAME: ${{ inputs.codedeploy-application }}
          CODEDEPLOY_DEPLOY_GROUP: ${{ inputs.codedeploy-deployment-group }}
          GITHUB_SHA: ${{ inputs.github-sha }}
        run: |
          DEPLOYMENT_ID=$(aws deploy create-deployment \
            --application-name ${CODEDEPLOY_APP_NAME} \
            --deployment-group-name ${CODEDEPLOY_DEPLOY_GROUP} \
            --revision '${{ steps.revision-json.outputs.revision_json }}' \
            --description "Deploy from commit ${GITHUB_SHA}" \
            --output text)
          echo "deployment_id=${DEPLOYMENT_ID}" >> $GITHUB_OUTPUT
          echo "Started deployment: ${DEPLOYMENT_ID}"

※① AppSpec テンプレートファイルのサンプルは以下の通りです。

AppSpec のサンプル
version: 0.0
Resources:
  - TargetService:
      Type: AWS::ECS::Service
      Properties:
        TaskDefinition: <TASK_DEFINITION>
        LoadBalancerInfo:
          ContainerName: "snkk1210-sandbox-hoge-hoge01"
          ContainerPort: 80
        PlatformVersion: "1.4.0"

④ / ⑤

後続の処理は CodeDeploy が代行してくれるので、WorkFlow 上では wait を使って、処理完了まで待機します。
必要な処理はココまでです。

      - name: Wait for deployment to finish and show URL
        env:
          REGION: ${{ inputs.region }}
        run: |
          DEPLOYMENT_ID="${{ steps.create-deployment.outputs.deployment_id }}"
          echo "View deployment in AWS Console:"
          echo "https://${REGION}.console.aws.amazon.com/codesuite/codedeploy/deployments/${DEPLOYMENT_ID}?region=${REGION}"
          echo "Waiting for deployment to complete..."
          aws deploy wait deployment-successful --deployment-id "$DEPLOYMENT_ID"

終わりに

冒頭でも触れたとおり、Fargate ネイティブの Blue/Green デプロイが公式サポートされたようで、これからは CodeDeploy を使ったデプロイ機構が非推奨になる?ようです。
将来的に公式アクションもサポートされなくなる可能性があるかも?と思い、今回、中身の代替手段を調べた次第でした。