Skip to Content

gRPCアクション

grpcアクションはgRPCのメソッドを呼び出します。サービス定義はサーバーリフレクションで解決するため、実行時に.protoファイルは必要ありません。本番環境に多い、リフレクションのないサーバーは、protoに指定した.protoファイルの定義を使って呼び出します。protocol: connectを指定すると、HTTPのConnectプロトコルで呼び出します。

基本的な構文

gRPCのステップでは、サーバー、サービス、メソッド、リクエストの内容を指定します。

steps: - name: Get a user uses: grpc with: addr: "grpc.example.com:443" service: "user.v1.UserService" method: "GetUser" tls: true body: | {"id": "123"} test: res.status_code == "OK"

パラメータ

以下のフィールドで呼び出し内容を指定します。いずれもテンプレート式を書けます。

パラメータ型必須デフォルト説明
protocolString任意grpcgrpc、またはConnectプロトコルで呼び出すconnect
addrString必須-gRPCサーバーのホストとポート。protocol: connectではサービスを提供しているURL
serviceString必須-完全修飾のサービス名
methodString必須-メソッド名
bodyString、Object、List任意""リクエストメッセージ(JSON)。オブジェクトはJSONにシリアライズされます。リストはリクエストのストリームのメッセージです。ストリーミングのメソッドを参照
metadataObject任意{}リクエストメタデータ(HTTPのヘッダーに相当)
timeoutString任意30s接続とリフレクションによる解決を含めた、呼び出しの制限時間。"10s"のようなGoのduration形式。それ以外の値はエラー
tlsBoolean任意falseTLSを使う
insecureBoolean任意false証明書の検証をスキップする
cert_fileString任意-mTLS用のクライアント証明書
key_fileString任意-mTLS用のクライアント鍵
ca_fileString任意-サーバー検証に使うCA証明書
codecString任意jsonConnectの呼び出しのエンコード。jsonかproto。protocol: connectのときだけ
max_messagesInteger任意-レスポンスのストリームがこの件数届いた時点で打ち切る。ストリーミングのメソッドを参照
protoObjectまたはfalse任意-filesに挙げた.protoファイルと呼び出しを照合し、違反があればステップを失敗させます。falseはジョブのdefaultsが求める照合を外します。.protoファイルによる検証を参照

レスポンスオブジェクト

呼び出しの後、resに応答とgRPCのステータスが入ります。

プロパティ型説明
res.bodyAnyレスポンスメッセージ。JSON形式から解析したオブジェクトで、フィールド名はcreatedAtのようなlowerCamelCase
res.rawbodyString解析前のJSON形式のレスポンスメッセージ
res.status_codeStringgRPCのステータスコード(OK、NOT_FOUNDなど)
res.status_messageStringステータスメッセージ
res.metadataObjectサーバーが送ったヘッダーとトレーラー。各キーの最初の値が入り、同じ名前ならトレーラーが優先
rt.durationStringラウンドトリップ時間(例: "1.2ms")
rt.secFloatラウンドトリップ時間(秒)
statusIntegerステータスがOKのとき0、それ以外は1
reqObject送信したリクエスト
res.violationsArray呼び出しのうち.protoファイルが許可しないもの。すべて許可されていれば空です。protoを指定したときだけ入ります
res.messagesArrayレスポンスをストリーミングするメソッドの全レスポンス。それぞれres.bodyと同じくオブジェクトです。そのようなメソッドの場合だけ入ります
res.completeBooleanレスポンスのストリームを、呼び出しが打ち切ったのではなくサーバーが終えたか。レスポンスをストリーミングするメソッドの場合だけ入ります
res.contractObject.protoファイルで呼び出しを対応付けた先。spec(サービスを宣言するファイル。proto.filesに書いたパス)と、users.v1.UserService/GetUserのようなoperationが入ります。ファイルがそのメソッドを宣言している場合だけ入ります。probe coverageはこれを数えます

