GinとgRPC-WebでブラウザとgRPCを橋渡しする
Takashi Yamamoto
Infrastructure Engineer · Leapcell

はじめに
現代のWeb開発において、マイクロサービスアーキテクチャは、スケーラブルで保守性の高いアプリケーションを構築するための基盤となっています。
gRPCは、その高性能、強力な型付け、およびProtocol Buffersに基づく効率的なバイナリプロトコルにより、サービス間通信に優れた選択肢です。しかし、gRPCサービスをWebブラウザに直接統合しようとすると、大きな課題が生じます。ブラウザは本質的にHTTP/1.1とJSONを理解しますが、gRPCが使用するHTTP/2とバイナリProtobuf形式は理解しません。
この不整合により、歴史的に複雑な仲介業者やRESTfulプロキシレイヤーが必要となっていました。この記事では、Go向けの高性能HTTP WebフレームワークであるGinをgRPC-Webと組み合わせて、このギャップを効果的に埋める方法を掘り下げます。これにより、ブラウザはgRPCサービスと直接通信できるようになります。このアプローチはアーキテクチャを簡素化し、gRPCの利点をエンドツーエンドで活用し、開発者がより堅牢で効率的なフルスタックアプリケーションを構築することを可能にします。
コアコンセプトと実装
実装の詳細に入る前に、関連するコア技術を定義しましょう。
主要技術の理解
- gRPC (gRPC Remote Procedure Calls): Protocol Buffersをインターフェイス定義言語(IDL)として、HTTP/2をトランスポートとして使用する、高性能なオープンソースのユニバーサルRPCフレームワークです。さまざまな言語、ストリーミング、効率的なシリアライゼーションをサポートしており、マイクロサービスやポリグロット環境に最適です。
- Protocol Buffers (Protobuf): 言語に依存せず、プラットフォームに依存せず、構造化データをシリアライズするための拡張可能なメカニズムです。データマーシャリングにおいて、特にサービス間通信において、XMLやJSONよりも小さく、高速で、シンプルです。
- gRPC-Web: Webアプリケーションがブラウザから直接gRPCサービスと対話できるようにする仕様とライブラリのセットです。ブラウザ互換のリクエスト(HTTP/1.1、XHR/Fetch)をgRPCメッセージに、その逆も同様に変換するブリッジとして機能し、多くの場合、gRPCとgRPC-Webの両方を理解するプロキシが必要です。
- Gin: Go向けの高速、軽量、拡張可能なWebフレームワークです。RESTful APIの構築によく使用され、gRPC-Webリクエストを処理する優れたHTTPサーバーとして機能します。
- Envoy Proxy: クラウドネイティブアプリケーションのために設計された、人気の高いオープンソースのエッジおよびサービスプロキシです。リバースプロキシ、ロードバランサー、APIゲートウェイとしてよく使用されます。gRPC-Webのコンテキストでは、EnvoyはブラウザからのgRPC-Webリクエストをバックエンドサービス向けのネイティブgRPCに変換する重要な仲介者として機能できます。
動作原理
コアのアイデアは、ネイティブgRPC(HTTP/2)を話せないWebブラウザが、gRPC-Web(特定のヘッダーを持つHTTP/1.1)を使用してプロキシサービスと通信することです。このプロキシは、これらのgRPC-Webリクエストを標準gRPC呼び出し(HTTP/2)に変換し、実際のgRPCバックエンドサービスに転送します。応答は逆のパスをたどります。
Ginは、Webアプリケーションをホストし、プロキシロジックをGoアプリケーションに埋め込むことを選択した場合、gRPC-Webプロキシとしても機能する標準HTTPサーバーとして機能します。より一般的には、gRPC-Webの変換を処理する外部Envoyプロキシと連携します。
Webアプリケーションをホストし、EnvoyプロキシがgRPC-Webの変換を処理する、一般的なセットアップを例に説明します。
ステップバイステップ実装
簡単な「Greeter」サービスを構築してプロセスを概説します。
1. Protocol Bufferサービスを定義する
まず、Protocol Buffersを使用してgRPCサービスを定義します。proto/greeter.proto
を作成します。
syntax = "proto3"; option go_package = "github.com/your/repo/greetpb"; package greet; service Greeter { rpc SayHello (HelloRequest) returns (HelloResponse) {} } message HelloRequest { string name = 1; } message HelloResponse { string message = 1; }
gRPCサービスおよびgRPC-Webクライアントスタブ用のGoコードを生成します。
# protocとGo 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-web/protoc-gen-go-grpc-web@latest # Go gRPCコードを生成 protoc --go_out=. --go_opt=paths=source_relative \ --go-grpc_out=. --go-grpc_opt=paths=source_relative \ proto/greeter.proto # gRPC-Webクライアントスタブを生成(例: TypeScript用) protoc --js_out=import_style=commonjs,binary:. --grpc-web_out=import_style=typescript,mode=grpcwebtext:. \ proto/greeter.proto
これにより、サーバー用のgreetpb/greeter_grpc.pb.go
、greetpb/greeter.pb.go
、およびフロントエンド用のgreeter_pb.js
、greeter_grpc_web_pb.d.ts
が生成されます。
2. GoでgRPCサーバーを実装する
生成されたGoコードを使用してgRPCサーバーを作成します。ファイル: server/main.go
。
package main import ( "context" "fmt" "log" "net" "google.golang.org/grpc" "google.golang.org/grpc/reflection" // gRPC bloomサービス検出用 "github.com/your/repo/greetpb" // あなたの実際のパスに置き換えてください ) type server struct { greetpb.UnimplementedGreeterServer } func (s *server) SayHello(ctx context.Context, in *greetpb.HelloRequest) (*greetpb.HelloResponse, error) { log.Printf("Received: %v", in.GetName()) return &greetpb.HelloResponse{Message: "Hello " + in.GetName()}, nil } func main() { lis, err := net.Listen("tcp", ":50051") if err != nil { log.Fatalf("failed to listen: %v", err) } ss := grpc.NewServer() greetpb.RegisterGreeterServer(s, &server{}) reflection.Register(s) // gRPCurlなどのリフレクションを有効にする log.Printf("gRPC server listening at %v", lis.Addr()) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } }
このサーバーを実行します。go run server/main.go
。
3. Envoyプロキシをセットアップする
Envoyは、GinアプリケーションとgRPCサービスの両方をフロントし、gRPC-Web変換を処理します。envoy.yaml
を作成します。
static_resources: listeners: - name: listener_0 address: socket_address: protocol: TCP address: 0.0.0.0 port_value: 8080 # ブラウザが接続するポート filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: ingress_http codec_type: AUTO route_config: name: local_route virtual_hosts: - name: backend domains: ["*"] routes: - match: { prefix: "/greet.Greeter" } # gRPC-Webサービスプレフィックス route: { cluster: greeter_service, timeout: 0s } typed_per_filter_config: envoy.filters.http.grpc_web: {} - match: { prefix: "/" } # Ginの静的ファイル/APIへのキャッチオール route: { cluster: gin_web_app } http_filters: - name: envoy.filters.http.grpc_web # gRPC-Webフィルターを最初に有効にする typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb - name: envoy.filters.http.cors # ブラウザセキュリティのために重要 typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors allow_origin_string_match: - exact: "*" # 本番環境に合わせて調整 allow_methods: GET, PUT, DELETE, POST, OPTIONS allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web max_age: "1728000" - name: envoy.filters.http.router clusters: - name: greeter_service # gRPCバックエンド connect_timeout: 0.25s type: LOGICAL_DNS dns_lookup_family: V4_ONLY lb_policy: ROUND_ROBIN load_assignment: cluster_name: greeter_service endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 127.0.0.1 # gRPCサーバーのIP port_value: 50051 # gRPCサーバーのポート - name: gin_web_app # Ginアプリケーション connect_timeout: 0.25s type: LOGICAL_DNS dns_lookup_family: V4_ONLY lb_policy: ROUND_ROBIN load_assignment: cluster_name: gin_web_app endpoints: - lb_endpoints: - endpoint: address: socket_address: address: 127.0.0.1 # GinサーバーのIP port_value: 8081 # Ginサーバーのポート
Envoyを実行します(Dockerがインストールされていると仮定):
docker run --rm -it -p 8080:8080 -v $(pwd)/envoy.yaml:/etc/envoy/envoy.yaml envoyproxy/envoy:v1.27.0
これによりEnvoyが起動し、ポート8080
でリッスンします。/greet.Greeter
をgRPCサーバー(ポート50051
)に、それ以外のすべてのリクエストをGinアプリ(ポート8081
)にルーティングします。
4. Gin Webサーバーの作成
Ginは、静的index.html
ファイルやその他の従来のHTTP APIエンドポイントをサーブします。ファイル: web/main.go
。
package main import ( "log" "net/http" "github.com/gin-gonic/gin" ) func main() { r := gin.Default() // 'public'ディレクトリから静的ファイルをサーブする r.Static("/static", "./public") r.LoadHTMLFiles("./public/index.html") r.GET("/", func(c *gin.Context) { c.HTML(http.StatusOK, "index.html", gin.H{}) }) log.Printf("Gin server listening on :8081") if err := r.Run(":8081"); err != nil { log.Fatalf("failed to run gin server: %v", err) } }
public
ディレクトリを作成し、その中にindex.html
を配置します。
public/index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>gRPC-Web Greeter</title> <script src="/static/main.js" defer></script> </head> <body> <h1>gRPC-Web Greeter Example</h1> <input type="text" id="nameInput" placeholder="Enter your name"> <button id="greetButton">Say Hello</button> <p id="response"></p> </body> </html>
Ginサーバーを実行します。go run web/main.go
。
5. フロントエンドアプリケーションの構築
フロントエンドは、生成されたgRPC-Webクライアントを使用してEnvoyプロキシと通信します。
public/main.js
を作成します(またはTypeScriptを使用し、コンパイルします)。
import { GreeterClient } from '../greeter_grpc_web_pb.js'; import { HelloRequest } from '../greeter_pb.js'; const greeterClient = new GreeterClient('http://localhost:8080', null, null); // Envoyアドレス document.getElementById('greetButton').addEventListener('click', () => { const name = document.getElementById('nameInput').value; const request = new HelloRequest(); request.setName(name); greeterClient.sayHello(request, {}, (err, response) => { if (err) { console.error('Error calling SayHello:', err.code, err.message); document.getElementById('response').textContent = 'Error: ' + err.message; return; } document.getElementById('response').textContent = response.getMessage(); }); });
このクライアントサイドコードを実行するには、通常、WebpackやParcelなどのモジュールバンドラーが必要です。これはimport
ステートメントを使用するためです。簡単なテストのためには、単純なhttp-server
やlive-server
を使用し、import
パスを一時的に調整するか、バンドルすることができます。
# Parcelでの例(インストール: npm install -g parcel-bundler) parcel build public/main.js --out-dir public --public-url /static
public/main.js
が、Ginで設定されているように/static/main.js
でアクセス可能であることを確認してください。
アプリケーションシナリオ
このセットアップは以下に最適です。
- シングルページアプリケーション(SPA): React、Vue、Angularアプリケーションは、カスタムREST APIレイヤーなしで、gRPCサービスを直接利用できます。
- 社内ダッシュボード/ツール: バックエンドマイクロサービスと効率的に通信する管理インターフェースの構築。
- ハイブリッドアプリケーション: フロントエンドの一部は高性能なgRPCサービス通信を必要とし、他の部分は従来のHTTP API(Ginによってもサーブされる)を使用するような場合。
- ボイラープレートの削減: Protobufスキーマを直接使用することで、フロントエンドは自動生成されたクライアントコードを取得し、手動でのAPIクライアント開発を削減します。
このアプローチの利点
- パフォーマンス: gRPCのバイナリシリアライゼーションとHTTP/2(Envoyとバックエンド間)を活用した効率的なデータ転送。
- 強力な型付け: Protobufは、バックエンドからフロントエンドまで強力な型付けを提供し、保守性を向上させ、実行時エラーを削減します。
- 統一されたスキーマ: API定義(Protobuf)の単一の信頼できるソースが、gRPCおよびgRPC-Webクライアント双方に提供されます。
- レイテンシの削減: REST APIと比較して、JSONの解析/マーシャリングオーバーヘッドが不要になります。
- アーキテクチャの簡素化: APIゲートウェイまたは特定のRESTアダプターのためのカスタムコードが少なくなります。
結論
GinをWebサーバー、gRPC-Web変換のためのEnvoyプロキシ、そしてgRPCバックエンドを慎重にオーケストレーションすることにより、Webブラウザが高性能なgRPCサービスと直接対話できるようにしました。この強力な組み合わせは、フルスタックアーキテクチャにおけるgRPCの可能性を最大限に引き出し、強力な型付け、強化されたパフォーマンス、そして合理化された開発体験を提供します。
ブラウザからgRPCを話す機能は、現代のWebアプリケーションの設計と構築方法を根本的に変え、真のエンドツーエンドのgRPCエコシステムへと私たちを近づけます。