LoginSignup
68
35

More than 1 year has passed since last update.

【裏技】別ファイルに切り出した Job 間で Docker イメージを共有し,高速に GitHub Actions をぶん回す

Last updated at Posted at 2022-06-28

この記事は @yu-ichiro くんの記事と合わせて二本立てとなっています。こちらもどうぞご覧ください!
GitHub Actions上でdocker composeを使ってCIを回すためにうまいことキャッシュする方法

はじめに

ワイ「ふ〜,また一瞬で週末が過ぎてしもたわ〜」
ワイ「グータラ寝てばっかりで全然勉強しなかったし」
ワイ「たまには Qiita でも読むか〜」
ワイ「お,弊社の22卒で取締役@yu-ichiro くんが Qiita 記事書いとるな」

ワイ「GitHub Actions の記事か〜,CI 構築するの苦手なんよなぁ〜」
ワイ「ほとんど自分で作ったこともないし,重い腰上げて読んでみるか」

~1分後~

各パッケージ毎に、dockerのimageを利用しつつCIを回す方法(弊社の @fuwasegu くんが実装したのでもしかしたら書いてくれるかも・・?チラッチラッ)

ワイ「そんな実装したっけ?」
ワイ「そう言えばそんな記憶もあるような無いような...」
ワイ「全然思い出せないけど,もし本当ならワイ天才やな!」

2歳息子「お,パパ,何の絵本読んでるの?」

ワイ「これは絵本じゃなくて,CI の記事や」

息子「CI って,パパのこと?」

ワイ「誰が Conceited Intelligence (自惚れの強い知能)やねん」

息子「(自覚してるやん)」

そんな茶番は置いといて

ということで,弊社の @yu-ichiro くんからご指名いただきましたので, GitHubActions に関する知見を共有します.

彼の記事でも述べられていますが,「CI 環境で Docker を動かす」というのはいくらかツラミがあります.
なぜなら,愚直に 「Docker イメージをダウンロード → イメージをビルド → 静的解析やテストを実行」というフローを構築してしまうと, Docker 環境の準備に多くのリソースを使ってしまうからです.

そこで先の記事では, Docker イメージをキャッシュして使いまわす という手段でこの問題を解決しています.
この方法で,かなり CI の実行時間がかなり削減されていますね.

キャッシュヒット時:イメージの pull, build, push 込みで 35s
キャッシュミス時:イメージの pull, build, push 込みで 2m 45s

ただ,人間は欲深いもので,これだけでは飽き足りません.

ワイ「イメージのキャッシュを使って,CI の実行速度が爆速化したぞ!」
ワイ「でも,パッケージが10個もあるから,結局時間がかかってしまう...」
ワイ「そうや,各パッケージの処理を Job に切り出して,並列処理すればええんや!」
ワイ「あれ,でも,それぞれパッケージに対応する Job を作らんとあかんのかな...?」
ワイ「メソッドやクラスみたいに,共通の Job を作って使いまわしたいな...」
ワイ「そもそも,Job に Docker のイメージキャッシュって引き継げるん...?」

モジュラモノリスアーキテクチャのように,1つのモノリスの中に独立したパッケージが複数あるようなプロジェクトのでは,それぞれのパッケージの CI 処理を並列実行して,さらに実行時間を削減したいところですよね.

本記事では,Docker イメージのキャッシュを引き継ぎつつ,パッケージ毎の CI 処理を Job に切り出して並列実行する方法について解説します.

注意!
本記事では YAML の記述例が沢山出てきますので記事が長くなってしまい読みづいかもしれません.
適宜読み飛ばしていただけると幸いですm(_ _)m

今回解説すること・解説しないこと

解説すること

  • Job を別ファイルに切り出して各パッケージの処理を共通化し使い回す方法
  • 切り出した Job で Docker イメージのキャッシュを使う方法

解説しないこと

  • Docker イメージのキャッシュのやりかた
    • @yu-ichiro くんの記事を先に読んでいただくと,導入としてスムーズかなと思います!(再掲)