res.status_codeは、呼び出しが終わったときのステータスの正式名です。OK、CANCELLED、UNKNOWN、INVALID_ARGUMENT、DEADLINE_EXCEEDED、NOT_FOUND、ALREADY_EXISTS、PERMISSION_DENIED、RESOURCE_EXHAUSTED、FAILED_PRECONDITION、ABORTED、OUT_OF_RANGE、UNIMPLEMENTED、INTERNAL、UNAVAILABLE、DATA_LOSS、UNAUTHENTICATEDのいずれかになります。OK以外のステータスもサーバーの応答なので、ステップはそのままテストに進み、テストでそのステータスを期待できます。サーバーからステータスが得られなかった呼び出しだけが、エラーとしてステップを終わらせます。接続できないサーバーや、.protoファイルが宣言していないのにリフレクションにもサービスが載っていない場合と、サーバーが応答する前にtimeoutを過ぎたり接続が切れたりした場合です。

使用例

以下では、メタデータを付けて呼び出す場合と、エラーが返ることを検証する場合の書き方を示します。

メタデータを付けて呼び出す

認証情報などはメタデータとして渡します。

steps: - name: Authenticated call id: get-user uses: grpc with: addr: "{{vars.grpc_addr}}" service: "user.v1.UserService" method: "GetUser" tls: true timeout: "10s" metadata: authorization: "Bearer {{vars.token}}" body: | {"id": "{{vars.user_id}}"} test: res.status_code == "OK" outputs: user: res.body

エラーを検証する

ここではエラーが期待される結果です。そのためステータスコードを検証します。

steps: - name: Unknown user returns NOT_FOUND uses: grpc with: addr: "{{vars.grpc_addr}}" service: "user.v1.UserService" method: "GetUser" tls: true body: | {"id": "does-not-exist"} test: res.status_code == "NOT_FOUND"

.protoファイルによる検証

呼び出しは、サーバーがリフレクションで示す自身の定義を使って行います。そのため、サーバーが本来守るべき定義を守っているかどうかは分かりません。protoを指定すると、サービスを生成したリポジトリにある.protoファイルなどとも呼び出しを照合します。リフレクションのないサーバーや、定義にそのメソッドがないサーバーは、ファイルから生成したクライアントと同じく、ファイルの定義を使って呼び出します。

- name: Users defaults: grpc: addr: "{{vars.grpc_addr}}" service: user.v1.UserService proto: files: [./proto/user/v1/user.proto] import_paths: [./proto] steps: - name: Get a user uses: grpc with: method: GetUser body: id: "123" test: res.status_code == "OK" && res.body.user.name != ""

filesは、protocと同じくimport_pathsを使ってコンパイルします。import_pathsを指定しなければ、Probeを実行したディレクトリを使います。各ファイルは、それを含む最初のインポートパスからのパスで名前が付き、インポートするときもその名前で指定します。ファイルは呼び出しの前にコンパイルするため、コンパイルできないファイルを指定したステップは、何も呼び出さずにアクションのエラーとして失敗します。

リクエストのボディ(JSON)がファイルのリクエストメッセージとして解釈できない場合、ステップは種類contract_requestで失敗します。次の場合は種類contract_responseで失敗します。

  • サービスにそのメソッドをファイルが宣言していない
  • サーバーのリクエストまたはレスポンスのメッセージ(入れ子のメッセージも含む)が、ファイルと名前が違う、ファイルが宣言するフィールドを持たない、または同じ番号のフィールドを別の名前、型、単数か複数かで宣言している
  • レスポンスのフィールドが、ファイルがその番号に宣言するとおりにエンコードされていない(ファイルはint32と宣言しているのに文字列が届いた場合など)

strict: trueを指定すると、ファイルが宣言していないフィールドをサーバーが宣言したり送ったりした場合も失敗します。指定しなければ、protobufが新しい相手のフィールドを通すのと同じく、そのフィールドを通します。ファイルの定義で呼び出した場合は、サーバーの定義なしで行える範囲で照合します。リクエスト、レスポンスの読み直し、後述の制約は照合しますが、定義の突き合わせは行いません。リフレクションが示すサービスにそのメソッドがなければ、contract_responseで失敗し、サーバーはその呼び出しにUNIMPLEMENTEDで応答します。

サーバー自身の定義でも解釈できないリクエストのボディは送れないため、何も送らず、アクションのエラーではなくcontract_requestとしてステップを失敗させます。このときres.status_codeは空です。request: falseはサーバーとそのレスポンスだけを照合します。ファイルが許可しないものをわざと送るステップに使います。OK以外のステータスの応答には照合するメッセージがありません。違反はそれぞれres.violations、端末、レポートに、$.user.emailのようなフィールドとともに入ります。

ファイルが注釈する制約

