Tasuke Hubのロゴ

ITを中心に困っている人を助けるメディア

分かりやすく解決策を提供することで、あなたの困ったをサポート。 全ての人々がスムーズに生活できる世界を目指します。

gRPCの基礎から実装まで:マイクロサービス開発者向け完全ガイド

記事のサムネイル
TH

Tasuke Hub管理人

東証プライム市場上場企業エンジニア

情報系修士卒業後、大手IT企業にてフルスタックエンジニアとして活躍。 Webアプリケーション開発からクラウドインフラ構築まで幅広い技術に精通し、 複数のプロジェクトでリードエンジニアを担当。 技術ブログやオープンソースへの貢献を通じて、日本のIT技術コミュニティに積極的に関わっている。

🎓情報系修士🏢東証プライム上場企業💻フルスタックエンジニア📝技術ブログ執筆者

gRPCの基礎から実装まで:マイクロサービス開発者向け完全ガイド

gRPCとは何か?基本概念と従来のRESTとの違い

gRPCは、Googleが開発した高性能なオープンソースのRPC(Remote Procedure Call)フレームワークです。マイクロサービスアーキテクチャの普及に伴い、サービス間通信の効率化が求められる中で注目を集めています。

基本概念

gRPCの基本的な仕組みは以下の通りです:

  • HTTP/2ベース: 低遅延で多重化された通信を実現
  • Protocol Bufersによるシリアライズ: 高速かつコンパクトなデータ交換
  • コード生成: 複数言語でのクライアント・サーバー実装を自動生成
  • 双方向ストリーミング: 単一の接続で複数のリクエスト・レスポンスを処理
graph LR
    Client[クライアント] -->|Protocol Buffers| gRPC
    gRPC -->|HTTP/2| Server[サーバー]
    Server -->|レスポンス| Client

RESTとの主な違い

gRPCとRESTの主な違いは以下の表のとおりです:

特徴 gRPC REST
プロトコル HTTP/2 HTTP/1.1主体
データフォーマット Protocol Buffers(バイナリ) JSON(テキスト)
通信モデル 単方向・双方向ストリーミング可能 リクエスト・レスポンス
コード生成 自動生成 OpenAPIなどを使用可能だが任意
ブラウザ対応 制限あり(gRPC-Web使用) ネイティブ対応
人間可読性 低い(バイナリ) 高い(JSON)
パフォーマンス 高速 中程度

使用に適したケース

gRPCは以下のようなケースで特に効果を発揮します:

  • マイクロサービスアーキテクチャ内のサービス間通信
  • 低遅延・高スループットが要求される環境
  • 多言語環境での開発
  • リアルタイム通信やストリーミングデータ処理
  • リソース制約のあるモバイルクライアント

RESTよりもパフォーマンスを重視する場合や、型安全性を確保したい場合にgRPCは優れた選択肢となります。しかし、ブラウザ連携やデバッグのしやすさではRESTに軍配が上がることも覚えておきましょう。

Protocol Buffersの基礎とIDL定義の書き方

Protocol Buffers(略してprotobuf)は、gRPCの基盤となる言語・プラットフォーム中立的なデータシリアライズ形式です。JSONやXMLに比べて高速かつコンパクトに動作するよう設計されています。

Protocol Buffersの特徴

  • 言語中立: 多言語環境での開発をサポート
  • バイナリ形式: 高効率なシリアライズとデシリアライズ
  • スキーマ定義: .protoファイルで型やサービスを定義
  • バージョン互換性: 後方互換性を持つスキーマ更新が可能
  • コード生成: 定義からクラスを自動生成

.protoファイルの基本構文

Protocol Buffersでは.protoファイル内でメッセージ型とサービスを定義します:

syntax = "proto3";  // protoバージョンの宣言

package example;    // パッケージ名の宣言

// サービス定義
service UserService {
  // RPCメソッド定義
  rpc GetUser (GetUserRequest) returns (User) {}
  rpc ListUsers (ListUsersRequest) returns (stream User) {}
  rpc UpdateUser (stream UpdateUserRequest) returns (User) {}
  rpc ChatWithService (stream ChatMessage) returns (stream ChatMessage) {}
}