前提

  • 先の記事の方法でキャッシュした Docker イメージがある
  • マルチパッケージ構成の Laravel プロジェクトの CI を考える
    • 「User パッケージ」,「AWS パッケージ」,「Auth パッケージ」「Slack パッケージ」の4パッケージで構成されているとします.
  • 今回は,各パッケージで PHPUnit によるユニットテストを実行してみることを目標にします.

一応,先の記事で成果物として示してある ci.yml を引用して置いておきます.
今回の記事では,この ci.yml の値を使ったり,ここにさらにコードを追加したりしていきます.

ci.yml
on:
    [ push ]
env:
    TERM: xterm-256color
    COMPOSE_FILE: compose.ci.yaml
name: CI
jobs:
    docker-build:
        runs-on: ubuntu-latest
        outputs:
            docker_image_tag_ci: ${{ steps.generate_docker_image_tag.outputs.docker_image_tag_ci }}
        steps:
            -   name: Git Checkout
                uses: actions/checkout@v3
            -   name: Cache Docker Build Cache
                uses: actions/cache@v3
                with:
                    path: /tmp/.buildx-cache
                    key: docker-build-cache-${{ github.ref }}-${{ github.sha }}
                    restore-keys: |
                        docker-build-cache-${{ github.ref }}
                        docker-build-cache-
            -   name: Set up Docker Buildx
                id: buildx
                uses: docker/setup-buildx-action@v1
                with:
                    driver-opts: network=host
            -   name: Generate Docker Image Tag
                id: generate_docker_image_tag
                run: |
                    SHA=${{ github.sha }}
                    TAG=$(TZ=UTC-9 date '+%Y%m')-${SHA:0:7}
                    echo "DOCKER_IMAGE_TAG_CI=$TAG" >> $GITHUB_ENV
                    echo TAG $TAG
                    echo "::set-output name=docker_image_tag_ci::$TAG"
            -   name: Cache Docker Registry
                uses: actions/cache@v3
                with:
                    path: /tmp/docker-registry
                    key: docker-registry-${{ github.ref }}-${{ github.sha }}
                    restore-keys: |
                        docker-registry-${{ github.ref }}
                        docker-registry-
            -   name: Boot-up Local Docker Registry
                run: docker run -d -p 5000:5000 --restart=always --name registry -v /tmp/docker-registry:/var/lib/registry registry:2
            -   name: Wait for Docker Registry
                run: npx wait-on tcp:5000
            -   name: Build Docker Image
                env:
                    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
                run: |
                    cd docker &&
                    docker buildx bake \
                        --builder="${{ steps.buildx.outputs.name }}" \
                        --set='*.cache-from=type=local,src=/tmp/.buildx-cache' \
                        --set='*.cache-to=type=local,dest=/tmp/.buildx-cache' \
                        --push \
                        -f "${COMPOSE_FILE}"

まずは愚直実装

とりあえず順番に処理を書き連ねてみます.
現在の ci.yml の段階で, Docker イメージのビルドまではできているので,あとはコンテナに入って composer install をして必要なライブラリなどを取得し, Unit テストを実行すれば良さそうですね.
以下に ci.yml に付け加える形で実装例を示します.(長いのでアコーディオンで閉じます)

ci.yml
ci.yml
on:
    [ push ]
env:
    TERM: xterm-256color
    COMPOSE_FILE: compose.ci.yaml
