AWS CodeBuildを利用してUnity ML-AgentsのDockerイメージを作成する(v0.9.1対応)

Unity ML-AgentsのDockerイメージを作成するのに、AWS CodeBuild(CodeBuild)を利用してみました。
CodeBuildでUnity ML-Agentsのソースを取得してビルド、ビルドができたらAmazon Elastic Container Registry(ECR)のリポジトリにイメージをPushします。

Unity ML-AgentsやCodeBuildについては省略。

同じようなことをGCPのGoogle Cloud Buildでもやっているので、GCPな方はそちらをご参考ください。

Google Cloud Buildを利用してUnity ML-AgentsのDockerイメージを作成する(v0.9.1対応) - Qiita
https://qiita.com/kai_kou/items/5eabbaa2b564527e0b4e


## 前提

- AWSアカウントがある
- ローカルで`aws` コマンドが利用できる
- ローカルでDockerが実行できる


## 手順

### AWS CloudFormation(CFn)で環境構築する

CFnを利用して、必要となるリソースを作成します。

- InputBucket: S3バケット。ソース置き場(使わない)
- OutputBucket: S3バケット。アーティファクト(使わない)
- ECRRepository: ECRリポジトリ。DockerイメージをPushします
- CodeBuildServiceRole: CodeBuildで利用するサービスロール
- CodeBuildProject: CodeBuildのプロジェクト


`InputBucket` と`OutputBucket` は利用しないのですが、CodeBuildのプロジェクトを作成・実行する際に必須となるので用意しています。

テンプレート作成するのに下記を参考にさせてもらいました。

AWS CodeBuildを使ってDockerイメージをビルドし、Amazon EC2 Container Registry(ECR)へpushする ｜ DevelopersIO
https://dev.classmethod.jp/tool/docker/20170225-codebuild-docker/

CodeBuild で Docker イメージに Git のコミットIDをタグ付けてバージョン管理する ｜ DevelopersIO
https://dev.classmethod.jp/cloud/aws/docker-image-tag-git-commit-id-by-codebuild/

DockerイメージをCodeBuildでビルドする場合、プロジェクト作成時に`PrivilegedMode` を指定しないとだめだったのがポイントです。

CodeBuildでDocker in Dockerする(CloudFormation) - Qiita
https://qiita.com/sot528/items/ab0285b9907a29be840a

AWS::CodeBuild::Project Environment - AWS CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-codebuild-project-environment.html

ECRリポジトリに`RepositoryPolicyText` を指定していますが、これはPushしたイメージを別のCodeBuildでPullする場合に必要となります。

CodeBuild の Amazon ECR サンプル - AWS CodeBuild
https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/sample-ecr.html

```yaml:template.yaml
---
AWSTemplateFormatVersion: 2010-09-09
Description: Create Unity ML-Agents Docker Image

Parameters:
  ProjectName:
    Description: Project Name for CodeBuild
    Default: ml-agents-build-test
    Type: String
  InputBucketName:
    Description: Input Bucket Name
    Default: codebuild-input
    Type: String
  OutputBucketName:
    Description: Output Bucket Name
    Default: codebuild-output
    Type: String
  ECRRepositoryName:
    Description: ECR Repository Name
    Default: ml-agents
    Type: String

Resources:
  InputBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${InputBucketName}
      AccessControl: Private
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True

  OutputBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${OutputBucketName}
      AccessControl: Private
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True

  ECRRepository:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: !Sub ${ECRRepositoryName}
      RepositoryPolicyText:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - codebuild.amazonaws.com
            Action:
              - ecr:GetDownloadUrlForLayer
              - ecr:BatchGetImage
              - ecr:BatchCheckLayerAvailability

  CodeBuildServiceRole:
    Type: AWS::IAM::Role
    Properties:
      Path: /
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - codebuild.amazonaws.com
            Action:
                - sts:AssumeRole
      Policies:
        - PolicyName: !Sub CodeBuildPolicy-${ProjectName}
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Resource:
                  - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${ProjectName}
                  - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/${ProjectName}:*
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
              - Effect: Allow
                Resource:
                  - !Sub arn:aws:s3:::codepipeline-${AWS::Region}-*
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:GetObjectVersion
              - Effect: Allow
                Resource:
                  - !Sub arn:aws:s3:::${InputBucketName}/*
                Action:
                  - s3:GetObject
                  - s3:GetObjectVersion
              - Effect: Allow
                Resource:
                  - !Sub arn:aws:s3:::${OutputBucketName}/*
                Action:
                  - s3:PutObject
              - Effect: Allow
                Resource:
                  - "*"
                Action:
                  - ecr:BatchCheckLayerAvailability
                  - ecr:CompleteLayerUpload
                  - ecr:GetAuthorizationToken
                  - ecr:InitiateLayerUpload
                  - ecr:PutImage
                  - ecr:UploadLayerPart

  CodeBuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
      Name: !Ref ProjectName
      Description: this is a test prj
      ServiceRole: !Ref CodeBuildServiceRole
      Artifacts:
        Location: !Ref OutputBucketName
        Type: S3
        Name: artifacts
        Path: !Sub ${ProjectName}
        NamespaceType: BUILD_ID
      Environment:
        Type: LINUX_CONTAINER
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/standard:2.0
        PrivilegedMode: True
      Source:
        Location: !Join [ "/", [ !Ref InputBucketName, "dummy.zip" ] ]
        Type: S3
      TimeoutInMinutes: 10
      Tags:
        - Key: Name
          Value: !Ref ProjectName
```