// メッセージ型定義
message User {
  int32 id = 1;
  string name = 2;
  string email = 3;
  enum Role {
    GUEST = 0;
    USER = 1;
    ADMIN = 2;
  }
  Role role = 4;
  repeated string tags = 5;
}

message GetUserRequest {
  int32 user_id = 1;
}

message ListUsersRequest {
  int32 page_size = 1;
  int32 page_token = 2;
}

message UpdateUserRequest {
  User user = 1;
  repeated string update_mask = 2;
}

message ChatMessage {
  string content = 1;
  int64 timestamp = 2;
  int32 user_id = 3;
}

フィールド型とタグ

Protocol Buffersでは、各フィールドに型と一意のタグ番号を割り当てます:

  • スカラー型: int32, int64, uint32, uint64, sint32, sint64, bool, float, double, string, bytes
  • 列挙型(enum): 事前定義された値のセット
  • メッセージ型: 他のメッセージを型として使用
  • タグ番号: 1~536,870,911の範囲の整数(1~15が推奨)
  • フィールド修飾子:
    • singular: デフォルト、0または1つのフィールド
    • repeated: 0~多数のフィールド(配列)
    • map: キーと値のペア

通信パターンの種類

gRPCでは4つの通信パターンをサポートしています:

  1. 単項RPC(Unary RPC): クライアントが単一のリクエストを送信し、サーバーが単一のレスポンスを返す

    rpc GetUser (GetUserRequest) returns (User) {}
  2. サーバーストリーミングRPC: クライアントが単一のリクエストを送信し、サーバーが複数のレスポンスをストリームで返す

    rpc ListUsers (ListUsersRequest) returns (stream User) {}
  3. クライアントストリーミングRPC: クライアントが複数のリクエストをストリームで送信し、サーバーが単一のレスポンスを返す

    rpc UpdateUser (stream UpdateUserRequest) returns (User) {}
  4. 双方向ストリーミングRPC: クライアントとサーバーの両方が独立したストリームでメッセージを送受信する

    rpc ChatWithService (stream ChatMessage) returns (stream ChatMessage) {}

Protocol Buffersの定義からコードを生成するには、protocコンパイラと言語固有のプラグインを使用します。例えば、Go言語の場合:

protoc --go_out=. --go-grpc_out=. example.proto

これにより、Proto定義からメッセージ型とクライアント・サーバースタブのコードが生成されます。このコード生成により、異なる言語間での型安全なRPC通信が可能になります。

gRPCサーバーの実装方法と注意点

gRPCサーバーを実装するには、まず定義した.protoファイルからコード生成を行い、そのインターフェースを実装します。ここでは、Go言語を例に解説します。

基本的なサーバー実装手順

  1. コード生成: .protoファイルからサーバーコードを生成
  2. サービスインターフェースの実装: 生成されたインターフェースを満たす構造体の作成
  3. gRPCサーバーの初期化: サーバーインスタンスの作成と設定
  4. サービスの登録: 実装したサービスをサーバーに登録
  5. サーバーの起動: 特定のポートでリクエスト待ち受け開始

Goによるgサーバー実装例

まず、前のセクションで定義したexample.protoからコードを生成します:

protoc --go_out=. --go-grpc_out=. example.proto

生成されたコードを使用してサーバーを実装します:

package main

import (
    "context"
    "log"
    "net"
    
    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
    
    pb "example/generated"  // 生成されたコード
)

// UserServiceServerの実装
type userServiceServer struct {
    pb.UnimplementedUserServiceServer  // 必須: 将来のAPIとの互換性のため
    users map[int32]*pb.User           // インメモリのユーザーデータ
}

// サーバーの初期化
func newServer() *userServiceServer {
    return &userServiceServer{
        users: make(map[int32]*pb.User),
    }
}

// GetUserの実装
func (s *userServiceServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
    user, exists := s.users[req.UserId]
    if !exists {
        return nil, status.Errorf(codes.NotFound, "ユーザーID %d が見つかりません", req.UserId)
    }
    return user, nil
}

