🌊

GithubActionsでdockerのビルドをキャッシュしてテストを動かす

2024/02/09に公開

GithubActions内で、docker composeを使ってテストを動かそうとすると、毎回Dockerfileをビルドせねばならず、ビルドに時間が掛かってしまいます。

そのため、ビルドファイルをキャッシュしておいて、Dockerfile に変更が無いなら、キャッシュをimportして使うようにするという、そんな設定ファイルです。

細かくコメントを書いたyamlファイルを用意したので、見てもらえればと思います。
(おまけで、DBサーバの起動待ちでハマったので、リトライによる回避方法も書いておきました。)

また、step名の先頭と最後に✨を付けていますが、ログを見る時に自分の処理とGithubActions側の処理が一発で見分けられて良いので、何かしら絵文字を使ってみるのもおすすめします。

# GithubActionsの名前
name: hoge

# 動かすタイミングの設定
on:
  workflow_dispatch: # 手動実行できるようにする
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

# Githubの利用権限設定(contentsがないとチェックアウトすら動かないなど)
permissions:
  id-token: write
  contents: read
  actions: read

# GithubActions内で使える環境変数の設定
env:
  IMEGE_CACHE_DIR: /tmp/docker-img
  IMAGE_NAME: aaa:latest

# ここからが実際の処理の流れ
jobs:
  Tests:
    runs-on: ubuntu-latest
    steps:

      # リポジトリをチェックアウトする設定
      - name: ✨ actions/checkout@v3 ✨
        uses: actions/checkout@v3

      # キャッシュディレクトリの復元(保存処理は実行完了後に後処理で行われる)
      # actions/cache@v3は、ディレクトリ単位でキャッシュする仕組みとなる
      # そのため、ディレクトリにゴミが溜まっていくようだと、キャッシュサイズが肥大化していくため、定期的に削除する必要がある。
      # このサンプルでは、ファイル名固定のため、上書きとなり、キャッシュサイズが肥大化することはないため、削除処理は入れていない。
      - name: ✨ actions/cache@v3 docker dir ✨
        # キャッシュを複数使っている場合、キャッシュごとに復元可否判定を行えるように、idを指定する
        # 今回のサンプルでは、不要かもしれないが、書いておいた方が良い。
        id: image-cache-id
        uses: actions/cache@v3
        with:
          # 保存するディレクトリ名を指定する
          path: ${{ env.IMEGE_CACHE_DIR }}
          # keyに、Dockerfileのhash値を含めている
          # そのため、Dockerfileに変更があったらkeyに該当するキャッシュが無いと判定され、復元されず、新たにビルドされる
          key: ${{ runner.os }}-docker-image-${{ hashFiles('./Dockerfile') }}

      # envだと動的な名称設定ができないため、ここで作成して$GITHUB_ENVに書き込んでいる
      - name: ✨ イメージキャッシュファイル名をフルパスで作成 ✨
        run: echo "FILE_PATH=${{env.IMEGE_CACHE_DIR}}/aaa-latest.tar" >> "$GITHUB_ENV"

      - name: ✨ イメージキャッシュがある場合、キャッシュからイメージをロードする ✨
        # steps.image-cache-idの部分が、ターゲットを指定している部分
        if: steps.image-cache-id.outputs.cache-hit == 'true'
        # イメージのloadは、下記のコマンドで行える
        run: docker load --input ${{ env.FILE_PATH }}

      - name: ✨ イメージキャッシュが無い場合、ビルドし、作成したイメージをキャッシュディレクトリに出力する ✨
        if: steps.image-cache-id.outputs.cache-hit != 'true'
        # イメージのbuildとディレクトリへのファイルアウトプットは、下記のコマンドで行える
        run: |
          mkdir -p ${{ env.IMEGE_CACHE_DIR }}
          docker build . --file ./Dockerfile --tag ${{ env.IMAGE_NAME }}
          docker save --output ${{ env.FILE_PATH }} ${{ env.IMAGE_NAME }}

      - name: ✨ docker compose up -d ✨
        run: |
          docker compose up -d

      - name: ✨ DBの起動を待ってmigrationとかする ✨
        run: |
          # docker-composeのlinksやdepends_onを使えば、コンテナの起動順を制御することはできるが、コンテナ内サーバなどの起動を待つことはできない
          # そのため、sleepとリトライで待つ処理を入れた(wait-for-it または dockerize を入れれば待てるらしいが、確認はしていない)
          # また、実行エラーで処理が止まってしまうため、set +eで、一時的にエラーオプションを外している
          # さらに、まれにだが、mysqlのselect実行が、失敗したのにリトライにならず次に進んでしまうことがあるため、3行書いている。
          set +e
          MAX_RETRY=10
          INTERVAL=1
          for i in $(seq 1 $MAX_RETRY); do
            echo "$i 回目"
            docker exec -e MYSQL_PWD=password aaa-mysql mysql -uroot db_name -e 'select 1;'
            docker exec -e MYSQL_PWD=password aaa-mysql mysql -uroot db_name -e 'select 1;'
            docker exec -e MYSQL_PWD=password aaa-mysql mysql -uroot db_name -e 'select 1;' && break
            sleep $INTERVAL && /bin/false
          done
          if [ $? -eq 0 ]; then
              echo "migration実行"
          else
              echo "ギブアップ!"
              exit 1
          fi
          # エラーオプションを戻す(セクションごとにリセットであれば、要らないはずだが、確認していないため書いとく)
          set -e

      # サンプルなのでechoを出力するだけ
      - name: ✨ テストの実行 ✨
        run: docker exec aaa echo "テスト実行"

      # 上記の実行が正常終了すれば、この後にキャッシュディレクトリの保存処理が実行される
      # 復元するディレクトリにゴミが溜まっていくのであれば、ここで削除すると良い

Discussion