CodeBuildでBatch用イメージと同時にジョブ定義を作ってみた

AWS Batch用のコンテナイメージのビルドと同時にジョブ定義も作られたら便利だよね、という話です。
2021.03.31

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんちにちは。AWS事業本部のKyoです。

AWS Batch用のコンテナイメージのビルドと同時にジョブ定義も作られたら便利だよね、をやってみました。

パイプラインの構築は出来ているものとして、主にbuildspec.ymlやBatchのジョブ定義の設定JSON(以下、ジョブ定義JSON)などを紹介します。また、ハマったところについても触れています。

3行まとめ

  • CodeCommitにDockerfile, buildspec.yml に加えてジョブ定義JSONを格納
  • CodeBuildの中でjqを使ってビルドしたイメージの名前とタグをジョブ定義JSONに反映
  • イメージをECRにプッシュ後、AWS CLIを使ってジョブ定義JSONをBatchへ登録

やってみる

CodeCommit

以下のファイルを格納します。

  • Dockerfile
  • job-definition-template.json
  • buildspec.yml

それぞれについて詳しく見ていきます。

Dockerfile

ビルドするイメージのDockerfileです。特別な対応は不要でビルドが通るものであればなんでも良いです。

今回はAmazon Linuxのイメージを利用します。

FROM amazonlinux:latest

job-definition-template.json

Batchのジョブ定義の設定値が含まれるJSONです。

CodeCommitに格納するものは、イメージ名とタグが入っていない状態なのでjob-definition-template.jsonという名前にしてあります。ビルドの途中でjob-definition.jsonを生成します。

AWS CLIからregister-job-definitionコマンドで利用します。

  • imageの値はビルドしたイメージ名とタグで上書きされるので任意の値でかまいません
  • ハイライトしたjobRoleArnはジョブで利用するロールのARNに書き換えてください
  • このJSONの設定はEC2起動タイプ用です
{
    "image": "To Be Determined",
    "vcpus": 1,
    "memory": 2048,
    "command": [
        "echo 'hello world'"
    ],
    "jobRoleArn": "【your jobRoleArn】",
    "executionRoleArn": "",
    "volumes": [
        {
            "host": {
                "sourcePath": ""
            },
            "name": ""
        }
    ],
            "environment": [
            ],
    "mountPoints": [],
    "readonlyRootFilesystem": true,
    "privileged": true,
    "ulimits": [
        {
            "hardLimit": 0,
            "name": "",
            "softLimit": 0
        }
    ],
    "user": "",
    "resourceRequirements": [],
    "linuxParameters": {
        "devices": [],
        "tmpfs": [],
        "maxSwap": 0,
        "swappiness": 0
    },
    "secrets": []
}

buildspec.yml

オーソドックスなコンテナイメージのビルドに加えて以下のことを行っています。

  • JSONLintでjob-definition-template.jsonのフォーマットチェック
  • jqでビルドしたイメージ名とタグをjob-definition-template.jsonへ反映
  • AWS CLIでBatchへのジョブ登録
version: 0.2

phases:
  install:
    commands:
      # JSONLintをインストール
      - npm install jsonlint -g
      # AWS CLI v2を最新バージョンで利用するため再インストール
      - pip3 uninstall awscli -y
      - curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
      - unzip awscliv2.zip
      - ./aws/install -i /usr/local/aws-cli -b /usr/local/bin
      
  pre_build:
    commands:
      # ジョブ定義テンプレートのフォーマットをチェック
      - jsonlint -q job-definition-template.json
      - IMAGE_TAG=$(echo ${CODEBUILD_RESOLVED_SOURCE_VERSION} | head -c 7)
      - echo Logging in to Amazon ECR...
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com

  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...          
      - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG .
      - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG

  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker image...
      - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
      # 今回ビルドしたイメージ名とタグをジョブ定義テンプレートに反映し、Batchのジョブ定義として登録
      - cat job-definition-template.json | jq --arg IMAGE_URI $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG '.image|=$IMAGE_URI' > job-definition.json
      - aws batch register-job-definition --job-definition-name $JOB_DEFINITION_NAME --type container --container-properties file://job-definition.json

CodeBuild

環境変数