name: CI
jobs:
    docker-build:
        runs-on: ubuntu-latest
        outputs:
            docker_image_tag_ci: ${{ steps.generate_docker_image_tag.outputs.docker_image_tag_ci }}
        steps:
            -   name: Git Checkout
                uses: actions/checkout@v3
            -   name: Cache Docker Build Cache
                uses: actions/cache@v3
                with:
                    path: /tmp/.buildx-cache
                    key: docker-build-cache-${{ github.ref }}-${{ github.sha }}
                    restore-keys: |
                        docker-build-cache-${{ github.ref }}
                        docker-build-cache-
            -   name: Set up Docker Buildx
                id: buildx
                uses: docker/setup-buildx-action@v1
                with:
                    driver-opts: network=host
            -   name: Generate Docker Image Tag
                id: generate_docker_image_tag
                run: |
                    SHA=${{ github.sha }}
                    TAG=$(TZ=UTC-9 date '+%Y%m')-${SHA:0:7}
                    echo "DOCKER_IMAGE_TAG_CI=$TAG" >> $GITHUB_ENV
                    echo TAG $TAG
                    echo "::set-output name=docker_image_tag_ci::$TAG"
            -   name: Cache Docker Registry
                uses: actions/cache@v3
                with:
                    path: /tmp/docker-registry
                    key: docker-registry-${{ github.ref }}-${{ github.sha }}
                    restore-keys: |
                        docker-registry-${{ github.ref }}
                        docker-registry-
            -   name: Boot-up Local Docker Registry
                run: docker run -d -p 5000:5000 --restart=always --name registry -v /tmp/docker-registry:/var/lib/registry registry:2
            -   name: Wait for Docker Registry
                run: npx wait-on tcp:5000
            -   name: Build Docker Image
                env:
                    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
                run: |
                    cd docker &&
                    docker buildx bake \
                        --builder="${{ steps.buildx.outputs.name }}" \
                        --set='*.cache-from=type=local,src=/tmp/.buildx-cache' \
                        --set='*.cache-to=type=local,dest=/tmp/.buildx-cache' \
                        --push \
                        -f "${COMPOSE_FILE}"
            -   name: Docker Compose Up
                run: |
                    cd docker &&
                    docker compose up -d php postgres
            -   name: user-package composer install
                run: docker exec -it php bash -c "cd ./packages/user-package &&composer install"
            -   name: user-package unit test
                run:
                    docker exec -it php bash -c "cd ./packages/user-package && vendor/bin/phpunit"
            -   name: aws-package composer install
                run: docker exec -it php bash -c "cd ./packages/aws-package &&composer install"
            -   name: aws-package unit test
                run:
                    docker exec -it php bash -c "cd ./packages/aws-package && vendor/bin/phpunit"
            -   name: auth-package composer install
                run: docker exec -it php bash -c "cd ./packages/auth-package &&composer install"
            -   name: auth-package unit test
                run:
                    docker exec -it php bash -c "cd ./packages/auth-package && vendor/bin/phpunit"
            -   name: slack-package composer install
                run: docker exec -it php bash -c "cd ./packages/slack-package &&composer install"
            -   name: slack-package unit test
                run:
                    docker exec -it php bash -c "cd ./packages/slack-package && vendor/bin/phpunit"
 steps:
     # 省略
     -   name: Build Docker Image
         env:
             GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         run: |
             cd docker &&
             docker buildx bake \
                 --builder="${{ steps.buildx.outputs.name }}" \
                 --set='*.cache-from=type=local,src=/tmp/.buildx-cache' \
                 --set='*.cache-to=type=local,dest=/tmp/.buildx-cache' \
                 --push \
                 -f "${COMPOSE_FILE}"
+    -   name: Docker Compose Up
+        run: cd docker && docker compose up -d php postgres
+    -   name: user-package composer install
+        run: docker exec -it php bash -c "cd ./packages/user-package &&composer install"
+    -   name: user-package unit test
+        run: docker exec -it php bash -c "cd ./packages/user-package && vendor/bin/phpunit"

(パイプを使った YAML の改行部分が, Qiita の diff 機能では上手く表示できなかったためつなげています)

お,わりと簡単にできました!
しかしながら,実はここで1つ問題があります. 当然ながら,同じ Job 内の Step は直列で実行されます.
Unit テストくらいなら十数秒くらいで終わりますが,ここに PHPStan や PHP CS Fixer などの静的解析 も入ってくると, 各パッケージ毎に 2 〜 5 分ほどかかってしまって,パッケージが増えると単調増加で実行時間が伸びていってしまいます.

さて,ここでなんとかするために思いつくのが 並列実行 ですね!

Step の並列実行はできる?

