🍳

GitHub Actions実行時間短縮のレシピ集

2023/09/29に公開

What is this Article?

Github Actionsの実行時間短縮につながるレシピ集です。
実際のケースに合わせて説明していきます。

モチベーション

Github Actionsの実行時間短縮につながるTipsやナレッジなど包括的に紹介されているものがないなーと思ったので、色々なユースケースごとのパターンを紹介していければと思い本記事を公開します。

それでは書き進めていきますので、よろしくどうぞ。

実行時間短縮のためのアプローチ

実行時間の短縮につながるアプローチは大枠以下の2つ(細かくはまだまだあると思いますが....)

  1. jobの並列化
  2. cache利用

上記アプローチを採用することのデメリット

実行時間の短縮につながりますが、以下のようなデメリットもあるのでメリデメ考えて導入を検討してください。

  1. 単一のjobを並列化するために細分化することによるtotalのランナー実行時間の増加

    • workflowが完了するまでの時間は短縮されますが、jobを並列に細分化することにより、ランナーの実行時間合計が増加する可能性があります。プランによりますが、利用時間に応じての重量課金のため支払い額が増加する可能性があるため注意が必要です[1]
  2. Github Actionsのキャッシュは7日間以上アクセスがないと自動的にキャッシュを削除します[2]。workflowの実行頻度が低い場合、キャッシュミスにより結果実行時間が思ったように短縮できないことがあります。(もしくは改悪につながる可能性もあります)

  3. またキャッシュしたエントリーをリストアする際にもそれなりに時間を要するので、思ったように時間短縮されないケースも考えられます。

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

各言語ごとの例は以下を参考にしてください。
https://github.com/actions/cache#implementation-examples

💡️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で共有する、というような使い方もできます。

https://docs.github.com/ja/actions/using-workflows/storing-workflow-data-as-artifacts

脚注
  1. https://docs.github.com/ja/billing/managing-billing-for-github-actions/about-billing-for-github-actions#per-minute-rates ↩︎

  2. https://docs.github.com/ja/actions/using-workflows/caching-dependencies-to-speed-up-workflows#usage-limits-and-eviction-policy ↩︎

Discussion