GitHub Actions、キャッシュで時間短縮(とLambda更新で少し苦労)した話

# 取り組んだこと
本記事は、[GitHub Actions Advent Calendar 2023](https://qiita.com/advent-calendar/2023/github-actions)の 20日目の記事です。

以前投稿した[こちらの記事](https://qiita.com/eno49conan/items/f4c272c650b546bd96a6)で、workflowを作成しました。
このときのworkflowは、とりあえず動くものでした。

**作業前のworkflow**

https://github.com/eno-conan/githubaction-python-docker-cache/blob/main/.github/old-workflows/old.yaml

今回は、**キャッシュを利用して、実行時間短縮を図ろう**！
ということで作業を行っていました。

キャッシュは以下の2カ所に設定していきます。
- python関係のライブラリをインストール
- DockerイメージのPushまでの過程

## python関係のライブラリをインストール
`github actions pip cache`と検索すると、以下リポジトリが見つかりました。

https://github.com/actions/cache

関連する実装を色々確認して、以下の実装を使わせていただきました。
Pythonとpipのそれぞれのレイヤーでキャッシュを活用する方法です。

https://github.com/actions/setup-python/issues/330#issuecomment-1710135400

> I have one more suggestion that I decided to use, because I want to avoid downloading all pip dependencies everytime any of them changes - to introduce a second cache just for pip cache.

> （訳）pip依存関係が変更されるたびに、すべての再ダウンロードするのは非効率的だと考えたためです、すべてのpip依存関係を毎回ダウンロードすることを避けるため、pipのキャッシュ専用の2つ目のキャッシュを導入することにしました。
```yaml
  - name: "Cache: Cache Python"
    id: python-cache
    uses: actions/cache@v3.3.1
    with:
      path: ${{env.pythonLocation}}
      key: ${{env.pythonLocation}}-${{hashFiles('requirements.txt')}}
  - name: "Shell: Get pip cache dir"
    id: pip-cache-dir
    if: steps.python-cache.outputs.cache-hit != 'true'
    run: |
        python -m pip install -U pip
        pip install -U wheel
        echo "pip-cache-dir=$(pip cache dir)" >> ${GITHUB_OUTPUT}
  -   name: "Cache: Cache pip"
      if: steps.python-cache.outputs.cache-hit != 'true'
      uses: actions/cache@v3.3.1
      with:
          path: ${{steps.pip-cache-dir.outputs.pip-cache-dir}}
          key: 3.12-${{hashFiles('requirements.txt')}}
          restore-keys: |
              3.12-${{hashFiles('requirements.txt')}}-
              3.12-
  -   name: "Shell: Install pip dependencies"
      if: steps.python-cache.outputs.cache-hit != 'true'
      run: pip install -r requirements.txt
```

続けて、Dockerイメージのキャッシュに関する作業に入ります。

## DockerイメージのPushまでの過程
続いて、DockerイメージをPushするまでにキャッシュを使って、時間短縮を図ります。
#### build-push-actionの利用
GitHub Actionsエコシステムにある便利なアクションです。Dockerイメージのビルド、プッシュ、キャッシュを管理してくれます。
build-push-actionのオプションを追加します。

build-push-actionの[Examples](https://github.com/docker/build-push-action#examples)にあった、`Cache management`から確認しました。

https://docs.docker.com/build/ci/github-actions/cache/

大きく4種類ありそうです。
- Inline cache
- Registry cache
- GitHub cache(experimental)
- Local cache

`inline`推し、みたいです。しかし`inline`は、min キャッシュモードのみサポートのようです。
レジストリキャッシュエクスポーターを cache-to オプションに設定することで、max キャッシュモードを使用可能なようです。
min , maxについては以下の通りです。（[参考](https://qiita.com/chroju/items/d98ea757f906a9b5d683)）
- mix：最終レイヤーのみキャッシュ
- max：中間レイヤー含めてキャッシュ

今回はせっかくなので、**GitHub cache(experimental)を使います。**

<details><summary>余談：キャッシュ保存先はs3も選択可能</summary>

`Cache management`のページにあったリンクから、詳細を確認してみました。
s3とazblobが`unreleased`となっています。

https://docs.docker.com/build/cache/backends/

リリース前か、と思いましたが、
以下ページでは`S3 cache (experimental)`でした。
動作確認してみたので、気になる方はご覧ください。（記事の最後で）

https://github.com/moby/buildkit#s3-cache-experimental

</details>

##### GitHub cache(experimental)

https://docs.docker.com/build/cache/backends/gha/

基本的にはドキュメント通りで、`Scope`は確認しました。

> Scope is a key used to identify the cache object. By default, it is set to buildkit. If you build multiple images, each build will overwrite the cache of the previous, leaving only the final cache.
（訳）複数のイメージに対してキャッシュを利用したい場合に、どのイメージを何というキー名で設定するか、明記してください。`scope`に何も設定しないと、最後のイメージしかキャッシュされませんよ～

今回私はイメージを一つしか利用しませんが、使ってみます。
一旦まとめると、`build-push-action`部分は以下の通りです。
```yaml
  - name: Docker build and push to ECR         
    uses: docker/build-push-action@v5
    with:
      context: .
      file: ./Dockerfile
      tags: ${{ secrets.AWS_REGISTRY_URL }}:latest
      push: true
      cache-from: type=gha,scope=FastAPI-backend-dev
      cache-to: type=gha,scope=FastAPI-backend-dev,mode=max
```
続いて、参考にしていた[yamlファイル](https://docs.docker.com/build/ci/github-actions/cache/#github-cache)に、`setup-buildx-action`とあるので、これを確認します。

#### setup-buildx-actionの利用
https://github.com/docker/setup-buildx-action
> GitHub Action to set up Docker Buildx.

Buildxとは？

https://github.com/docker/buildx
> buildx is a Docker CLI plugin for extended build capabilities with BuildKit.

BuildKitとは？

https://github.com/moby/buildkit
> BuildKit is a toolkit for converting source code to build artifacts in an efficient, expressive and repeatable manner.

**BuildKitを使うために**
- GitHub ActionでBuildxを使うためには、`setup-buildx-action`が必要
- Buildxが「BuildKitを使った拡張ビルド機能を提供するためのプラグイン」

**`setup-buildx-action`をyamlファイルに記述すれば、BuildKitが使えそう。**

「[docker-buildxとmulti-platform build周りについてまとめ](https://zenn.dev/bells17/articles/docker-buildx#docker/build-push-action)」も参考になりました！
> コンテナイメージを作成する際にamd64アーキテクチャ向けのイメージだけじゃなくて、arm向けのイメージとかも作りたいときに使うものだと思えばOK

ちなみに、`setup-buildx-action`を定義せずworkflowを実行すると、エラーとなりました。
> buildx failed with: ERROR: Cache export feature is currently not supported for docker driver. Please switch to a different driver (eg. "docker buildx create --use")
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/6dbbe6cc-dc4a-bf1f-f5f0-5993e2bd65c9.png)

ここまでを踏まえて、`setup-buildx-action`部分は以下の通りです。（[参考](https://github.com/aws/containers-roadmap/issues/876)）

```yaml
- name: Set up Docker Buildx      
  uses: docker/setup-buildx-action@v3
　with:
    version: latest
　  driver-opts: image=moby/buildkit:latest
# build-push-actionをこれ以降に実行
```

## AWSのLambda関数更新に失敗
以下2つについて、実装が完了しました。
- `build-push-action`
- `setup-buildx-action`
あと、Lambda関数更新で終わり！と思って動作確認すると、
> InvalidParameterValueException: The image manifest or layer media type for the source image ***:latest is not supported.
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/a125a982-d394-c809-0c3e-44ee4631afc6.png)

マネジメントコンソールからECRの画面を確認すると、おかしいな。。。

<details><summary>想定していた状態</summary>

イメージタグが`latest`、アーティファクト・タイプが`Image`の状態

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/caebcfe8-af5a-ca82-e203-5a00eb85bb31.png)

</details>

<details><summary>実際の状態 ...</summary>

アーティファクト・タイプが`Image Index`？
アーティファクト・タイプが`Image`が2つ？サイズも異なる？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/f0c3f508-006a-895e-11c0-4fbc47ad298e.png)

</details>

## 犯人探し
今回追加した以下2つを調べます。`setup-buildx-action`はエラーないので、まずは`build-push-action`から確認しました。
- `setup-buildx-action`
- `build-push-action`

#### build-push-action内の容疑者

`build-push-action ECR`と検索すると、以下の記事が見つかりました。
今回とは異なる部分もありますが、ECRにPushしたら謎な状態に、という質問でした。

https://stackoverflow.com/questions/75811044/github-action-docker-build-push-actionv4-to-ecr-returns-untagged-images

以下、回答のコメントです。別の回答にも`provenance`とあり、少し調べました。
> Just add provenance: false in docker/build-push-action@v4

##### provenanceというオプション
私は全然知らない領域の話がでした。`SLSA`というものが関係しているようです。

https://chroju.dev/blog/docker_buildx_slsa_provenance

> ソフトウェアのサプライチェーン、つまり開発からビルドを経てデプロイされるまでの一連の過程において、外部の攻撃からその完全性を守るためのフレームワークがSLSAである。

ここは掘り下げしないで、進みます！
**`provenance: false`とすることで、Lambda関数の更新に成功しました。**

#### setup-buildx-action内の容疑者
`setup-buildx-action`側には問題がないのか確認します。
> マネジメントコンソールからECRの画面を確認すると、おかしい。。。

この文章を記載した箇所で載せた画像にある通り、
アーティファクトタイプが`Image Index`、エラー文には`image Manifest`とあります。両者は関係ありそうなので、調べました。
- [Image Manifest](https://github.com/opencontainers/image-spec/blob/main/manifest.md#image-manifest)：特定のアーキテクチャとオペレーティングシステム向けの単一のコンテナイメージの設定とレイヤーのセットを提供
- [Image Index](https://github.com/opencontainers/image-spec/blob/main/image-index.md)：様々なアーキテクチャとオペレーティングシステムにまたがるイメージセットに関する情報を含んでおり、（上位マニフェスト）コンシューマはインデックスを処理できるようにしておくべき

##### mediaTypeの確認
workflow失敗時に出力されていたmetaデータを見ました。
エラー文に`media type`という単語もあったので、調べます。
```json
{
  "containerimage.descriptor": {
    "mediaType": "application/vnd.oci.image.index.v1+json",
    "digest": "sha256:40ec8fca07e81d832bae55168d7c5d8ec4e359f3d1dd886238a8ccd8eb623548",
    "size": 856
  },
  "containerimage.digest": "sha256:40ec8fca07e81d832bae55168d7c5d8ec4e359f3d1dd886238a8ccd8eb623548",
  "image.name": "***:latest"
}
```
以下を確認しました。

https://docs.aws.amazon.com/lambda/latest/dg/images-create.html#images-types

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/75463132-a40c-f75d-bc10-e5d8ce494e6f.png)

あれ、ociのv1以上だよな？？？なんで動かないんだ？
調べる中で、、まさに起きている事象通りのissueがありました。

https://github.com/docker/build-push-action/issues/755#issuecomment-1378273495

> v0.11.0: unsupported manifest media type and no default available: application/vnd.oci.image.manifest.v1+json moby/buildkit#3491
（訳）バージョン0.11.0では、デフォルトで対応できるフォーマットはありません。

バージョンの問題が悪さをしている模様です。

記載の通り、`moby/buildkit`のバージョンを下げました。このときは
**`provenance: false`を削除しても、Lambda関数の更新に成功しました。**

**まとめると・・・**
- moby/buildkitのバージョンを最新で進めるなら、`provenance: false`を記述
- moby/buildkitのバージョンを下げるなら、`provenance: false`は不要

## 実行時間は短縮できた？
詳しく話していない部分もありますが、最終的に以下のworkflowを実行しました。
- Linterツール（[ruff](https://github.com/astral-sh/ruff)）によるチェック
- アプリケーションテスト
- DockerイメージのBuild、ECRへPush、Lambda関数の更新

キャッシュ利用なし：1m30s-1m50s
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/3e00068f-4519-544c-30d8-16c4ae4feaff.png)

**キャッシュ利用あり：40-50s**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/57ae5781-edd8-cbc1-7b20-e2983f2484a4.png)

実行時間を半分近く短くできました！

#### キャッシュを使いたくない場合
キャッシュを使った話が中心でしたが、workflow_dispatchを用意すれば、手動実行でキャッシュを使わない形のworkflowを実行できます。
<details><summary>yamlファイルの内容</summary>

ここでは、pipの方は省略します。
```yaml
on: 
  push:
  workflow_dispatch:
    inputs:
      docker-no-cache:
        description: "Build docker images with no cache" # ブラウザでチェックボックス横に表示
        default: false
        required: false
        type: boolean
        # 略
      - name: Docker build and push to ECR         
        uses: docker/build-push-action@v5
        with:
     　 # 略
          push: true
          no-cache: ${{ inputs.docker-no-cache == true }} # チェックありなら、キャッシュを利用しない
          cache-from: type=gha,scope=${{ github.workflow }}
          cache-to: type=gha,mode=max,scope=${{ github.workflow }}
```
</details>

ブラウザの表示
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/ca378a05-24da-8596-14ae-5314111b4a0b.png)

## 感想と今後
**最終的なworkflow**

https://github.com/eno-conan/githubaction-python-docker-cache/blob/main/.github/workflows/sample.yaml

workflowで自分自身でキャッシュまで導入したのは初めてだったので、勉強になりました。公式ドキュメントの漁り方も少しは理解できた気がします。

Github Actionsについて、理解すべきことはたーくさんありそうなので、少しずつやっていきたいと思いました。記事を書いた時点では、以下のようなことが気になっています。
- AWSの認証情報に関して、OIDCを使ってセキュアなワークフローに（[参考](https://note.com/shift_tech/n/n61146784b54f)）
- AWS Lambdaはマルチアーキテクチャ対応のイメージはビルドできないようなので、そのあたりの追加確認（[参考](https://docs.aws.amazon.com/lambda/latest/dg/lambda-deploy-functions.html)）
- Lambda関数更新で利用した[リポジトリ](https://github.com/int128/deploy-lambda-action)などを参考に、Custom Action作成方法やOSSライセンスに関する理解

<details><summary>余談続：s3へのキャッシュアップロード</summary>

> 余談：s3とazblobはもう選択可能？

以下のように実装を変更して実施してみました。そしてうまくいきましたね。
```yaml
  - name: Docker build and push to ECR         
    uses: docker/build-push-action@v5
    with:
      context: .
      file: ./Dockerfile
      tags: ${{ secrets.AWS_REGISTRY_URL }}:latest
      push: true
      cache-from: type=s3,region=ap-northeast-1,bucket=github-cache-bucket,name=${{ github.workflow }}
      cache-to: type=s3,region=ap-northeast-1,bucket=github-cache-bucket,name=${{ github.workflow }},mode=max
```

実施後、s3にファイルがアップロードされていました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/cbde885e-bde3-d623-322b-3d36193606e8.png)

1回目のworkflow実行（キャッシュ設定前）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/4ab5b5fb-45cd-6673-f80a-5dd98b11c33b.png)

2回目のworkflow実行（**キャッシュ設定後**）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/345414/3d138f3c-6404-f590-f6a4-c14ff5e79433.png)

</details>