GoによるgRPCファーストのマイクロサービス構築:RESTfulゲートウェイ併用アーキテクチャ

開発環境のセットアップ

以下の環境で動作確認済みです。

  • Windows 10 + Go 1.24

必要なGoツールをインストールします。

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
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest

さらにprotocコンパイラが必要です。Protocol Buffersのリリースページからprotoc-<version>-win64.zipをダウンロードし、解凍後にbinディレクトリをシステムPATHに追加してください。

ディレクトリ構成

microservice-demo/
├── proto/
│   └── member/
│       └── member.proto
├── internal/
│   ├── grpcserver/        # gRPCサーバ実装
│   │   └── cmd/
│   │       └── main.go
│   └── httpgw/            # HTTPゲートウェイ
│       └── cmd/
│           └── main.go
├── pkg/
│   └── generated/         # protoc生成コード
│       └── proto/
│           └── member/
├── third_party/           # 外部proto定義
│   └── googleapis/
│       └── google/
│           └── api/
├── go.mod
└── scripts/
    └── generate.go        # コード生成スクリプト

ステップ1:Protocol Buffersの定義

proto/member/member.protoを作成します。

syntax = "proto3";

package member;

option go_package = "microservice-demo/pkg/generated/proto/member";

import "google/api/annotations.proto";

service MemberManager {
  rpc FetchMember (MemberLookup) returns (MemberInfo) {
    option (google.api.http) = {
      get: "/api/v1/members/{member_id}"
    };
  }
  
  rpc RegisterMember (MemberRegistration) returns (MemberInfo) {
    option (google.api.http) = {
      post: "/api/v1/members"
      body: "payload"
    };
  }
}

message MemberLookup {
  string member_id = 1;
}

message MemberRegistration {
  MemberPayload payload = 1;
}

message MemberPayload {
  string full_name = 1;
  string mail_address = 2;
}

message MemberInfo {
  string member_id = 1;
  string full_name = 2;
  string mail_address = 3;
  int64 registered_at = 4;
}

重要google/api/annotations.protoはgoogleapisリポジトリから取得し、third_party/googleapisに配置するか、protoc実行時に-Iでインクルードパスを指定してください。

ステップ2:Goコードの自動生成

scripts/generate.goに以下の内容を記述します。

//go:build ignore

package main

import (
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
)

func main() {
	protoRoot := "proto"
	googleAPI := "third_party/googleapis"
	outputDir := "pkg/generated"

	// 出力ディレクトリのクリーンアップ
	os.RemoveAll(outputDir)
	os.MkdirAll(outputDir, 0755)

	protoFiles := []string{
		"proto/member/member.proto",
	}

	for _, pf := range protoFiles {
		// 標準のGoコードとgRPCコードを生成
		cmd := exec.Command("protoc",
			"-I="+protoRoot,
			"-I="+googleAPI,
			"--go_out="+outputDir,
			"--go_opt=paths=source_relative",
			"--go-grpc_out="+outputDir,
			"--go-grpc_opt=paths=source_relative",
			"--grpc-gateway_out="+outputDir,
			"--grpc-gateway_opt=paths=source_relative",
			pf,
		)
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
		if err := cmd.Run(); err != nil {
			fmt.Fprintf(os.Stderr, "生成エラー (%s): %v\n", filepath.Base(pf), err)
			os.Exit(1)
		}
	}

	fmt.Println("コード生成完了")
}

実行:go run scripts/generate.go

生成されるファイル:

  • member.pb.go — メッセージ構造体
  • member_grpc.pb.go — gRPCインターフェース
  • member.pb.gw.go — HTTPゲートウェイハンドラ

ステップ3:gRPCサーバの実装

internal/grpcserver/cmd/main.go

package main