// ListUsersの実装
func (s *userServiceServer) ListUsers(req *pb.ListUsersRequest, stream pb.UserService_ListUsersServer) error {
    // ページサイズのバリデーション
    if req.PageSize <= 0 {
        req.PageSize = 10  // デフォルト値
    }
    
    var i int32 = 0
    for _, user := range s.users {
        // ページトークンのチェック
        if i < req.PageToken {
            i++
            continue
        }
        
        // レスポンスをストリームで送信
        if err := stream.Send(user); err != nil {
            return status.Errorf(codes.Internal, "送信エラー: %v", err)
        }
        
        i++
        // ページサイズに達したら終了
        if i >= req.PageToken + req.PageSize {
            break
        }
    }
    
    return nil
}

// UpdateUserの実装
func (s *userServiceServer) UpdateUser(stream pb.UserService_UpdateUserServer) error {
    var latestUser *pb.User
    
    // クライアントからのストリームを受信
    for {
        req, err := stream.Recv()
        if err != nil {
            // ストリーム終了
            break
        }
        
        // ユーザー情報を更新
        user := req.GetUser()
        if user == nil {
            return status.Errorf(codes.InvalidArgument, "ユーザー情報が含まれていません")
        }
        
        // ここでは簡略化のためにデータを上書き
        s.users[user.Id] = user
        latestUser = user
    }
    
    // 最後に更新されたユーザー情報を返す
    return stream.SendAndClose(latestUser)
}

// ChatWithServiceの実装
func (s *userServiceServer) ChatWithService(stream pb.UserService_ChatWithServiceServer) error {
    // 双方向ストリーミングの実装
    for {
        // クライアントからのメッセージを受信
        inMsg, err := stream.Recv()
        if err != nil {
            // ストリーム終了
            return nil
        }
        
        log.Printf("受信: %s", inMsg.Content)
        
        // エコーバックするだけの簡単な例
        outMsg := &pb.ChatMessage{
            Content:   "ECHO: " + inMsg.Content,
            Timestamp: inMsg.Timestamp,
            UserId:    inMsg.UserId,
        }
        
        // クライアントにメッセージを送信
        if err := stream.Send(outMsg); err != nil {
            return err
        }
    }
}

func main() {
    // TCPリスナーを作成
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("ポート50051をリッスンできませんでした: %v", err)
    }
    
    // gRPCサーバーを作成
    grpcServer := grpc.NewServer()
    
    // サービスをgRPCサーバーに登録
    pb.RegisterUserServiceServer(grpcServer, newServer())
    
    log.Println("gRPCサーバーを起動しています... @ localhost:50051")
    
    // サーバー起動(ブロッキング呼び出し)
    if err := grpcServer.Serve(lis); err != nil {
        log.Fatalf("サーバーの起動に失敗しました: %v", err)
    }
}

サーバー実装の注意点

  1. コンテキスト管理:

    • contextを活用してタイムアウトやキャンセル処理を実装
    • デッドラインの確認を定期的に行い、長時間処理の中断を適切に処理
  2. エラーハンドリング:

    • gRPC標準のステータスコードを使用してエラーを返す
    • status.Errorf()を使用してコードとメッセージを組み合わせる
    • エラー詳細はstatus.WithDetail()で追加可能
  3. リソース管理:

    • ストリーミング処理でのリソースリークに注意
    • 適切なバッファリングでメモリ使用量を最適化
    • ゴルーチンやコネクションの適切な終了処理
  4. パフォーマンス考慮事項:

    • リクエスト処理の並列化
    • バッチ処理によるI/O効率の向上
    • キャッシングによるデータベースアクセスの削減
  5. バージョニング:

    • コード生成時の互換性を維持
    • 後方互換性を持つAPIバージョニング戦略
    • UnimplementedXXXServiceServerを埋め込んで将来のAPIとの互換性を確保

実際の運用では、これらに加えて認証・認可、レート制限、監視などの機能も実装することになります。次のセクションでは、クライアント側の実装について解説します。

gRPCクライアントの実装とサーバーとの通信

gRPCクライアントは、サーバー側と同様に生成されたコードを利用して実装します。ここでは、前述のサーバーに接続するGoクライアントの例を見てみましょう。

クライアント実装の基本手順

  1. コード生成: .protoファイルからクライアントコードを生成(サーバーと同じ手順)
  2. 接続の確立: gRPCサーバーへの接続を作成
  3. クライアントスタブの作成: 生成されたコードを使ってクライアントスタブを初期化
  4. RPCメソッドの呼び出し: スタブを介してサーバー側のメソッドを呼び出し
  5. レスポンス処理: 返されたデータを処理または表示

