M2 MacでCodeBuildをローカルで実行する。

# はじめに
勉強のため、`AWS`の`CodeBuild`を使ってみようと思ってドキュメントを見ていたところ、いちいちマネジメントコンソール画面から`CodeBuild`を実行しなくてもローカルマシンにビルド環境を構築することができるとのことなので、`M2 Mac`のマシンでローカルビルド環境を構築してみました。

# CodeBuildとは
アプリケーションを開発して動作させるようにするには、ソースコードをコンパイルしたり、必要なファイル等をまとめて資材を作成したりする必要がありますが、そのような面倒な作業を`AWS`側のサービスとして提供して、開発者の手間を減らしたり、`CodeDeploy`や`CodePipeline`等の他サービスと連携することでアプリケーションのリリース作業を自動化したりするためのサービスです。

実際にどのファイルをどのようなコマンドオプションでコンパイルする等の`CodeBuild`で行う作業は`buildspec.yml`というファイルに記述して、その内容を`CodeBuild`で実行することで、指定のビルドが行えるようになっております。

# CodeBuildをローカルで実行するということ
上述の通り、`CodeBuild`は`AWS`のマネージドサービスとなるため、`buildspec.yml`で記述した設定が正しいかを確かめるためには通常`AWS`のマネジメントコンソール画面や`AWS CLI`から実際に実行してみるしかありませんが、作成中の`buildspec.yml`をいちいち`CodeBuild`の画面から実行してデバッグしていくのは一苦労ということで、ローカルマシン上でも環境を用意すれば`CodeBuild`を実行できるようにしているそうです。

# ローカルマシンで実行する場合とAWS上でCodeBuildを実行する場合の違い
`CodeBuild`はビルドプロジェクトと言うビルドの設計図を実行する際、**Dockerコンテナ**を起動して、`buildspec.yml`の内容を実行する仕組みとなっています。

ローカルで実行する場合も`CodeBuild`で実際に使用している**コンテナイメージ**を使用してローカルで`buildspec.yml`の内容を実行する仕組みとなるため、**Dockerコンテナ**をどこで実行しているかという違いはありますが、環境の違い以外は同等のことが行えるようです。

ちなみに、`CodeBuild`はアプリケーションのビルドを実行することを目的としておりますが、そもそもが**Dockerコンテナ**なので`buildspec.yml`の書き方次第でビルドとは関係ない処理も実行することができるそうなので、知っておくと設計の幅を広げることもできるかもしれません。

- [AWS CodeBuild エージェントを使用してビルドをローカルで実行](https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/use-codebuild-agent.html)

# CodeBuildをローカルで実行する準備
導入方法は以下のAWS公式ドキュメントに記載されているため、こちらに沿って実行していきます。

- [AWS CodeBuild エージェントを使用してビルドをローカルで実行](https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/use-codebuild-agent.html)

`M2`の`MacBook Air`にAWS公式イメージを導入する場合の流れは以下となります。

　- Gitクライアントインストール
　- Dockerインストール
　- Amazon Linux 2 のDockerイメージのダウンロード
　- エージェントのDockerイメージのダウンロード
　- CodeBuildエージェントのダウンロード
　- buildspec.ymlの作成
　- CodeBuildエージェントの実行
　
## Gitクライアント、Dockerインストール
こちらは今回は紹介しませんが、それぞれダウンロードしてインストールしておきます。

## Amazon Linux 2 のDockerイメージダウンロード
[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/use-codebuild-agent.html)通り、`docker pull`コマンドでDockerイメージをダウンロードします。
バージョンについてはドキュメントのバージョンより新しいバージョンが出ているようなので、以下のように指定します。

```bash:Dockerイメージダウンロード
docker pull public.ecr.aws/codebuild/amazonlinux2-x86_64-standard:4.0
```

## エージェントのDockerイメージダウンロード
上記と同様`docker pull`コマンドでダウンロードします。

今回は実行マシンとして`M2 Mac`を使用するので、ARM版をダウンロードします。

