AWSのCodeBuildのチューニングについて

この記事は[Wano Group Advent Calendar](https://qiita.com/advent-calendar/2023/wano-group) 2023の17日目の記事となります。グループ会社のEDOCODEの[Advent Calendar](https://adventar.org/calendars/8641)もありますので、よろしければそちらもどうぞ。
***
# 目次
[1. はじめに](#1-はじめに)
[2. キャッシュについて](#2-キャッシュについて)
[3. 独自のキャッシュ実装](#3-独自のキャッシュ実装)
[4. その他やった方が良い設定](#4-その他やった方が良い設定)
[5. まとめ](#5-まとめ)

### 1. はじめに
AWSさんのCodeBuildのチューニングについて色々試したことの記録となります。調査ターゲットのRepoはNext.jsであり、150以上の外部モジュールを依存しています。大型ではないかもしれませんが、規模あるRepoです。buildspec.ymlの抜粋したコアな部分は以下のようなシンプルなものです。
```yml
phases:
  pre_build:
    commands:
      - npm install
  build:
    commands:
      - npm run build
      - npm install --production
artifacts:
  files:
    - .next/**/*
    - node_modules/**/*
    - その他必要なファイル
```
Build環境に関しては、本番環境に合わせてARMタイプをメインに調査しました。

### 2. キャッシュについて
CodeBuild現在サポートしているのローカル、 S3の2種類です。AWSの公式Docは[こちら](https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-caching.html)です。
1. 基準としまして、まずキャッシュなし
    複数回を実施した平均値はこんな感じです。
    | 合計 | PRE_BUILD | BUILD | その他 |
    |----:|----------:|------:|------:|
    | 1003s | 305s | 565s | 133s |

1. ローカル キャッシュ利用
    まずbuildspec.ymlにcache部分を追加します。
    ```yml
    cache:
      paths:
        - .next/**/*
        - node_modules/**/*
    ```
    次にAWSコンソール上、アーティファクト部分を以下のようにキャッシュをローカルに設定します。
    ![local_cache.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3631191/43dd1e7d-c7a5-8cf9-905d-8ae3283b47cd.png)
    ローカル キャッシュは一番シンプルで、ヒットする場合の効果も大きいです。しかしこちらは短時間の再Build向けで、時間を空けるとほぼキャッシュヒットしなくなります。AWSの公式ドキュメントにキャッシュヒットするためのポリシーや、Best Practiseなどが公開されていないらしいです。ネット上も様々な記事がありますが、キャッシュヒット率を確実に高まる方法は無いようです。今回のテスト中で、前回のBuild完了後の約15分以内に再度Buildする場合はキャッシュヒットすることが多いです。ヒットする場合は約50%ビルド時間が短縮されこともあります。非公式ですが、[2019年のこんな記事](https://stackoverflow.com/a/59070807)があります。
1. Amazon S3利用
    buildspec.ymlはローカル キャッシュと同じのままでOKです。
    次にAWSコンソール上、アーティファクト部分を以下のようにキャッシュをAmazon S3に設定します。
    ![s3 cache.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3631191/f20a6801-d60c-afa5-86f5-84ba04c4b1ff.png)
    
    実際にS3上保存されたキャッシュファイルは１つのみです。ファイル名はUUIDで、拡張子がありません。
    ファイルの中身はAmazonさん独自仕様の.tar.gzファイルです。具体的に以下のような感じです。
    ```bash
    # tar -ztf S3キャッシュファイル | head -4
    codebuild.json
    0
    1
    2

    # codebuild.json | jq .
    {
      "version": "1.0",
      "content": {
        "files": [
          {
            "path": "node_modules/xxxxx/yyyy",
            "rel": "src"
          },
          {
            "path": "dist/next/cache/.tsbuildinfo",
            "rel": "src"
          },
        ],
        "exclude-paths": null
      }
    }
    ```
    同じく複数回実行した結果では
    | 合計 | PRE_BUILD | BUILD | その他 |
    |----:|----------:|------:|------:|
    | 995s |     280s | 610s  |  105s |
    
    これまでだとキャッシュなしとほぼ変わらない結果になってしまいます。
    
### 3. 独自のキャッシュ実装
実際のbuildログを見ると、初回のnpm installにはキャッシュがあるにも関わらず依然として時間が必要の状況です。当たり前ですが、npm insall --productionではdevDependenciesのモジュールを削除したからです。こちら何か施策を取る必要があります。以下は考えられた方法です。
1. devDependenciesの部分を他のフォルダーにinstallする
    旧バージョンのnpmでは、--onlyという公式の方法はありますが、最新バージョンだと削除されました。npm/yarnなどツールは標準のnode_modulesとは別のフォルダにインストールできますが、dependencies & devDependenciesの全てのモジュールになるので、理想のdependencies=>node_modules, devDependencies=>dev_dep_only_modulesみたいな構成は到底難しいです。
1. npm install --productionを辞める
    大きいのデメリットはBuild結果であるアーティファクトがかなり大きくなることです。そして本番など実行環境が遅くなる可能性が出ます。もちろんdevDependenciesのモジュールによりますが、今回のテストRepoではnode_modulesが2倍以上に膨らみますので、この手もお見送りです。
1. npmのキャッシュもS3に保存する
    試しましたか、逆効果でした。npmのキャッシュは実際node側のダウンロード時間を省けましたが、その分のキャッシュ作成・保存・展開の方のコストが大きいです。
1. CodeBuildのキャッシュ作成 & アーティファクトの間に差し込めるか
    現時点のCodeBuildのbuildspec仕様(v0.2)では、キャッシュ作成はpost_buildのfinallyの後、アーティファクト作成の前で実行されるため、シンプルなphaseなど調整する方法はありません。でもAWSさんのS3キャッシュもtar.gzですので、独自でキャッシュ実装すればいけるはずです。buildspec.ymlを以下のように改修する
	```diff
	  phases:
	    pre_build:
	      commands:
	+       - |
	+         if [[ -e node_modules_cache.tar.gz ]]; then
	+           rm node_modules_cache.tar.gz
	+         fi
	+         aws s3 cp --only-show-errors s3://指定したCodeBuild用S3Bucket/node_modules_cache.tar.gz node_modules_cache.tar.gz && tar -zxf node_modules_cache.tar.gz
	+         if [[ -e next_cache.tar.gz ]]; then
	+           rm next_cache.tar.gz
	+         fi
	+         aws s3 cp --only-show-errors s3://指定したCodeBuild用S3Bucket/next_cache.tar.gz next_cache.tar.gz && tar -zxf next_cache.tar.gz
	+         true
	        - npm install
	    build:
	      commands:
	        - npm run build
	+       - |
	+         if [[ -e node_modules_cache.tar.gz ]]; then
	+           rm node_modules_cache.tar.gz
	+         fi
	+         export NODE_MODULE_CHECK_END_EPOCH=`date +%s`
	+         export NODE_MODULE_DURATION_MINUTES=$(( (( ${NODE_MODULE_CHECK_END_EPOCH} - ${NODE_MODULE_CHECK_START_EPOCH} ) / 60 ) + 1 ))
	+         if [[ -z $(find node_modules -mmin -${NODE_MODULE_DURATION_MINUTES} | head -1) ]]; then
	+           echo no new modules. skip create cache
	+         else
	+           echo found new modules. recreate cache
	+           tar --acls --xattrs -cpzf node_modules_cache.tar.gz node_modules/ && aws s3 cp --only-show-errors node_modules_cache.tar.gz s3://指定したCodeBuild用S3Bucket/node_modules_cache.tar.gz
	+         fi
	+         tar --acls --xattrs -cpzf next_cache.tar.gz .next/ && aws s3 cp --only-show-errors next_cache.tar.gz s3://指定したCodeBuild用S3Bucket/next_cache.tar.gz
	+         true
	        - npm install --production
	  artifacts:
	    files:
	      - .next/**/*
	      - node_modules/**/*
	      - その他必要なファイル
	```
    nodeモジュールの更新はそんなに多くない想定です。その場合の実行結果は
    | 合計 | PRE_BUILD | BUILD | その他 |
    |----:|----------:|------:|------:|
    | 680s | 34s | 567s | 79s |

    ローカル キャッシュほどでは無いですが、約5分20秒ほど短縮できました。
    
### 4. その他やった方が良い設定
1. Computeサイズ
今回のメイン調査対象ではあるARM系ですが、EC2の場合は２つプランがあります。`4GB RAM && 2 vCPU` と `16 GB RAM && 8 vCPU`です。もちろん実際の実行時間次第ではありますが、基本的に大きいインスタンスを選ぶと良いでしょう。実行時間が短縮される分でトータル的にコストは予算範囲内に収めるのは多いはずです。Lambdaの場合はEC2より小さい傾向です。ARMの場合は10GBまでなので、本番環境に合わせたい場合も良い選択肢です。ちなみにx86_64系EC2の場合は`145 GB RAM && 72 vCPU`というとんでもないプランもあります。
1. Amazon S3キャッシュ利用の場合
有効期限も実際のニーズに合わせて設定すべきです。またS3ではあるので、使うS3 Bucketのライフサイクルで古いObjectや不完全なマルチパートアップロードなどの削除設定も忘れないでください。
1. Docker化したRepoであれば、ローカル キャッシュのDockerLayerCacheも検証した方が良いです。但し同じく15分以内前提のはずで、時間を空けるとヒットしないと思います。DockerのbuildkitやECRなども合わせて設定すべきと思います。ECRは別料金になりますので、実行頻度やイメージサイズなども計算すると良いと思います。

### 5. まとめ
1. ローカルキャッシュは連続Build(15分以内)のユースケース向けです。それ以外あんまり期待できません。
1. 連続Buildではないのユースケースの場合、S3キャッシュを利用するのも選択肢に入れた方が良いです。設定自体もシンプルで、検証するのをおすすめです。
1. Build Onlyのモジュールが沢山ある、かつフォルダー分けが難しい場合は、独自実装した方が良いかもしれません。
<br>

***
現在、Wanoグループ / TuneCore Japan では人材募集をしています。興味のある方は下記を参照してください。
Wano | [Wano Group JOBS](https://group.wano.co.jp/jobs/)
TuneCore Japan | [TuneCore Japan JOBS](https://www.tunecore.co.jp/jobs)