Goによるクライアント実装例

package main

import (
    "context"
    "io"
    "log"
    "time"
    
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    
    pb "example/generated"  // 生成されたクライアントコード
)

func main() {
    // gRPCサーバーへの接続を作成(ここでは簡略化のため非SSL接続)
    conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatalf("接続できませんでした: %v", err)
    }
    defer conn.Close()
    
    // クライアントスタブの作成
    client := pb.NewUserServiceClient(conn)
    
    // コンテキストの作成(タイムアウト付き)
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    
    // ユーザー作成(サーバー側の実装には含まれていないが、例として)
    createUser(ctx, client)
    
    // 単項RPC呼び出し
    getUser(ctx, client, 1)
    
    // サーバーストリーミングRPC
    listUsers(ctx, client)
    
    // クライアントストリーミングRPC
    updateUser(ctx, client)
    
    // 双方向ストリーミングRPC
    chatWithService(ctx, client)
}

// 単項RPC: GetUser
func getUser(ctx context.Context, client pb.UserServiceClient, id int32) {
    log.Printf("GetUser RPC呼び出し: ID = %d", id)
    
    // RPCメソッド呼び出し
    resp, err := client.GetUser(ctx, &pb.GetUserRequest{UserId: id})
    if err != nil {
        log.Printf("GetUser失敗: %v", err)
        return
    }
    
    log.Printf("ユーザー情報: ID=%d, 名前=%s, メール=%s", 
        resp.Id, resp.Name, resp.Email)
}

// サーバーストリーミングRPC: ListUsers
func listUsers(ctx context.Context, client pb.UserServiceClient) {
    log.Println("ListUsers RPC呼び出し")
    
    // ページサイズ10でリクエスト
    req := &pb.ListUsersRequest{
        PageSize: 10,
        PageToken: 0,
    }
    
    // ストリームを開始
    stream, err := client.ListUsers(ctx, req)
    if err != nil {
        log.Printf("ListUsers開始失敗: %v", err)
        return
    }
    
    // ストリームからデータを読み込む
    for {
        user, err := stream.Recv()
        if err == io.EOF {
            // ストリーム終了
            break
        }
        if err != nil {
            log.Printf("ストリーム受信エラー: %v", err)
            return
        }
        
        log.Printf("受信ユーザー: ID=%d, 名前=%s", user.Id, user.Name)
    }
    
    log.Println("ListUsers完了")
}

// クライアントストリーミングRPC: UpdateUser
func updateUser(ctx context.Context, client pb.UserServiceClient) {
    log.Println("UpdateUser RPC呼び出し")
    
    // ストリームを開始
    stream, err := client.UpdateUser(ctx)
    if err != nil {
        log.Printf("UpdateUser開始失敗: %v", err)
        return
    }
    
    // 複数のユーザー更新リクエストを送信
    users := []*pb.User{
        {Id: 1, Name: "田中一郎", Email: "[email protected]", Role: pb.User_USER},
        {Id: 2, Name: "鈴木花子", Email: "[email protected]", Role: pb.User_ADMIN},
        {Id: 3, Name: "佐藤太郎", Email: "[email protected]", Role: pb.User_GUEST},
    }
    
    for _, user := range users {
        // ユーザー情報を送信
        if err := stream.Send(&pb.UpdateUserRequest{
            User: user,
            UpdateMask: []string{"name", "email", "role"},
        }); err != nil {
            log.Printf("ユーザー送信エラー: %v", err)
            return
        }
        log.Printf("ユーザー情報送信: ID=%d, 名前=%s", user.Id, user.Name)
        
        // 少し待機(簡略化のため)
        time.Sleep(100 * time.Millisecond)
    }
    
    // ストリームを閉じてレスポンスを受信
    resp, err := stream.CloseAndRecv()
    if err != nil {
        log.Printf("UpdateUser終了失敗: %v", err)
        return
    }
    
    log.Printf("最終更新ユーザー: ID=%d, 名前=%s", resp.Id, resp.Name)
}