結論から言うと, Step の並列実行はできません
しかしながら, Step の集まりである Job は,縦に書き並べるだけでデフォルトで並列実行されます.
したがって,パッケージごとに Job を使ってまとめ,それぞれを並列で実行するように修正してみたいと思います.

Job を切り出す

GitHub Actions における Job とは,複数のステップ(シェルスクリプトの1コマンドに相当)の集まりのことを指します.

例えば

sample.yml
jobs:
    sample_job;
        steps:
            - name: change dir
            - run: cd /path/to/dir
            - name: echo hello
            - run: echo 'hello'

みたいな感じで定義されるやつで,順番に名前のついたコマンドを実行していくものになります.

ここでは,各パッケージで行いたい共通の処理を Job に切り出してみます.
今回共通化したい処理は大まかに次の通りです.

  1. Docker イメージを持ってきてコンテナを起動する
  2. composer install など,環境構築に必要なコマンドを実行する
  3. テストを実行する

では,上記の ci.yml に新たな Job を追加してみましょう.

1. Docker イメージを持ってきてコンテナを起動する

1つ前の Job である docker-build では, outputs キーワードを使って, Job で処理した結果を出力しています.
outputs は Job に設定できるキーワードで,その Job の出力(メソッドで言う return のようなもの)を設定できます.この値は,この Job に依存している(needs キーワード)すべての下流の Job で利用することができます.
また,後述しますが,inputs を使ってこの値を取り込んで使うことができます.

outputs に関する詳細な情報はこちら.

ここで出力しているのは docker_image_tag_ci です.これは,キャッシュしたイメージのタグですね.これを使って,どのイメージを使えばいいかを指定することができます.

【重要ポイント】 Job 間での Docker イメージの共有方法 【裏技】

通常,GitHub Actions の Job 間でのデータのやり取りはこの outputsinputs を使った方法でのみ行われますが,今回は Docker のイメージ自体(ファイル)を共有したいので,この方法が使えません.
そこで,裏技的に 同じ key, 同じ path のキャッシュを使う ことで,擬似的なファイル共有を実現しました.

jobs:
    docker-build:
        runs-on: ubuntu-latest
        outputs:
            docker_image_tag_ci: ${{ steps.generate_docker_image_tag.outputs.docker_image_tag_ci }}
        steps:
            # 省略
            -   name: Cache Docker Registry # このステップのデータが
                uses: actions/cache@v3
                with:
                    path: /tmp/docker-registry # 同じパス
                    key: docker-registry-${{ github.ref }}-${{ github.sha }} # 同じキーにすると
                    restore-keys: |
                        docker-registry-${{ github.ref }}
                        docker-registry-
        # 省略
    sample-package-test:
        runs-on: ubuntu-latest
        needs: docker-build
        steps:
            # 省略
            -   name: Cache Docker Registry # こっちで読み取れる(共有されてる)
                uses: actions/cache@v3
                with:
                    path: /tmp/docker-registry
                    key: docker-registry-${{ github.ref }}-${{ github.sha }}
                    restore-keys: |
                        docker-registry-${{ github.ref }}
                        docker-registry-

この方法を使うことで,docker-build でビルドした Docker イメージを,別の Job でも使うことができるため,Job 毎に毎回 Docker イメージをビルドしなくてすみます.(天才)

実際にはリポジトリからファイルをチェックアウトしたりだとか,ローカルの Docker レジストリを起動したりだとか,細々とした処理が並びますが,この辺の処理はほとんど docker-build Job からコピペしてくれば大丈夫です.

以下に実装例を示します.(長いためアコーディオンで閉じます)

