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

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つの通信パターンをサポートしています:
単項RPC(Unary RPC): クライアントが単一のリクエストを送信し、サーバーが単一のレスポンスを返す
rpc GetUser (GetUserRequest) returns (User) {}
サーバーストリーミングRPC: クライアントが単一のリクエストを送信し、サーバーが複数のレスポンスをストリームで返す
rpc ListUsers (ListUsersRequest) returns (stream User) {}
クライアントストリーミングRPC: クライアントが複数のリクエストをストリームで送信し、サーバーが単一のレスポンスを返す
rpc UpdateUser (stream UpdateUserRequest) returns (User) {}
双方向ストリーミング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言語を例に解説します。
基本的なサーバー実装手順
- コード生成:
.proto
ファイルからサーバーコードを生成 - サービスインターフェースの実装: 生成されたインターフェースを満たす構造体の作成
- gRPCサーバーの初期化: サーバーインスタンスの作成と設定
- サービスの登録: 実装したサービスをサーバーに登録
- サーバーの起動: 特定のポートでリクエスト待ち受け開始
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)
}
}
サーバー実装の注意点
コンテキスト管理:
context
を活用してタイムアウトやキャンセル処理を実装- デッドラインの確認を定期的に行い、長時間処理の中断を適切に処理
エラーハンドリング:
- gRPC標準のステータスコードを使用してエラーを返す
status.Errorf()
を使用してコードとメッセージを組み合わせる- エラー詳細は
status.WithDetail()
で追加可能
リソース管理:
- ストリーミング処理でのリソースリークに注意
- 適切なバッファリングでメモリ使用量を最適化
- ゴルーチンやコネクションの適切な終了処理
パフォーマンス考慮事項:
- リクエスト処理の並列化
- バッチ処理によるI/O効率の向上
- キャッシングによるデータベースアクセスの削減
バージョニング:
- コード生成時の互換性を維持
- 後方互換性を持つAPIバージョニング戦略
UnimplementedXXXServiceServer
を埋め込んで将来のAPIとの互換性を確保
実際の運用では、これらに加えて認証・認可、レート制限、監視などの機能も実装することになります。次のセクションでは、クライアント側の実装について解説します。
gRPCクライアントの実装とサーバーとの通信
gRPCクライアントは、サーバー側と同様に生成されたコードを利用して実装します。ここでは、前述のサーバーに接続するGoクライアントの例を見てみましょう。
クライアント実装の基本手順
- コード生成:
.proto
ファイルからクライアントコードを生成(サーバーと同じ手順) - 接続の確立: gRPCサーバーへの接続を作成
- クライアントスタブの作成: 生成されたコードを使ってクライアントスタブを初期化
- RPCメソッドの呼び出し: スタブを介してサーバー側のメソッドを呼び出し
- レスポンス処理: 返されたデータを処理または表示
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("サーバー側にユーザーが存在すると仮定します")
}
クライアント実装の注意点
接続管理:
- 適切なタイムアウトとデッドラインの設定
- 接続プーリングによるリソース効率の向上
- 長時間稼働するアプリケーションでの接続再試行メカニズム
エラー処理:
- gRPCステータスコードに基づいた適切なエラーハンドリング
- 一時的なエラーに対する再試行戦略
- エラーの適切なロギングとユーザーへのフィードバック
ストリーミング処理:
- 非同期パターンによる効率的なストリーム処理
- goroutineまたは非同期メカニズムの適切な管理
- バックプレッシャー(過負荷保護)の実装
セキュリティ:
- 本番環境では必ず暗号化された接続(TLS)を使用
- 認証情報の安全な管理
- インターセプターによる認証トークンの自動付与
取り消しと競合状態:
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では主に以下の認証方式を実装できます:
- SSL/TLS認証:クライアント/サーバー間の通信を暗号化
- トークンベース認証:JWTなどのトークンを使用
- 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活用パターン
このトピックはこちらの書籍で勉強するのがおすすめ!
この記事の内容をさらに深く理解したい方におすすめの一冊です。実践的な知識を身につけたい方は、ぜひチェックしてみてください!
おすすめコンテンツ
おすすめDevOps2025/5/1実践的マイクロサービスのためのKubernetes入門ガイド
マイクロサービスアーキテクチャを効率的に運用するためのKubernetesの基本概念と実践手法を解説。デプロイメント戦略、スケーリング、監視など、現場で役立つノウハウをコード例とともに紹介します。
続きを読む 生成AI2025/5/12【初心者向け】生成AI開発の実装アプローチ完全ガイド:プロンプト設計からシステム統合まで
生成AIアプリケーション開発において効果的なプロンプト設計からAPI連携、エラーハンドリングまでを解説。初心者エンジニアでも理解できる実践的なコード例とベストプラクティスを提供し、生成AIを活用したシ...
続きを読む SRE2025/5/12【2025年最新】SREプラクティス完全ガイド:信頼性エンジニアリングの基礎から実践まで
SRE(Site Reliability Engineering)の基礎知識から2025年最新のベストプラクティスまで。信頼性指標の設定方法、インシデント対応、自動化ツール、キャリアパスまで初心者にも...
続きを読む Docker2025/5/16Docker Composeで開発環境を簡単構築!初心者向け完全ガイド
Docker Composeの基本から実践的な使い方まで初心者にもわかりやすく解説。複数コンテナの管理や開発環境の効率化方法を具体的なサンプルとともに紹介します。
続きを読む Flutter2025/5/14【2025年最新】Flutterで始めるクロスプラットフォーム開発:初心者向け完全ガイド
Flutterを使ったモバイルアプリ開発の基礎から実践まで。初心者でも理解できるステップバイステップの解説と、効率的なクロスプラットフォーム開発のコツを紹介します。
続きを読む HTML2025/5/5【初心者からプロまで】Web開発者のためのアクセシビリティ完全ガイド:実践的な実装手法と検証テクニック
Web開発者向けのアクセシビリティ実装ガイド。WAI-ARIAの基本から高度なスクリーンリーダー対応まで、実践的なコード例と検証方法を網羅。SEO効果も高めながら、誰もが使いやすいWebサイト制作の方...
続きを読む JavaScript2025/5/16WebRTCを使ったリアルタイム通信アプリ開発入門:初心者でも理解できる基礎から実装まで
WebRTCの基本概念からリアルタイム通信アプリの実装方法まで、初心者でもわかりやすく解説します。ビデオ・音声通話、データ共有の仕組みと実装例を紹介し、WebRTCを使った開発の第一歩を踏み出しましょ...
続きを読む IT技術2023/11/2「モノリス?マイクロサービス?」初心者エンジニアのためのIT用語解説!
IT業界は日々急速に進化しており、初心者エンジニアがついていくためには多くの概念や用語を理解しなければなりません。今回はそんな初心者エンジニアのために、「モノリス」と「マイクロサービス」という二つの重...
続きを読む