JMDC TECH BLOG

JMDCのエンジニアブログです

GoプロジェクトのCI/CD高速化の話

こんにちは!Pep Upモバイルアプリチームの楊です。

今年、JMDCではアドベントカレンダーに参加しています。

qiita.com

本記事は、JMDC Advent Calendar 2025 9日目の記事です。

はじめに

皆さんは、GitHub ActionsでGoプロジェクトをビルドする際に、ビルド時間が長すぎて困った経験はありませんか?

直近担当していたGoのプロジェクトではGitHub Actionsでのデプロイに約58分もかかっていました。ランチを食べて戻ってきても、まだビルドが終わっていない...そんな状況でした。私たちは様々な最適化手法を実施し、とうとうデプロ時間を2分(97%削減)短縮できました!

この記事では、私たちがどのようにしてデプロイプロセスを最適化し、その過程で学んだDocker/Github Actions/Go buildの最適化テクニックを共有します。

目次

  1. 問題分析
  2. 最適化の全体像
  3. 技術的な実装
  4. まとめ

問題分析

初期状態

プロジェクトは以下のような構成でした:

プロジェクト構成:
- Go 1.25.3
- 依存関係: 144個のモジュール
- API: メインはGraphQL(一部はRESTful)
- コードの自動生成: GraphQL (gqlgen) 約12,000行
- Database: PostgreSQL、テストはSQLite
- AWS SDK, GORM, Gin, Redisなどのフレームワークを使用

最初GitHub Actionsのログを分析したところ、一番時間がかかったのはgo buildでした:

Deploy backend app
├─ Total: 58m 17s 
└─ #16 [builder 7/7] RUN go build: 約55分

手軽にかつ効果もでやすいことで、最初に実施したのはDockerビルドキャッシュのGithubへの外だしでした。

Workflow最適化

実施した改善策:

  • BuildKitキャッシュマウント導入
  • buildkit-cache-dance統合
  • GitHub Actionsキャッシュ活用
# .github/workflows/deploy-reusable.yml
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Go Build Cache for Docker
        uses: actions/cache@v4
        with:
          path: |
            go-mod-cache
            go-build-cache
          key: ${{ runner.os }}-go${{ steps.go-version.outputs.go_version }}-arm64-build-${{ hashFiles('**/go.sum') }}

      - name: Inject go-build-cache
        uses: reproducible-containers/buildkit-cache-dance@v3

      - name: Deploy to ECS with deploy script
        env:
          GO_BUILD_PARALLELISM: 8
        run: ./deploy-ecs.sh main

buildkit-cache-danceの役割

GitHub ActionsのキャッシュとDockerのキャッシュマウントを橋渡し:

GitHub Actions Cache
       ↓ (export)
  go-mod-cache/
  go-build-cache/
       ↓ (inject)
Docker Build Cache
       ↓ (mount)
  /root/.cache/go-build
  /root/.cache/go-mod

Dockerビルドキャッシュ戦略実施後:

Deploy backend app
├─ Total: 28m 51s 
└─ #16 [builder 7/7] RUN go build: 約28分

改善率: 約50%削減 (58分 → 28分)

しかし、28分でもまだ長すぎます。さらに短縮できないか色々改善策を実施しました。

goビルド時間が長い主な原因

以下の4点が考えられます:

  1. 並列度が低かった: -p 4では限られたCPUリソースを活かせていない
  2. 不要な依存関係: テスト専用のSQLiteをビルドに含めていた
  3. 最適化不足: キャッシュ戦略やコンパイラフラグが最適化されていない
  4. ブランクインポートの乱用: gqlgen生成コード用の依存が本番ビルドに混入

私たちはこの4つを改善すべくさらなる高速化に取り込みました。

最適化の全体像

高速化のフロー

go buildの最適化

カテゴリ 最適化項目 実際の効果
依存関係 SQLite依存削除 CGO無効化により並列度向上
依存関係 不要なブランクインポート削除 (5個) パッケージダウンロード・ビルド削減
並列化 GOMAXPROCS=8設定 ランタイム並列化
キャッシュ トリプルキャッシュマウント ディスクI/O削減
コンパイラ -mod=readonly キャッシュヒット率向上
ビルドフラグ 静的バイナリ化 イメージサイズ削減

技術的な実装

1. 不要な依存関係の削除

ブランクインポートの乱用

main.goに以下のような不要なインポートがありました:

// ❌ Before: gqlgen生成コード専用の依存を本番コードに含めていた
package main

import (
    "log"
    "sync"

    _ "github.com/russross/blackfriday/v2"
    _ "github.com/urfave/cli/v2"
    _ "github.com/xrash/smetrics"
    _ "golang.org/x/tools/go/ast/astutil"
    _ "golang.org/x/tools/go/packages"
    _ "golang.org/x/tools/imports"

    "app/config"
    "app/jobs"
    "app/routes"
    "fmt"
)

これらはgqlgenの生成コードでのみ必要なもので、実行時には完全に不要です。

// ✅ After: 不要なインポートを削除
package main

import (
    "log"
    "sync"

    "app/config"
    "app/jobs"
    "app/routes"
    "fmt"
)

依存関係掃除:

$ go mod tidy

結果

5個の直接依存関係1個の間接依存関係が削除されました:

  • github.com/russross/blackfriday/v2
  • github.com/urfave/cli/v2
  • github.com/xrash/smetrics
  • github.com/cpuguy83/go-md2man/v2

2. SQLite依存の削除

// config/database.go
func ConnectTestDatabase() {
    var err error
    DB, err = gorm.Open(sqlite.Open("file:test.db?cache=shared&mode=memory"), &gorm.Config{})
    // ...
}

SQLiteはテスト専用で、本番環境ではPostgreSQLのみ使用していました。