実装例
jobs:
    # ココから上は docker-build Job
    sample-package-test:
        runs-on: ubuntu-latest
        needs: docker-build
        steps:
            -   name: Git Checkout
                uses: actions/checkout@v3
            -   name: Cache Docker Registry
                uses: actions/cache@v3
                with:
                    path: /tmp/docker-registry
                    key: docker-registry-${{ github.ref }}-${{ github.sha }}
                    restore-keys: |
                        docker-registry-${{ github.ref }}
                        docker-registry-
            -   name: Boot-up Local Docker Registry
                run: docker run -d -p 5000:5000 --restart=always --name registry -v /tmp/docker-registry:/var/lib/registry registry:2
            -   name: Wait for Docker Registry
                run: npx wait-on tcp:5000
            -   name: Get Docker Image Tag
                env:
                    TAG: ${{ inputs.docker_image_tag_ci }}
                run: |
                    echo "DOCKER_IMAGE_TAG_CI=$TAG" >> $GITHUB_ENV
                    echo TAG $TAG
            -   name: Docker Compose Pull
                run: |
                    cd docker &&
                    docker compose pull php postgres
            -   name: Docker Compose Up
                run: |
                    cd docker &&
                    docker compose up -d php postgres

今回の方法でキャッシュして共有しているのは Docker イメージを持っている レジストリ のデータです.
したがって,この Docker イメージを他の Job で利用するためには,レジストリからデータを Pull してくる必要があるので,docker compose pull を実行するステップが追加されていることに注意してください.

2. composer install など,環境構築に必要なコマンドを実行する

次に, composer install などの環境構築をやっていきます.
ここはそこまで難しくはないため解説は割愛します.
実装例を以下に示します.

-   name: composer install
    run: docker exec -it php bash -c "composer install"

Laravel を使っているなら,このタイミングで .env の準備や, APP_KEY の準備なんかも必要です.今回のアーキテクチャでは .env の準備や, APP_KEY の準備は Laravel アプリケーション本体に関する CI のときのみ必要になりますので,その場合だけ以下の step を追加してください.

-   name: Setup
    run: |
        cp src/.env.example src/.env &&
        docker exec -it php bash -c "php artisan key:generate"

3. テストを実行する

最後は,テストを実行して終わりですね.
ここもそこまで難しくはないため解説は割愛します.

-   name: test
    run:
        docker exec -it php bash -c "cd ./packages/sample-package && vendor/bin/phpunit"

一旦完成したけど...?

さて, パッケージのテストを行う Job が完成したので,もともとあった ci.yml に付け足して,実際にテストを行う一連の YAML を書いてみましょう!
以下に,それぞれのパッケージの CI 処理を行うように, ci.yml の実装例を示します.(長いのでアコーディオンで閉じます)

ci.yml
ci.yml
on:
    [ push ]
env:
    TERM: xterm-256color
    COMPOSE_FILE: compose.ci.yaml
