GitHub Actions の Workflow で Docker を使っています。しかし、デバッグなどでトライ & エラーをするたびにベース・イメージを
pull
したりイメージをビルドするので無料枠のコスト(時間あたりの API リクエスト数制限)が気になります。しかし actions/cache@v2 でイメージをキャッシュしようにも、権限の問題で
/var/lib/docker/
のディレクトリがキャッシュできません。できればroot
権限は使いたくないのです。
以下の 1 つでもマッチした人向けの記事です:
-
GitHub 公式の
actions/cache@v2
アクションだけでキャッシュ処理を完結させたい。 - Docker Hub や GitHub などのコンテナ・レジストリにキャッシュ目的で
push
pull
したくない。 - ベース・イメージが重いのでキャッシュしておきたい(無駄な
docker pull
を避けたい) - ビルドに時間がかかるのでキャッシュして再利用したい。
- 高速化だけでなく、外部への無駄な通信を抑えて GitHub や API のリクエスト負荷を下げたい。
TL; DR (今北産業)
-
actions/cache@v2
は、指定したディレクトリのキャッシュとリストアをする GitHub アクション。 -
docker save
docker load
は、指定した Docker イメージを保存・読み込みするコマンド。 - 上記 2 つを応用して、キャッシュ・ディレクトリに Docker イメージを保存しておけば、勝手にリカバリ(リストア)してくれる。
TS; DR (actions/cache@v2 のキャッシュを理解するまでのコマケーこと)
マスター動くものをくれ
-
actions/cache@v2
アクションを使って指定したディレクトリをキャッシュする。# すでに同じ key(キャッシュの ID)が存在する場合は、 # キャッシュを path にリストア(復元)して cache-hit # を true にセットします。 - name: Enable Cache/Restore for image archive id: cache uses: actions/cache@v2 with: path: <キャッシュしたいディレクトリ> key: <キャッシュの ID>
-
キャッシュがない場合の処理
- Docker イメージをビルドしてキャッシュしたいディレクトリに tar 形式でイメージを出力する。(キャッシュされる)
- 条件式:
if: steps.cache.outputs.cache-hit != 'true'
- 実行内容:
docker save --output <キャッシュ先のファイルパス> <キャッシュしたいイメージ名>
- 条件式:
- Docker イメージをビルドしてキャッシュしたいディレクトリに tar 形式でイメージを出力する。(キャッシュされる)
-
キャッシュがある場合の処理
-
docker load
で事前にtar
をインポートする。(docker run
やdocker build
前に読み込んでおく)- 条件式:
if: steps.cache.outputs.cache-hit == 'true'
- 実行内容:
docker load --input <キャッシュ先のファイルパス>
- 条件式:
-
マスター、瓶ごとくれ
以下は、テストの実行環境の整ったコンテナに、チェックアウトしたリポジトリをマウントしてテスト(hogefuga.sh
)を実行する例です。
この例では、月をまたぐか、Dockerfile が変更されるとキャッシュは再作成されます。これは「キャッシュ ID」に Dockerfile のハッシュ値と日付を利用しているためです。また、5 GB のキャッシュ領域を超えた場合は古いキャッシュが削除されます。
注目すべき箇所は 2 つあります。
- 「キャッシュがある」場合は、
docker load
で tar 出力されたイメージを読み込む箇所。 - 「キャッシュがない」場合はビルドした Docker イメージを tar 出力してキャッシュする箇所。
name: "Sample Workflow"
on: [push]
env:
# Docker run する際のコンテナにリポジトリをマウントする先のパス
PATH_MOUNT: /workspaces/myapp
# Docker イメージの tar アーカイブ出力先のパス
PATH_CACHE: /tmp/docker-img-arch
jobs:
Tests:
runs-on: ubuntu-latest
steps:
# リポジトリの読み込み(fetch-depth=1)
- name: Check out repo under workspace
uses: actions/checkout@v2
# キャッシュ ID の作成(イメージのハッシュと日付から作成。Windows 非互換、事項参照)
- name: Create image tag
id: imagetag
run: |
: # Dockerfile からハッシュ値を作成
HASH_IMAGE=${{ hashFiles('./Dockerfile') }}
: # 日付と 7 文字のハッシュ値で合計 13 文字の ID を作成
VARIANT=$(TZ=UTC-9 date '+%Y%m')${HASH_IMAGE:0:7}
: # イメージのタグを作成
NAME_IMAGE=myTestImage
TAG="${NAME_IMAGE}:${VARIANT}"
: # キャッシュする tar アーカイブ名とパスの設定
NAME_TAR="${NAME_IMAGE}.${VARIANT}.tar"
PATH_TAR=${{ env.PATH_CACHE }}"/${NAME_TAR}"
: # 変数を他の run でも使えるように output
echo "::set-output name=TAG::${TAG}"
echo "::set-output name=PATH_TAR::${PATH_TAR}"
# この Workflow が正常に終了したら path をキャッシュ。
# key が存在する場合、ついでに path にリストアする。
- name: Enable cache
id: cache
uses: actions/cache@v2
with:
path: ${{ env.PATH_CACHE }}
key: ${{ steps.imagetag.outputs.TAG }}
# キャッシュがある場合は tar をロードしてイメージ一覧に追加
- name: Load Docker image if exists
if: steps.cache.outputs.cache-hit == 'true'
run: docker load --input ${{ steps.imagetag.outputs.PATH_TAR }}
# キャッシュがない場合は Docker イメージをビルド後、tar アーカイブをキャッシュ先に保存
- name: Build Docker image and save
if: steps.cache.outputs.cache-hit != 'true'
run: |
: # キャッシュディレクトリを作成
mkdir -p ${{ env.PATH_CACHE }}
: # 安定したビルドのためにベース・イメージを pull しておく(オプション)
docker pull mybaseimage:latest
: # イメージのビルド
docker build -f './.devcontainer/Dockerfile' -t ${{ steps.imagetag.outputs.TAG }} .
: # イメージのキャッシュ(tar をキャッシュ・ディレクトリに出力)
docker save --output ${{ steps.imagetag.outputs.PATH_TAR }} ${{ steps.imagetag.outputs.TAG }}
# 本処理
# イメージからコンテナを起動してテスト(entrypoint.sh)を実行
- name: Run tests for both Go and Shell Script
run: |
: # コンテナ内のスクリプトのパスを作成(マウントポイントから見たパス)
path_entrypoint=${{ env.PATH_MOUNT }}/path/to/hogefuga.sh
: # 実行
docker run -u root -v "$(pwd)":${{ env.PATH_MOUNT }} -w ${{ env.PATH_MOUNT }} ${{ steps.imagetag.outputs.TAG }} "$path_entrypoint"
# その他の処理(例えばカバレッジのアップデート)
- name: Upload coverage
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
キャッシュ ID (ファイルのハッシュ化)部分を汎用化させてみた
上記の例は、ランナー(GitHub Actions を走らせる OS/プラットホーム)が Linux および macOS 限定です。キャッシュ ID の作成部分が Windows プラットホーム互換ではないからです。
Win・Mac・Linux の、すべてのプラットフォームでキャッシュをしたい場合は、汎用のキャッシュ ID 作成用の GitHub Actions を作成したので利用ください。
- File Hash for Cache | Actions | Marketplace @ GitHub
この記事の内容をベースに Windows にも対応させたものですが、スッキリと記載できるようになります。
例えば、以下は Go 言語のパッケージを複数の Go バージョンでユニット・テストを行い、Go プログラムの出力結果を PHP および Python で検証するテストです。
各々 docker compose
を利用してコンテナ上でテストしていますが、いずれもキャッシュされます。
name: UnitTests
on:
workflow_dispatch:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
PATH_CACHE: /tmp/docker-img-arch
jobs:
go:
name: Run tests on Go via container
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Create cache ID from file hash
uses: KEINOS/gh-action-hash-for-cache@main
id: imagetag
# Udate the hash if Dockerfile/go.mod is changed or the month has changed.
with:
path: |
./go.mod
./.github/Dockerfile
./.github/docker-compose.yml
./.github/workflows/unit-tests.yml
variant: $(TZ=UTC-9 date '+%Y%m')
- name: Enable Cache
id: cache
uses: actions/cache@v2
with:
path: ${{ env.PATH_CACHE }}
key: ${{ steps.imagetag.outputs.hash }}
- name: Load cached Docker images if any
if: steps.cache.outputs.cache-hit == 'true'
run: |
docker load --input ${{ env.PATH_CACHE }}/${{ steps.imagetag.outputs.hash }}/github_v1_14_1.tar
docker load --input ${{ env.PATH_CACHE }}/${{ steps.imagetag.outputs.hash }}/github_v1_15_1.tar
docker load --input ${{ env.PATH_CACHE }}/${{ steps.imagetag.outputs.hash }}/github_v1_16_1.tar
docker load --input ${{ env.PATH_CACHE }}/${{ steps.imagetag.outputs.hash }}/github_v1_17_1.tar
docker load --input ${{ env.PATH_CACHE }}/${{ steps.imagetag.outputs.hash }}/github_v1_18_1.tar
docker load --input ${{ env.PATH_CACHE }}/${{ steps.imagetag.outputs.hash }}/github_v1_19_1.tar
docker load --input ${{ env.PATH_CACHE }}/${{ steps.imagetag.outputs.hash }}/github_latest_1.tar
docker load --input ${{ env.PATH_CACHE }}/${{ steps.imagetag.outputs.hash }}/github_php_1.tar
docker load --input ${{ env.PATH_CACHE }}/${{ steps.imagetag.outputs.hash }}/github_python_1.tar
- name: Pull base images if no-cache
if: steps.cache.outputs.cache-hit != 'true'
run: |
: # Pull images one-by-one for stability
docker pull golang:1.14-alpine
docker pull golang:1.15-alpine
docker pull golang:1.16-alpine
docker pull golang:1.17-alpine
docker pull golang:1.18-alpine
docker pull golang:1.19-alpine
docker pull golang:alpine
docker pull php:alpine
docker pull python:alpine
- name: Build Docker images if no-cache
if: steps.cache.outputs.cache-hit != 'true'
run: |
docker-compose --file ./.github/docker-compose.yml build
- name: Save/export built images to cache dir if no-cache
if: steps.cache.outputs.cache-hit != 'true'
run: |
mkdir -p ${{ env.PATH_CACHE }}/${{ steps.imagetag.outputs.hash }}
docker save --output ${{ env.PATH_CACHE }}/${{ steps.imagetag.outputs.hash }}/github_v1_14_1.tar github_v1_14:latest
docker save --output ${{ env.PATH_CACHE }}/${{ steps.imagetag.outputs.hash }}/github_v1_15_1.tar github_v1_15:latest
docker save --output ${{ env.PATH_CACHE }}/${{ steps.imagetag.outputs.hash }}/github_v1_16_1.tar github_v1_16:latest
docker save --output ${{ env.PATH_CACHE }}/${{ steps.imagetag.outputs.hash }}/github_v1_17_1.tar github_v1_17:latest
docker save --output ${{ env.PATH_CACHE }}/${{ steps.imagetag.outputs.hash }}/github_v1_18_1.tar github_v1_18:latest
docker save --output ${{ env.PATH_CACHE }}/${{ steps.imagetag.outputs.hash }}/github_v1_19_1.tar github_v1_19:latest
docker save --output ${{ env.PATH_CACHE }}/${{ steps.imagetag.outputs.hash }}/github_latest_1.tar github_latest:latest
docker save --output ${{ env.PATH_CACHE }}/${{ steps.imagetag.outputs.hash }}/github_php_1.tar github_php:latest
docker save --output ${{ env.PATH_CACHE }}/${{ steps.imagetag.outputs.hash }}/github_python_1.tar github_python:latest
- name: Run tests on Go 1.14
run: docker-compose --file ./.github/docker-compose.yml run v1_14
- name: Run tests on Go 1.15
run: docker-compose --file ./.github/docker-compose.yml run v1_15
- name: Run tests on Go 1.16
run: docker-compose --file ./.github/docker-compose.yml run v1_16
- name: Run tests on Go 1.17
run: docker-compose --file ./.github/docker-compose.yml run v1_17
- name: Run tests on Go 1.18
run: docker-compose --file ./.github/docker-compose.yml run v1_18
- name: Run tests on Go 1.19
run: docker-compose --file ./.github/docker-compose.yml run v1_19
- name: Run tests on latest Go
run: docker-compose --file ./.github/docker-compose.yml run latest
- name: Compatibility test on PHP
run: docker-compose --file ./.github/docker-compose.yml run php
- name: Compatibility test on Python
run: docker-compose --file ./.github/docker-compose.yml run python
- github.com/KEINOS/go-argonize @ GitHub より