GitHub Actions で Dockerfile のビルドをしてその中でテストを動かす
zenn 初投稿です、よろしくね
きっかけ
普段はいわゆる Web 周りのお仕事がメインなので久々に低レイヤー側に触れたくなって 低レイヤを知りたい人のためのCコンパイラ作成入門 をコツコツと進めています。
その中でテストを自動で動かしたいな、と思いました。
普段仕事では CircleCI を使っているので GitHub Actions でいい感じにやりたいな、そう思ったときにトラップにハマったので備忘録を残します
やりたかったこと
- ローカルでは Dockerfile に書いた環境上で make したい
- Actions 上でも同じ Dockerfile をビルドし、その中でやりたい
- Actions 側では make test を自動実行し、コケたら main にマージできないようにしたい(よくやるやつ)
- コンテナは root ではなく、ubuntu ユーザーで動かしたい(一応)
- イメージを毎回ビルドすると時間かかるのでイメージのビルドはキャッシュしたい
最終的な設定
# Usage:
# docker build -t slcc .
FROM ubuntu:22.04
RUN apt-get update && apt-get upgrade -y \
&& apt-get install -y --no-install-recommends locales \
tzdata \
&& locale-gen ja_JP.UTF-8
ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8
RUN apt-get install -y --no-install-recommends zsh gcc make git binutils libc6-dev gdb sudo \
&& useradd -m -s /bin/zsh ubuntu
USER ubuntu
WORKDIR /home/ubuntu
name: CMake_x64
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
ARCH: x64
jobs:
test:
timeout-minutes: 10
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Create image tag
id: image_tag
run: |
# Define cache dir
CACHE_PATH="/tmp/docker_cache_${{ env.ARCH }}"
# Get Dockerfile hash for image cache
IMAGE_HASH="${{ hashFiles('./Dockerfile') }}"
# Create image tag
VARIANT="$(TZ=UTC-9 date +%Y%m%d)_${IMAGE_HASH:0:7}"
IMAGE_NAME="slcc_cmake_${{ env.ARCH }}"
TAG="${IMAGE_NAME}:${VARIANT}"
# Cache dir setting
TAR_NAME="${IMAGE_NAME}_${VARIANT}.tar"
TAR_PATH="${CACHE_PATH}/${TAR_NAME}"
echo "TAG=${TAG}" >> $GITHUB_OUTPUT
echo "IMAGE_NAME=${IMAGE_NAME}" >> $GITHUB_OUTPUT
echo "TAR_PATH=${TAR_PATH}" >> $GITHUB_OUTPUT
echo "CACHE_PATH=${CACHE_PATH}" >> $GITHUB_OUTPUT
echo "CACHE_KEY=${IMAGE_NAME}_${VARIANT}" >> $GITHUB_OUTPUT
- name: Enable cache
id: cache
uses: actions/cache@v3
with:
path: ${{ steps.image_tag.outputs.CACHE_PATH }}
key: ${{ steps.image_tag.outputs.CACHE_KEY }}
- name: Load image from cache if exists
if: steps.cache.outputs.cache-hit == 'true'
run: |
docker load -i ${{ steps.image_tag.outputs.TAR_PATH }}
- name: Build image if cache does not exist
if: steps.cache.outputs.cache-hit != 'true'
run: |
docker build -t ${{ steps.image_tag.outputs.TAG }} .
mkdir -p ${{ steps.image_tag.outputs.CACHE_PATH }}
docker save ${{ steps.image_tag.outputs.TAG }} > ${{ steps.image_tag.outputs.TAR_PATH }}
- name: Run tests in container
run: |
# Change owner of workspace to ubuntu user
sudo chown -R 1000:1000 ${{ github.workspace }}
docker run --rm -v ${{ github.workspace }}:/work -w /work ${{ steps.image_tag.outputs.TAG }} make test
要所の説明
キャッシュに使うキーとかをうまく生成する
- name: Create image tag
id: image_tag
run: |
# Define cache dir
CACHE_PATH="/tmp/docker_cache_${{ env.ARCH }}"
# Get Dockerfile hash for image cache
IMAGE_HASH="${{ hashFiles('./Dockerfile') }}"
# Create image tag
VARIANT="$(TZ=UTC-9 date +%Y%m%d)_${IMAGE_HASH:0:7}"
IMAGE_NAME="slcc_cmake_${{ env.ARCH }}"
TAG="${IMAGE_NAME}:${VARIANT}"
# Cache dir setting
TAR_NAME="${IMAGE_NAME}_${VARIANT}.tar"
TAR_PATH="${CACHE_PATH}/${TAR_NAME}"
echo "TAG=${TAG}" >> $GITHUB_OUTPUT
echo "IMAGE_NAME=${IMAGE_NAME}" >> $GITHUB_OUTPUT
echo "TAR_PATH=${TAR_PATH}" >> $GITHUB_OUTPUT
echo "CACHE_PATH=${CACHE_PATH}" >> $GITHUB_OUTPUT
echo "CACHE_KEY=${IMAGE_NAME}_${VARIANT}" >> $GITHUB_OUTPUT
キャッシュには actions/cache@v3
を使っています。
これを使うにはキャッシュしたいもののパスとキャッシュのキーを指定する必要があります。
今回は docker build
の成果物をキャッシュしたいのでそのあたりを上手くやるために、予めパスとキーを作っています。
キーは適当に日付と Dockerfile のハッシュから作っています。
ハッシュだけでも良いけど、日付変わったらまあキャッシュ吹き飛ばしてもいいかな、と思ってそうしています。
作ったパスとかは echo "HOGE=${HOGE}" >> $GITHUB_OUTPUT
みたいな形で $GITHUB_OUTPUT
に対して追記で突っ込んでやることで後続のステップでも使えます。
以前は set-output
のようなものを使っていたようですが、これは非推奨となっていてそのうち使えなくなるっぽいので上記の方法を使ったほうが良いです。
キャッシュを使う
- name: Enable cache
id: cache
uses: actions/cache@v3
with:
path: ${{ steps.image_tag.outputs.CACHE_PATH }}
key: ${{ steps.image_tag.outputs.CACHE_KEY }}
${{ steps.[id].output.[NAME] }}
みたいな形で先程の出力が取り出せるので利用します。
たったこれだけでキャッシュがあればロードしてくれて、さらにジョブが成功したタイミングでキャッシュを保存してくれます。
キャッシュがヒットしたらそれを使う、ヒットしなければビルドするみたいなことは以下のようにすればよいです。
- name: Load image from cache if exists
if: steps.cache.outputs.cache-hit == 'true'
run: |
docker load -i ${{ steps.image_tag.outputs.TAR_PATH }}
- name: Build image if cache does not exist
if: steps.cache.outputs.cache-hit != 'true'
run: |
docker build -t ${{ steps.image_tag.outputs.TAG }} .
mkdir -p ${{ steps.image_tag.outputs.CACHE_PATH }}
docker save ${{ steps.image_tag.outputs.TAG }} > ${{ steps.image_tag.outputs.TAR_PATH }}
steps.cache.outputs.cache-hit
にキャッシュがあったかどうかが入っています。
ので単純にこれで分岐してあげればよいです。
ヒットしなかったら適当にビルドして、キャッシュの保存先を mkdir
して docker save
しましょう。
こんな感じでキャッシュヒットするとロードに成功し、
実行時間も半分以下に短縮されました。
今回の Dockerfile は比較的単純なため、ビルドしていてもあまり時間はかかりませんが、複雑なイメージを作ったり、ライブラリ等を入れまくったりする場合にはかなり有用になるはず。
make test
する
コンテナの中で トラップのところで解説しています。
大本が runner ユーザーで走ってしまうのでトラップがあります。
トラップ
env の中で env は呼び出せない
ARCH を変える仕組みは GitHub Actions にはないっぽいですが、いい感じに変数として使い回すだけ使いまわしたいなーと思ってやってみたが、普通に怒られた
runner ユーザーで GitHub Actions は走る
runs_on: ubuntu-22.04
としているので ubuntu ユーザーで動くと思ってましたが、違う。
そして ubuntu ユーザーは大体の場合、uid: 1000 になるのに対して、runner は 1001 となります。
docker コンテナの中でビルドをしようとすると当然ユーザーが異なるので怒られます。
今回はどうにかしてローカルでも GitHub Actions 上でも同じ Dockerfile を使いたかったので ubuntu ユーザーでどちらも動くようにしたいです。
無料でやりたいのでセルフホステッド的なやつも選択肢から除外されます。
結局、あまりいい方法が思い浮かばなかったのでコンテナ内でコマンドを実行する前に chown
で無理やりユーザーを書き換えてやっています。
- name: Run tests in container
run: |
# Change owner of workspace to ubuntu user
sudo chown -R 1000:1000 ${{ github.workspace }}
docker run --rm -v ${{ github.workspace }}:/work -w /work ${{ steps.image_tag.outputs.TAG }} make test
Discussion