// 双方向ストリーミングRPC: ChatWithService
func chatWithService(ctx context.Context, client pb.UserServiceClient) {
    log.Println("ChatWithService RPC呼び出し")
    
    // ストリームを開始
    stream, err := client.ChatWithService(ctx)
    if err != nil {
        log.Printf("ChatWithService開始失敗: %v", err)
        return
    }
    
    // 送信と受信を同時に行うため、goroutineで処理
    waitc := make(chan struct{})
    
    // メッセージ受信用goroutine
    go func() {
        for {
            msg, err := stream.Recv()
            if err == io.EOF {
                // サーバーからのストリーム終了
                close(waitc)
                return
            }
            if err != nil {
                log.Printf("メッセージ受信エラー: %v", err)
                close(waitc)
                return
            }
            log.Printf("サーバーからのメッセージ: %s", msg.Content)
        }
    }()
    
    // メッセージ送信
    messages := []string{
        "こんにちは!",
        "gRPCの双方向ストリーミングテスト中",
        "最後のメッセージです",
    }
    
    for _, text := range messages {
        msg := &pb.ChatMessage{
            Content:   text,
            Timestamp: time.Now().Unix(),
            UserId:    1,
        }
        if err := stream.Send(msg); err != nil {
            log.Printf("メッセージ送信エラー: %v", err)
            return
        }
        log.Printf("メッセージ送信: %s", text)
        time.Sleep(500 * time.Millisecond)
    }
    
    // ストリームを閉じる
    stream.CloseSend()
    
    // 受信が完了するまで待機
    <-waitc
    log.Println("ChatWithService完了")
}

// ユーザー作成(例として)
func createUser(ctx context.Context, client pb.UserServiceClient) {
    // 注: この関数はサーバー側の実装には含まれていない
    log.Println("サーバー側にユーザーが存在すると仮定します")
}

クライアント実装の注意点

  1. 接続管理:

    • 適切なタイムアウトとデッドラインの設定
    • 接続プーリングによるリソース効率の向上
    • 長時間稼働するアプリケーションでの接続再試行メカニズム
  2. エラー処理:

    • gRPCステータスコードに基づいた適切なエラーハンドリング
    • 一時的なエラーに対する再試行戦略
    • エラーの適切なロギングとユーザーへのフィードバック
  3. ストリーミング処理:

    • 非同期パターンによる効率的なストリーム処理
    • goroutineまたは非同期メカニズムの適切な管理
    • バックプレッシャー(過負荷保護)の実装
  4. セキュリティ:

    • 本番環境では必ず暗号化された接続(TLS)を使用
    • 認証情報の安全な管理
    • インターセプターによる認証トークンの自動付与
  5. 取り消しと競合状態:

    • context.WithCancelを使用したRPCの適切な取り消し
    • タイムアウト後のリソース漏れ防止
    • 並列リクエストでの競合状態の考慮

クライアント側の実装言語

gRPCはさまざまな言語をサポートしており、.protoファイルから各言語用のクライアントコードを生成できます。主要な言語での基本的な使い方の違いを以下に示します:

言語 クライアント作成例 特徴
Go pb.NewXXXClient(conn) チャネルベースの並行処理が強み
Java XXXGrpc.newBlockingStub(channel) ブロッキング/非ブロッキング両方のスタブを提供
Python XXXStub(channel) シンプルなAPI、Asyncioによる非同期サポート
Node.js new XXXClient(addr, credentials) Promiseベースで非同期処理が直感的
C# new XXXClient(channel) Task型による非同期処理

gRPCの強みの一つは、異なる言語間でもシームレスに通信できることです。つまり、Go言語で書かれたサーバーにPythonのクライアントが接続するといった使い方も問題なく動作します。

エラーハンドリングと認証の実装テクニック

gRPCを実用的なシステムで利用するには、適切なエラーハンドリングと認証の実装が欠かせません。このセクションでは、これらの重要な側面について解説します。

gRPCにおけるエラー処理

gRPCでは、標準化されたステータスコードを使用してエラー情報を伝達します。

主要なステータスコード

gRPCでは以下のようなステータスコードを使用します:

