AWS DevOps - AWS CodeBuildのビルド時間短縮

# はじめに
本記事では、AWS CodeBuildのキャッシュを利用してビルド時間を短縮する方法について紹介します。

AWSマネージドサービスを使用したCICDパイプラインでは、AWS Codeシリーズ[^1]を使用します。検証環境でAWS Codeシリーズを用いてCICDパイプラインを構築・運用した際、特に**CodeBuildを使用したビルド時間に大幅な時間を費やしていること**に対して課題と感じました。本記事では、その課題に対する改善方法についての調査結果を共有します。

尚、本記事の目的は、CodeBuildによるビルド時間を短縮する方法の情報提供となります。測定は検証環境で要したサンプルアプリ[^2]を使用して得られたものであり、測定結果の正確性、有用性などにつきましては、一切保証するものではない点をご了承ください。

また、本記事ではAWS各種サービスについての概要、およびビルド時間短縮に対して直接的に関係しない以下の設計・構築方法については割愛させていただきます。

* ブランチ戦略(Git flowなど)
* パラメータ管理方法
* デプロイ方法(ローリングアップデート/カナリアリリース/Blue Greenデプロイなど)

# 検証の背景
検証環境にて、疑似システム(以降、[モデルシステム](#モデルシステム)と呼称)の開発を行った際、以下のCICDパイプラインを構築しました。

![CICD_Pipeline.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641141/746c8c33-eec9-d495-23d3-d20ed6620dd0.png)

開発者がローカル開発環境で開発したソースコードをAWS CodeCommitへPushしたことを契機に、CICDパイプラインが起動します。アプリケーションのビルド、Dockerイメージの作成、ECRにDockerイメージをPush、CodeDeployによるECS on Fargateへのデプロイまでのプロセスを自動化しています。

なお、[モデルシステム](#モデルシステム)の実行環境はECS on Fargateを採用していますが、以降に説明するビルド時間短縮方法は、それ以外のAWSサービス(例えば、EC2やECS on EC2、EKS on EC2など)でも実現可能です。

## 課題
CICDパイプラインを構築してデリバリを自動化し、開発を効率化することができました。一方、頻繁にソースコードの更新を行い、CICDパイプラインを実行すると、アプリケーションのビルド～デプロイまでに時間がかかることがストレスに感じました。
工程ごとに処理時間を調査したところ、CodeBuildによる`PROVISIONING`と`PRE_BUILD`に大幅な時間を費やしていることが分かりました。

![build_phase_long_time.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641141/8f707192-f486-3839-03fa-79ded6750376.png)

`PROVISIONING`とは、ビルド用のコンテナを準備している工程です。以下のようなビルド環境の条件に左右されます。

* ビルド用のコンテナの環境イメージ
* ビルド用のコンテナをVPCに配置するか否か
* ビルド用のコンテナのスペック

例えば、ビルド用コンテナのコンピューティングリソースのスペックを上げる方法などで`PROVISIONING`の処理時間を短縮できますが、本記事では対象外とします。

`PRE_BUILD`とは、ビルドする前の準備工程です。具体的には、以下のビルド仕様を定義した`buildspec.yml`の`pre_build`で実行している箇所になります。

```yml:buildspec.yml
version: 0.2
env:
  parameter-store:
    AAC_REPOSITORY_URI: "mdlsys-ssm-pram-aac-ecr-uri"
    AAC_IMAGE_TAG: "mdlsys-ssm-pram-aac-img-tag"
phases:
  install:
    runtime-versions:
      docker: 19
  pre_build:
    commands:
      - echo Entered the pre_build phase...
      - apt-get update -y
      - apt-get install -y maven
      - mvn -f aac/ctr/pom.xml package
  build:
    commands:
      - echo Entered the build phase...
      - echo Logging in to Amazon ECR...
      - $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)    
      - echo Building the Docker image...
      - docker build -f ./aac/ctr/src/main/docker/Dockerfile.jvm -t $AAC_REPOSITORY_URI:latest ./aac/ctr
      - docker tag $AAC_REPOSITORY_URI:latest $AAC_REPOSITORY_URI:$AAC_IMAGE_TAG
  post_build:
    commands:
      - echo Entered the post_build phase...
      - echo Build completed on `date`
      - echo Pushing the Docker image...
      - docker push $AAC_REPOSITORY_URI:$AAC_IMAGE_TAG
      - printf '{"Version":"1.0","ImageURI":"%s"}' $AAC_REPOSITORY_URI:$AAC_IMAGE_TAG > imageDetail.json
artifacts:
  files:
    - imageDetail.json
```


`buildspec.yml`の`pre_build`では、大きく以下の2つを実施しています。

1. `apt-get update -y` と `apt-get install -y maven`により、Apache Mavenをインストール
2. `mvn -f aac/ctr/pom.xml package`にて、ソースコードをコンパイル、テストして、ビルド出力アーティファクトにパッケージ化

特に、No.2にて、パッケージ化する際、**依存関係のあるライブラリをインターネット経由で、外部のリポジトリから取得する工程(以降、ライブラリ取得工程と呼称)**に大幅な時間を費やしていることが課題でした。


## 目的
一般的にCICDパイプラインを構築すると、[課題](#課題)のようにライブラリ取得工程に時間がかかります。
本記事では、その工程にかかる時間を短縮できる方法を調査し、結果を共有することで、少しでも読者の皆さんが構築しているCICDパイプラインの改善の参考になれば、と思い執筆しました。

# AWS CodeBuildによるビルドキャッシュ方法
ライブラリ取得工程に費やす時間は**キャッシュ**の活用により短縮できます。
キャッシュの活用方法は、以下の3つがあります。

| # | キャッシュ方法 | 概要 | ユースケース | 
| :--- | :--- | :--- | :--- | 
| 1 | CodeBuildのローカルキャッシュ | ビルドホストのみが利用できるキャッシュをそのビルドホストのローカルに保存する。 | <ul><li>キャッシュはビルドホストですぐに利用できるため、中から大規模へのビルドアーティファクトに適している</li><li>ビルドの頻度が低い場合は向かない</li></ul> | 
| 2 | CodeBuildのS3キャッシュ | 複数のビルドホスト間で利用できるキャッシュをAWS S3バケットに保存する。 | <ul><li>小から中間サイズのビルドアーティファクトに適している</li><li>ビルド頻度が低い場合や複数のパイプラインでキャッシュを使いまわす場合</li></ul> | 
| 3 | EFSキャッシュ | AWS EFSにキャッシュを保存する。CodeBuildネイティブではないため、ユーザ側でキャッシュ処理を行う必要がある。(※) | <ul><li>ビルドコンテナのディスク容量を多く使いたい場合</li><li>複数のビルド間でキャッシュを使いまわす場合</li><li>データの読み取り(ダウンロード)を高速化したい場合</li></ul> | 


※事前にキャッシュ対象をZip化しEFSにコピーする必要があります。キャッシュ利用時はCodeBuildがEFSをマウントし、EFSからZipをローカルにダウンロードし展開して利用します。


複数のビルドホスト間でキャッシュを使いまわすことがモデルシステム開発の要件であった為、**#2 CodeBuildのS3キャッシュ**と**#3 EFSキャッシュ**について、どのくらい時間が短縮できるか検証しました。

# 検証
## 検証観点
単純に**ライブラリ取得工程に費やす時間**を検証観点とします。

また、S3キャッシュとEFSキャッシュともに、以下のビルド用コンテナを使用し、実行環境の条件を同一にして検証を進めます。

* ビルドコンテナのイメージ : `aws/codebuild/standard:4.0-20.03.13`
* コンピューティングリソース : `3 GB メモリ、2 vCPU`
* ビルドコンテナ : `VPC内に配置`


## 検証手順
### S3キャッシュ

実際に検証してみましょう。まず、S3キャッシュの設定方法について簡単に説明します。

#### 事前作業
以下の事前作業を行ってください。本記事では、手順を割愛させていただきます。

* キャッシュを保存するS3バケットの作成

#### 手順
CodeBuildのS3キャッシュを使用する為の手順は、以下2つのステップのみです。

##### 1. ビルドプロジェクトの編集

対象となるビルドプロジェクトを選択し、「編集」「アーティファクト」を押下します。

![how_to_set_up_s3_cache_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641141/905fc62c-ccc5-3492-0bac-742119f22bca.png)


青枠で囲われたように、キャッシュを保存するS3バケット(事前作業にて作成したS3バケット)を指定します。

![Inkedhow_to_set_up_s3_cache_2_v2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641141/c58a07eb-167b-5edf-e496-83e7c88088fb.jpeg)

##### 2. `buildspec.yml`にて、`cache`フェーズを追加

S3バケットにキャッシュとしてアップロードする対象のファイルを指定し、キャッシュを利用できるようにします。
mavenのデフォルト設定で、依存関係のあるライブラリは`.m2`配下に保存されるため、今回はそれらがキャッシュ対象となります。
`cache`フェーズに、キャッシュ対象を追記します。

```yml:buildspec.yml
version: 0.2
env:
  parameter-store:
    AAC_REPOSITORY_URI: "mdlsys-ssm-pram-aac-ecr-uri"
    AAC_IMAGE_TAG: "mdlsys-ssm-pram-aac-img-tag"
phases:
  install:
    runtime-versions:
      docker: 19
  pre_build:
    commands:
      - echo Entered the pre_build phase...
      - apt-get update -y
      - apt-get install -y maven
      - mvn -f aac/ctr/pom.xml package
  build:
    commands:
      - echo Entered the build phase...
      - echo Logging in to Amazon ECR...
      - $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)    
      - echo Building the Docker image...
      - docker build -f ./aac/ctr/src/main/docker/Dockerfile.jvm -t $AAC_REPOSITORY_URI:latest ./aac/ctr
      - docker tag $AAC_REPOSITORY_URI:latest $AAC_REPOSITORY_URI:$AAC_IMAGE_TAG
  post_build:
    commands:
      - echo Entered the post_build phase...
      - echo Build completed on `date`
      - echo Pushing the Docker image...
      - docker push $AAC_REPOSITORY_URI:$AAC_IMAGE_TAG
      - printf '{"Version":"1.0","ImageURI":"%s"}' $AAC_REPOSITORY_URI:$AAC_IMAGE_TAG > imageDetail.json
artifacts:
  files:
    - imageDetail.json
cache:
  paths:
    - '/root/.m2/**/*'    
```

以上がS3キャッシュの設定方法となります。

#### 結果

実際にCICDパイプラインを実行し、CodeBuildのフェーズ詳細を確認してみましょう。

![build_phase_short_time_by_s3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641141/82765c51-6bdb-096c-198f-31911f8d85b2.png)


`PRE_BUILD`フェーズの時間が短縮されており、キャッシュが効いていることが分かります。

### EFSキャッシュ

続いて、EFSキャッシュの設定方法を簡単に説明します。

#### 事前作業
以下の事前作業を行ってください。本記事では、手順を割愛させていただきます。

* キャッシュを保存するEFSの作成
* ローカル開発環境にて、ビルド時に取得した`.m2`配下のライブラリをZip化し、EFSにアップロード

#### 手順
CodeBuildのEFSキャッシュを使用する為の手順は、以下の2つのステップで設定できます。

##### 1. ビルドプロジェクトの編集

対象となるビルドプロジェクトを選択し、「編集」「環境」を押下します。

![how_to_set_up_efs_cache_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641141/04dfecf6-43b9-e5a2-570e-10691640fe5e.png)


CodeBuildがEFSをマウントできるように設定します。事前作業で作成したEFSを指定してください。

![how_to_set_up_efs_cache_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641141/f75bc762-def3-88c0-2a2c-88d47c346728.png)


##### 2. buildspec.ymlにて、EFSにあるZipファイルを展開する処理を追加

`pre_build`フェーズに事前作業でアップロードしたZipファイルをCodeBuildのビルドホストで解凍する処理を追記します。


```yml:buildspec.yml
version: 0.2
env:
  parameter-store:
    AAC_REPOSITORY_URI: "mdlsys-ssm-pram-aac-ecr-uri"
    AAC_IMAGE_TAG: "mdlsys-ssm-pram-aac-img-tag"
phases:
  install:
    runtime-versions:
      docker: 19
  pre_build:
    commands:
      - echo Entered the pre_build phase...
      - apt-get update -y
      - apt-get install -y maven
      - unzip $CODEBUILD_MDLSYS_BUILD_CACHE/repository.zip
      - mkdir ~/.m2/repository      
      - mv root/.m2/repository/* ~/.m2/repository
      - mvn -f aac/ctr/pom.xml package -Dmaven.test.skip=true
  build:
    commands:
      - echo Entered the build phase...
      - echo Logging in to Amazon ECR...
      - $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)    
      - echo Building the Docker image...
      - docker build -f ./aac/ctr/src/main/docker/Dockerfile.jvm -t $AAC_REPOSITORY_URI:latest ./aac/ctr
      - docker tag $AAC_REPOSITORY_URI:latest $AAC_REPOSITORY_URI:$AAC_IMAGE_TAG
  post_build:
    commands:
      - echo Entered the post_build phase...
      - echo Build completed on `date`
      - echo Pushing the Docker image...
      - docker push $AAC_REPOSITORY_URI:$AAC_IMAGE_TAG
      - printf '{"Version":"1.0","ImageURI":"%s"}' $AAC_REPOSITORY_URI:$AAC_IMAGE_TAG > imageDetail.json
      - echo Entered the post_build phase...
artifacts:
  files:
    - imageDetail.json
```

以上がEFSキャッシュの設定方法になります。

#### 結果

実際にパイプラインを実行し、CodeBuildのフェーズ詳細を確認してみましょう。

![build_phase_short_time_by_efs.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641141/932f2589-b76c-fe62-b6a3-51bfee4bac00.png)

EFSキャッシュも同様に`PRE_BUILD`フェーズの時間が短縮されており、キャッシュが効いていることが分かります。


# 検証結果まとめ
検証結果は以下になります。

| # | キャッシュ方法 | 時間(秒) |
| :--- | :--- | ---: |
| 1 | キャッシュなし | 299 |
| 2 | S3キャッシュ | 53 |
| 3 | EFSキャッシュ | 38 |

# まとめ
CodeBuildのキャッシュ方法を調査し、実際に検証してライブラリ取得工程の時間を測定しました。
[検証観点](#検証観点)が**ライブラリ取得工程で費やす時間**だと、単純にデータの読み取り(ダウンロード)速度がS3に比べて速いEFSに軍配が上がりました。

ただ、実際の開発現場だと観点が時間だけではなく、使用するAWSサービスの費用や運用負荷、シンプルな構成等が観点に入ると思います。
今回の場合、以下の理由から私はS3キャッシュを採用しています。

* S3キャッシュとEFSキャッシュの時間差が15秒と少なく、S3キャッシュの時間が許容範囲であった
* リソースの費用がS3の方が安い

また、EFSキャッシュのほうが構築作業に工数がかかるため、多数のパイプラインを作成するようなケースでは、その点も考慮する必要があります。

これらの検証結果を踏まえて、皆さんの快適なCICDライフの参考になれば幸いです。

# 補足
## モデルシステム
モデルシステムは、以下のようにECS on Fargate上で稼働しているシンプルなアーキテクチャで構成しています。
![mdlsys.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641141/e5c57365-3684-8ca2-17ee-43bd34a6b2d4.png)


# 参考情報
* [AWS CodeBuild ローカルキャッシュ](https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-caching.html#caching-local)
* [AWS CodeBuild S3キャッシュ](https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-caching.html#caching-s3)
* [AWS CodeBuild EFSキャッシュのサンプル](https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/sample-efs.html)
* [Apache Maven](https://maven.apache.org/)

---
* Amazon Web Services およびその他のAWS 商標は，米国およびその他の諸国におけるAmazon.com,Inc.またはその関連会社の商標です。
* Javaは，Oracle Corporationおよびその子会社，関連会社の米国およびその他の国における登録商標です。
* Dockerは、Docker Inc. の米国およびその他の国における登録商標もしくは商標です。
* Mavenは、Apache Software Foundationの米国およびその他の国における登録商標もしくは商標です。
* その他記載の会社名、製品名などは、それぞれの会社の商標もしくは登録商標です。

[^1]: AWS CodeCommit, CodePipeline, CodeBuild, CodeDeploy

[^2]: サンプルアプリは、[Quarkusの初期プロジェクト](https://quarkus.io/guides/rest-json)作成時に自動生成されるアプリベースに作成。本記事では、サンプルアプリの名前をAACと呼称