GitHub Actions実行時間短縮のレシピ集
What is this Article?
Github Actionsの実行時間短縮につながるレシピ集です。
実際のケースに合わせて説明していきます。
モチベーション
Github Actionsの実行時間短縮につながるTipsやナレッジなど包括的に紹介されているものがないなーと思ったので、色々なユースケースごとのパターンを紹介していければと思い本記事を公開します。
それでは書き進めていきますので、よろしくどうぞ。
実行時間短縮のためのアプローチ
実行時間の短縮につながるアプローチは大枠以下の2つ(細かくはまだまだあると思いますが....)
- jobの並列化
- cache利用
上記アプローチを採用することのデメリット
実行時間の短縮につながりますが、以下のようなデメリットもあるのでメリデメ考えて導入を検討してください。
-
単一のjobを並列化するために細分化することによるtotalのランナー実行時間の増加。
- workflowが完了するまでの時間は短縮されますが、jobを並列に細分化することにより、ランナーの実行時間合計が増加する可能性があります。プランによりますが、利用時間に応じての重量課金のため支払い額が増加する可能性があるため注意が必要です[1]。
-
Github Actionsのキャッシュは7日間以上アクセスがないと自動的にキャッシュを削除します[2]。workflowの実行頻度が低い場合、キャッシュミスにより結果実行時間が思ったように短縮できないことがあります。(もしくは改悪につながる可能性もあります)
-
またキャッシュしたエントリーをリストアする際にもそれなりに時間を要するので、思ったように時間短縮されないケースも考えられます。
testやlinterをworkflowで動作させる
💡️1. 依存関係をcacheする。
before
name: test and lint
on:
pull_request:
branches:
- main
env:
NODE_VERSION: '16'
jobs:
test-lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Test
run: npm run test
after
name: test and lint
on:
pull_request:
branches:
- main
env:
NODE_VERSION: '16'
jobs:
test-lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Get npm cache directory
# 後続のstepでこのstepの出力を参照するためにidを指定。
id: npm-cache-dir
run: echo "CACHE_DIR=$(npm config get cache)" >> $GITHUB_OUTPUT
- name: Cache node modules
uses: actions/cache@v3
with:
path: ${{ steps.npm-cache-dir.outputs.CACHE_DIR }}
key: node-modules-build-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
node-modules-build-${{ runner.os }}-
node-modules-build-
- name: node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Test
run: npm run test
各言語ごとの例は以下を参考にしてください。
💡️1-2. さらに並列化する。
after
name: test and lint
on:
pull_request:
branches:
- main
env:
NODE_VERSION: '16'
jobs:
# npmのキャッシュdirを取得する処理を前段のjobとして切り出す。
# lintとtest個別に記載しても良いが冗長なので共通化してみた。
set-up:
runs-on: ubuntu-latest
outputs:
CACHE_DIR: ${{ steps.npm-cache-dir.outputs.CACHE_DIR }}
steps:
- uses: actions/checkout@v4
- name: Get npm cache directory
# outputsでこのstepの出力を参照するためにidを指定。
id: npm-cache-dir
run: echo "CACHE_DIR=$(npm config get cache)" >> $GITHUB_OUTPUT
lint:
runs-on: ubuntu-latest
# set-upでoutputsでセットした値は必要なので、needsでjobの実行を待つ
needs: set-up
steps:
- uses: actions/checkout@v3
- name: Cache node modules
uses: actions/cache@v3
with:
path: ${{ needs.set-up.outputs.CACHE_DIR }}
key: node-modules-build-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
node-modules-build-${{ runner.os }}-
node-modules-build-
- name: node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
test:
runs-on: ubuntu-latest
needs: set-up
steps:
- uses: actions/checkout@v3
- name: Cache node modules
uses: actions/cache@v3
with:
path: ${{ needs.set-up.outputs.CACHE_DIR }}
key: node-modules-build-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
node-modules-build-${{ runner.os }}-
node-modules-build-
- name: node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: npm ci
- name: Test
run: npm run test
同じようなstepsはComposite Actionに切り出して再利用して良いかも。
publicリポジトリのみならず、privateリポジトリでもactonsが共有できるようになったので、Organization内で使い回せるように切り出しても良いかも知れません。
💡️2. imageをキャッシュする。
before
name: test
on:
pull_request:
branches:
- main
env:
GO_VERSION: '1.21'
DB_USER: 'postgres'
DB_PW: 'postgres_pw'
AWS_REGION: 'ap-northeast-1'
jobs:
services:
# appはpostgres、S3を使用する前提
postgres:
image: postgres
env:
POSTGRES_PASSWORD: ${{ env.DB_PW }}
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
localstack:
image: localstack/localstack
env:
SERVICES: s3
DEFAULT_REGION: ${{ env.AWS_REGION }}
DATA_DIR: /tmp/localstack/data
ports:
- 4566:4566
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- name: run migration
run: |
go install github.com/pressly/goose/v3/cmd/goose@latest
goose up
goose --dir=internal/db/migrate postgres "user=postgres port=5432 password=postgres_pw host=localhost dbname=postgres sslmode=disable" up
- name: run seeder
run: go run some_seed.go
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...
after
name: test
on:
pull_request:
branches:
- main
env:
GO_VERSION: '1.21'
DB_USER: 'postgres'
DB_PW: 'postgres_pw'
AWS_REGION: 'ap-northeast-1'
DOCKER_IMAGE_CACHE: /tmp/docker-imgae-cache
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- name: Cache docker image
id: cache
uses: actions/cache@v3
with:
path: ${{ env.DOCKER_IMAGE_CACHE }}
key: docker-image-${{ github.ref }}-${{ github.sha }}
restore-key: |
docker-image-${{ github.ref }}-
docker-image-
- name: Load docker image if exists
if: steps.cache.outputs.cache-hit == 'true'
run: docker load --input ${{ env.DOCKER_IMAGE_CACHE }}/postgres.tar
run: docker load --input ${{ env.DOCKER_IMAGE_CACHE }}/localstack.tar
- name: Pull and save docker image
if: steps.cache.outputs.cache-hit != 'true'
run: |
mkdir -p ${{ env.PATH_CACHE }}
docker pull postgres:latest
docker pull localstack/localstack:latest
docker save postgres -o ${{ env.DOCKER_IMAGE_CACHE }}/postgres.tar
docker save localstack/localstack -o ${{ env.DOCKER_IMAGE_CACHE }}/localstack.tar
- name: Run docker containers
run: |
docker run -d --name postgres \
--health-cmd pg_isready \
--health-interval 10s \
--health-timeout 5s \
--health-retries 5
docker run -itd localstack/localstack \
--env SERVICES=s3 \
--env DEFAULT_REGION=${{ env.AWS_REGION }} \
--env DATA_DIR=/tmp/localstack/data \
-p 4566:4566
- name: run migration
run: |
go install github.com/pressly/goose/v3/cmd/goose@latest
goose up
goose --dir=internal/db/migrate postgres "user=postgres port=5432 password=postgres_pw host=localhost dbname=postgres sslmode=disable" up
- name: run seeder
run: go run some_seed.go
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...
サービスコンテナを使用していたworkflowをjobs内でコンテナを立てるように変更しています。
💡️3. バージョンごとのテストを実施する。
after
name: test
on:
pull_request:
branches:
- main
env:
DB_USER: 'postgres'
DB_PW: 'postgres_pw'
AWS_REGION: 'ap-northeast-1'
DOCKER_IMAGE_CACHE: /tmp/docker-imgae-cache
jobs:
build-test:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.19', '1.20', '1.21']
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
- name: Cache docker image
id: cache
uses: actions/cache@v3
with:
path: ${{ env.DOCKER_IMAGE_CACHE }}
key: docker-image-${{ github.ref }}-${{ github.sha }}
restore-key: |
docker-image-${{ github.ref }}-
docker-image-
- name: Load docker image if exists
if: steps.cache.outputs.cache-hit == 'true'
run: docker load --input ${{ env.DOCKER_IMAGE_CACHE }}/postgres.tar
run: docker load --input ${{ env.DOCKER_IMAGE_CACHE }}/localstack.tar
- name: Pull and save docker image
if: steps.cache.outputs.cache-hit != 'true'
run: |
mkdir -p ${{ env.PATH_CACHE }}
docker pull postgres:latest
docker pull localstack/localstack:latest
docker save postgres -o ${{ env.DOCKER_IMAGE_CACHE }}/postgres.tar
docker save localstack/localstack -o ${{ env.DOCKER_IMAGE_CACHE }}/localstack.tar
- name: Run docker containers
run: |
docker run -d --name postgres \
--health-cmd pg_isready \
--health-interval 10s \
--health-timeout 5s \
--health-retries 5
docker run -itd localstack/localstack \
--env SERVICES=s3 \
--env DEFAULT_REGION=${{ env.AWS_REGION }} \
--env DATA_DIR=/tmp/localstack/data \
-p 4566:4566
- name: run migration
run: |
go install github.com/pressly/goose/v3/cmd/goose@latest
goose up
goose --dir=internal/db/migrate postgres "user=postgres port=5432 password=postgres_pw host=localhost dbname=postgres sslmode=disable" up
- name: run seeder
run: go run some_seed.go
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...
マトリックスを使用すれば、バージョンごとのtestのjobsを生成して並列に実行してくれます。
多次元でのマトリックスも可能で、各組み合わせのjobsを生成して実行します。
💡️4. artifactを利用する。
before
name: test
on:
pull_request:
branches:
- main
env:
GO_VERSION: '1.21'
DB_USER: 'postgres'
DB_PW: 'postgres_pw'
AWS_REGION: 'ap-northeast-1'
jobs:
services:
# appはpostgres、S3を使用する前提
postgres:
image: postgres
env:
POSTGRES_PASSWORD: ${{ env.DB_PW }}
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
localstack:
image: localstack/localstack
env:
SERVICES: s3
DEFAULT_REGION: ${{ env.AWS_REGION }}
DATA_DIR: /tmp/localstack/data
ports:
- 4566:4566
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- name: run migration
run: |
go install github.com/pressly/goose/v3/cmd/goose@latest
goose up
goose --dir=internal/db/migrate postgres "user=postgres port=5432 password=postgres_pw host=localhost dbname=postgres sslmode=disable" up
- name: run seeder
run: go run some_seed.go
- name: run oapi code-gen
run: |
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest
mkdir -p oapi
oapi-codegen -package generated ./api.yaml > ./oapi/api.go
- name: Build
run: go build -v ./...
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
- name: Test
run: go test -v ./...
after
name: test
on:
pull_request:
branches:
- main
env:
GO_VERSION: '1.21'
DB_USER: 'postgres'
DB_PW: 'postgres_pw'
AWS_REGION: 'ap-northeast-1'
jobs:
services:
# appはpostgres、S3を使用する前提
postgres:
image: postgres
env:
POSTGRES_PASSWORD: ${{ env.DB_PW }}
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
localstack:
image: localstack/localstack
env:
SERVICES: s3
DEFAULT_REGION: ${{ env.AWS_REGION }}
DATA_DIR: /tmp/localstack/data
ports:
- 4566:4566
set-up:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- name: run oapi code-gen
run: |
go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest
mkdir -p oapi
oapi-codegen -package generated ./api.yaml > ./oapi/api.go
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: gen-api-file
path: oapi/api.go
# 保持期限の設定
retention-days: 5
lint:
runs-on: ubuntu-latest
needs: set-up
steps:
- uses: actions/checkout@v4
- name: Download all workflow run artifacts
uses: actions/download-artifact@v3
with:
name: gen-api-file
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
- name: Build
run: go build -v ./...
build-test:
runs-on: ubuntu-latest
needs: set-up
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- name: run migration
run: |
go install github.com/pressly/goose/v3/cmd/goose@latest
goose up
goose --dir=internal/db/migrate postgres "user=postgres port=5432 password=postgres_pw host=localhost dbname=postgres sslmode=disable" up
- name: run seeder
run: go run some_seed.go
- name: Download all workflow run artifacts
uses: actions/download-artifact@v3
with:
name: gen-api-file
- name: Build
run: go build -v ./...
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
- name: Test
run: go test -v ./...
本来の用途はテスト結果やパフォーマンスレポートなどの成果物を永続化するための仕組みですが、jobs間で成果物を共有できるため、上記のようにopen apiのyamlからコードを自動生成するようなケースなどで事前に生成しておきlinterとtestで共有する、というような使い方もできます。
Discussion