はじめに(対象読者・この記事でわかること)
本記事は、GoでWeb APIサーバ、バッチ、CLIツールなど複数のバイナリを1つのGitリポジトリで保守したいと考えている方を対象にしています。
「1リポジトリ1バイナリ」が基本とされるGoですが、マイクロサービスが増えてくるとリポジトリが散在し、CI/CDや依存管理が煩雑になりがちです。
この記事を読むと、以下のことがわかります。
- マルチコマンドを自然に配置するディレクトリ構成
- ビルドタグとMakefileを使って、必要なバイナリだけを高速ビルドする方法
- リリース用イメージに無駄を含めないDockerビルド戦略
- GitHub Actionsでターゲットごとに並列ビルド&配布するまでの実装例
前提知識
- Go Modulesの基本操作(
go mod init/go get) - Makefileに慣れていること(ターゲットと変数の書き方)
- Dockerでマルチステージビルドを行った経験
なぜ「1リポジトリ複数バイナリ」が必要なのか
Goはgo buildで単一のmainパッケージをコンパイルする設計です。
そのため、プロジェクトごとにリポジトリを切ると以下の弊害が出ます。
- 共通のロギング、設定、DBドライバなどが重複し、バージョンが離れる
- CI/CDのYAMLが各リポジトリに必要で、変更時に横展開が大変
- 社内ライブラリのバージョンアップがリポジトリ数分のPRを生む
マルチバイナリ化すれば、コードの重複を防ぎ、一元的にリリースができます。
ただし、ビルドスピードや成果物のサイズを気にしなければなりません。以下では、それを解決する実践的なテクニックを紹介します。
実践:マルチコマンドをスケールさせるディレクトリ構成とビルド
ステップ1:ディレクトリを「機能軸」で切る
Goらしくcmd/以下に各バイナリのmain.goを置きますが、機能ごとにサブパッケージを切るとビルドタグを効かせやすくなります。
go-multi-bin/
├── go.mod
├── Makefile
├── README.md
├── internal/
│ ├── config/ # 環境変数読み出し(共用)
│ ├── logger/ # ロギングラッパー(共用)
│ └── repository/ # DBアクセス(共用)
├── cmd/
│ ├── api/ # Web APIサーバ
│ │ └── main.go
│ ├── migrate/ # マイグレーションツール
│ │ └── main.go
│ └── job/ # バッチ(cron用)
│ └── main.go
├── pkg/
│ └── validator/ # 社内ライブラリとして切り出したパッケージ
├── build/ # ビルド成果物(.gitignore)
└── docker/
├── api.Dockerfile
├── migrate.Dockerfile
└── job.Dockerfile
cmd/以下のディレクトリ名をバイナリ名と一致させると、Makefileが短く書けます。
ステップ2:Makefileで「何を」「どこに」ビルドするを宣言する
以下の3行を守るだけで、以降のコマンドが短縮できます。
BIN_DIR:ビルド成果物を置くディレクトリCMD_DIR:バイナリのソースがあるルートTARGETS:ビルド対象のディレクトリ名をスペース区切り
MakefileBIN_DIR := ./build CMD_DIR := ./cmd TARGETS := api migrate job # クロスコンパイル用変数(CIで上書きしやすい) GOOS ?= linux GOARCH ?= amd64 CGO ?= 0 # デフォルトで全バイナリを並列ビルド .PHONY: build build: $(TARGETS) # 個別ビルドターゲット(%はTARGETSの要素にマッチ) $(TARGETS): @echo "===> building $@" @CGO_ENABLED=$(CGO) GOOS=$(GOOS) GOARCH=$(GOARCH) \ go build -ldflags "-s -w" -o $(BIN_DIR)/$@ $(CMD_DIR)/$@/main.go
make apiとすればbuild/apiが生成されます。
ldflagsで-s -wを付けると、シンボルテーブルとデバッグ情報を削除し、バイナリが約30 %縮む効果もあります。
ステップ3:ビルドタグで「本番はAPI、ローカルはjobまで」にする
例えば、ローカル開発時はjobをビルドしたくないケースがあります。
Makefileに条件分岐を書くより、ビルドタグで制御するとCI/CDのYAMLがすっきりします。
-
対象のmain.goにビルドタグを書く
cmd/job/main.gogo //go:build include_job package main -
Makefileでタグを付与する変数を用意
makefile BUILD_TAGS ?= ifdef include_job BUILD_TAGS := -tags include_job endif -
ビルドコマンドを修正
makefile go build $(BUILD_TAGS) -o $(BIN_DIR)/$@ $(CMD_DIR)/$@/main.go
GitHub Actionsのワークフローでは、ジョブマトリクスでinclude_jobを渡すだけで、本番用イメージにはjobバイナリが含まれなくなります。
ステップ4:イメージサイズを最小にするDockerfile
apiを例に、マルチステージビルドで最適化します。
Dockerfile# ---- build stage ---- FROM golang:1.22-alpine AS builder RUN apk add --no-cache make git WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . ARG BIN_NAME RUN make ${BIN_NAME} # ---- runtime stage ---- FROM alpine:3.19 RUN apk add --no-cache ca-certificates tzdata COPY --from=builder /src/build/${BIN_NAME} /usr/local/bin/${BIN_NAME} ENTRYPOINT ["/usr/local/bin/api"]
.dockerignoreを事前に作成しておくと、ビルドコンテキストが小さくなります。
# .dockerignore
.git
.gitignore
build/
README.md
Makefile
docker/
ハマった点:「ビルドキャッシュが効かない」
マルチバイナリリポジトリにすると、DockerfileのCOPY . .で依存レイヤーが無効化され、毎回go mod downloadが走ってしまいました。
解決策:依存キャッシュ用の中間イメージを切る
go.modだけ先にコピーしてモジュールキャッシュレイヤーを独立させます(上記Dockerfile参照)。
また、GitHub Actionsではactions/cache@v4で~/go/pkg/modをキャッシュキーに含めると、ビルド時間が約60 %短縮されました。
Yaml- uses: actions/cache@v4 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
まとめ
本記事では、Goで複数のバイナリを1リポジトリで保守するための構成とビルド戦略を紹介しました。
cmd/以下にバイナリ別ディレクトリを置くことで、Makefileの%ルールで楽にビルド- ビルドタグを使って本番・開発でビルド対象を切り替え
- マルチステージビルドとキャッシュ戦略で、イメージサイム・ビルド時間を削減
この記事を通して、リポジトリ数を減らしながら高速リリースができる体制を築けるはずです。
次回は、マルチバイナリ構成でのテスト戦略(統合テスト・ビルドタグごとのユニットテスト)を取り上げます。
参考資料
- Go Command Documentation - Build Constraints
- Makefileの自動変数
- Docker公式Best Practices for Writing Dockerfiles
- Go 1.22 Release Notes - Enhanced caching