```bash:エージェント用Dockerイメージダウンロード
docker pull public.ecr.aws/codebuild/local-builds:aarch64
```

# CodeBuildエージェントのダウンロード
以下のように実行してエージェントをダウンロードします。

公式ドキュメントは`wget`でダウンロードしておりますが、`M2 Mac`にはデフォルトで`wget`が含まれていないため、`curl`コマンドを以下のようにしてダウンロードします。

```bash:CodeBuildエージェントのダウンロード
curl -OL https://raw.githubusercontent.com/aws/aws-codebuild-docker-images/master/local_builds/codebuild_build.sh
chmod +x codebuild_build.sh
```

# buildspec.ymlの作成
テストのため以下のような内容の`buildspec.yml`ファイルを作成して、先程ダウンロードしたCodeBuildエージェントのシェルと同じ場所に格納しておきます。

定番の**Hello World**を表示するビルド定義ファイルです。

```json:buildspec.yml
version: 0.2

phases:
  install:
    commands:
      - echo 'Hello World'
```

# CodeBuildエージェントの実行
最低限以下のようにして実行することでローカルで実行できます。

`-a`で指定するアーティファクト（生成物）は特に生成するような設定をしていなくても指定する必要があるので、適当な場所を指定しておきます。

```bash:CloudBuildエージェントの実行
./codebuild_build.sh -i public.ecr.aws/codebuild/amazonlinux2-x86_64-standard:4.0 -a artifact_output -l public.ecr.aws/codebuild/local-builds:aarch64
```

以下の結果のように`Hello World`の表示が出力され、exitコードが0になっていればOKです。

```bash:実行結果の抜粋
（省略）
agent_1  | [Container] 2022/09/01 12:54:01 Running command echo 'Hello World'
agent_1  | Hello World
agent_1  |
（省略）
agent-resources_agent_1 exited with code 0
Stopping agent-resources_build_1 ... done
Aborting on container exit...
```

# codebuild_build.shのオプション
`codebuild_build.sh`をオプション無しで実行した際の`USAGE`でも内容が確認できますが、以下のオプションが指定可能です。

下記の書式の通り、`-i`と`-a`が必須オプションとなります。

```bash:codebuild_build.shの書式
codebuild_build.sh [-i image_name] [-a artifact_output_directory] [options]
```

|オプション|説明|
|:--|:--|
|-i [イメージ名]|使用するコンテナイメージの指定（必須）|
|-a [出力先]|アーティファクト（生成物）の出力先の指定（必須）|
|-l [イメージ名]|デフォルトのローカルエージェントイメージを上書き|
|-r [出力先]|レポートの出力先の指定|
|-s [ディレクトリパス]|ソースディレクトリの場所の指定<br>２つ目以降は、`<sourceIdentifier>:<sourceLocation>`の書式で指定|
|-c|ローカルのAWS認証情報を使用する場合に指定|
|-p [プロファイル名]|デフォルト以外のプロファイルを使用する場合に指定<br>指定する場合は-cと一緒に使用|
|-b [ファイルパス]|buildspec.ymlの名前以外でファイル名を使用する場合に指定<br>指定する場合、カレントディレクトリからの相対パスか絶対パスで指定|
|-m|ソースディレクトリをコンテナに直接マウントする場合に指定|
|-d|特権モードでDockerコンテナを実行する場合に指定|
|-e [環境変数ファイルパス]|環境変数を記載したファイルを使用する場合に指定|

それぞれ`CodeBuild`のマネジメントコンソール画面の設定値に当てはまる内容となり、現状は上記のオプションが全てとなります。

以下でいくつかのオプションについて補足します。

## -l デフォルトローカルエージェントイメージ上書き
特にオプションを指定しない場合、デフォルトの`public.ecr.aws/codebuild/local-builds:latest`が指定されますが、デフォルト以外のローカルエージェントイメージを使用したい場合に指定します。

例えばARM用のローカルエージェントイメージとなる`public.ecr.aws/codebuild/local-builds:aarch64`を使用したい場合は`-l`が必要となります。