proto3には必須のフィールドも値の範囲もないため、ファイルがフィールドに注釈を付けていなければ、ファイルが示すのはメッセージの形だけです。ファイルが次の2種類の注釈を使っていれば、それも照合します。

  • protovalidate のルール([(buf.validate.field)...]と、メッセージやoneofのルール)は、リクエストとレスポンスの両方で照合します。ルールに違反したフィールドがあれば、string.emailのようなルールとフィールドを示してステップを失敗させます。
  • google.api.field_behaviorはAIP-203 のとおりに照合します。リクエストがREQUIREDのフィールドを持たなければ(リクエストが持つ入れ子のメッセージも含む)、contract_requestで失敗します。レスポンスがINPUT_ONLYのフィールドを持っていれば、レスポンスが決して含めてはならないフィールドとして、contract_responseで失敗します。
syntax = "proto3"; import "buf/validate/validate.proto"; import "google/api/field_behavior.proto"; message CreateUserRequest { string email = 1 [(buf.validate.field).string.email = true, (google.api.field_behavior) = REQUIRED]; string password = 2 [(google.api.field_behavior) = INPUT_ONLY]; }

buf/validate/validate.protoとgoogle/api/field_behavior.protoはProbeに組み込まれているため、インポートパスに置かなくてもファイルからインポートできます。インポートパスにあるコピーは読みません。

Connectプロトコル

protocol: connectを指定すると、gRPCではなくConnectプロトコル でメソッドを呼び出します。ConnectプロトコルはHTTPのPOSTなので、HTTP/1.1のプロキシやロードバランサーも通ります。ブラウザのconnect-web クライアントが送る呼び出しと同じ経路です。connect-goやconnect-esで作ったサーバーが応答します。

- name: Get a user uses: grpc with: protocol: connect addr: https://api.example.com/rpc service: users.v1.UserService method: GetUser body: id: "123" metadata: authorization: "Bearer {{vars.token}}" test: res.status_code == "OK" && res.body.user.name != ""

addrにはサービスを提供しているURLを、マウントしているパスがあればそれも含めて書きます。スキームのないホストとポートはhttpとして扱い、tls: trueならhttpsにします。呼び出し先は<addr>/<service>/<method>です。metadataはヘッダーとして送り、timeoutはConnect-Timeout-Msでサーバーにも伝えます。TLSのパラメータは、httpsのURLに対してgRPCの呼び出しと同じように効きます。

codecはメッセージのエンコードで、json(デフォルト)かprotoです。HTTP/1.1の接続では運べないストリームが要るため、リフレクションは使いません。

  • codec: jsonでprotoがない場合、bodyは書いたとおりに送り、res.bodyはサーバーが返したJSONです。このときserviceはusers.v1.UserServiceのような完全な名前で書き、本文のフィールド名はサーバーが読む名前(connect-goならuserIdのようなlowerCamelCase)にします。
  • protoがある場合、リクエストはファイルの定義から組み立てます。そのため短いサービス名やsnake_caseのフィールド名もgRPCの呼び出しと同じように使え、res.bodyもgRPCの呼び出しと同じ形になります。ファイルとの照合は上で説明したとおりですが、比べるサーバーの定義はありません。codec: jsonでは、レスポンスにファイルの宣言と型の違う値があるとcontract_responseでステップが失敗し、strictのもとではファイルにないフィールドも同じく失敗します。
  • codec: protoにはprotoが必要です。リクエストのエンコードとレスポンスの読み取りにその定義を使います。

resのフィールドはgRPCの呼び出しと同じです。HTTPステータス404で返る{"code": "not_found", "message": "..."}のようなConnectのエラーは、res.status_codeがNOT_FOUNDになり、メッセージも入ります。ConnectのコードはgRPCのコードと同じだからです。Connectのエラーを含まない応答もサーバーの応答として扱い、コードはconnect-goと同じくプロトコルの決まりに従って決めます。HTTPステータスから決める場合(プロキシの502のページならUNAVAILABLE)はステータス行をメッセージにし、200でもcodecと違うContent-TypeならUNKNOWNかINTERNALにします。res.metadataはレスポンスヘッダーです。サーバーがメッセージの後にTrailer-を付けて送ったものは、それを外した名前で入り、同じ名前のヘッダーより優先されます。アクションのエラーになるのは、到達できないサーバーへの呼び出しやtimeoutを過ぎた呼び出しのように、応答が得られなかったときだけです。リダイレクトはたどりません。