テンプレートが用意できたらAWS CLIでスタック作成します。サービスロールを作成するので`--capabilities CAPABILITY_IAM` が必要になります。
`--stack-name` や`--region` はお好みで。`--parameters` を指定するとバケット名やプロジェクト名も指定できます。

```console
> cd テンプレートがあるディレクトリ
> aws cloudformation create-stack \
  --stack-name kai-ml-agents-build \
  --template-body file://template.yaml \
  --capabilities CAPABILITY_IAM \
  --region ap-northeast-1 \
  --parameters '[
      {
        "ParameterKey": "ProjectName",
        "ParameterValue": "ml-agents-build-test"
      },
      {
        "ParameterKey": "InputBucketName",
        "ParameterValue": "kai-ml-agents-build-input"
      },
      {
        "ParameterKey": "OutputBucketName",
        "ParameterValue": "kai-ml-agents-build-output"
      },
      {
        "ParameterKey": "ECRRepositoryName",
        "ParameterValue": "ml-agents"
      }
  ]'

{
    "StackId": "arn:aws:cloudformation:ap-northeast-1:xxxxxxxxxxxx:stack/kai-ml-agents-build/0fa901a0-f56f-11e9-b734-06fcc76a2472"
}
```

スタック実行が完了したか確認します。`"CREATE_COMPLETE"` になったらOK。

```console
> aws cloudformation describe-stacks \
  --region ap-northeast-1 \
  --stack-name kai-ml-agents-build \
  --query "Stacks[0].StackStatus"

"CREATE_COMPLETE"
```

### CodeBuildのビルド実行準備

作成したCodeBuildのプロジェクトでビルド実行するための前準備をします。


#### S3バケットにダミーファイルをアップロードする

Unity ML-Agentsのソースはビルド実行時に取得するので、ビルドに必要となるソースがありません。
それでも、CodeBuildでビルド実行するにはCFnでプロジェクト作成時に指定した`Source.Location` のファイルがないとビルド実行時にダウンロードエラーとなるのでダミーのファイルを用意します。ファイルはZipファイルじゃないとだめでした。

```console
> touch dummy.txt
> zip dummy.zip dummy.txt
  adding: dummy.txt (stored 0%)

> aws s3 cp dummy.zip s3://kai-ml-agents-build-input/
upload: ./dummy.zip to s3://kai-ml-agents-build-input/dummy.zip
```

#### buildspec.yamlを用意する

CodeBuildのプロジェクトでビルド実行する際の手順を記述した`buildspec.yaml` を用意します。

