はじめに
業務でKubernetes上のPod間通信をgRPCで実装することになりました。
それまではRESTでのAPI開発経験はあったものの、gRPCは名前は知っているけど触ったことがない、というレベルでした。
当初は「なんとなく難しそう」という印象で、実際に実装してみると思ったより概念はシンプルでした。
この記事では、初めてgRPCを触るエンジニアに向けて、Kubernetes上でのPod間通信という実務の文脈で、実装の流れや気づきをまとめていきます。
システム構成の概要
構成の全体像
同一Kubernetesクラスター・同一Namespace内に以下の2種類のPodが存在する。
- 対象Pod:複数のYAMLファイルをConfigMapとしてボリュームマウントで保持
- メンテナンスPod:CLIツールを持ち、オペレーターが操作する
オペレーター
↓ Pod名を引数に指定してコマンド実行
メンテナンスPod
↓ gRPC リクエスト(同一Namespace内)
対象Pod
↓ ボリュームマウントされたYAMLを読み込んでレスポンス
メンテナンスPod
↓ CLIに内容を表示
ConfigMapとは
KubernetesのConfigMapとは、設定ファイルをPodに渡す方法のひとつ。
今回は対象PodにYAMLファイルとしてマウントされており、Go側では os.ReadFile() でファイルとして読み込んでいる。
gRPCの基礎整理
gRPCとは
Googleが開発したRPC(Remote Procedure Call)フレームワーク。
HTTP/2ベースのバイナリ通信で、スキーマを .proto ファイルで定義する。
RESTとの違い
| REST | gRPC | |
|---|---|---|
| 通信形式 | JSON(テキスト) | Protocol Buffers(バイナリ) |
| スキーマ定義 | 任意 |
.proto ファイルで必須 |
| 向いている用途 | 外部・内部どちらにも広く使われる | マイクロサービス間など型安全な通信が求められる場面で強みを発揮 |
protoファイルとは
リクエスト・レスポンスの構造とメソッドを定義するファイル。
protoc で自動的にGoのコードが生成されるため、型安全な通信コードが手に入る。
通信の種類
gRPCには複数の通信方式があるが、使用したのは Unary RPC。
1つのリクエストに対して1つのレスポンスを返すシンプルな方式で、RESTのイメージに最も近い。
connect-goについて
GoでgRPCを実装するライブラリはいくつか存在し、既存コードでは connect-goが採用されていました。
Buf Technologies社が開発したライブラリで、gRPC・gRPC-Web・HTTP/JSONの通信プロトコルに対応しており、シンプルにコードが書けるのが特徴。
実装の流れ
今回の担当はクライアント側(メンテナンスPod)のみ。
まずサーバー側の既存コードとprotoファイルを読んで、インターフェースを把握するところから始めました。
1. protoファイルの確認・修正
既存のprotoファイルに取得したいフィールドを追加。
protoc でGoコードを自動生成する。
2. 自動生成されたコードの確認
生成されたGoコードにUnary RPCのメソッドが定義されている。
このインターフェースに沿ってクライアントを実装する。
3. gRPCクライアントの実装
接続先は環境変数から取得する。
4. レスポンスをCLIに表示
取得したレスポンスの特定フィールドを標準出力する。
Kubernetes上での接続設定
Pod間通信に必要なKubernetesの設定
同一Namespace内のPod間通信であっても、直接IPアドレスで繋ぐのではなく、KubernetesのServiceを経由して接続する。
Serviceを使うことでPodのIPが変わっても名前で解決できる。
Serviceによる名前解決
対象PodにClusterIPのServiceが設定されており、
同一Namespace内からは以下の形式でアクセスできる。
<service-name>:<port>
接続先アドレスをSecretで管理
クライアント側(メンテナンスPod)には接続先アドレスを環境変数で渡しているが、
その値はKubernetesの Secret で管理されている。
env:
- name: TARGET_POD_ADDR
valueFrom:
secretKeyRef:
name: grpc-secret
key: target-addr
Secretを使うことでアドレス情報をコードにハードコードせず、
環境ごとに切り替えやすい構成になっている。
なぜgRPCだったのか(考察)
設計には関わっていないため断言はできませんが、実装を通じて「なるほど」と感じた点がありました。
RESTでも POST /config/reload のようにURLで操作を表現することは可能です。ただしそれはあくまで慣習であり、強制力は弱いと感じました。
一方gRPCはprotoファイルでメソッドを定義するため、操作の意図がスキーマとして明文化・強制化されます。さらにprotocによるコード自動生成で型安全な実装が手に入ります。
今回のようなコマンドツールの用途では、この「明示的なスキーマ定義」によってサーバー・クライアント間の仕様の認識を合わせやすい点で特に有効だと感じました。
ハマったこと・気づき
1. コードが自動生成されることを知らなかった
protoファイルからGoのコードが自動生成されることを最初は知らなかった。
生成されたファイルを自分で書こうとしたところ、ファイルの冒頭に英語のコメントがあり、チームメンバーに確認したところ「自動生成されるファイルだよ」と教えてもらった。
手書きするものだと思い込んでいたが、protoファイルさえ正しく定義すれば、Goのコードはprotocが生成してくれるという仕組みを理解してからは実装がスムーズになった。
2. 自動生成コードのインターフェースに沿った実装が必要
gRPCではprotoファイルから自動生成されたコードのインターフェースに沿って実装する必要があります。
そのためまず自動生成の仕組みを理解することが実装の前提になる点で、RESTとは異なる学習コストがありました。
また自動生成されたコードの中に Unimplemented というメソッドが含まれていました。
これは実装がないメソッドが呼ばれた場合に「未実装」のステータスコードをデフォルトで返すもので、if文で制御しているわけではなく自動生成の仕組みとしてインターフェースに組み込まれています。
最初は何のためにあるのか分からず詰まりました。
3. GoのgRPCライブラリが複数存在する
GoのgRPC向けの実装ライブラリが複数存在することを最初は知りませんでした。
既存コードが connect-go を使っていたため調べてみると、よりシンプルに書けるライブラリだと分かりました。
調査の際には grpc-go の情報も多く出てくるため、どちらのライブラリの情報かを意識して調べる必要がありました。
おわりに
初めてgRPCを実装してみて、正直なところ難しかったです。
protoファイルの定義、自動生成の仕組み、インターフェースに沿った実装など、
最初は分からないことだらけでした。
ただキャッチアップを進めるうちに、gRPCが高速で安定したプロトコルであることは肌で感じることができました。
特にprotoによるスキーマ定義と自動生成の仕組みは、一度理解してしまえば非常に開発体験が良いと感じました。
今回はクライアント側・Unary RPCのみの実装だったので、次は以下のことに挑戦してみたいと思っています。
- Unary以外のストリーミング通信
- サーバー側の実装
- 別のユースケースでのgRPC活用
同じように「gRPCは知っているけど触ったことがない」というエンジニアの参考に少しでもなれば嬉しいです。