grpc-development について
gRPC サービスと API の開発向けスキルです。Protocol Buffers の契約設計、ストリーミング RPC、セキュリティ、テスト、可観測性を扱います。
主な機能
- 01Protocol Buffers の契約設計
- 024 種類の RPC パターン
- 03TLS と RPC 単位の認証
- 04インターセプターとミドルウェア
- 05テスト、トレース、メトリクス
活用シーン
- 01後方互換性のある gRPC API の設計
- 02クライアント・サーバーストリーミングの実装
- 03認証と RPC リトライの設定
- 04サービスのテストと稼働状況の監視
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ mindrally/ skills --skill grpc-developmentインストール後、Claude Code で/grpc-developmentを入力して読み込まれたか確認します。
詳細
gRPC 開発
このスキルでは、Protocol Buffers を使用して gRPC ベースのサービスと API を構築する際のベストプラクティスを扱います。サービス設計、ストリーミングパターン、インターセプター、セキュリティ、オブザーバビリティが含まれます。
gRPC サービスを構築するワークフロー
- サービスコントラクトを定義する — 以下のスタイルと命名規則に従い、サービス定義、RPC メソッド、メッセージ型を含む
.protoファイルを作成します。 - 言語スタブを生成する — 適切な言語プラグイン(例:
protoc-gen-go-grpc、grpcio-tools)を指定してprotocを実行し、サーバーコードとクライアントコードを生成します。 - サーバーを実装する — 各 RPC メソッドのハンドラー関数を作成し、gRPC サーバーに登録して、TLS、インターセプター、ヘルスチェックを設定します。
- クライアントを実装する — サーバーへのチャネルを作成し、生成されたクライアントスタブをインスタンス化して、適切なデッドラインとエラー処理を設定して RPC メソッドを呼び出します。
- インターセプターを追加する — ロギング、認証、メトリクス、トレーシング用のサーバー側およびクライアント側インターセプターを組み込みます。
- テストを作成する — モックした依存関係を使用してハンドラーの単体テストを行い、実際の gRPC 接続を使用して統合テストを行います。
- デプロイして監視する — 本番環境に移行する前に、分散トレーシング(OpenTelemetry)、構造化ログ、メトリクスダッシュボードを有効にします。
基本原則
- gRPC は Protocol Buffers をインターフェース定義言語(IDL)とメッセージ交換形式の両方として使用します
- パラメーターと戻り値の型を指定してリモートから呼び出せるメソッドを定義するという考え方に基づいてサービスを設計します
- 型安全性、パフォーマンス、後方互換性を優先します
- 実装に未完了のタスク、プレースホルダー、未実装箇所を一切残さないでください
Protocol Buffer のベストプラクティス
ファイル構成(1-1-1 パターン)
- 定義は、1 つの .proto ファイルにつきトップレベルのエンティティ(message、enum、または extension)を 1 つにして構成します
- 各 .proto ファイルを単一のビルドルールに対応させます
- これにより、小さくモジュール化された proto 定義を作成できます
- リファクタリングの簡素化、ビルド時間の短縮、バイナリサイズの縮小などのメリットがあります
メッセージ設計
- 拡張性を高めるため、構造化メッセージを使用します。Protocol Buffers では、既存のクライアントを壊さずにフィールドを追加できます
- 後からフィールドを追加する可能性がある箇所では、構造体を使用するよう注意してください
- RPC 間でメッセージを再利用しないでください。API は時間とともに変更される可能性があるため、別々の RPC 呼び出しを密接に結び付けないようにします
- フィールドは常に互いに独立させてください。あるフィールドによって、別のフィールドの意味が変わるようにしてはいけません
フィールドのガイドライン
- underscore_separated_names を使用して、説明的なフィールド名を付けます
- 将来の競合を防ぐため、削除したフィールドの番号を予約します
- 常に存在するとは限らないフィールドには
optionalを使用します - ユーザーが相互排他的な選択肢から選ぶ必要がある場合は、
oneofの使用を検討します
Enum のベストプラクティス
- 最初の値は必ず 0 にします
- デフォルト値には「UNSPECIFIED」を使用します(例:
STATUS_UNSPECIFIED = 0) - 名前の衝突を避けるため、プレフィックスを使用します(例:
ORDER_STATUS_CREATEDとSTATUS_PENDING) - 誤って再利用されるのを防ぐため、削除した enum 値を予約します
スタイルガイドライン
- 行の長さは 80 文字に収めます
- 文字列には二重引用符を優先して使用します
- パッケージ名は小文字にします
- メッセージ名には CamelCase(先頭大文字)を使用します
- フィールド名には underscore_separated_names を使用します
- サービス名と RPC メソッド名には CamelCase を使用します
サービス設計
RPC パターン
- 単項 RPC:クライアントが単一のリクエストを送信し、サーバーが単一のレスポンスを返します
- サーバーストリーミング:クライアントがリクエストを送信し、サーバーがメッセージのストリームを返します
- クライアントストリーミング:クライアントがメッセージのストリームを送信し、サーバーが単一のレスポンスを返します
- 双方向ストリーミング:双方がメッセージのストリームを送信します
例:Proto 定義
syntax = "proto3";
package order.v1;
option go_package = "gen/order/v1;orderv1";
// OrderService manages customer orders.
service OrderService {
// Creates a new order and returns the created resource.
rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
// Streams real-time status updates for an order.
rpc WatchOrder(WatchOrderRequest) returns (stream OrderStatus);
}
message CreateOrderRequest {
string customer_id = 1;
repeated OrderItem items = 2;
}
message CreateOrderResponse {
string order_id = 1;
OrderStatus status = 2;
}
message WatchOrderRequest {
string order_id = 1;
}
message OrderItem {
string product_id = 1;
int32 quantity = 2;
}
message OrderStatus {
string order_id = 1;
OrderState state = 2;
string updated_at = 3;
}
enum OrderState {
ORDER_STATE_UNSPECIFIED = 0;
ORDER_STATE_CREATED = 1;
ORDER_STATE_PROCESSING = 2;
ORDER_STATE_SHIPPED = 3;
ORDER_STATE_DELIVERED = 4;
}
例:Go サーバーの実装
package main
import (
"context"
"log"
"net"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
pb "example.com/gen/order/v1"
)
type orderServer struct {
pb.UnimplementedOrderServiceServer
}
func (s *orderServer) CreateOrder(ctx context.Context, req *pb.CreateOrderRequest) (*pb.CreateOrderResponse, error) {
if req.GetCustomerId() == "" {
return nil, status.Error(codes.InvalidArgument, "customer_id is required")
}
orderID := "ord-" + time.Now().Format("20060102150405")
return &pb.CreateOrderResponse{
OrderId: orderID,
Status: &pb.OrderStatus{
OrderId: orderID,
State: pb.OrderState_ORDER_STATE_CREATED,
},
}, nil
}
func (s *orderServer) WatchOrder(req *pb.WatchOrderRequest, stream pb.OrderService_WatchOrderServer) error {
for i, state := range []pb.OrderState{
pb.OrderState_ORDER_STATE_PROCESSING,
pb.OrderState_ORDER_STATE_SHIPPED,
pb.OrderState_ORDER_STATE_DELIVERED,
} {
select {
case <-stream.Context().Done():
return stream.Context().Err()
case <-time.After(time.Duration(i) * time.Second):
if err := stream.Send(&pb.OrderStatus{
OrderId: req.GetOrderId(),
State: state,
UpdatedAt: time.Now().Format(time.RFC3339),
}); err != nil {
return err
}
}
}
return nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
srv := grpc.NewServer(
grpc.UnaryInterceptor(loggingUnaryInterceptor),
)
pb.RegisterOrderServiceServer(srv, &orderServer{})
log.Println("serving on :50051")
if err := srv.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
// loggingUnaryInterceptor logs each unary RPC call.
func loggingUnaryInterceptor(
ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
) (any, error) {
start := time.Now()
resp, err := handler(ctx, req)
log.Printf("method=%s duration=%s err=%v", info.FullMethod, time.Since(start), err)
return resp, err
}
API 設計
- 明確で直感的なサービスインターフェースを設計します
- 関連するメソッドを同じサービスにまとめます
- アクションを説明する、意味のあるメソッド名を使用します
- 動作、パラメーター、戻り値を説明するコメントで各 RPC を文書化します
パフォーマンスの最適化
チャネル管理
- gRPC を使用する際はチャネルを再利用します
- gRPC チャネルの作成では新しい HTTP/2 接続が作成されるため、コストがかかります
- 高スループットのシナリオでは接続プーリングを実装します
- キープアライブ設定を適切に構成します
メッセージの最適化
- メッセージのサイズを適切に保ちます。大きなメッセージはパフォーマンスに影響します
- 大量のデータ転送ではストリーミングを検討します
- 帯域幅が限られた環境では圧縮を使用します
- 深くネストされたメッセージ構造は避けます
エラー処理
ステータスコード
- 適切な gRPC ステータスコード(OK、INVALID_ARGUMENT、NOT_FOUND など)を使用します
- ステータスの詳細に意味のあるエラーメッセージを含めます
- 複雑なエラーシナリオではリッチエラー詳細を使用します
- サービス定義に想定されるエラー条件を記載します
再試行ロジック
- 一時的な障害に対して、指数バックオフを用いた再試行を実装します
- すべての RPC 呼び出しでデッドライン/タイムアウトを使用します
- UNAVAILABLE と RESOURCE_EXHAUSTED は再試行で処理します
- 冪等でない操作をむやみに再試行しないでください
セキュリティ
認証
- 本番環境ではトランスポートセキュリティに TLS を使用します
- メタデータ/ヘッダーを使用した RPC ごとの認証を実装します
- 複数の認証方式(JWT、OAuth2、mTLS)をサポートします
- すべてのリクエストで認証情報を検証します
認可
- メソッドレベルのアクセス制御を実装します
- 一元化された認可ロジックにはインターセプターを使用します
- 認証状態にかかわらず、すべての入力データを検証します
- 最小権限の原則に従います
インターセプターとミドルウェア
サーバーインターセプター
- 横断的関心事(ロギング、認証、メトリクス)にはインターセプターを使用します
- インターセプターの順序は実行順序に影響するため、慎重に設定します
- インターセプターは単一の責務に集中させます
- インターセプター内でエラーを適切に処理します
クライアントインターセプター
- トレースと認証のためにメタデータ(ヘッダー)を追加します
- リクエスト/レスポンスのロギングを実装します
- 自動再試行ロジックを追加します
- クライアント側のメトリクスを収集します
テスト
単体テスト
- 分離してテストするために gRPC サービスをモックします
- メッセージのシリアライズ/デシリアライズをテストします
- エラー処理の経路を検証します
- インターセプターのロジックを個別にテストします
統合テスト
- 可能な場合は実際の gRPC 接続を使用してテストします
- ストリーミングの動作をエンドツーエンドで検証します
- タイムアウトとキャンセルのシナリオをテストします
- 現実的なトラフィックパターンで負荷テストを行います
オブザーバビリティ
分散トレーシング
- サービス境界をまたぐ分散トレーシングには OpenTelemetry を使用します
- メタデータでトレースコンテキストを伝播させます
- クライアント側とサーバー側の両方を計装します
- 各 RPC 呼び出しでスパンを開始します
メトリクス
- RPC レイテンシのヒストグラムを追跡します
- メソッドとステータスコードごとにエラー率を監視します
- アクティブな接続数とストリーム数をカウントします
- 異常や SLA 違反についてアラートを設定します
ロギング
- 一貫したフィールドを持つ構造化ロギングを使用します
- RPC メソッド、所要時間、ステータスを記録します
- 相関付けのためにトレース ID を含めます
- 機密データの記録は避けます
言語別のガイドライン
Go
- 公式の
google.golang.org/grpcパッケージを使用します - インターフェース型としてサービスを実装します
- キャンセルとデッドラインにはコンテキストを使用します
protoc-gen-go-grpcによるコード生成を活用します
Python
grpcioとgrpcio-toolsパッケージを使用します- 並行性を高めるには、
grpcio-aioで非同期サービスを実装します - 生成されたスタブで型ヒントを使用します
- 非同期コンテキスト内でブロッキング呼び出しを適切に処理します
Node.js/TypeScript
@grpc/grpc-js(純粋な JavaScript 実装)を使用します- TypeScript のサポートを強化するには、
nice-grpcの使用を検討します - async/await パターンを活用します
- 型安全性のために静的コード生成を使用します
コメント
Markdown 対応