```yaml:buildspec.yaml
version: 0.2

phases:
  install:
    runtime-versions:
      docker: 18
  pre_build:
    commands:
      - echo Clone ML-Agents...
      - git clone https://github.com/Unity-Technologies/ml-agents.git -b $IMAGE_TAG --depth 1
      - echo Logging in to Amazon ECR...
      - $(aws ecr get-login --no-include-email --region $AWS_DEFAULT_REGION)
      - nohup /usr/local/bin/dockerd --host=unix:///var/run/docker.sock --host=tcp://127.0.0.1:2375 --storage-driver=overlay&
      - timeout 15 sh -c "until docker info; do echo .; sleep 1; done"
      - AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
  build:
    commands:
      - docker build -t $ECR_REPOSITORY_NAME:$IMAGE_TAG ./ml-agents
      - docker tag $ECR_REPOSITORY_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$ECR_REPOSITORY_NAME:$IMAGE_TAG
  post_build:
    commands:
      - echo Pushing the Docker image...
      - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$ECR_REPOSITORY_NAME:$IMAGE_TAG
```

ポイントは以下となります。

##### DockerイメージをビルドするのにDockerデーモンを初期化する

DockerイメージをCodeBuildでビルドするのに、プロジェクト作成時に`PrivilegedMode` を指定しましたが、ビルド時にもDockerデーモンを初期化する必要がありました。

AWS::CodeBuild::Project Environment - AWS CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-codebuild-project-environment.html

>ビルドが Docker デーモンと連係動作できるように、Docker デーモンも起動する必要があります。これを行う 1 つの方法は、以下のビルドコマンドを実行してビルド仕様のインストールフェーズで Docker デーモンを初期化することです。(指定したビルド環境イメージの提供元が Docker 対応の AWS CodeBuild である場合は、これらのコマンドを実行しないでください。)


```yaml:buildspec.yaml抜粋
  pre_build:
    commands:
      (略)
      - nohup /usr/local/bin/dockerd --host=unix:///var/run/docker.sock --host=tcp://127.0.0.1:2375 --storage-driver=overlay&
      - timeout 15 sh -c "until docker info; do echo .; sleep 1; done"
      (略)
```

### ビルド実行する

`buildspec.yaml` が用意できたらCodeBuildのプロジェクトでビルド実行します。
だいたい10分くらいでビルド完了します。CFnでプロジェクト作成時にタイムアウト時間を10分にしましたが、念の為`--timeout-in-minutes-override` オプションで20分に変更しています。

```console
> aws codebuild start-build \
  --region ap-northeast-1 \
  --project-name ml-agents-build-test \
  --buildspec-override file://buildspec.yaml \
  --timeout-in-minutes-override 20 \
  --environment-variables-override '[
    {
      "name": "ECR_REPOSITORY_NAME",
      "value": "ml-agents",
      "type": "PLAINTEXT"
    },
    {
      "name": "IMAGE_TAG",
      "value": "0.9.1",
      "type": "PLAINTEXT"
    }
  ]'

{
    "build": {
        "id": "ml-agents-build-test:17bfb5c6-8c8e-480c-a445-9e5dbb4b6004",
        "arn": "arn:aws:codebuild:ap-northeast-1:xxxxxxxxxxxx:build/ml-agents-build-test:17bfb5c6-8c8e-480c-a445-9e5dbb4b6004",
        "startTime": 1571821284.995,
        "currentPhase": "QUEUED",
        "buildStatus": "IN_PROGRESS",
(略)
```

`aws codebuild batch-get-builds` コマンドでビルドが完了したか確認します。各フェーズのステータスが`SUCCEEDED` で最後に`"phaseType": "COMPLETED"` があったらビルド完了です。