name: CI
jobs:
    docker-build:
        runs-on: ubuntu-latest
        outputs:
            docker_image_tag_ci: ${{ steps.generate_docker_image_tag.outputs.docker_image_tag_ci }}
        steps:
            -   name: Git Checkout
                uses: actions/checkout@v3
            -   name: Cache Docker Build Cache
                uses: actions/cache@v3
                with:
                    path: /tmp/.buildx-cache
                    key: docker-build-cache-${{ github.ref }}-${{ github.sha }}
                    restore-keys: |
                        docker-build-cache-${{ github.ref }}
                        docker-build-cache-
            -   name: Set up Docker Buildx
                id: buildx
                uses: docker/setup-buildx-action@v1
                with:
                    driver-opts: network=host
            -   name: Generate Docker Image Tag
                id: generate_docker_image_tag
                run: |
                    SHA=${{ github.sha }}
                    TAG=$(TZ=UTC-9 date '+%Y%m')-${SHA:0:7}
                    echo "DOCKER_IMAGE_TAG_CI=$TAG" >> $GITHUB_ENV
                    echo TAG $TAG
                    echo "::set-output name=docker_image_tag_ci::$TAG"
            -   name: Cache Docker Registry
                uses: actions/cache@v3
                with:
                    path: /tmp/docker-registry
                    key: docker-registry-${{ github.ref }}-${{ github.sha }}
                    restore-keys: |
                        docker-registry-${{ github.ref }}
                        docker-registry-
            -   name: Boot-up Local Docker Registry
                run: docker run -d -p 5000:5000 --restart=always --name registry -v /tmp/docker-registry:/var/lib/registry registry:2
            -   name: Wait for Docker Registry
                run: npx wait-on tcp:5000
            -   name: Build Docker Image
                env:
                    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
                run: |
                    cd docker &&
                    docker buildx bake \
                        --builder="${{ steps.buildx.outputs.name }}" \
                        --set='*.cache-from=type=local,src=/tmp/.buildx-cache' \
                        --set='*.cache-to=type=local,dest=/tmp/.buildx-cache' \
                        --push \
                        -f "${COMPOSE_FILE}"
    user-package:
        runs-on: ubuntu-latest
        needs: docker-build
        steps:
            -   name: Git Checkout
                uses: actions/checkout@v3
            -   name: Cache Docker Registry
                uses: actions/cache@v3
                with:
                    path: /tmp/docker-registry
                    key: docker-registry-${{ github.ref }}-${{ github.sha }}
                    restore-keys: |
                        docker-registry-${{ github.ref }}
                        docker-registry-
            -   name: Boot-up Local Docker Registry
                run: docker run -d -p 5000:5000 --restart=always --name registry -v /tmp/docker-registry:/var/lib/registry registry:2
            -   name: Wait for Docker Registry
                run: npx wait-on tcp:5000
            -   name: Get Docker Image Tag
                run: |
                    SHA=${{ github.sha }}
                    TAG=$(TZ=UTC-9 date '+%Y%m')-${SHA:0:7}
                    echo "DOCKER_IMAGE_TAG_CI=$TAG" >> $GITHUB_ENV
                    echo TAG $TAG
            -   name: Docker Compose Pull
                run: |
                    cd docker &&
                    docker compose pull php postgres
            -   name: Docker Compose Up
                run: |
                    cd docker &&
                    docker compose up -d php postgres
            -   name: composer install
                run: docker exec -it php bash -c "cd ./packages/user && composer install"
            -   name: test
                run:
                    docker exec -it php bash -c "cd ./packages/user && vendor/bin/phpunit"

    aws-package:
        # え?
    auth-package:
        # あと 3 回も
    slack-package:
        # 同じこと書くの?

はい,おわかりいただけたでしょうか.上記の実装だと数十行ある処理をパッケージの数だけコピペしなければいけないのです.
これはちょっとかっこよくないですよね...
そこで,次にこのパッケージ用 Job を別ファイルに切り出してみたいと思います.

再利用できる形で Job を別ファイルに切り出す

Github Actions では,ワークフローをファイル分割して再利用することができ,上記の長々とした処理を任意のパッケージに対して行うことができる Job として別の YAML に定義することができます.
ここで,この「任意のパッケージに対して行うことができる」を実現するには Job に引数を与えて,パッケージ名(パッケージパス)を外から渡せるようにする必要がありますね.

GitHub Actions の Job は, 以下のように input キーワードを使うことで引数として値を受け取ることができます.

on:
  workflow_call:
    inputs:
      package_name:
        required: true
        type: string

型を指定したり,必須かどうかを決めることができるので,割と使い勝手が良いと思います.
詳しくは以下のページを御覧ください.

今回は「どの Docker イメージのキャッシュを使って」「どのパッケージに対して CI を実行するか」を決めてあげる必要があるため,以下のように引数を決めておきます.
Docker イメージは, 先程も使った ci.ymloutputs キーワードで設定してある docker_image_tag_ci を使って決定します.

on:
    workflow_call:
        inputs:
            package_name:
                required: true
                type: string
            docker_image_tag_ci:
                required: true
                type: string
        secret: {}

そして,ここで受け取った引数は, ${{ inputs.引数名 }} で使うことができます.

on: workflow_call: secrets: は,何も指定するものが無くても YAML 項目として書いておかないと,この Job が reusable な Job として認識されないようでした.

パッケージ毎に Job を実行する

さぁ, Job の別ファイルへの切り出しはできました.あとは,この Job をパッケージの数だけそれぞれ並列で実行するのみです.
先程何度も同じことを書いていたところへ,今切り出した Job ファイルを使うよう書き換えてみます.