// よく使われるgRPCステータスコード
codes.OK                 // 0: 成功
codes.Canceled           // 1: クライアントによるキャンセル
codes.Unknown            // 2: 未知のエラー
codes.InvalidArgument    // 3: クライアントが不正な引数を指定
codes.DeadlineExceeded   // 4: 処理期限超過
codes.NotFound           // 5: 要求されたリソースが見つからない
codes.AlreadyExists      // 6: リソースがすでに存在する
codes.PermissionDenied   // 7: 権限がない
codes.ResourceExhausted  // 8: リソース(クォータなど)を使い果たした
codes.FailedPrecondition // 9: リクエストを処理できる状態でない
codes.Aborted            // 10: 処理が中断された
codes.OutOfRange         // 11: 操作が範囲外
codes.Unimplemented      // 12: 実装されていないメソッド
codes.Internal           // 13: 内部エラー
codes.Unavailable        // 14: サービスが一時的に利用不可
codes.DataLoss           // 15: 回復不能なデータ損失
codes.Unauthenticated    // 16: 認証されていない
エラー処理のベストプラクティス

サーバー側のエラー処理:

// サーバー側でのエラー返却
if user == nil {
    return nil, status.Errorf(codes.NotFound, "ユーザーID %d が見つかりません", id)
}

// 追加情報を含むエラー
st := status.New(codes.FailedPrecondition, "トランザクションエラー")
detailedSt, err := st.WithDetails(
    &errdetails.PreconditionFailure{
        Violations: []*errdetails.PreconditionFailure_Violation{
            {
                Type:        "TRANSACTION",
                Subject:     "user:update",
                Description: "他のトランザクションが進行中です",
            },
        },
    },
)
if err != nil {
    // WithDetailsでエラーが発生した場合はオリジナルのエラーを返す
    return nil, st.Err()
}
return nil, detailedSt.Err()

クライアント側のエラー処理:

resp, err := client.GetUser(ctx, &pb.GetUserRequest{UserId: id})
if err != nil {
    // gRPCステータスの抽出
    st := status.Convert(err)
    switch st.Code() {
    case codes.NotFound:
        log.Printf("ユーザーが見つかりません: %s", st.Message())
        // ユーザー向けのエラーメッセージを表示
    case codes.DeadlineExceeded, codes.Unavailable:
        log.Printf("サービスに接続できませんでした: %s", st.Message())
        // 再試行ロジックをトリガー
    case codes.PermissionDenied, codes.Unauthenticated:
        log.Printf("認証/認可エラー: %s", st.Message())
        // 認証トークンの更新をトリガー
    default:
        log.Printf("予期しないエラー: %s", st.Message())
    }

    // 詳細情報の抽出(あれば)
    for _, detail := range st.Details() {
        switch d := detail.(type) {
        case *errdetails.PreconditionFailure:
            for _, v := range d.Violations {
                log.Printf("前提条件エラー: %s - %s", v.Type, v.Description)
            }
        // 他の詳細タイプも同様に処理
        }
    }
    return
}

インターセプターを使ったエラーハンドリング

インターセプターを使用することで、共通のエラーハンドリングロジックを一元管理できます。

サーバー側インターセプター:

// サーバー側のエラーハンドリングインターセプター
func errorInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
    // パニック回復
    defer func() {
        if r := recover(); r != nil {
            err = status.Errorf(codes.Internal, "内部エラー: %v", r)
            log.Printf("パニック回復: %v", r)
            debug.PrintStack()
        }
    }()

    // ハンドラー実行とエラーログ
    resp, err = handler(ctx, req)
    if err != nil {
        st, _ := status.FromError(err)
        log.Printf("メソッド %s でエラー: %s", info.FullMethod, st.Message())
    }
    return resp, err
}

// インターセプターの登録
server := grpc.NewServer(
    grpc.UnaryInterceptor(errorInterceptor),
)

クライアント側インターセプター:

// クライアント側の再試行インターセプター
func retryInterceptor(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
    var lastErr error
    for attempt := 0; attempt < maxRetries; attempt++ {
        err := invoker(ctx, method, req, reply, cc, opts...)
        if err == nil {
            return nil
        }

        lastErr = err
        st, ok := status.FromError(err)
        
        // 再試行可能なエラーのみ再試行
        if !ok || (st.Code() != codes.Unavailable && st.Code() != codes.DeadlineExceeded) {
            return err
        }

        // 指数バックオフ
        backoff := time.Duration(math.Pow(2, float64(attempt))) * baseRetryDelay
        log.Printf("RPC %s 失敗(%d回目)、%v後に再試行: %v", method, attempt+1, backoff, err)
        time.Sleep(backoff)
    }
    return lastErr
}