```console
> aws codebuild batch-get-builds \
  --region ap-northeast-1 \
  --ids ml-agents-build-test:17bfb5c6-8c8e-480c-a445-9e5dbb4b6004 \
  --query "builds[0].phases"

[
    {
        "phaseStatus": "SUCCEEDED",
        "endTime": 1571821285.18,
        "phaseType": "SUBMITTED",
        "durationInSeconds": 0,
        "startTime": 1571821284.995
    },
    {
        "phaseStatus": "SUCCEEDED",
        "endTime": 1571821286.424,
        "phaseType": "QUEUED",
        "durationInSeconds": 1,
        "startTime": 1571821285.18
    },
    {
        "contexts": [
            {
                "message": "",
                "statusCode": ""
            }
        ],
        "phaseType": "PROVISIONING",
        "phaseStatus": "SUCCEEDED",
        "durationInSeconds": 21,
        "startTime": 1571821286.424,
        "endTime": 1571821307.801
    },
    {
        "contexts": [
            {
                "message": "",
                "statusCode": ""
            }
        ],
        "phaseType": "DOWNLOAD_SOURCE",
        "phaseStatus": "SUCCEEDED",
        "durationInSeconds": 0,
        "startTime": 1571821307.801,
        "endTime": 1571821308.509
    },
    {
        "contexts": [
            {
                "message": "",
                "statusCode": ""
            }
        ],
        "phaseType": "INSTALL",
        "phaseStatus": "SUCCEEDED",
        "durationInSeconds": 0,
        "startTime": 1571821308.509,
        "endTime": 1571821308.667
    },
    {
        "contexts": [
            {
                "message": "",
                "statusCode": ""
            }
        ],
        "phaseType": "PRE_BUILD",
        "phaseStatus": "SUCCEEDED",
        "durationInSeconds": 17,
        "startTime": 1571821308.667,
        "endTime": 1571821326.049
    },
    {
        "contexts": [
            {
                "message": "",
                "statusCode": ""
            }
        ],
        "phaseType": "BUILD",
        "phaseStatus": "SUCCEEDED",
        "durationInSeconds": 744,
        "startTime": 1571821326.049,
        "endTime": 1571822070.764
    },
    {
        "contexts": [
            {
                "message": "",
                "statusCode": ""
            }
        ],
        "phaseType": "POST_BUILD",
        "phaseStatus": "SUCCEEDED",
        "durationInSeconds": 38,
        "startTime": 1571822070.764,
        "endTime": 1571822109.51
    },
    {
        "contexts": [
            {
                "message": "",
                "statusCode": ""
            }
        ],
        "phaseType": "UPLOAD_ARTIFACTS",
        "phaseStatus": "SUCCEEDED",
        "durationInSeconds": 0,
        "startTime": 1571822109.51,
        "endTime": 1571822109.701
    },
    {
        "contexts": [
            {
                "message": "",
                "statusCode": ""
            }
        ],
        "phaseType": "FINALIZING",
        "phaseStatus": "SUCCEEDED",
        "durationInSeconds": 2,
        "startTime": 1571822109.701,
        "endTime": 1571822111.876
    },
    {
        "phaseType": "COMPLETED",
        "startTime": 1571822111.876
    }
]

```

### AWS ECRのリポジトリからイメージを取得する

AWS ECRのリポジトリにイメージが登録されたか確認して、実際にイメージを取得・利用してみます。
`<AWS_ACCOUNT_ID>` と`<REGION>` は置き換えてください。

```console
> aws ecr list-images \
  --repository-name ml-agents \
  --region <REGION>

{
    "imageIds": [
        {
            "imageTag": "0.9.1",
            "imageDigest": "sha256:b9da2732606536ada8d3eabe258ac44286ae8c0706c9325d008880a2f2855f1c"
        }
    ]
}


# デフォルトレジストリに対してDockerを認証する
> $(aws ecr get-login --no-include-email --region <REGION>)

WARNING! Using --password via the CLI is insecure. Use --password-stdin.
Login Succeeded


> docker pull <AWS_ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com/ml-agents:0.9.1

(略)
1b38598abf23: Pull complete
Digest: sha256:b9da2732606536ada8d3eabe258ac44286ae8c0706c9325d008880a2f2855f1c
Status: Downloaded newer image for <AWS_ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com/ml-agents:0.9.1
<AWS_ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com/ml-agents:0.9.1


> docker run -it --rm \
  <AWS_ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com/ml-agents:0.9.1



                        ▄▄▄▓▓▓▓
                   ╓▓▓▓▓▓▓█▓▓▓▓▓
              ,▄▄▄m▀▀▀'  ,▓▓▓▀▓▓▄                           ▓▓▓  ▓▓▌
            ▄▓▓▓▀'      ▄▓▓▀  ▓▓▓      ▄▄     ▄▄ ,▄▄ ▄▄▄▄   ,▄▄ ▄▓▓▌▄ ▄▄▄    ,▄▄
          ▄▓▓▓▀        ▄▓▓▀   ▐▓▓▌     ▓▓▌   ▐▓▓ ▐▓▓▓▀▀▀▓▓▌ ▓▓▓ ▀▓▓▌▀ ^▓▓▌  ╒▓▓▌
        ▄▓▓▓▓▓▄▄▄▄▄▄▄▄▓▓▓      ▓▀      ▓▓▌   ▐▓▓ ▐▓▓    ▓▓▓ ▓▓▓  ▓▓▌   ▐▓▓▄ ▓▓▌
        ▀▓▓▓▓▀▀▀▀▀▀▀▀▀▀▓▓▄     ▓▓      ▓▓▌   ▐▓▓ ▐▓▓    ▓▓▓ ▓▓▓  ▓▓▌    ▐▓▓▐▓▓
          ^█▓▓▓        ▀▓▓▄   ▐▓▓▌     ▓▓▓▓▄▓▓▓▓ ▐▓▓    ▓▓▓ ▓▓▓  ▓▓▓▄    ▓▓▓▓`
            '▀▓▓▓▄      ^▓▓▓  ▓▓▓       └▀▀▀▀ ▀▀ ^▀▀    `▀▀ `▀▀   '▀▀    ▐▓▓▌
               ▀▀▀▀▓▄▄▄   ▓▓▓▓▓▓,                                      ▓▓▓▓▀
                   `▀█▓▓▓▓▓▓▓▓▓▌
                        ¬`▀▀▀█▓


