🙌

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

に公開

背景

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サーバーが完成します。

以下に本記事で使っているコードもあります。ご参考まで。

事前準備

以下の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)
	}
}

gRPCゲートウェイを作る

RESTfulにアクセスするために追加します。gatewayディレクトリを作ります。

mkdir gateway

gateway内に以下の内容のmain.goを作ります。参照記事では gatewaygreeterServer という接続を一度作ってから、gatewaygRPCサーバgreeterServer という接続を作る手順を踏んでいますので見てみてください。

これでgatewayサーバーがlocalhost:50051で動作しているgRPCサーバーにリクエストをフォーワードします。

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ポートを開放します。

# ビルド環境
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!"}%

まとめ

以上です!以下の動作確認をできました!

  • Protocol Bufferを使ったgRPCサービスを定義できました
  • gRPCサーバーをgoで実装できました
  • gRPC-Gatewayを使うことでREST経由でgRPCサービスにアクセスできました
  • gRPCサーバーとgRPC-GatewayサーバーをDockerで起動できました
  • docker-composeで連携動作させることができました

Discussion