kakakakakku blog

Weekly Tech Blog: Keep on Learning!

source-version-override: aws-actions/aws-codebuild-run-build でプルリクエストブランチを AWS CodeBuild のビルド対象にする

AWS CodeBuild Run Build for GitHub Actions (aws-actions/aws-codebuild-run-build) を使って GitHub Actions から AWS CodeBuild のビルドを実行すると buildspec.yml やビルド環境タイプを上書きできて便利〜という話は前にまとめた👌

kakakakakku.hatenablog.com

今回は source-version-override パラメータを活用して,プルリクエストを出したときにプルリクエストブランチを AWS CodeBuild のビルド対象にする仕組みを試してみた.以下に検証用の GitHub Actions ワークフローを載せておく📝プルリクエストをマージする前に動作確認ができるようになるから便利なパラメータだと思う❗️

name: Start AWS CodeBuild build

on:
  workflow_dispatch:
  push:
    branches:
      - master
  pull_request:
    branches:
      - master

permissions:
  id-token: write
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ap-northeast-1
      - name: Start AWS CodeBuild build
        uses: aws-actions/aws-codebuild-run-build@v1
        with:
          project-name: sandbox
          source-version-override: ${{ github.head_ref }}
        if: github.event_name == 'pull_request'
      - name: Start AWS CodeBuild build
        uses: aws-actions/aws-codebuild-run-build@v1
        with:
          project-name: sandbox
          source-version-override: pr/${{ github.event.pull_request.number }}
        if: github.event_name == 'pull_request'
      - name: Start AWS CodeBuild build
        uses: aws-actions/aws-codebuild-run-build@v1
        with:
          project-name: sandbox
          source-version-override: ${{ github.ref_name }}
        if: github.event_name == 'push'

AWS CodeBuild Run Build for GitHub Actions の source-version-override パラメータには GitHub の場合「コミット ID/プルリクエスト ID/ブランチ名/タグ名」を指定できる.AWS CodeBuild の API Reference (StartBuild) に以下のように書いてあった📝

The commit ID, pull request ID, branch name, or tag name that corresponds to the version of the source code you want to build. If a pull request ID is specified, it must use the format pr/pull-request-ID (for example pr/25). If a branch name is specified, the branch's HEAD commit ID is used. If not specified, the default branch's HEAD commit ID is used.

プルリクエストブランチ

プルリクエストを作った場合は github.event_name == 'pull_request' で判定しつつ,source-version-override パラメータにブランチ名を表す ${{ github.head_ref }} を設定すれば OK👌

- name: Start AWS CodeBuild build
  uses: aws-actions/aws-codebuild-run-build@v1
  with:
    project-name: sandbox
    source-version-override: ${{ github.head_ref }}
  if: github.event_name == 'pull_request'

もしブランチ名ではなく「プルリクエスト ID」を指定する場合は pr/pull-request-ID というフォーマットにする必要がある.source-version-override パラメータにプルリクエスト ID を表す pr/${{ github.event.pull_request.number }} を設定すれば OK👌

- name: Start AWS CodeBuild build
  uses: aws-actions/aws-codebuild-run-build@v1
  with:
    project-name: sandbox
    source-version-override: pr/${{ github.event.pull_request.number }}
  if: github.event_name == 'pull_request'

メインブランチ

プルリクエストをマージした場合は github.event_name == 'push' で判定しつつ,source-version-override パラメータにブランチ名を表す ${{ github.ref_name }} を設定すれば OK👌

- name: Start AWS CodeBuild build
  uses: aws-actions/aws-codebuild-run-build@v1
  with:
    project-name: sandbox
    source-version-override: ${{ github.ref_name }}
  if: github.event_name == 'push'

動作確認

期待通りに動いたー👏

AWS CDK で Amazon EventBridge Pipes の「ターゲット入力トランスフォーマー」を設定する

AWS CDK で Amazon SQS x Amazon EventBridge Pipes x AWS Step Functions の構成を設定する流れは前にまとめた📝

kakakakakku.hatenablog.com

前にまとめた設定では Amazon SQS キューに登録したメッセージをデフォルト設定のまま Amazon EventBridge Pipes 経由で AWS Step Functions に流しているけど,実際に使ってみると Amazon SQS のメッセージ形式のまま AWS Step Functions に流れてくるため,AWS Step Functions 側でインプットの取り回しがしにくく微妙に使いにくいことに気付く💨