切り出した Job の YAML ファイルの呼び出し方は次の通りです.

{Job 名}:
        needs: docker-build
        uses: ./.github/workflows/package-job.yml
        with:
            package_name: {パッケージ名}
            docker_image_tag_ci: ${{ needs.docker_build.outputs.docker_image_tag_ci }}

先程も出てきましたが,needs には依存する Job を書きます.今回は, docker-build という Job の結果を持って各パッケージの処理を行うため, docker-build Job に依存することになります.
また,uses には,どの YAML ファイルを実行するかを指定します.
そして with には, uses で指定した Job に渡す値を設定します.今回は「どの Docker イメージのキャッシュを使って」「どのパッケージに対して CI を実行するか」を決めてあげる必要があるため,パッケージ名と Docker イメージのタグを指定するようにしましたね.

完成版の ci.ymlpackage-job.yml を以下に示します.

ci.yml
on:
    [ push ]
env:
    TERM: xterm-256color
    COMPOSE_FILE: compose.ci.yaml
name: CI
jobs:
    docker-build:
        runs-on: ubuntu-latest
        outputs:
            docker_image_tag_ci: ${{ steps.generate_docker_image_tag.outputs.docker_image_tag_ci }}
        steps:
            -   name: Git Checkout
                uses: actions/checkout@v3
            -   name: Cache Docker Build Cache
                uses: actions/cache@v3
                with:
                    path: /tmp/.buildx-cache
                    key: docker-build-cache-${{ github.ref }}-${{ github.sha }}
                    restore-keys: |
                        docker-build-cache-${{ github.ref }}
                        docker-build-cache-
            -   name: Set up Docker Buildx
                id: buildx
                uses: docker/setup-buildx-action@v1
                with:
                    driver-opts: network=host
            -   name: Generate Docker Image Tag
                id: generate_docker_image_tag
                run: |
                    SHA=${{ github.sha }}
                    TAG=$(TZ=UTC-9 date '+%Y%m')-${SHA:0:7}
                    echo "DOCKER_IMAGE_TAG_CI=$TAG" >> $GITHUB_ENV
                    echo TAG $TAG
                    echo "::set-output name=docker_image_tag_ci::$TAG"
            -   name: Cache Docker Registry
                uses: actions/cache@v3
                with:
                    path: /tmp/docker-registry
                    key: docker-registry-${{ github.ref }}-${{ github.sha }}
                    restore-keys: |
                        docker-registry-${{ github.ref }}
                        docker-registry-
            -   name: Boot-up Local Docker Registry
                run: docker run -d -p 5000:5000 --restart=always --name registry -v /tmp/docker-registry:/var/lib/registry registry:2
            -   name: Wait for Docker Registry
                run: npx wait-on tcp:5000
            -   name: Build Docker Image
                env:
                    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
                run: |
                    cd docker &&
                    docker buildx bake \
                        --builder="${{ steps.buildx.outputs.name }}" \
                        --set='*.cache-from=type=local,src=/tmp/.buildx-cache' \
                        --set='*.cache-to=type=local,dest=/tmp/.buildx-cache' \
                        --push \
                        -f "${COMPOSE_FILE}"
    user-package:
        needs: docker-build
        uses: ./.github/workflows/package-job.yml
        with:
            package_name: 'user'
            docker_image_tag_ci: ${{ needs.docker_build.outputs.docker_image_tag_ci }}
    aws-package:
        needs: docker-build
        uses: ./.github/workflows/package-job.yml
        with:
            package_name: 'aws'
            docker_image_tag_ci: ${{ needs.docker_build.outputs.docker_image_tag_ci }}
    auth-package:
        needs: docker-build
        uses: ./.github/workflows/package-job.yml
        with:
            package_name: 'auth'
            docker_image_tag_ci: ${{ needs.docker_build.outputs.docker_image_tag_ci }}
    slack-package:
        needs: docker-build
        uses: ./.github/workflows/package-job.yml
        with:
            package_name: 'slack'
            docker_image_tag_ci: ${{ needs.docker_build.outputs.docker_image_tag_ci }}