## -r テストレポートの出力先の指定
`JUnit`や`JaCoCo`といったテストフレームワークを使ったテストを`buildspec.yml`に盛り込んでいる場合、テストレポートを指定の場所に出力できるとのこと。

テストフレームワークについて私は詳しくないので、気になる方は以下のようなサイトをご確認ください。

- [CodeBuildのReportを試してみた](https://hack.nikkei.com/blog/advent20201215/)


## -s ソースディレクトリの場所の指定
マネジメントコンソール上の設定で言うと、**ソース**の設定に当たります。

ローカルで実行する場合、`CodeCommit`や`S3`をソースに指定できないため、指定するディレクトリはローカルのパスとなり、何も指定しなかった場合はカレントディレクトリがソースディレクトリとなります。

また、１つ目のソースディレクトリに`buildspec.yml`の名前でビルド定義ファイルを格納している場合は、自動で認識するため、`-b`オプションは不要となります。

逆に１つ目のソースディレクトリ内に`buildspec.yml`が存在しなかったり、別のファイル名にしている場合は`-b`オプションで指定する必要があります。

２つ以上のソースディレクトリを指定する場合は、ディレクトリパス以外に`<sourceIdentifier>`という識別子の指定が必要となります。

これは、ソースディレクトリを呼び出す際、内部的に以下のような予約環境変数を使用しているため、識別子がないと識別できないからとなります。

|変数|説明|
|:--|:--|
|CODEBUILD_SRC_DIR|１つ目のソースディレクトリパスを格納している変数|
|CODEBUILD_SRC_DIR_[識別子]|１つ目以降のソースディレクトリパスを格納している変数|

予約されている変数は以下を参照してください。

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

## -c、-p AWS認証情報の使用
`-c`は`buildspec.yml`内で`AWS CLI`を使用したい場合にローカルマシンに設定しているAWS認証情報（`~/.aws`配下の情報）を使いたい場合に指定するオプションです。

また、`-p`は`default`プロファイル以外のプロファイルを使用したい場合に指定するオプションです。

## -m ソースディレクトリをコンテナに直接マウント
ローカルで`CodeBuild`を実行する場合、通常はDockerコンテナ起動時にソースディレクトリのファイルをコンテナ内にコピーしてから処理が実行されるそうですが、`-m`を使用すると、ソースディレクトリのファイルをコピーするのではなく、ソースディレクトリを直接マウントする動作となるようです。

試しにソースディレクトリ配下に1GBのファイルを作成し、起動してみたところ、`-m`をつけない場合はソースディレクトリ配下のファイルダウンロードに20秒かかっているのに対し、`-m`をつけてDockerコンテナに直接マウントさせたところ、1秒ですみました。

```bash:-mをつけない場合のダウンロード時間
（抜粋）
agent_1  | [Container] 2022/09/02 11:58:20 Waiting for agent ping
agent_1  | [Container] 2022/09/02 11:58:21 Waiting for DOWNLOAD_SOURCE
agent_1  | [Container] 2022/09/02 11:58:41 Phase is DOWNLOAD_SOURCE
```

```bash:-mをつけた場合のダウンロード時間
（抜粋）
agent_1  | [Container] 2022/09/02 11:59:01 Waiting for agent ping
agent_1  | [Container] 2022/09/02 11:59:02 Waiting for DOWNLOAD_SOURCE
agent_1  | [Container] 2022/09/02 11:59:03 Phase is DOWNLOAD_SOURCE
```

実際の`CodeCommit`のマネジメントコンソール画面上にはそのような項目は見当たらなかったので、ローカル実行する場合だけの設定なのかな？

## -e 環境変数の指定
`CodeBuild`で環境変数を設定したい場合、`buildspec.yml`の`env`シーケンスに直接記載するか、マネジメントコンソールから`CodeBuild`の定義を行う際に設定するなどの方法で設定できますが、ローカル実行する場合、マネジメントコンソールから環境変数を設定する代わりに、環境変数を記載したファイルを作成して、そのファイルを`CodeBuild`実行時に読み込ませることでビルド定義ファイル内で変数として使用できます。

変数の書き方は一般的なシェルの環境変数の指定方法で記載すればOKです。

# codebuild_build.shで作られるDockerコンテナ削除
`codebuild_build.sh`の仕様というより`Docker`の仕様ですが、`codebuild_build.sh`で`CodeBuild`をローカル実行するとローカルビルド時に使用した終了済みのコンテナが残ってしまいリソースを圧迫してしまいます。（以下の場合`NAMES`が`agent-resources_build_1`と`agent-resources_agent_1`以外のコンテナが実行後の不要コンテナ）

```bash:何度かローカル実行した後のdocker_ps_-a結果
CONTAINER ID   IMAGE                                                       COMMAND                  CREATED         STATUS                       PORTS                    NAMES
50d3da581ad7   public.ecr.aws/codebuild/amazonlinux2-x86_64-standard:4.0   "sh -c 'while [ ! -f…"   3 minutes ago   Exited (137) 3 minutes ago                            agent-resources_build_1
c9968c8c4740   public.ecr.aws/codebuild/local-builds:aarch64               "local_build.sh"         3 minutes ago   Exited (0) 3 minutes ago                              agent-resources_agent_1
fe6fb6edb995   public.ecr.aws/codebuild/local-builds:aarch64               "local_build.sh"         3 minutes ago   Exited (0) 3 minutes ago                              stoic_cartwright
1f3947824af4   public.ecr.aws/codebuild/local-builds:aarch64               "local_build.sh"         3 minutes ago   Exited (0) 3 minutes ago                              sad_wilbur
5a2aefe94637   public.ecr.aws/codebuild/local-builds:aarch64               "local_build.sh"         4 minutes ago   Exited (0) 3 minutes ago                              jovial_lichterman
4f95a2ce88f4   public.ecr.aws/codebuild/local-builds:aarch64               "local_build.sh"         4 minutes ago   Exited (0) 4 minutes ago                              infallible_hellman
795095d32638   public.ecr.aws/codebuild/local-builds:aarch64               "local_build.sh"         4 minutes ago   Exited (0) 4 minutes ago                              sleepy_cori
6d6acece1148   public.ecr.aws/codebuild/local-builds:aarch64               "local_build.sh"         4 minutes ago   Exited (0) 4 minutes ago                              intelligent_williamson
```

そもそも`codebuild_build.sh`を使う目的として`CodeBuild`の動作を確認するためであり、再利用する必要も無いことから`codebuild_build.sh`の中身を以下のように少し編集して、実行完了後に不要となるコンテナが削除されるようにしたほうが良いでしょう。

```bash:codebuild_build.sh実行終了後に不要コンテナを削除するオプション（99行目付近）
docker_command="docker run -it "
↓↓以下に変更↓↓
docker_command="docker run --rm -it "
```

# （おまけ）オプションもりもりの実行
オプション動作が分かりづらいものもあるため、実行例かねてオプションもりもりで実行してみようと思います。

以下のような想定で実施します。

|設定|説明|
|:--|:--|
|アーティファクトディレクトリ|codebuild/artifact|
|１つ目のソースディレクトリ|codebuild/sourcedir1|
|2つ目のソースディレクトリ|codebuild/sourcedir2|
|識別子|IDENTIFIER|
|AWS認証情報プロファイル|codebuild|
|ソースディレクトリマウント|する|
|ビルド定義ファイル名|codebuild.yml|
|環境変数ファイル名|variable.env|

```bash:variable.env
VAR="variable_test"
```

```yml:codebuild.yml
version: 0.2

phases:
  install:
    commands:
      - echo ${VAR}
      - ls -l ${CODEBUILD_SRC_DIR}/
      - ls -l ${CODEBUILD_SRC_DIR_IDENTIFIER}/
      - aws s3 ls
artifacts:
  files:
    - '**/*'
  name: codebuild/artifact
```

```shell:実行マシン側ディレクトリ構成
~/
├── codebuild/
│　　　　　　├──　artifact/
│　　　　　　├──　sourcedir1/
│   │   ├── codebuild.yml
│   │   └── testfile1
│　　　　　　└──　sourcedir2/
│       └── testfile2
├── codebuild_build.sh
└── variable.env
```

ビルド定義ファイルはDockerでの動作を確認するため、以下のコマンドを実行します。

|コマンド|用途|
|:--|:--|
|echo ${VAR}|環境変数呼び出し確認|
|ls -l ${CODEBUILD_SRC_DIR}/|１つ目のソースディレクトリ変数が呼び出せるかの確認|
|ls -l ${CODEBUILD_SRC_DIR_IDENTIFIER}/|２つ目のソースディレクトリ変数が呼び出せるかの確認|
|aws s3 ls|aws-cliでAWS環境にアクセスできるかの確認|

２つ目のソースディレクトリを指定している変数は実行時に指定する識別子によって変わるので環境によって変更してください。

また、aws-cliは、実行マシン側のIAMユーザ情報を使ってアクセスするため、もしアクセスできなかった場合は権限周りを確認しましょう。

```bash:オプションもりもりのコマンド例
./codebuild_build.sh -c -m \
-i public.ecr.aws/codebuild/amazonlinux2-x86_64-standard:4.0 \
-a codebuild/artifact \
-l public.ecr.aws/codebuild/local-builds:aarch64 \
-e variable.env \
-p codebuild \
-s codebuild/sourcedir1 \
-s IDENTIFIER:codebuild/sourcedir2 \
-b codebuild/sourcedir1/codebuild.yml
```

<details><summary>オプションもりもりのコマンド実行例（展開してください）</summary>

```bash:オプションもりもりのコマンド実行結果
Removing agent-resources_build_1 ... done
Removing agent-resources_agent_1 ... done
Removing network agent-resources_default
Removing volume agent-resources_source_volume
Removing volume agent-resources_user_volume
Creating network "agent-resources_default" with the default driver
Creating volume "agent-resources_source_volume" with local driver
Creating volume "agent-resources_user_volume" with local driver
Creating agent-resources_agent_1 ... done
Creating agent-resources_build_1 ... done
Attaching to agent-resources_agent_1, agent-resources_build_1
agent_1  | [Container] 2022/09/02 13:05:05 Waiting for agent ping
agent_1  | [Container] 2022/09/02 13:05:06 Waiting for DOWNLOAD_SOURCE
agent_1  | [Container] 2022/09/02 13:05:07 Phase is DOWNLOAD_SOURCE
agent_1  | [Container] 2022/09/02 13:05:07 CODEBUILD_SRC_DIR=/codebuild/output/src041/src
agent_1  | [Container] 2022/09/02 13:05:07 CODEBUILD_SRC_DIR_IDENTIFIER=/codebuild/output/src041/secSrc/IDENTIFIER
agent_1  | [Container] 2022/09/02 13:05:07 YAML location is /codebuild/output/srcDownload/src/codebuild.yml
agent_1  | [Container] 2022/09/02 13:05:07 Processing environment variables
agent_1  | [Container] 2022/09/02 13:05:08 Moving to directory /codebuild/output/src041/src
agent_1  | [Container] 2022/09/02 13:05:08 Registering with agent
agent_1  | [Container] 2022/09/02 13:05:08 Phases found in YAML: 1
agent_1  | [Container] 2022/09/02 13:05:08  INSTALL: 4 commands
agent_1  | [Container] 2022/09/02 13:05:08 Phase complete: DOWNLOAD_SOURCE State: SUCCEEDED
agent_1  | [Container] 2022/09/02 13:05:08 Phase context status code:  Message:
agent_1  | [Container] 2022/09/02 13:05:08 Entering phase INSTALL
agent_1  | [Container] 2022/09/02 13:05:08 Running command echo ${VAR}
agent_1  | "variable_test"
agent_1  |
agent_1  | [Container] 2022/09/02 13:05:08 Running command ls -l ${CODEBUILD_SRC_DIR}/
agent_1  | total 4
agent_1  | -rw-r--r-- 1 root root 228 Sep  2 12:59 codebuild.yml
agent_1  | -rw-r--r-- 1 root root   0 Sep  1 13:19 testfile1
agent_1  |
agent_1  | [Container] 2022/09/02 13:05:08 Running command ls -l ${CODEBUILD_SRC_DIR_IDENTIFIER}/
agent_1  | total 0
agent_1  | -rw-r--r-- 1 root root 0 Sep  1 13:19 testfile2
agent_1  |
agent_1  | [Container] 2022/09/02 13:05:08 Running command aws s3 ls
agent_1  | 2022-08-26 12:31:35 [s3バケット名]
agent_1  | 2022-07-23 04:28:16 [s3バケット名]
agent_1  | 2022-07-24 23:58:28 [s3バケット名]
agent_1  |
agent_1  | [Container] 2022/09/02 13:05:12 Phase complete: INSTALL State: SUCCEEDED
agent_1  | [Container] 2022/09/02 13:05:12 Phase context status code:  Message:
agent_1  | [Container] 2022/09/02 13:05:12 Entering phase PRE_BUILD
agent_1  | [Container] 2022/09/02 13:05:12 Phase complete: PRE_BUILD State: SUCCEEDED
agent_1  | [Container] 2022/09/02 13:05:12 Phase context status code:  Message:
agent_1  | [Container] 2022/09/02 13:05:12 Entering phase BUILD
agent_1  | [Container] 2022/09/02 13:05:12 Phase complete: BUILD State: SUCCEEDED
agent_1  | [Container] 2022/09/02 13:05:12 Phase context status code:  Message:
agent_1  | [Container] 2022/09/02 13:05:12 Entering phase POST_BUILD
agent_1  | [Container] 2022/09/02 13:05:12 Phase complete: POST_BUILD State: SUCCEEDED
agent_1  | [Container] 2022/09/02 13:05:12 Phase context status code:  Message:
agent_1  | [Container] 2022/09/02 13:05:12 Expanding base directory path: .
agent_1  | [Container] 2022/09/02 13:05:12 Assembling file list
agent_1  | [Container] 2022/09/02 13:05:12 Expanding .
agent_1  | [Container] 2022/09/02 13:05:12 Expanding file paths for base directory .
agent_1  | [Container] 2022/09/02 13:05:12 Assembling file list
agent_1  | [Container] 2022/09/02 13:05:12 Expanding **/*
agent_1  | [Container] 2022/09/02 13:05:13 Found 2 file(s)
agent_1  | [Container] 2022/09/02 13:05:13 Preparing to copy secondary artifacts
agent_1  | [Container] 2022/09/02 13:05:13 No secondary artifacts defined in buildspec
agent_1  | [Container] 2022/09/02 13:05:13 Phase complete: UPLOAD_ARTIFACTS State: SUCCEEDED
agent_1  | [Container] 2022/09/02 13:05:13 Phase context status code:  Message:
agent-resources_build_1 exited with code 0
Aborting on container exit...
```
</details>

S3バケット名だけ一部マスク。

コマンド実行完了後、`codebuild/artifact`にソースディレクトリが固められたZIPファイルも存在確認できました。

# おわりに
`CodeBuild`をマネジメントコンソール上で実行する場合、単純な内容のビルドでも数分かかりますが、ローカルで実行した場合、いくつか制約はあるにせよ、マネジメントコンソールから実行するより早く確認できるので、`buildspec.yml`自体の動作チェックを行うのに最適です。

また、`codebuild_build.sh`はオプションさえ覚えてしまえば非常に簡単にローカルで`CodeBuild`が試せることがわかりました。

`CodeBuild`はあまり触ったことがなかったのですが、実際にローカルで動かしてみて、`CodeBuild`自体、かなり自由度が高いサービスなのだと感じました。