具体例

例えば以下の JSON を Amazon SQS キューに登録する.

{
    "key1": "value1",
    "key2": "value2",
    "key3": "value3"
}

AWS Step Functions の入力としては以下のような JSON が流れてくる(一部の値は書き換えた).AWS Step Functions 側では body に含まれている JSON のみで十分ということも多いと思う💡

[
  {
    "messageId": "0f6ddca6-8fc2-45e3-8c51-226eca45dbb0",
    "receiptHandle": "xxxxx",
    "body": "{\n    \"key1\": \"value1\",\n    \"key2\": \"value2\",\n    \"key3\": \"value3\"\n}",
    "attributes": {
      "ApproximateReceiveCount": "1",
      "SentTimestamp": "1709209227935",
      "SenderId": "xxxxx",
      "ApproximateFirstReceiveTimestamp": "1709209227937"
    },
    "messageAttributes": {},
    "md5OfBody": "b4fc128c9cb169639ef7083a9a4d78dd",
    "eventSource": "aws:sqs",
    "eventSourceARN": "arn:aws:sqs:ap-northeast-1:000000000000:sandbox-cdk-sqs-pipes-stepfunctions-queue",
    "awsRegion": "ap-northeast-1"
  }
]

ターゲット入力トランスフォーマーを使う

そんなときは Amazon EventBridge Pipes で「ターゲット入力トランスフォーマー (Target Input Transformer)」を設定すれば OK👌今回は Amazon SQS キューに登録したメッセージの body のみ AWS Step Functions に流したいため,以下のように設定する❗️

マネジメントコンソールなら

{
  "body": <$.body>
}

AWS CDK なら

CfnPipeinputTemplate に設定する.AWS CDK コード全体は AWS CDK で Amazon EventBridge Pipes(SQS ソース・Step Functions ターゲット)を設定する - kakakakakku blog 参照📝

new aws_pipes.CfnPipe(this, 'SandboxCdkPipes', {
  name: 'sandbox-cdk-sqs-pipes-stepfunctions-pipes',
  roleArn: pipeRole.roleArn,
  source: queue.queueArn,
  target: stateMachine.stateMachineArn,
  targetParameters: {
    inputTemplate: '{ "body": <$.body> }',
    stepFunctionStateMachineParameters: {
      invocationType: 'FIRE_AND_FORGET',
    }
  }
});

実行結果

「ターゲット入力トランスフォーマー」を設定してもう1度 Amazon SQS キューに同じ JSON を登録すると,以下のように body に含まれている JSON のみ AWS Step Functions に流せるようになった \( 'ω')/

[
  {
    "body": {
      "key1": "value1",
      "key2": "value2",
      "key3": "value3"
    }
  }
]

ちなみに AWS Step Functions 側で JSON 配列になっているのは Amazon EventBridge Pipes の仕様で,ドキュメントには Lambda または Step Functions エンリッチメントまたはターゲットの場合、バッチサイズが 1 であっても、バッチは JSON 配列としてターゲットに配信されます。 と書いてある.これはハマりポイントの1つかも💨

docs.aws.amazon.com

Dependabot で Terraform Provider を自動的にアップデートしよう

Dependabot version updates を使うと Terraform Provider のアップデートを自動化できる❗️設定は比較的簡単で package-ecosystemterraform を設定して,あとは必須の directoryschedule.interval でアップデートの対象ディレクトリとスケジュールを決めれば OK👌個人的な Terraform 検証用プライベートリポジトリに設定して数週間試してみた \( 'ω')/

さらに package-ecosystemgithub-actions を設定すると actions/checkout@v4 など GitHub Actions のアクションも自動的にアップデートできる❗️一度入れたらそのままということもよくあるし助かる〜.

docs.github.com

🤖 .github/dependabot.yml

version: 2
updates:
  - package-ecosystem: terraform
    directory: /
    schedule:
      interval: daily
    open-pull-requests-limit: 2
    target-branch: master
    ignore:
      - dependency-name: "*"
        update-types: ["version-update:semver-major"]
  - package-ecosystem: github-actions
    directory: /
    schedule:
      interval: daily
    open-pull-requests-limit: 2
    target-branch: master

動作確認

Terraform の AWS Provider と Terraform の GitHub リポジトリに設定してる GitHub Actions のアクション (actions/checkout) を自動的にアップデートするプルリクエストが作れたー👏

