AWS Batch の CI/CD を試してみる

# 概要

AWS Batch の理解のために以下のハンズオンを実施してみた。

https://catalog.us-east-1.prod.workshops.aws/workshops/b6ea8509-42a3-4792-9020-24f9210d32f2/ja-JP

なお、一部手順は手動から CLI または CloudFormation で実行するようにアレンジしています。


# 事前準備

## CloudFormation のデプロイ

必要な AWS リソースを CloudFormation でデプロイしておきます。

```
wget https://static.us-east-1.prod.workshops.aws/public/66da963d-7962-425a-bf2b-082e9436fb97/static/handson.yaml

aws cloudformation create-stack --stack-name batch-handson --template-body file://./handson.yaml --capabilities CAPABILITY_NAMED_IAM
```

|論理ID|タイプ|
|---|---|
|MyComputeEnv|AWS::Batch::ComputeEnvironment|
|MyJobDefinition|AWS::Batch::JobDefinition|
|MyJobQueue|AWS::Batch::JobQueue|
|IGW|AWS::EC2::InternetGateway|
|PublicSubnetRoute|AWS::EC2::Route|
|PublicSubnetRT|AWS::EC2::RouteTable|
|PublicSubnet1|AWS::EC2::Subnet|
|PublicSubnet2|AWS::EC2::Subnet|
|PublicSubnetRTAssociation1|AWS::EC2::SubnetRouteTableAssociation|
|PublicSubnetRTAssociation2|AWS::EC2::SubnetRouteTableAssociation|
|VPC|AWS::EC2::VPC|
|VPCIGWAttachement|AWS::EC2::VPCGatewayAttachment|
|AWSBatchServiceRole|AWS::IAM::Role|
|CodeBuildRole|AWS::IAM::Role|
|ecsTaskExecutionRole|AWS::IAM::Role|

# 手動デプロイ

## ECR リポジトリの作成

ECR リポジトリを作成します。

```
aws ecr create-repository --repository-name queens 
```

## アプリケーションの準備と動作確認

コンパイルし、動作確認を行います。

```
curl -O https://www.arch.cs.titech.ac.jp/~kise/nq/package/qn24b-version1.0.tgz
tar -zxvf qn24b-version1.0.tgz
gcc -O2 version1.0/base/queens.c -o queens
```

```
./queens 16


qn24b base version 1.0.1 2004-09-02
=============================================
qn24b base version 1.0.1 2004-09-02
problem size n        : 16
total   solutions     : 14772512
correct solutions     : 14772512
million solutions/sec : 4.962
elapsed time (sec)    : 2.977
=============================================
```

## アプリケーションのコンテナ化

Dockerfile を作成し、ビルド後動作確認をします。

```Dockerfile:Dockerfile
FROM public.ecr.aws/amazonlinux/amazonlinux:latest

COPY ./queens /usr/local/bin/queens

# program execute
CMD /usr/local/bin/queens 16
```

```
docker build -t queens .
```

```
docker run queens

qn24b base version 1.0.1 2004-09-02
=============================================
qn24b base version 1.0.1 2004-09-02
problem size n        : 16
total   solutions     : 14772512
correct solutions     : 14772512
million solutions/sec : 2.694
elapsed time (sec)    : 5.483
=============================================
```

## ECR へ push

ECR へ コンテナイメージ を push します

```
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com

docker tag queens:latest 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/queens:latest

docker push 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/queens:latest
```

## ジョブ定義の更新

AWS Batch > ジョブ定義 > handson-def を開き、「アクション」から「リビジョンの作成」を開きます。
![スクリーンショット 2024-04-02 16.34.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280929/b55a7521-31a3-c15d-6941-f2d73cbcde84.png)

「ステップ 2」の「コンテナ設定」で「イメージ」を自身のイメージの URI に書き換えます。
![スクリーンショット 2024-04-02 16.38.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280929/bcdd0b15-eb96-83a2-8f9d-41394dcb0f76.png)

## ジョブの実行と確認

AWS Batch > ジョブ > 新しいジョブを送信 を開き、「名前」、「ジョブ定義」、「ジョブキュー」を次の図の通り指定します。
![スクリーンショット 2024-04-02 16.43.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280929/1e6dabc8-f235-9e2d-2c0c-4c230d02f525.png)