buildspec.ymlで利用しているので以下をCodeBuildに環境変数として持たせてください。

  • AWS_DEFAULT_REGION : 利用するリージョン
  • AWS_ACCOUNT_ID : 利用するAWSアカウントの12桁ID
  • IMAGE_REPO_NAME : 利用するECRのレポジトリ名
  • JOB_DEFINITION_NAME : ジョブ定義を登録する際の名称。今回はsample-al

IAMロール

CodeBuildのサービスロールです。

公式ドキュメントを参考にポリシーとロールを作成しました。 ポリシーは以下の通りで、Batchへの対応としてハイライト部分の権限を追加しています。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:GetObjectVersion",
                "s3:GetBucketAcl",
                "s3:GetBucketLocation"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "codecommit:GitPull"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "ecr:BatchCheckLayerAvailability",
                "ecr:CompleteLayerUpload",
                "ecr:GetAuthorizationToken",
                "ecr:InitiateLayerUpload",
                "ecr:PutImage",
                "ecr:UploadLayerPart"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Action": [
                "batch:RegisterJobDefinition"
            ],
            "Resource": "*",
            "Effect": "Allow"
        },
        {
            "Condition": {
                "StringEqualsIfExists": {
                    "iam:PassedToService": [
                        "ecs-tasks.amazonaws.com"
                    ]
                }
            },
            "Action": [
                "iam:PassRole"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}

PassRoleを見落としていると、ジョブ登録時に以下のエラーが発生します。

An error occurred (AccessDeniedException) when calling the RegisterJobDefinition operation: User: arn:aws:sts::XXXXXXXXXXXX:assumed-role/CodeBuildServiceRole/AWSCodeBuild-5abf6672-7cda-444b-8faa-c7dd957767de is not authorized to perform: iam:PassRole on resource: arn:aws:iam::XXXXXXXXXXXX:role/BatchJobRole

ビルドの実行

CodeCommitをソースにCodeBuildでビルドを実行します。

出力例

###### 前略 ######
[Container] 2021/03/30 15:10:12 Running command docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
The push refers to repository [XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/sample-al]
e84e86a2ce7e: Preparing
e84e86a2ce7e: Pushed
2c158eb: digest: sha256:b288803ecb4fe526981317f7574cc87160132f635300eb3325859e9cb4a75eb9 size: 529

[Container] 2021/03/30 15:10:21 Running command cat job-definition-template.json | jq --arg IMAGE_URI $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG '.image|=$IMAGE_URI' > job-definition.json

[Container] 2021/03/30 15:10:22 Running command aws batch register-job-definition --job-definition-name $CONTAINER_NAME --type container --container-properties file://job-definition.json
{
    "jobDefinitionName": "sample-al",
    "jobDefinitionArn": "arn:aws:batch:ap-northeast-1:XXXXXXXXXXXX:job-definition/sample-al:1",
    "revision": 1
}

[Container] 2021/03/30 15:10:23 Phase complete: POST_BUILD State: SUCCEEDED
[Container] 2021/03/30 15:10:23 Phase context status code:  Message:

ECRにイメージが格納され、Batchのジョブ定義が作成(or リビジョンが作成)されていることが確認できれば完了です。

ハマったところ

AWS CLIからのregister-job-definitionでは、ジョブ定義JSONを参照する形になりますが、これに必要なパラメータを探るのに苦労しました。

Batchの実体はECSのため、以下のブログの「タスク定義」を「ジョブ定義」に読み替えるとイメージが伝わると思います。

「そんなん、既存の登録内容からその内容をJSONで出力してそれをそのまま使えばええやん?」と思いますよね。はい。そんなAPIはありません。

最初の1回はとにかくエラーを潰す泥臭い作業が必要になりますが、作ってしまえばコードとして管理できるので変更や再利用は行いやすいです。

やってしまった系のミスとしては、メモリを1桁多く設定し、「Batchインスタンスがいつまでたっても起動してこない。。」みたいなこともありました。

AWS Batch ジョブが RUNNABLE ステータスで止まっているのはなぜですか?

このように試行錯誤していると、DockerHubのダウンロード制限にも結構な頻度でひっかかったりもしました。ビルドが失敗した際にログを見るまで、設定のせいなのか、制限のせいか分からず、割とモヤモヤが募るので、検証する前にぜひ以下の設定をしておきましょう。

おわりに

本ブログではコンテナイメージをビルドする度にジョブ定義も作成される設定を紹介しました。またこの設定を煮詰めていく中で出会ったハマりどころについても紹介しました。

以上、何かのお役に立てれば幸いです。