関連記事

kakakakakku.hatenablog.com

AWS CDK の DockerImageAsset と cdk-ecr-deployment でビルドしたイメージを Amazon ECR に保存する

AWS CDK で「Dockerfile をビルドして Amazon ECR リポジトリにイメージを保存する」選択肢として DockerImageAssetcdklabs/cdk-ecr-deployment を紹介する❗️

aws_ecr_assets.DockerImageAsset を使う

まず,1番簡単なのは aws_ecr_assets.DockerImageAsset を使うという選択肢だと思う.

docs.aws.amazon.com

👾 sandbox-cdk-ecr-deployment-stack.ts (Step.1)

Dockerfile を準備して(今回は ../images/app ディレクトリに置いた)以下のような AWS CDK コードを実装すれば OK👌

import {
  Stack,
  StackProps,
  aws_ecr_assets,
} from 'aws-cdk-lib'
import { Construct } from 'constructs'
import path = require('path')

export class SandboxCdkEcrDeploymentStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props)

    new aws_ecr_assets.DockerImageAsset(this, 'SandboxAppImage', {
      directory: path.join(__dirname, '../images/app'),
    })
  }
}

デプロイすると自動的に cdk-hnb659fds-container-assets-${ACCOUNTID}-ap-northeast-1 という Amazon ECR リポジトリにイメージが保存される.hnb659fds は AWS CDK 側で付けられた名前でドキュメントこの値には意味がありません と書かれてておもしろい😃実装も少なく1番お手軽だと思う (๑•̀ㅂ•́)و✧

デメリットとしては任意の Amazon ECR リポジトリにイメージを保存できないというところだと思う.言い換えるとリポジトリ名・ライフサイクルポリシー・リポジトリタグなどは自由に設定できず,AWS CDK 側に管理されてしまうため微妙な使いづらさがある.ちなみにこれはドキュメントにも明記されていて「そういう意図である」と読み取れる📝

DockerImageAsset is designed for seamless build & consumption of image assets by CDK code deployed to multiple environments through the CDK CLI or through CI/CD workflows. To that end, the ECR repository behind this construct is controlled by the AWS CDK. The mechanics of where these images are published and how are intentionally kept as an implementation detail, and the construct does not support customizations such as specifying the ECR repository name or tags.

cdklabs/cdk-ecr-deployment と組み合わせて使う

そこで aws_ecr_assets.DockerImageAssetcdklabs/cdk-ecr-deployment を組み合わせて使うという選択肢もある💡cdklabs/cdk-ecr-deployment を使うと,指定した Docker Hub・Amazon ECR リポジトリなどのイメージを任意の Amazon ECR リポジトリにコピーできる.

github.com

👾 sandbox-cdk-ecr-deployment-stack.ts (Step.2)

例えば,以下のような AWS CDK コードを実装してデプロイすると,Docker Hub の amazonlinux:2023 イメージを自分で実装した Amazon ECR リポジトリ(今回は sandbox-repository)にコピーできる.

import {
  Stack,
  StackProps,
  aws_ecr,
} from 'aws-cdk-lib'
import * as ecrdeploy from 'cdk-ecr-deployment'
import { Construct } from 'constructs'

export class SandboxCdkEcrDeploymentStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props)

    const repository = new aws_ecr.Repository(this, 'SandboxRepository', {
      repositoryName: 'sandbox-repository',
    })

    new ecrdeploy.ECRDeployment(this, 'SandboxImageDeployment', {
      src: new ecrdeploy.DockerImageName('amazonlinux:2023'),
      dest: new ecrdeploy.DockerImageName(repository.repositoryUriForTag('2023')),
    })
  }
}

👾 sandbox-cdk-ecr-deployment-stack.ts (Step.3)

よって aws_ecr_assets.DockerImageAsset で Amazon ECR リポジトリ cdk-hnb659fds-container-assets-${ACCOUNTID}-ap-northeast-1 に保存されたイメージを自分で実装した Amazon ECR リポジトリにコピーすることで,Dockerfile をビルドして Amazon ECR リポジトリにイメージを保存する流れを AWS CDK で柔軟に実現できる👏

import {
  Stack,
  StackProps,
  aws_ecr,
  aws_ecr_assets,
} from 'aws-cdk-lib'
import * as ecrdeploy from 'cdk-ecr-deployment'
import { Construct } from 'constructs'
import path = require('path')

export class SandboxCdkEcrDeploymentStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props)

    const image = new aws_ecr_assets.DockerImageAsset(this, 'SandboxAppImage', {
      directory: path.join(__dirname, '../images/app'),
    })

    const repository = new aws_ecr.Repository(this, 'SandboxRepository', {
      repositoryName: 'sandbox-repository',
    })

    new ecrdeploy.ECRDeployment(this, 'SandboxImageDeployment', {
      src: new ecrdeploy.DockerImageName(image.imageUri),
      dest: new ecrdeploy.DockerImageName(repository.repositoryUriForTag('my-tag')),
    })
  }
}

cdk-ecr-deployment で事前にビルドされたイメージを使う

cdklabs/cdk-ecr-deployment は内部的にイメージをコピーするための AWS Lambda 関数もデプロイされている.AWS CDK をデプロイするときに環境変数として FORCE_PREBUILT_LAMBDA=1 を設定しておくと,AWS Lambda 関数を作るときに事前にビルドされたイメージを使うようになる.ビルド時間を短縮できるので使っても良いと思う👌実装を見たところ GitHub リポジトリのリリースアセットからダウンロードしているようだった.

github.com

Terraform で「最新の」Amazon ECS タスク定義を追跡できる aws_ecs_task_definition の track_latest オプション

Terraform で Amazon ECS タスク定義を作りつつ,アプリケーションのライフサイクルとして GitHub Actions などの「Terraform 以外で」イメージタグを差し替えて Amazon ECS タスク定義を更新(正確には更新ではなくリビジョン追加)する運用を選択することがあると思う.さらにデプロイを繰り返すと使わなくなった Amazon ECS タスク定義が増えるため,定期的に「登録解除 (INACTIVE)」をすることもあると思う.ちなみに2023年2月からは削除もできるようになっているけど〜 \( 'ω')/

track_latest とは

しかし Amazon ECS タスク定義を登録解除すると Terraform でリソースを追跡できず,terraform plan を実行すると Amazon ECS タスク定義を作り直そうとしてしまう.少し前置きは長くなったけど,2024年2月16日にリリースされた Terraform AWS Provider v5.37.0 aws_ecs_task_definition に新しく track_latest オプションが追加された❗️

track_latest - (Optional) Whether should track latest task definition or the one created with the resource. Default is false.

この track_latest を使うと Terraform 側は常に「最新の」Amazon ECS タスク定義を追跡して比較してくれるため,Amazon ECS タスク定義の作り直しがなくなり,運用しやすくなる可能性がある👏

github.com

とは言え実際に試さないとイメージが沸かず,プルリクエストに書いてあるシナリオを参考に試してみた.

github.com

デフォルト or track_latest = false の場合

Step.1

今回はイメージの例として amazonlinux のタグを 2.0.20231218.02.0.20240109.02.0.20240131.0 と更新する流れを試す.まずは Terraform で Amazon ECS タスク定義(リビジョン1)を作る.

resource "aws_ecs_task_definition" "sandbox_track_latest_false" {
  family                   = "sandbox-track-latest-false"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = "256"
  memory                   = "512"

  container_definitions = jsonencode([
    {
      name  = "amazonlinux",
      image = "amazonlinux:2.0.20231218.0"
    }
  ])
}

Terraform で Amazon ECS タスク定義(リビジョン1)を作った

Step.2

次は Terraform 以外(今回は AWS CLI を使う)でイメージタグを更新した Amazon ECS タスク定義を2個を作る(リビジョン2・リビジョン3).この状態でアプリケーションとして稼働する最新の Amazon ECS タスク定義は「リビジョン3」となる.

$ aws ecs register-task-definition --cli-input-json fileb://taskdefinition-2.0.20240109.0.json
$ aws ecs register-task-definition --cli-input-json fileb://taskdefinition-2.0.20240131.0.json

📝 taskdefinition-2.0.20240109.0.json

{
    "family": "sandbox-track-latest-false",
    "networkMode": "awsvpc",
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "cpu": "256",
    "memory": "512",
    "containerDefinitions": [
        {
            "name": "amazonlinux",
            "image": "amazonlinux:2.0.20240109.0"
        }
    ]
}

📝 taskdefinition-2.0.20240131.0.json

