メインコンテンツへスキップ

gRPC - connect - Render でwebサービスを作ってみる:gRPC Docker環境構築

· loading · loading ·
kiitosu
著者
kiitosu
画像処理やデバイスドライバ、データ基盤構築からWebバックエンドまで、多様な領域に携わってきました。地図解析や地図アプリケーションの仕組みにも経験があり、幅広い技術を活かした開発に取り組んでいます。休日は草野球とランニングを楽しんでいます。
目次

背景
#

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を作ります。参照記事では gatewaygreeterServer という接続を一度作ってから、gatewaygRPCサーバ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で連携動作させることができました
Reply by Email

関連記事

gRPC - connect - Render でwebサービスを作ってみる:server side connect
· loading · loading
gRPC - connect - Render でwebサービスを作ってみる:gRPC with connect on Docker
· loading · loading
gRPC - connect - Render でwebサービスを作ってみる:web service with connect
· loading · loading