GitHub Actionsのキャッシュを活用してGoのビルド時間を短縮してみる

GitHub Actionsのキャッシュを活用してGoのビルド時間を短縮してみる
この記事をシェアする

こんにちは、クラウドビルダーズのきむです。

GitHub Actionsを使用してGoのDockerイメージをビルドしているのですが、毎回それなりの時間がかかってしまうのが悩みでした。

GitHub Actionsではキャッシュ機能が存在し、うまく活用することでビルドの時間を短縮することができます。

今回はキャッシュを使用することでどれくらいの時間が短縮できるか確認していきます。

キャッシュなし

まずはキャッシュを使用しない場合の実行時間を見ていきます。

定義ファイル

下記が今回使用するDockerfileとワークフローの定義です。

FROM golang:1.22.5 AS builder

WORKDIR /work

COPY server/go.mod server/go.sum ./
RUN go mod download

COPY server/ ./

RUN go build -ldflags="-s -w" -o main .

FROM alpine:3.19

RUN apk add --no-cache tzdata

WORKDIR /app

COPY --from=builder /work/main .

EXPOSE 8080

CMD ["./main"]
name: No Cache Build

on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Extract metadata for Go Image
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: go-image
          tags: value=${{ github.sha }}

      - name: Build Go Image
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/arm64
          push: false
          provenance: false
          tags: '${{ steps.meta.outputs.tags }}'
          file: docker/go/Dockerfile

Dockerfileではマルチビルドステージを使用して最終的なイメージの軽量化を行い、Dockerイメージのビルドにはbuild-push-actionを使用しています。

1回目

1回目の実行結果は下記のとおりとなりました。

イメージのビルドで12分半ほど時間がかかっており、そのうち9割以上がgo buildにかかっている時間です。

2回目

2回目の実行結果は下記のとおりです。

当然キャッシュを使用していないので1回目と同等の実行時間となりました。

キャッシュあり(Dockerレイヤー)

build-push-actionではcache-toオプションを使用することでDockerレイヤーのキャッシュをエクスポートすることができます。キャッシュを使用する場合はcache-fromオプションを使用します。

定義ファイル

Dockerfileの内容に変更はありません。

下記がキャッシュオプションを追加したワークフローの定義です。

name: Cache Docker Layer

on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Extract metadata for Go Image
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: go-image
          tags: value=${{ github.sha }}

      - name: Build Go Image
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/arm64
          push: false
          provenance: false
          tags: '${{ steps.meta.outputs.tags }}'
          file: docker/go/Dockerfile
          cache-from: type=gha,scope=go-build
          cache-to: type=gha,scope=go-build,mode=max

1回目

1回目の実行結果は下記のとおりです。

1回目の実行ではキャッシュが存在しないのでビルド時間に大きな変更はありません。

キャッシュをエクスポートする処理が追加されているので20秒ほど実行時間が増加しています。

2回目

ソースコードの変更は行わずに実行した結果が下記の通りとなります。

Dockerレイヤーのキャッシュが効いて大幅に時間が短縮されました。

3回目

3回目の実行ではソースコードの変更を行なってからワークフローを実行します。

後続の処理ではキャッシュが効いていますが、ソースコードを変更してしまうとgo buildでのキャッシュが効かなくなるので通常の実行時間となります。

キャッシュあり(Dockerレイヤー & ビルドキャッシュ)

ソースコードの変更を行った場合でもビルド時間を短縮させるためにGoのビルドキャッシュを使用します。

Goのビルドキャッシュについては公式ドキュメントを参照してみてください。

定義ファイル

下記がGoのビルドキャッシュを使用するための変更を追加したDockerfileとワークフローの定義です。

FROM golang:1.22.5 AS builder

WORKDIR /work

COPY server/go.mod server/go.sum ./
RUN go mod download

COPY server/ ./

RUN --mount=type=cache,target=/root/.cache/go-build \
  go build -ldflags="-s -w" -o main .

FROM alpine:3.19

RUN apk add --no-cache tzdata

WORKDIR /app

COPY --from=builder /work/main .

EXPOSE 8080

CMD ["./main"]
name: Cache Docker Layer and Go Build Cache

on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Cache Go Build
        uses: actions/cache@v4
        id: go-build-cache
        with:
          path: go-build-cache
          key: ${{ runner.os }}-go-build-cache-${{ hashFiles('docker/go/Dockerfile') }}

      - name: Inject BuildKit Cache
        uses: reproducible-containers/buildkit-cache-dance@v3
        with:
          cache-map: |
            {
              "go-build-cache": "/root/.cache/go-build"
            }
          skip-extraction: ${{ steps.go-build-cache.outputs.cache-hit }}

      - name: Extract metadata for Go Image
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: go-image
          tags: value=${{ github.sha }}

      - name: Build Go Image
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/arm64
          push: false
          provenance: false
          tags: '${{ steps.meta.outputs.tags }}'
          file: docker/go-build-cache/Dockerfile
          cache-from: type=gha,scope=go-build
          cache-to: type=gha,scope=go-build,mode=max

Go のビルドキャッシュは/root/.cache/go-buildに保存されます。

buildkit-cache-danceを使用することで/root/.cache/go-buildに保存されているキャッシュのエクスポートと使用が可能になります。

Dockerfileでは–mount=type=cache,target=/root/.cache/go-buildを使用することでGoのビルドキャッシュディレクトリをマウントしています。

1回目

1回目の実行結果です。

2回目

ソースコードの変更は行わずにもう一度ワークフローを実行します。

キャッシュあり(Dockerレイヤー)の場合と同じくキャッシュが効いて時間が短縮されています。

3回目

ソースコードの変更を行なってワークフローを実行します。

go build時にDockerレイヤーのキャッシュは効いていませんが、ビルドキャッシュが効いているので1回目のビルドと比較して大幅に実行時間を短縮することができました。

キャッシュ利用時の注意

GitHub Actionsのキャッシュ機能には下記の制限・制約が存在するので注意が必要です。

  • キャッシュはブランチごとに保存される
  • リポジトリ内のすべてのキャッシュの合計サイズは10GB
  • 7日間以上アクセスされていないキャッシュは削除される

上記を意識して適切にキャッシュを管理しましょう。

さいごに

GitHub Actionsのキャッシュを活用することでワークフローの実行速度を大幅に改善することができました。

特にGoのビルドにおいてはbuildkit-cache-danceを使用することでビルドを効率化できるので同じ様な悩みを持っている方はぜひ活用してみてください。

この記事をシェアする
著者:きむ
フェスとポケカに夢中です