import (
	"context"
	"log"
	"net"
	"time"

	pb "microservice-demo/pkg/generated/proto/member"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

type memberRepository struct {
	storage map[string]*pb.MemberInfo
}

func newRepository() *memberRepository {
	return &memberRepository{
		storage: make(map[string]*pb.MemberInfo),
	}
}

type memberHandler struct {
	pb.UnimplementedMemberManagerServer
	repo *memberRepository
}

func (h *memberHandler) FetchMember(ctx context.Context, req *pb.MemberLookup) (*pb.MemberInfo, error) {
	record, exists := h.repo.storage[req.MemberId]
	if !exists {
		return nil, status.Errorf(codes.NotFound, "member not found: %s", req.MemberId)
	}
	return record, nil
}

func (h *memberHandler) RegisterMember(ctx context.Context, req *pb.MemberRegistration) (*pb.MemberInfo, error) {
	payload := req.Payload
	if payload.FullName == "" {
		return nil, status.Errorf(codes.InvalidArgument, "full_name is required")
	}

	newID := generateID()
	now := time.Now().Unix()

	entity := &pb.MemberInfo{
		MemberId:      newID,
		FullName:      payload.FullName,
		MailAddress:   payload.MailAddress,
		RegisteredAt:  now,
	}

	h.repo.storage[newID] = entity
	return entity, nil
}

func generateID() string {
	// 簡易実装:実際にはUUID等を使用
	return fmt.Sprintf("M%d", time.Now().UnixNano())
}

func main() {
	port := ":50051"
	sock, err := net.Listen("tcp", port)
	if err != nil {
		log.Fatalf("ポートリッスン失敗: %v", err)
	}

	grpcSrv := grpc.NewServer()
	pb.RegisterMemberManagerServer(grpcSrv, &memberHandler{
		repo: newRepository(),
	})

	log.Printf("gRPCサーバ起動: %s", port)
	if err := grpcSrv.Serve(sock); err != nil {
		log.Fatalf("サーバ実行失敗: %v", err)
	}
}

ステップ4:HTTPゲートウェイの実装

internal/httpgw/cmd/main.go

package main

import (
	"context"
	"log"
	"net/http"
	"time"

	pb "microservice-demo/pkg/generated/proto/member"
	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	// gRPCサーバへの接続
	grpcConn, err := grpc.DialContext(ctx,
		"localhost:50051",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithBlock(),
	)
	if err != nil {
		log.Fatalf("gRPC接続失敗: %v", err)
	}
	defer grpcConn.Close()

	// ゲートウェイマルチプレクサの構築
	mux := runtime.NewServeMux()

	// HTTPハンドラの登録
	if err := pb.RegisterMemberManagerHandler(ctx, mux, grpcConn); err != nil {
		log.Fatalf("ハンドラ登録失敗: %v", err)
	}

	httpAddr := ":8080"
	log.Printf("HTTPゲートウェイ起動: %s", httpAddr)

	server := &http.Server{
		Addr:    httpAddr,
		Handler: mux,
	}

	if err := server.ListenAndServe(); err != nil {
		log.Fatalf("HTTPサーバ失敗: %v", err)
	}
}

ステップ5:依存関係の管理

go mod init microservice-demo
go get google.golang.org/grpc
go get google.golang.org/protobuf
go get github.com/grpc-ecosystem/grpc-gateway/v2
go mod tidy

ステップ6:動作確認

1. gRPCサーバの起動

go run internal/grpcserver/cmd/main.go

2. HTTPゲートウェイの起動

go run internal/httpgw/cmd/main.go

3. RESTful APIのテスト

メンバー登録:

curl -X POST http://localhost:8080/api/v1/members \
  -H "Content-Type: application/json" \
  -d '{"full_name":"山田太郎","mail_address":"yamada@example.com"}'

メンバー取得:

curl http://localhost:8080/api/v1/members/[返却されたmember_id]

4. gRPC直接呼び出し(オプション)

grpcurl -plaintext -d '{"member_id":"Mxxxx"}' localhost:50051 member.MemberManager/FetchMember

アーキテクチャの解説

gRPCを第一級の通信プロトコルとする理由

HTTP/2ベースのgRPCは、ストリーム多重化、バイナリシリアライズ、厳格なスキーマによる型安全性を提供します。サービス間通信ではこれらの特性がパフォーマンスと信頼性に直結します。

gRPC-GatewayによるRESTful変換

protoファイル内のgoogle.api.httpアノテーションにより、HTTPメソッド・パスとgRPCメソッドの対応関係を宣言的に定義します。protoc-gen-grpc-gatewayはこれを解析し、HTTPリクエストをgRPC呼び出しに変換するリバースプロキシを生成します。JSON↔Protobufの変換、パスパラメータの抽出、クエリ文字列のマッピングを自動処理します。

設計上の利点

  • 単一の真実源:.protoファイルのみをメンテナンスすれば、gRPCクライアントとRESTクライアント双方のインターフェースが同期されます
  • 段階的な移行:外部向けにRESTを維持しつつ、内部マイクロサービス間はgRPCで通信することで、既存システムとの互換性を保ちながら近代化が可能です
  • ツール連携:OpenAPIドキュメント、各種言語のクライアントSDKを自動生成できます

運用時の考慮点

  • 本番環境ではgRPCサーバとゲートウェイを別プロセス・別コンテナとしてデプロイし、Kubernetes等で個別にスケーリング可能に設計してください
  • mTLSによる相互認証、認可ミドルウェア、構造化ロギング、分散トレーシングの導入を検討してください
  • ゲートウェイ層でタイムアウト、リトライ、サーキットブレーカーを適切に設定し、グレースフルデグラデーションを実現してください

タグ: gRPC gRPC-Gateway Protocol Buffers Go Microservices

8月20日 22:37 投稿