🙆‍♀️

GitHub Actions で Dockerfile のビルドをしてその中でテストを動かす

2023/01/28に公開

zenn 初投稿です、よろしくね

きっかけ

普段はいわゆる Web 周りのお仕事がメインなので久々に低レイヤー側に触れたくなって 低レイヤを知りたい人のためのCコンパイラ作成入門 をコツコツと進めています。

その中でテストを自動で動かしたいな、と思いました。
普段仕事では CircleCI を使っているので GitHub Actions でいい感じにやりたいな、そう思ったときにトラップにハマったので備忘録を残します

やりたかったこと

  • ローカルでは Dockerfile に書いた環境上で make したい
  • Actions 上でも同じ Dockerfile をビルドし、その中でやりたい
    • Actions 側では make test を自動実行し、コケたら main にマージできないようにしたい(よくやるやつ)
  • コンテナは root ではなく、ubuntu ユーザーで動かしたい(一応)
  • イメージを毎回ビルドすると時間かかるのでイメージのビルドはキャッシュしたい

最終的な設定

https://github.com/SeeLog/slcc/pull/7

# 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 しましょう。

https://github.com/SeeLog/slcc/actions/runs/4027489592/jobs/6923279346

こんな感じでキャッシュヒットするとロードに成功し、

実行時間も半分以下に短縮されました。

今回の Dockerfile は比較的単純なため、ビルドしていてもあまり時間はかかりませんが、複雑なイメージを作ったり、ライブラリ等を入れまくったりする場合にはかなり有用になるはず。

コンテナの中で make test する

トラップのところで解説しています。
大本が runner ユーザーで走ってしまうのでトラップがあります。

トラップ

env の中で env は呼び出せない

https://github.com/SeeLog/slcc/actions/runs/4026773668/workflow

ARCH を変える仕組みは GitHub Actions にはないっぽいですが、いい感じに変数として使い回すだけ使いまわしたいなーと思ってやってみたが、普通に怒られた

runner ユーザーで GitHub Actions は走る

https://github.com/SeeLog/slcc/actions/runs/4026964982/jobs/6922121515

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