ストリーミングのメソッドも、protoが宣言していればprotocol: connectで呼び出せます。HTTP/1.1でも通ります。詳しくはストリーミングのメソッドを参照してください。双方向にストリーミングするメソッドは呼び出しません。

ストリーミングのメソッド

レスポンスかリクエストをストリーミングするメソッドも、ストリーミングしないメソッドと同じパラメータで、gRPCでもprotocol: connectでも呼び出せます。呼び出しではすべてのリクエストを送ってこちら側を閉じ、それからレスポンスを読みます。

- name: Watch users uses: grpc with: method: WatchUsers body: filter: active max_messages: 3 test: res.status_code == "" && len(res.messages) == 3 && !res.complete - name: Import users uses: grpc with: method: ImportUsers body: - name: Ada - name: Grace test: res.status_code == "OK" && res.body.imported == 2
  • リクエストをストリーミングするメソッドには、bodyにリストを書きます。各要素がストリームの1メッセージで、順に送ります。オブジェクト1つなら唯一のメッセージになり、[]なら何も送りません。それ以外のメソッドにリストを書くとエラーです。
  • レスポンスをストリーミングするメソッドでは、届いた順にすべてのメッセージがres.messagesに入り、最後のメッセージがres.bodyに入ります。サーバーがストリームを終えた場合はres.completeがtrueになり、終えたときのステータスがres.status_codeに入ります。
  • max_messagesを指定すると、その件数のレスポンスが届いた時点で呼び出しを打ち切ります。自分では終わらないストリームに使います。このときサーバーはまだステータスを返していないため、res.status_codeは空、res.completeはfalseです。レスポンスをストリーミングしないメソッドに指定するとエラーです。
  • timeoutで切れた場合のように、いくつかのレスポンスが届いた後でストリームが途切れたときは、届いたレスポンスと、途切れたときのステータス(DEADLINE_EXCEEDEDなど)が入り、res.completeはfalseです。レスポンスが1つも届いていなければ、応答のない呼び出しと同じくアクションのエラーです。timeoutはサーバーにも伝えるため、サーバーが期限切れでストリームを自ら終えることもあり、その場合はDEADLINE_EXCEEDEDでres.completeはtrueです。
  • 双方向にストリーミングするメソッドは呼び出しません。何も送らずに、アクションのエラーでステップを失敗させます。

ストリーミングするかどうかは、呼び出しに使う定義(サーバーのリフレクションか、protoの.protoファイル)で判断します。Connectの呼び出しにはリフレクションがないため、ストリーミングのメソッドはprotoがあるときだけ呼び出せます。protoがなければ、リストのbodyとmax_messagesはエラーになり、メソッドはストリーミングしないものとして呼び出すため、サーバーが拒否します。protoがあれば、ストリームの各メッセージをファイルと照合し、違反は$[1].user.emailのように何件目のメッセージかを付けて示します。サーバーの定義がファイルの宣言と違う形でストリーミングする場合は、contract_responseでステップが失敗します。

ガードの下での実行

grpcアクションは実行のガードを守ります。ガードが許可しない呼び出しは何も送らずに拒否し、種類refusedでステップを失敗させます。

  • --allow-hostでは、addrのホストとポートを確かめます。ポートがなければ、grpc-goと同じく443として扱います。前に付いたdns:///とpassthrough:///は外して読みます。dns://server/のDNSサーバーも確かめ、ポートがなければ53として扱います。unix:///tmp/grpc.sockのようにホストのない宛先は拒否します。protocol: connectではURLのホストを確かめ、ポートがなければスキームの既定のポートとして扱います。
  • --read-onlyでは、手元にあるメソッドの定義がすべてoption idempotency_level = NO_SIDE_EFFECTS;で副作用がないと宣言しているときだけ呼び出します。手元の定義とは、リフレクションで得たサーバーの定義と、protoがあればその.protoファイルの定義です。リフレクションでの問い合わせは読み取りなので行います。Connectの呼び出しにはリフレクションがないためprotoが必要で、ない場合は拒否します。
service UserService { rpc GetUser(GetUserRequest) returns (GetUserResponse) { option idempotency_level = NO_SIDE_EFFECTS; } }

関連項目

  • 変数 - ステップで使える変数
  • YAML設定 - ステップのプロパティ
更新日時