概要
Protocol Buffers(gRPC)のコード生成を行う際、チームメンバー間で protoc やプラグイン(protoc-gen-go 等)のバージョンが異なり、生成されるコードに意図しない差分が発生します。これを防ぐため、コード生成環境を Docker 化し、ローカルへのツールインストール不要&バージョン管理の手間をなくすようにしました。
実装
公式から protoc を取得し、Go 製のプラグインをインストールして軽量な Distroless イメージにまとめています。
FROM golang:bookworm AS golang
ARG PROTOBUF_GO_VERSION=1.36.10
ARG GRPC_GO_VERSION=1.6.0
ARG PROTOBUF_VERSION=33.1
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go install google.golang.org/protobuf/cmd/protoc-gen-go@v${PROTOBUF_GO_VERSION}
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v${GRPC_GO_VERSION}
RUN apt-get update && \
apt-get install -y --no-install-recommends curl unzip && \
[[ $(uname -m) == "aarch64" ]] && ARCH="aarch_64" || ARCH="x86_64" && \
curl -L https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOBUF_VERSION}/protoc-${PROTOBUF_VERSION}-linux-${ARCH}.zip -o protoc.zip && \
unzip protoc.zip -d /usr/local/
FROM gcr.io/distroless/static-debian12:nonroot AS builder
WORKDIR /tmp/work
COPY --from=golang --chown=nonroot:nonroot /go/bin/protoc-gen-go /usr/local/bin/protoc-gen-go
COPY --from=golang --chown=nonroot:nonroot /go/bin/protoc-gen-go-grpc /usr/local/bin/protoc-gen-go-grpc
COPY --from=golang --chown=nonroot:nonroot /usr/local/bin/protoc /usr/local/bin/protoc
COPY --from=golang --chown=nonroot:nonroot /usr/local/include /usr/local/include
ENTRYPOINT [ "protoc" ]
image 作成
docker buildx build . -t protoc
gRPC 生成してみた
ディレクトリ構造
検証用のディレクトリ構成は以下になります。
.
├── proto/
│ ├── common/ # 共通メッセージ定義
│ └── helloworld/ # 個別のgRPCサービス定義
├── gen/ # 生成コードの出力先
└── go.mod
user_type.proto
syntax = "proto3";
package common;
option go_package = "project/gen/proto/common";
message UserInfo {
string user_id = 1;
string name = 2;
int32 age = 3;
UserStatus status = 4;
}
message UserStatus {
bool is_active = 1;
string last_login = 2;
}
helloworld.proto
syntax = "proto3";
package helloworld;
option go_package = "project/gen/proto/helloworld";
import "google/protobuf/timestamp.proto";
import "proto/common/user_type.proto";
// The greeting service definition.
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
common.UserInfo user = 2;
google.protobuf.Timestamp date = 3;
}
※ go_package は実際のプロジェクトに合わせて変更してください。
生成コマンド
zsh の場合
docker run --rm -u $UID:$GID -v $PWD:/tmp/work protoc --go_out=./gen --go_opt=paths=source_relative --go-grpc_out=./gen --go-grpc_opt=paths=source_relative **/*.proto
bash の場合
# shopt -s globstar を利用するかパスを直接指定してください
docker run --rm -u $UID:$GID -v $PWD:/tmp/work protoc --go_out=./gen --go_opt=paths=source_relative --go-grpc_out=./gen --go-grpc_opt=paths=source_relative proto/*/*.proto
※ -u オプションをつけることで、生成されるファイルの所有者を コマンド実施者と一緒にします。
生成結果
image 内で install した tool のバージョンになってます。

運用の効率化
コマンドが長くなるため、スクリプト化してMakefileに記載しておくと便利です。
以下は、参考程度のシェルスクリプトです。※動作確認は細かくしてません。
generate(クリックで展開)
#!/usr/bin/env bash
set -euo pipefail
# 出力先のクリーンアップ
rm -rf gen
mkdir -p gen
echo "Generating gRPC code..."
# プロジェクトルートで実行することを想定
docker run --rm \
-u "$(id -u):$(id -g)" \
-v "$PWD":/tmp/work \
protoc \
--go_out=./gen --go_opt=paths=source_relative \
--go-grpc_out=./gen --go-grpc_opt=paths=source_relative \
$(find proto -name '*.proto')
echo "Done."
おわり
Docker 化により、バージョン差異によるトラブルから解放されました。