解決策

Dockerfile.cmdから以下を削除:

# ❌ Before
RUN apk update && apk add --no-cache gcc musl-dev sqlite sqlite-dev
ENV CGO_ENABLED=1

# ✅ After
# No build dependencies needed for CGO_ENABLED=0
ENV CGO_ENABLED=0

効果:

  • ビルド時間: 10-20秒削減
  • イメージサイズ: 約5-10MB削減
  • gcc/musl-devのインストール不要

3. GOMAXPROCS最適化

Goランタイムの並列処理

GOMAXPROCSは、Goランタイムが同時に実行できるOSスレッド数を制御します。

# ✅ Dockerfile.main / Dockerfile.cmd
ARG GO_BUILD_PARALLELISM=8
RUN --mount=type=cache,target=/root/.cache/go-build \
    --mount=type=cache,target=/root/.cache/go-mod \
    GOMAXPROCS=${GO_BUILD_PARALLELISM} \
    CGO_ENABLED=0 \
    go build -p ${GO_BUILD_PARALLELISM} \
    -o main .

GOMAXPROCSについて

CPU利用率の比較:

Before (GOMAXPROCS未設定):
CPU: ████░░░░ (約50%利用)

After (GOMAXPROCS=8):
CPU: ████████ (約80-90%利用)

期待効果: 10-20%の高速化

4. トリプルキャッシュマウント戦略

BuildKitのキャッシュマウント

Docker BuildKitの--mount=type=cacheを使用して、3つのキャッシュディレクトリをマウント:

RUN --mount=type=cache,target=/root/.cache/go-build \    # ①
    --mount=type=cache,target=/root/.cache/go-mod \      # ②
    --mount=type=cache,target=/go/pkg/mod \              # ③
    GOCACHE=/root/.cache/go-build \
    GOMODCACHE=/root/.cache/go-mod \
    go build -o main .

各キャッシュの役割

キャッシュ パス 用途 効果
① Build Cache /root/.cache/go-build コンパイル済みオブジェクト 最大の効果
② Module Cache /root/.cache/go-mod ダウンロード済みモジュール モジュール再取得不要
③ Pkg Cache /go/pkg/mod 標準パッケージキャッシュ 互換性向上

キャッシュの動作フロー

キャッシュ・フロー

5. -mod=readonly フラグ

go.modの不変性を保証

go build -mod=readonly \
    -tags=production \
    -o main .

効果:

  • go.modの意図しない変更を防止
  • キャッシュの無効化を回避
  • ビルドの再現性向上

6. 完全な静的バイナリ化

go build \
    -ldflags="-s -w -extldflags '-static'" \
    -o main .

フラグの意味

フラグ 効果
-s シンボルテーブル削除
-w DWARFデバッグ情報削除
-extldflags '-static' 完全静的リンク

-CGO_ENABLED=0の場合、Goはデフォルトで静的バイナリをビルドするため、-extldflags '-static'は明示する以外は不要です。

上記の対策を実施した結果::

  • 実行時依存: 完全に排除
  • バイナリサイズ: 約10-20%削減
  • 28分 → 2分 (93%削減!)

まとめ

1. プロファイリングの重要性

問題を解決する前に、まず計測することが重要です。ボトルネックはどこにあるのか、まず改善コストが少なく結果が出やすい部分に着手しましょう。

# ビルド時間の詳細を確認
docker build --progress=plain -t myapp . 2>&1 | tee build.log

# パッケージ数を確認
go list -f '{{.ImportPath}}' ./... | wc -l

# 依存関係を確認
go mod graph | wc -l

2. キャッシュは複数層で

1層のキャッシュだけでなく、複数の層でキャッシュを活用:

  • GitHub Actionsのキャッシュ
  • Dockerのレイヤーキャッシュ
  • BuildKitのキャッシュマウント
  • Goのビルドキャッシュ

3. 不要なものを削除

今回の改善は環境へのデプロイする部分なので、それと関係ないものをすべて削除しました。

今後の改善案

8コアランナーの導入(さらなる高速化)

現在は標準の4コアランナーを使用していますが、有料プラン(GitHub Team/Enterprise)で利用可能な8コアランナーを使うことで、さらなる高速化が見込めます。

設定変更のポイント:

runs-on: ubuntu-latest-8-cores  # 8コアランナーに変更
env:
  GO_BUILD_PARALLELISM: 16      # 並列度を倍増

期待される効果:

  • 並列コンパイル数を8 → 16に増加
  • ビルド時間: 2分17秒 → 1分30秒-2分(約20-30%削減)
  • 総合削減率: 58分17秒 → 1分30秒-2分(97-98%削減

コスト:

  • コストは約2倍になるが、実行時間も短縮されるため実質コストはほぼ同等
  • より高速なフィードバックループによる開発体験の向上

参考資料

Go Build関連

Docker & BuildKit

GitHub Actions

その他


最後まで読んでいただき、ありがとうございました!

この記事が、皆さんのGoプロジェクトのCI/CD改善の参考になれば幸いです。質問やフィードバックがあれば、ぜひコメントください!

明日の JMDC Advent Calendar 2025 もお楽しみに!

JMDCでは、ヘルスケア領域の課題解決に一緒に取り組んでいただける方を積極採用中です! フロントエンド /バックエンド/ データベースエンジニア等、様々なポジションで募集をしています。 詳細は下記の募集一覧からご確認ください。

hrmos.co

まずはカジュアルにJMDCメンバーと話してみたい/経験が活かせそうなポジションの話を聞いてみたい等ございましたら、下記よりエントリーいただけますと幸いです。

hrmos.co

★最新記事のお知らせはぜひ X(Twitter)、またはBlueskyをご覧ください!

x.com

bsky.app