{
    "family": "sandbox-track-latest-false",
    "networkMode": "awsvpc",
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "cpu": "256",
    "memory": "512",
    "containerDefinitions": [
        {
            "name": "amazonlinux",
            "image": "amazonlinux:2.0.20240131.0"
        }
    ]
}

AWS CLI で Amazon ECS タスク定義(リビジョン2・リビジョン3)を作った

Step.3

ここで使わなくなった Amazon ECS タスク定義(リビジョン1・リビジョン2)を登録解除する.

$ aws ecs deregister-task-definition --task-definition sandbox-track-latest-false:1
$ aws ecs deregister-task-definition --task-definition sandbox-track-latest-false:2

Amazon ECS タスク定義(リビジョン1・リビジョン2)を登録解除した

Step.4

最後に terraform plan を実行すると Amazon ECS タスク定義を作り直そうとする😨 ちなみに個人用の Terraform Cloud のキャプチャを載せている.

terraform plan を実行した

track_latest = true の場合

Step.1

基本的には同じだけど,名前を sandbox-track-latest-true に変更して,track_latest = true も設定した.まずは Terraform で Amazon ECS タスク定義(リビジョン1)を作る.

resource "aws_ecs_task_definition" "sandbox_track_latest_true" {
  family                   = "sandbox-track-latest-true"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = "256"
  memory                   = "512"
  track_latest             = true

  container_definitions = jsonencode([
    {
      name  = "amazonlinux",
      image = "amazonlinux:2.0.20231218.0"
    }
  ])
}

Step.2

Step.2 も基本的には同じ.Amazon ECS タスク定義を2個作る(リビジョン2・リビジョン3).

$ aws ecs register-task-definition --cli-input-json fileb://taskdefinition-2.0.20240109.0.json
$ aws ecs register-task-definition --cli-input-json fileb://taskdefinition-2.0.20240131.0.json

📝 taskdefinition-2.0.20240109.0.json

{
    "family": "sandbox-track-latest-true",
    "networkMode": "awsvpc",
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "cpu": "256",
    "memory": "512",
    "containerDefinitions": [
        {
            "name": "amazonlinux",
            "image": "amazonlinux:2.0.20240109.0"
        }
    ]
}

📝 taskdefinition-2.0.20240131.0.json

{
    "family": "sandbox-track-latest-true",
    "networkMode": "awsvpc",
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "cpu": "256",
    "memory": "512",
    "containerDefinitions": [
        {
            "name": "amazonlinux",
            "image": "amazonlinux:2.0.20240131.0"
        }
    ]
}

Step.3

ここで使わなくなった Amazon ECS タスク定義(リビジョン1・リビジョン2)を登録解除する.

$ aws ecs deregister-task-definition --task-definition sandbox-track-latest-true:1
$ aws ecs deregister-task-definition --task-definition sandbox-track-latest-true:2

Amazon ECS タスク定義(リビジョン1・リビジョン2)を登録解除した

Step.4

track_latest = true を設定してると Step.4 から挙動が変わってくる💡ここで terraform plan を実行すると Amazon ECS タスク定義を作り直しではなく「最新の」Amazon ECS タスク定義を追跡して比較してくれる.実際に containerDefinitions.image の値を最新の amazonlinux:2.0.20240131.0 から,Terraform コードで指定している amazonlinux:2.0.20231218.0 に戻すような差分が出ている❗️

terraform plan を実行した

次に Terraform コードを修正して最新の Amazon ECS タスク定義と一致させる📝

resource "aws_ecs_task_definition" "sandbox_track_latest_true" {
  family                   = "sandbox-track-latest-true"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = "256"
  memory                   = "512"
  track_latest             = true

  container_definitions = jsonencode([
    {
      name  = "amazonlinux",
      image = "amazonlinux:2.0.20240131.0"
    }
  ])
}

もう一度 terraform plan を実行すると No changes で差分なしになる👌

なるほどー \( 'ω')/

terraform plan を実行した

まとめ

2024年2月16日にリリースされた Terraform AWS Provider v5.37.0 で追加された aws_ecs_task_definition の新しいオプション track_latest オプションを試してみた❗️ドキュメントを読むだけではイメージが沸かず,実際に試してみて良かった.とは言え track_latest = true を設定するとアプリケーションを頻繁にデプロイするときに変更の追随も大変そうなので ignore_changes で変更を無視する運用も引き続き残りそう.まずは track_latest = true の存在を覚えておこうー💡