ジョブが実行され、「Succeeded」になったことが確認できました。また、「ログ記録」を確認するとローカルで実行した内容と同じ結果が得られました。
![スクリーンショット 2024-04-02 16.49.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280929/fa772fe5-b829-a53d-d6b0-eb38fd707703.png)
![スクリーンショット 2024-04-02 16.50.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280929/d44eb6fc-977b-cddc-f327-f488a5d5df7e.png)

# 自動デプロイ

## CodeCommit リポジトリの作成と clone

CodeCommit リポジトリを作成しておきます。

```
aws codecommit create-repository --repository-name handson-repo
```

作成した CodeCommit リポジトリを clone し、handson-repo リポジトリに移動しておきます。

```
git clone https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/handson-repo

cd handson-repo
```

## buildspec.yaml の作成

この buildspec.yaml は手動デプロイで行った作業を自動デプロイに落とし込んでいるだけのようなので、ここでの詳しい説明は省きます。

```yaml:buildspec.yaml
version: 0.2
phases:
    pre_build:
        commands:
            - AWS_ACCOUNT_ID=$(echo ${CODEBUILD_BUILD_ARN} | cut -f 5 -d :)
            - aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.ap-northeast-1.amazonaws.com
            - REPOSITORY_URI=$AWS_ACCOUNT_ID.dkr.ecr.ap-northeast-1.amazonaws.com/queens
            - IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
    build:
        commands:
            - docker build -t $REPOSITORY_URI:latest .
            - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
    post_build:
        commands:
            - docker push $REPOSITORY_URI:latest
            - docker push $REPOSITORY_URI:$IMAGE_TAG
            - cat job-definition-template.json | jq --arg IMAGE_URI $REPOSITORY_URI:$IMAGE_TAG '.image|=$IMAGE_URI' > job-definition.json
            - aws batch register-job-definition --job-definition-name handson-def --type container --platform-capabilities FARGATE --container-properties file://job-definition.json
```

## ジョブ定義の作成

ジョブ定義で指定するコンテナのプロパティを作成します。

```json:job-definition.json
{
  "image": "TBD",
  "command": [],
  "executionRoleArn": "arn:aws:iam::123456789012:role/service-role/ecsTaskExecutionRole-handson",
  "volumes": [],
  "environment": [],
  "mountPoints": [],
  "ulimits": [],
  "resourceRequirements": [
    {
      "value": "0.25",
      "type": "VCPU"
    },
    {
      "value": "512",
      "type": "MEMORY"
    }
  ],
  "secrets": [],
  "networkConfiguration": {
    "assignPublicIp": "ENABLED"
  },
  "fargatePlatformConfiguration": {
    "platformVersion": "LATEST"
  },
  "runtimePlatform": {
    "operatingSystemFamily": "LINUX",
    "cpuArchitecture": "X86_64"
  }
}

```

## リポジトリへ push

手動デプロイで作成した Dockerfile と queens を handson-repo ディレクトリ配下にコピーします。

```
cp ../Dockerfile .
cp ../queens .
```

リポジトリへ push するファイルは以下の通りとなります。

```
.
├── Dockerfile
├── buildspec.yaml
├── job-definition-template.json
└── queens
```

CodeCommit へ push します。

```
git add .
git commit -m "my first commit"
git push
```

## CodePipeline の作成

CloudFormation で CodePipeline をデプロイします。