// インターセプターの登録
conn, err := grpc.Dial(
    "localhost:50051",
    grpc.WithTransportCredentials(insecure.NewCredentials()),
    grpc.WithUnaryInterceptor(retryInterceptor),
)

gRPCでの認証実装

gRPCでは主に以下の認証方式を実装できます:

  1. SSL/TLS認証:クライアント/サーバー間の通信を暗号化
  2. トークンベース認証:JWTなどのトークンを使用
  3. OAuth2.0:外部認証サービスとの統合
SSL/TLS認証の実装

サーバー側:

// 証明書の読み込み
creds, err := credentials.NewServerTLSFromFile("server.crt", "server.key")
if err != nil {
    log.Fatalf("証明書の読み込みに失敗: %v", err)
}

// TLSを有効にしたサーバーの作成
server := grpc.NewServer(grpc.Creds(creds))

クライアント側:

// 証明書の読み込み
creds, err := credentials.NewClientTLSFromFile("server.crt", "")
if err != nil {
    log.Fatalf("証明書の読み込みに失敗: %v", err)
}

// TLSを有効にした接続の作成
conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(creds))
JWTトークン認証の実装

サーバー側インターセプター:

// JWT認証インターセプター
func jwtAuthInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    // メタデータから認証トークンを取得
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, status.Errorf(codes.Unauthenticated, "メタデータが見つかりません")
    }

    // Authorizationヘッダーからトークンを取得
    values := md["authorization"]
    if len(values) == 0 {
        return nil, status.Errorf(codes.Unauthenticated, "認証トークンがありません")
    }

    accessToken := values[0]
    // "Bearer "プレフィックスの除去
    if strings.HasPrefix(accessToken, "Bearer ") {
        accessToken = strings.TrimPrefix(accessToken, "Bearer ")
    }

    // トークンの検証(実際の実装はJWTライブラリを使用)
    userID, err := verifyToken(accessToken)
    if err != nil {
        return nil, status.Errorf(codes.Unauthenticated, "認証トークンが無効です: %v", err)
    }

    // 検証されたユーザーIDをコンテキストに追加
    newCtx := context.WithValue(ctx, "user_id", userID)
    
    // 認証済みコンテキストでハンドラーを呼び出し
    return handler(newCtx, req)
}

// トークン検証関数(実装例)
func verifyToken(tokenString string) (string, error) {
    // JWTトークンの検証ロジック
    // この例では省略
    return "verified_user_id", nil
}

クライアント側:

// JWT認証用のインターセプター
func jwtAuthClientInterceptor(token string) grpc.UnaryClientInterceptor {
    return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
        // 認証トークンをメタデータに追加
        ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+token)
        
        // 認証情報を含むコンテキストで呼び出し
        return invoker(ctx, method, req, reply, cc, opts...)
    }
}

// 認証インターセプターの使用
token := "your_jwt_token" // 実際のJWTトークン
conn, err := grpc.Dial(
    "localhost:50051", 
    grpc.WithTransportCredentials(creds),
    grpc.WithUnaryInterceptor(jwtAuthClientInterceptor(token)),
)
その他の認証パターン
  • 相互TLS(mTLS):クライアントとサーバーの両方が証明書で認証
  • API Key認証:メタデータにAPIキーを含める
  • 基本認証:ユーザー名/パスワードをBase64エンコードして送信

これらの認証とエラー処理の手法を組み合わせることで、安全で堅牢なgRPCサービスを構築できます。実際の実装では、セキュリティ要件に応じて適切な手法を選択しましょう。

実践的なマイクロサービスでのgRPC活用パターン

このトピックはこちらの書籍で勉強するのがおすすめ!

この記事の内容をさらに深く理解したい方におすすめの一冊です。実践的な知識を身につけたい方は、ぜひチェックしてみてください!

おすすめ記事

おすすめコンテンツ