package-job.yml
name: Package test workflow

on:
    workflow_call:
        inputs:
            package_name:
                required: true
                type: string
            docker_image_tag_ci:
                required: true
                type: string
        secrets: {} #<-- ここに注意!
env:
    COMPOSE_FILE: docker-compose.ci.yml
    TERM: xterm-256color

jobs:
    package_job:
        runs-on: ubuntu-latest
        needs: docker-build
        steps:
            -   name: Git Checkout
                uses: actions/checkout@v3
            -   name: Cache Docker Registry
                uses: actions/cache@v3
                with:
                    path: /tmp/docker-registry
                    key: docker-registry-${{ github.ref }}-${{ github.sha }}
                    restore-keys: |
                        docker-registry-${{ github.ref }}
                        docker-registry-
            -   name: Boot-up Local Docker Registry
                run: docker run -d -p 5000:5000 --restart=always --name registry -v /tmp/docker-registry:/var/lib/registry registry:2
            -   name: Wait for Docker Registry
                run: npx wait-on tcp:5000
            -   name: Get Docker Image Tag
                env:
                    TAG: ${{ inputs.docker_image_tag_php_ci }}
                run: |
                    echo "DOCKER_IMAGE_TAG_CI=$TAG" >> $GITHUB_ENV
                    echo TAG $TAG
            -   name: Docker Compose Pull
                run: |
                    cd docker &&
                    docker compose pull php postgres
            -   name: Docker Compose Up
                run: |
                    cd docker &&
                    docker compose up -d php postgres
            -   name: composer install
                run: docker exec -it php bash -c "cd ./packages/${{ inputs.package_name }} && composer install"
            -   name: test
                run:
                    docker exec -it php bash -c "cd ./packages/${{ inputs.package_name }} && vendor/bin/phpunit"

完成です 🎉
パッケージが増えるごとに ci.yml にもパッケージを追加しないといけない点は変わりないですが,これで,無事パッケージ毎の CI を分離し, Docker イメージのキャッシュを使いまわしながら並列実行することに成功しました!

まとめ

今回は,モジュラモノリスのような,それぞれ独立したパッケージを複数もつ「モノリス」プロジェクトにおいて,それぞれのパッケージの CI で行う処理を共通化・別ファイルに切り出して, Docker イメージのキャッシュを使いながら並列で実行し処理時間を短くする工夫についてシェアしました.
愚直に直列で実行したときの時間や,同じ処理を何度もコピペして実装する煩わしさなどが払拭され,ある程度すっきりした CI 実装になったのではないでしょうか?
GitHub Actions は,便利なワークフローをマーケットプレースで公開したり, GitHub Actions 自体の機能もどんどんアップデートされているため,今回泥臭く実装したものよりももっと簡単で効率よく実行できる仕組みが現れるかもしれませんが,今回扱った Tips が何かの役に立つことを願っています.

みなさんも良い CI on docker-compose on GitHub Actions ライフをお送りください!

FAQ

Q. Matrix を使えば良くない?

たしかに Matrix を使って,処理を共通化したパッケージ毎の Job を生成することもできますが,Matrix は Reusable Workflow と併用できないという問題があります.
このため,Matrix を使う場合は結局同じファイルの中に処理を書かないといけないため,今回は採用しませんでした.

また,Reusable Workflow は去年の 11 月にリリースされた新しい機能ですので,それの紹介の意味も込めて,今回はファイル分割をする手法を選びました.

今後,Matrix と Reusable Workflow を併用できるようになったら,そちらのほうがより良いと思います.

Matrix の詳細はこちら.

Q. 結局料金は変わんなくない?

はい.並列実行した場合実行時間は短くなりますが,両金の計算に使われる時間は Job 毎に計算される ため,トータルの金額は Job 毎の実行時間を足し合わせたものになります.

参考に,以下の画像は,実際の実行時間は 4m 45s ですが,請求されているのは 43m 分の両金です.
image.png

Special Thanks: @yu-ichiro, @mpyw

68
35
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
68
35