背景 #
gRPCもRemixも触ったことがないので、触ってみたいと思います。 最終的には簡単なゲームを作りたいと考えていますが、まずはgRPCを使ってみたいと思います。
環境 #
動作確認した環境は以下です。
- MacBook Pro
- 14インチ 2021
- チップ:Apple M1 Pro
- メモリ:32GB
- macOS:15.5(24F74)
ゴール #
フロントエンドから、gRPCサーバーにアクセスできる。
参照記事 #
以下の情報を参考に動作確認を進めました。
gRPCのDocker環境を立ち上げます。ここではRESTful access
できるようにgRPC-Gateway
を使います。
開発環境の構築も含め、deploy可能な完全に機能するGoのgRPCサーバーが完成します。
::: message
gRPC-Gateway
とは。
gRPCは高速・型安全な通信ができる一方で、HTTP/JSONベースのREST APIとは互換性がありません。そこでgRPC-Gatewayを挟むことで、RESTfulなクライアント(curlやブラウザ、スマホアプリなど)からもgRPCサービスを利用できるようになります。
たとえば「社内のマイクロサービス間はgRPCで爆速通信、外部公開APIはRESTで柔軟に」という“いいとこ取り”構成が実現できます。
:::
以下に本記事で使っているコードもあります。ご参考まで。
事前準備 #
以下のinstallが必要です。
- Go (version 1.16 or higher)
- Docker (latest version)
- Docker Compose (latest version)
- Protobuf Compiler (protoc)
- Git (必須ではないが推奨)
私は以下をインストールしていなかったので入れました。DockerやGitについては適宜調べて入れてください。
インストールしたバージョンです。
$ go version
go version go1.24.3 darwin/arm64
$ protoc --version
libprotoc 31.0
プロジェクトセットアップ #
参照記事の手順通り進めます。 プロジェクトディレクトリ作成
mkdir go-grpc-docker
cd go-grpc-docker
Goモジュールを初期化します。Goモジュールとは、複数のGoパッケージをまとめて管理するものだそうです。ここではこの後追加していく仕組みをまとめて管理するためのモジュールを初期化するということですね。
go mod init github.com/yourusername/go-grpc-docker
Protobuf Serviceを作る #
protobuf serviceとはProtocol Buffersのインターフェース記述言語(IDL)である.proto
ファイルで定義されたサービス(API)のことを指すそうです。まずは.proto
ファイルを作成し、コンパイルして、gRPCサービスから使用するということですね。.proto
だけだと定義でありサービスというのは違和感があるのですが、APIの定義(サービスの定義)という意味でサービスという呼び方をしているようです。
mkdir proto
私はVSCodeで作業しているので以下の拡張を入れました。
protoディレクトリにservice.proto
を作成します。内容は以下です。SayHello
メソッドを持ったGreeter
サービスを定義し、gRPC-Gateway用のHTTPアノテーションを含めます。
// protoのバージョン定義
syntax = "proto3";
package pb;
// 本パッケージの定義
// `github.com/yourusername/go-grpc-docker` まではリポジトリを指定するようです。そうするとモジュールをgithubからインストールできるとか
// `proto` がモジュール名
option go_package = "github.com/yourusername/go-grpc-docker/proto"
import "google/api/annotations.proto";
// Greeterという名前のサービスを定義
service Greeter {
// SayHelloというRemote Procedure Callを定義
rpc SayHello (HelloRequest) returns (HelloResponse) {
// HTTPアノテーション
// SayHelloメソッドを "v1/hello/{name}" にマッピングしてHTTP呼び出しできるようにする
// nameは HelloRequest のname
option (google.api.http) = {
get: "/v1/hello/{name}"
};
}
}
// HelloRequestの型を定義
message HelloRequest {
// フィールド番号1
string name = 1;
}
message HelloResponse {
string message = 1;
}
ProtobufからGoのコードを生成する #
上記.proto
ファイルからGoのコードを生成します。まずGoプラグインをインストールします。
protoc-gen-go
は、データ構造を生成するプラグイン
protoc-gen-go-grpc
は、サーバコードを自動生成するプラグイン
protoc-gen-grpc-gateway
は、gRPCゲートウェイのプラグイン
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
コンパイルする前に依存するモジュールをインストールしておきます。
ビルドに必要なようです。 google.api.http
のあたりでしょうか。
git clone https://github.com/googleapis/googleapis.git third_party/googleapis
コンパイル実行前に以下のパス指定が必要でした。
export PATH=~/go/bin:$PATH
コンパイルする。
protoc -I proto \
-I third_party/googleapis \
--go_out proto --go_opt paths=source_relative \
--go-grpc_out proto --go-grpc_opt paths=source_relative \
--grpc-gateway_out proto --grpc-gateway_opt paths=source_relative \
proto/service.proto
コンパイルにより以下のファイルが生成されます。
- 型定義:
service.pb.go
protoc-gen-go
によって生成される- message や enum が入っている
- HelloRequest, HelloResponse
- インターフェース定義:
service_grpc.pb.go
protoc-gen-go-grpc
によって生成される- gRPC用のサーバ・クライアントのインターフェースが定義されている
- GreeterServer, GreeterClient
- Gateway実装:
service.pb.gw.go
protoc-gen-grpc-gateway
によって生成される- gRPC-Gateway用のHTTP ↔ gRPCの変換コード
- HTTPの
/v1/hello{name}
をgRPCのSayHelloに変換
gRPCサーバーを実装する #
サーバーコードを実装するディレクトリを生成します。
mkdir server
以下の内容のmain.go
を作り、SayHelloメソッドを呼び出せるgRPCサーバーを実装します。
package main
import (
"context"
"log"
"net"
pb "github.com/yourusername/go-grpc-docker/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
// 実装なしのgreeterServer型を定義。Typescriptのinterfaceみたいな状態
type greeterServer struct {
pb.UnimplementedGreeterServer
}
// `greeterServer`型に対して`SayHello`関数を追加する
func (s *greeterServer) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
log.Printf("Received request for name: %s", req.Name)
return &pb.HelloResponse{Message: "Hello, " + req.Name + "!"}, nil
}
// メイン関数
func main() {
// TCPサーバーを起動。ポート50051で受け付ける。
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("Failed to listen on port 50051: %v", err)
}
// gRPCのサーバーを立てる
s := grpc.NewServer()
// gRPCサーバー(s)にgreeterServerを追加
pb.RegisterGreeterServer(s, &greeterServer{})
log.Println("gRPC server listening on port 50051...")
// サーバー起動前に追加
reflection.Register(s)
// TCPサーバのリスナーをgRPCサーバーにインジェクトする
if err := s.Serve(lis); err != nil {
log.Fatalf("Failed to serve gRPC server: %v", err)
}
}
:::message
参照記事には記載がないのですが、reflectionの指定が必要になります。reflectionを使わずにgrpcurlなどのコマンドを使うには.proto
ファイルの明示的な指定が必要だそうです。reflectionを使わない場合の具体的なコマンドは後述します。
:::
gRPCゲートウェイを作る #
RESTfulにアクセスするために追加します。gateway
ディレクトリを作ります。
mkdir gateway
gateway
内に以下の内容のmain.go
を作ります。参照記事では gateway
↔ greeterServer
という接続を一度作ってから、gateway
↔ gRPCサーバ
↔ greeterServer
という接続を作る手順を踏んでいますので見てみてください。
これでgatewayサーバーがlocalhost:50051
で動作しているgRPCサーバーにリクエストをフォーワードします。
:::message 参照記事では環境変数を使う処理が実装されていませんので追加しています。 :::
package main
import (
"context"
"flag"
"log"
"net/http"
"os"
pb "github.com/yourusername/go-grpc-docker/proto"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"google.golang.org/grpc"
)
// mainの引数にgRPCエンドポイントを指定できるようにする
var (
// --grpc-server-endpoint という指定でエンドポイントを渡せるようになる。
// localhost:50051 がデフォルトのエンドポイント
grpcServerEndpoint = flag.String("grpc-server-endpoint", "localhost:50051", "gRPC server endpoint")
)
func main() {
// プログラムの引数追加。pythonの`argparse`みたいなやつ
flag.Parse()
// 環境変数 GRPC_SERVER_ENDPOINT が設定されていれば優先して使う
if envEndpoint := os.Getenv("GRPC_SERVER_ENDPOINT"); envEndpoint != "" {
*grpcServerEndpoint = envEndpoint
}
// トップレベルのコンテキストを生成。後でサーバーに渡してサーバー処理の制御をする
ctx := context.Background()
// コンテキストにキャンセルを追加
ctx, cancel := context.WithCancel(ctx)
// main関数終了後にcancelを遅延実行する
defer cancel()
// gRPC-Gatewayが提供するHTTPリクエストをgRPC呼び出しに変換するルータを生成
// マルチプレクササーバ
mux := runtime.NewServeMux()
// gRPC接続オプションを指定。本番は`WithTransportCredentials`を使う必要がある
opts := []grpc.DialOption{grpc.WithInsecure()}
// protoコンパイルで自動生成される`RegisterGreeterHandlerFromEndpoint`関数を使って、サーバ、マルチプレクサ、コンテキスト、を紐づける
// サーバ処理を、RESTfulに使え、キャンセルができる、ようになる
err := pb.RegisterGreeterHandlerFromEndpoint(ctx, mux, *grpcServerEndpoint, opts)
if err != nil {
log.Fatalf("Failed to register handler server: %v", err)
}
// 8080ポートでRESTfulリッスン
log.Println("HTTP server listening on port 8080...")
if err := http.ListenAndServe(":8080", mux); err != nil {
log.Fatalf("Failed to serve HTTP server: %v", err)
}
}
type greeterServer struct {
pb.UnimplementedGreeterServer
}
func (s *greeterServer) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloResponse, error) {
return &pb.HelloResponse{Message: "Hello, " + req.Name + "!"}, nil
}
アプリケーションをDockerizeする #
プロジェクトルートにDockerfile
を作ります。参照記事だとgo.sum
が作られているようですが、ここまで試してきて作られておらず、挙動が変わっているようです。go.sum
というのはgoパッケージの依存関係を管理するファイルでnodeにおけるpackage-lock.json
のようなもののようです。go.sum
を作るため以下のコマンドを実行します。tidy
は「きちんとする」という意味で、モジュール管理をきちんとするよ、ということですね。プログラミングしていると英語ネイティブが羨ましくなります。
go mod tidy
マルチステージDockerfileでgRPCサーバとgRPC-Gatewayサーバの両方それぞれに独立したイメージを作ります。gRPCサーバーは50051ポートを開放し、gRPC-gatewayサーバは8080ポートを開放します。 ::: message
Dockerマルチステージビルドの利点 #
マルチステージビルドを使うことで、ビルド用の重たいツール類(Goコンパイラなど)は最終イメージに含めず、実行に必要な最小限のファイルだけを抽出できます。 これにより「イメージサイズが小さくなる」「不要なツールが入らずセキュリティも向上」と、まさに“筋肉質なDockerイメージ”が手に入ります。 余計な脂肪(ビルドツール)はカット、プロテイン(実行バイナリ)だけで動くイメージです! :::
# ビルド環境
FROM golang:alpine3.21 AS builder
# Dockerコンテナ内の作業ディレクトリ
WORKDIR /app
# go.modとgo.sumをDockerコンテナにコピー
COPY go.mod go.sum ./
# Docker コンテナでモジュールをダウンロード
RUN go mod download
# ソースコードをDockerコンテナにコピー
COPY . .
# gRPCサーバーをビルド
RUN go build -o bin/server ./server/main.go
# gRPC-Gatewayサーバをビルド
RUN go build -o bin/gateway ./gateway/main.go
# gRPCサーバの実行環境
FROM alpine:latest AS server
WORKDIR /app
COPY --from=builder /app/bin/server .
EXPOSE 50051
ENTRYPOINT ["./server"]
# gRPC-Gatewayサーバの実行環境
FROM alpine:latest AS gateway
WORKDIR /app
COPY --from=builder /app/bin/gateway .
EXPOSE 8080
ENTRYPOINT ["./gateway"]
Docker Composeでサービスを連携する #
services:
server:
build:
context: .
# Dockerfileのserverステージを使う
target: server
# ビルドイメージに名前をつける
image: go-grpc-server
ports:
- "50051:50051"
gateway:
build:
context: .
# Dockerfileのgatwayステージを使う
target: gateway
# ビルドイメージに名前をつける
image: go-grpc-gateway
ports:
- "8080:8080"
depends_on:
- server
# gatewayからgRPCサーバへのエンドポイントを指定
environment:
- GRPC_SERVER_ENDPOINT=server:50051
サービスを実行する #
Docker Composeでサービスを実行します。
docker-compose up --build
serverとgatewayが動作していることを確認してください。
...
[+] Running 6/6way Building 12.8s
✔ Service server Built 26.7s
✔ Service gateway Built 12.8s
✔ gateway Built 0.0s
✔ server Built 0.0s
✔ Container go-grpc-docker-server-1 Created 0.0s
✔ Container go-grpc-docker-gateway-1 Created 0.0s
Attaching to gateway-1, server-1
server-1 | 2025/05/25 06:53:18 gRPC server listening on port 50051...
gateway-1 | 2025/05/25 06:53:18 HTTP server listening on port 8080...
APIをテストする #
grpcurl
をインストールしていない場合はインストールします。
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
テストします。
$ grpcurl -plaintext -d '{"name": "Docker"}' localhost:50051 pb.Greeter/SayHello
{
"message": "Hello, Docker!"
}
$ curl http://localhost:8080/v1/hello/Docker
{"message":"Hello, Docker!"}%
:::message
参照記事のコマンドは以下ですが、grpcurl
の仕様が変わったのか、パラメータの順番に誤りがあるようです。以下は参照記事に記載の実行できないコマンドです。
grpcurl -plaintext localhost:50051 pb.Greeter/SayHello -d '{"name": "Docker"}
:::
:::message reflectionを使わない場合
grpcurlはgRPCサーバーの動作確認に便利なCLIツールですが、サーバー側でreflection(サービス定義の動的公開)が有効かどうかで使い方が変わります。
- reflection有効:サービス名やメソッド名だけでOK。protoファイル不要。
- reflection無効:
-import-path
や-proto
でprotoファイルを明示的に指定する必要があります。
-import-path
はprotoファイルの依存ディレクトリ、-proto
はサービス定義そのものを指します。
「なぜ必要?」→ サーバーが“自分の仕様書(proto)”を公開していない場合、クライアント側で仕様書を持ち込むイメージです。
reflectionを使わない場合は以下のようなコマンドで.proto
ファイルを明示的に指定します。
grpcurl -plaintext \
-import-path ./proto \
-import-path ./third_party/googleapis \
-proto ./proto/service.proto \
-d '{"name": "Docker"}' \
localhost:50051 \
pb.Greeter/SayHello
{
"message": "Hello, Docker!"
}
:::
まとめ #
以上です!以下の動作確認をできました!
- Protocol Bufferを使ったgRPCサービスを定義できました
- gRPCサーバーをgoで実装できました
- gRPC-Gatewayを使うことでREST経由でgRPCサービスにアクセスできました
- gRPCサーバーとgRPC-GatewayサーバーをDockerで起動できました
- docker-composeで連携動作させることができました