CI/CD の一環として 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 のリクエスト負荷を下げたい
actions/cache の最新バージョンは v4
です。
動きは変わらないので、actions/cache@v2
を actions/cache@v4
に置き換えてご覧ください。
TL; DR (今北産業)
-
actions/cache@v2
は、指定したディレクトリのキャッシュとリストアをする GitHub アクション -
docker save
,docker load
は、指定した Docker イメージを保存・読み込みするコマンド - 上記 2 つを応用して、キャッシュ・ディレクトリに Docker イメージを保存しておくと、条件が合うと勝手にリカバリ(リストア)してくれるため、リカバリした Docker イメージを読み込むことで再利用できる
TS; DR (actions/cache@v2 のキャッシュを理解するまでのコマケーこと)
マスター、瓶ごとくれ
-
https://github.com/KEINOS/go-totp/blob/main/.github/workflows/unit-tests.yml
- 上記は、複数ファイルから 1 つのハッシュ値を取得するアクションで得られた値をキャッシュ用の
key
(キャッシュ ID)として利用しています。つまり、指定したファイルのいずれかに 1 ビットでも変更があるとkey
が変わります。仕組みは後述。 - キャッシュがある場合は、キャッシュから Docker コンテナのイメージを読み込み、コンテナ上でテストを実行します。
- キャッシュがない場合は、Docker コンテナのイメージを作成し、テストが全てパスしたら、キャッシュ・ディレクトリにイメージを保存します。
- 上記は、複数ファイルから 1 つのハッシュ値を取得するアクションで得られた値をキャッシュ用の
マスター、これ何が入ってるの?
以下は、テストの実行環境の整ったコンテナに、チェックアウトしたリポジトリをマウントしてテスト(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 バージョンでユニット・テストを行うサンプルです。各々 docker compose
を利用してコンテナ上でテストしていますが、いずれもビルドされたイメージはキャッシュされます。
# Unit testing on vaious Go versions, such as Go 1.18 and later.
# It will test the generated password hash verifying with PHP and Python.
#
# This workflow caches images built with Docker and docker-compose to speed up its execution.
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 image tag from file hash
uses: KEINOS/gh-action-hash-for-cache@main
id: imagetag
# Udate the hash if any file in the path has changed or the month has changed.
with:
path: |
./go.mod
./go.sum
./docker-compose.yml
./.github/Dockerfile
./.github/workflows/unit-tests.yml
variant: $(TZ=UTC-9 date '+%Y%m')
- name: Enable Cache
id: cache
uses: actions/cache@v3
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 }}/go-totp_v1_18_1.tar
docker load --input ${{ env.PATH_CACHE }}/${{ steps.imagetag.outputs.hash }}/go-totp_v1_19_1.tar
docker load --input ${{ env.PATH_CACHE }}/${{ steps.imagetag.outputs.hash }}/go-totp_latest_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.18-alpine
docker pull golang:1.19-alpine
docker pull golang:alpine
- name: Build Docker images if no-cache
if: steps.cache.outputs.cache-hit != 'true'
run: |
docker-compose 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 }}/go-totp_v1_18_1.tar go-totp_v1_18:latest
docker save --output ${{ env.PATH_CACHE }}/${{ steps.imagetag.outputs.hash }}/go-totp_v1_19_1.tar go-totp_v1_19:latest
docker save --output ${{ env.PATH_CACHE }}/${{ steps.imagetag.outputs.hash }}/go-totp_latest_1.tar go-totp_latest:latest
- name: Run tests on Go 1.18
run: docker-compose run v1_18
- name: Run tests on Go 1.19
run: docker-compose run v1_19
- name: Run tests on latest Go
run: docker-compose run latest
- github.com/KEINOS/go-totp @ GitHub より