Usage:
      mlagents-learn <trainer-config-path> [options]
      mlagents-learn --help
```

Dockerコンテナで`mlagents-learn` コマンドが実行されたのが確認できました。やったぜ

実際にML-Agentsを動作させる手順は下記をご参考ください。

DockerでUnity ML-Agentsを動作させる(v0.9.1対応) - Qiita
https://qiita.com/kai_kou/items/0c2f3c7d22363fd91e4e



## 参考


DockerイメージをCodeBuildでビルドするのに、プロジェクト作成時に`PrivilegedMode` を指定しましたが、ビルド時にもDockerデーモンを初期化する必要がありました。

Google Cloud Buildを利用してUnity ML-AgentsのDockerイメージを作成する(v0.9.1対応) - Qiita
https://qiita.com/kai_kou/items/5eabbaa2b564527e0b4e

AWS CodeBuildを使ってDockerイメージをビルドし、Amazon EC2 Container Registry(ECR)へpushする ｜ DevelopersIO
https://dev.classmethod.jp/tool/docker/20170225-codebuild-docker/

CodeBuild で Docker イメージに Git のコミットIDをタグ付けてバージョン管理する ｜ DevelopersIO
https://dev.classmethod.jp/cloud/aws/docker-image-tag-git-commit-id-by-codebuild/

CodeBuildでDocker in Dockerする(CloudFormation) - Qiita
https://qiita.com/sot528/items/ab0285b9907a29be840a

AWS::CodeBuild::Project Environment - AWS CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-codebuild-project-environment.html

AWS::CodeBuild::Project Environment - AWS CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-codebuild-project-environment.html

Amazon ECR で AWS CLI を使用する - Amazon ECR
https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/ECR_AWSCLI.html

CodeBuild のビルドの詳細を表示する - AWS CodeBuild https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/view-build-details.html#view-build-details-cli

ビルド環境の環境変数 - AWS CodeBuild
https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-env-ref-env-vars.html

CodeBuildでビルドプロジェクトを作成する-AWS CodeBuild
https://docs.aws.amazon.com/codebuild/latest/userguide/create-project.html#create-project-cli

CodeBuild のビルドの詳細を表示する - AWS CodeBuild
https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/view-build-details.html

CodeBuild のビルド仕様に関するリファレンス - AWS CodeBuild
https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-spec-ref.html#runtime-versions-buildspec-file

CodeBuild でビルドを実行する - AWS CodeBuild
https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/run-build.html#run-build-cli

CodeBuild の Docker サンプル - AWS CodeBuild
https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/sample-docker.html

Amazon ECR レジストリ - Amazon ECR
https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/Registries.html#registry_auth

CodeBuild の Amazon ECR サンプル - AWS CodeBuild
https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/sample-ecr.html