太陽がまぶしかったから

C'etait a cause du soleil.

GitHub Actions でカスタム Docker コンテナを GHCR に登録して CI 環境の pytest ジョブをコンテナ内で実行

GitHub Actions 実践入門 技術の泉シリーズ (技術の泉シリーズ(NextPublishing))

GitHub Actions を CI 活用

 上記のようなプログラムを管理するのにあたって CI 環境を構築したい。CI とは Continuous Integration の略で複数の開発者が継続的にメインブランチに統合していくこと。これを実現するためにはローカルテストだけではなく、例えばソースコード管理ツールに変更がプッシュされたらサーバー側で自動的にテストして結果の通知をしてあげるといった必要が出てくる。

 自分自身は独りでの開発をしているのだけど、ローカルで毎回毎回全てのテストを回すのは非効率的。サーバー側のコード変更をしたらサーバー側で統合的なテストを自動的にやった方がローカル環境の処理負荷を下げられるし、うっかりミスも減らせる。

 CI を実現する手段としては Circle CI が一般的であったが、GitHub 内の標準機能として GitHub Actions が公開されているため、こちらを利用することした。GitHub の標準機能を使うことで GitHub トークンを外部サービスに渡す必要がないし、公開リポジトリであれば無償で利用することができる。

GitHub Actions の実行と憂鬱

 実際的には下記のような yml をリポジトリの .github/workflows フォルダに配置している。詳ししい利用方法は公式ドキュメントアイキャッチになっている解説書を読んでほしい。

name: pyetst
on:
  push:
    branches:
      - main
    paths:
      - 'src/*'
      - 'tests/*'
      - 'config/*'
      - '.github/workflows/*'
  pull_request:
    branches:
        -main
jobs:
  unit-test:
    timeout-minutes: 15
    runs-on: ubuntu-latest
    container: ghcr.io/${{ github.repository }}:latest
    steps:
      - uses: actions/checkout@v2
      - run: pytest -s ./tests/

 yml ファイルではアクションのトリガーと実行するジョブを定義しているが、通常のお作法では ubuntu コンテナに対して python のインストールから pip インストール。さらには pip ダウンロードのキャッシュフォルダを Github Actions 側のキャッシュに登録・復元といったジョブを独自記法で記載していく必要があり、作ってはみたものの冗長かつ二重メンテナンスとなり、 Dockerfile で定義した環境とも微妙に異なるといった問題があった。

 そもそも環境依存をしないために Dockerfile を用意しているのだから Docker Image を GitHub Actions からも使い回したい。これを実現する方法として GHCR(GitHub Container Repository) にカスタムコンテナを公開イメージとして登録して、テストジョブの起動コンテナとして利用することとした。

GitHub Actions で GHCR に Docker イメージを登録

 GHCR とは GitHub Packages の後継で Docker などのコンテナイメージを登録できるリポジトリ。もともと使われていた Docker Hub の無料利用範囲が縮小していく中での選択肢として有力になっている。現在はβ版であるが近々標準機能となるであろう。

 GHCR を利用するためには GitHub のユーザーアイコンから Feature Preview から Improved container support を enable にする必要がある。

 GHCR も GitHub と統合しており、GitHubトークンさえあれば利用できるようになっている。またコンテナを Public 設定にしておけば GitHub Actions の実行コンテナとして簡単に指定できる。

 上記を参考に以下のジョブを作成。

name: docker-push
on:
  push:
    branches:
      - main
    paths:
      - 'Dockerfile'
jobs:
  docker_push:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v2
      - uses: docker/setup-buildx-action@v1
      - uses: docker/login-action@v1
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v2
        with:
          context: .
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:latest

 Dockerfile が main ブランチにプッシュされたら、チェックアウト → docker setup → docker login → docker build / push。github.repository 環境変数bulldra/python-boilerplate と展開されて登録される。GHCRの登録先はオーナー名/イメージ名形式で指定する必要があるため、基本的にはリポジトリ名での登録で良いと判断している。

 登録したコンテナは package 管理から公開設定可能。ここを含めて自動化したい気持ちもあるが最初の一回は手動で設定した。

GHCRに登録したコンテナで pytest ジョブを実行

 あとは、テスト側のジョブから container: ghcr.io/${{ github.repository }}:latest で実行環境として GHCR に登録されたコンテナを指定するだけだ。

jobs:
  unit-test:
    timeout-minutes: 15
    runs-on: ubuntu-latest
    container: ghcr.io/${{ github.repository }}:latest
    steps:
      - uses: actions/checkout@v2
      - run: pytest -s ./tests/

 Initialize Container の段階で先にビルドして登録された Docker Image をダウンロードして実行されるため、毎回のジョブで ubuntu イメージに python や pip モジュールなどのインストールをしていくよりも負荷をかけずに素早く実行できるし、ジョブファイル自体の独自記述量を少なくできる。

 ワークフローやコンテナレジストリといった領域も GitHub に統合されていくのは少し怖いが、その反面としてセキュリティ上のリスクを減らせたり、簡単な記法で統合活用できる便利さが勝るため活用していきたい。