```yaml:handson-pipeline.yaml
AWSTemplateFormatVersion: "2010-09-09"

Resources:

    S3Bucket:
        Type: 'AWS::S3::Bucket'
        Properties:
          BucketName: !Sub "codepipeline-${AWS::Region}-20240403"

    IAMRole:
        Type: "AWS::IAM::Role"
        Properties:
            Path: "/service-role/"
            RoleName: !Sub "AWSCodePipelineServiceRole-${AWS::Region}-${CodeBuildProject}"
            AssumeRolePolicyDocument: {
                                "Version": "2012-10-17",
                                "Statement": [
                                    {
                                    "Effect": "Allow",
                                    "Principal": {
                                        "Service": "codepipeline.amazonaws.com"
                                    },
                                    "Action": "sts:AssumeRole"
                                    }
                                ]
                }
            MaxSessionDuration: 3600
            ManagedPolicyArns: 
              - !Sub "arn:aws:iam::${AWS::AccountId}:policy/service-role/AWSCodePipelineServiceRole-${AWS::Region}-${CodeBuildProject}"

    CodeBuildProject:
        Type: "AWS::CodeBuild::Project"
        Properties:
            Name: "handson-pipeline"
            Source: 
                GitCloneDepth: 1
                GitSubmodulesConfig: 
                    FetchSubmodules: false
                InsecureSsl: false
                Location: !Sub "https://git-codecommit.${AWS::Region}.amazonaws.com/v1/repos/handson-repo"
                Type: "CODECOMMIT"
            Artifacts: 
                Type: "NO_ARTIFACTS"
            Cache: 
                Type: "NO_CACHE"
            Environment: 
                ComputeType: "BUILD_GENERAL1_SMALL"
                Image: "aws/codebuild/amazonlinux2-x86_64-standard:5.0-24.03.20"
                ImagePullCredentialsType: "CODEBUILD"
                PrivilegedMode: true
                Type: "LINUX_CONTAINER"
            ServiceRole: !Sub "arn:aws:iam::${AWS::AccountId}:role/service-role/codebuild-handson-role"
            TimeoutInMinutes: 60
            QueuedTimeoutInMinutes: 480
            EncryptionKey: !Sub "arn:aws:kms:${AWS::Region}:${AWS::AccountId}:alias/aws/s3"
            BadgeEnabled: false
            LogsConfig: 
                CloudWatchLogs: 
                    Status: "ENABLED"
                S3Logs: 
                    Status: "DISABLED"
                    EncryptionDisabled: false
            Visibility: "PRIVATE"

    CodePipelinePipeline:
        Type: "AWS::CodePipeline::Pipeline"
        Properties:
            Name: !Ref CodeBuildProject
            RoleArn: !GetAtt IAMRole.Arn
            ArtifactStore: 
                Location: !Ref S3Bucket
                Type: "S3"
            Stages: 
              - 
                Name: "Source"
                Actions: 
                  - 
                    Name: "Source"
                    ActionTypeId: 
                        Category: "Source"
                        Owner: "AWS"
                        Provider: "CodeCommit"
                        Version: "1"
                    Configuration: 
                        BranchName: "master"
                        OutputArtifactFormat: "CODE_ZIP"
                        PollForSourceChanges: "false"
                        RepositoryName: "handson-repo"
                    OutputArtifacts: 
                      - 
                        Name: "SourceArtifact"
                    Region: !Ref AWS::Region
                    Namespace: "SourceVariables"
                    RunOrder: 1
              - 
                Name: "Build"
                Actions: 
                  - 
                    Name: "Build"
                    ActionTypeId: 
                        Category: "Build"
                        Owner: "AWS"
                        Provider: "CodeBuild"
                        Version: "1"
                    Configuration: 
                        ProjectName: !Ref CodeBuildProject
                    InputArtifacts: 
                      - 
                        Name: "SourceArtifact"
                    OutputArtifacts: 
                      - 
                        Name: "BuildArtifact"
                    Region: !Ref AWS::Region
                    Namespace: "BuildVariables"
                    RunOrder: 1
```

```
aws cloudformation create-stack --stack-name handson-pipeline --template-body file://./handson-pipeline.yaml --capabilities CAPABILITY_NAMED_IAM
```

## パイプラインの実行

Dockerfile で 16 → 10 にし、push します。

```Dockerfile:Dockerfile
FROM public.ecr.aws/amazonlinux/amazonlinux:latest

COPY ./queens /usr/local/bin/queens

# program execute
CMD /usr/local/bin/queens 10
```

```
git add .
git commit -m "change queens param"
git push
```

デベロッパー用ツール > CodePipeline > パイプライン > handson-pipeline にいくと、自動デプロイが成功していることが確認できます。

![スクリーンショット 2024-04-03 16.02.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280929/73e70a81-0e39-d14a-c66a-f0793d8558a8.png)

## ジョブの実行と確認

AWS Batch > ジョブ > handson-def を確認すると、イメージが書きかわっていることがわかります。
buildspec.yaml で指定した`IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)`の部分です。

![スクリーンショット 2024-04-03 16.06.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280929/4afbfc7f-7775-a36d-35ea-532caa2cfff5.png)

AWS Batch > ジョブ > 新しいジョブを送信 を開き、「名前」、「ジョブ定義」、「ジョブキュー」を次の図の通り指定します。
 ![スクリーンショット 2024-04-03 16.09.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280929/e8e253f1-0b34-10ce-6711-81c2fd7537e1.png)

「ログ記録」を確認すると　Dockerfile で 16 → 10 に変更した内容で実行されたことが確認できました。
![スクリーンショット 2024-04-04 13.46.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280929/85cb4fe2-39d7-0af9-f720-d1b7201a7040.png)