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"パラメータ
以下のフィールドで呼び出し内容を指定します。いずれもテンプレート式を書けます。
| パラメータ | 型 | 必須 | デフォルト | 説明 |
|---|---|---|---|---|
protocol | String | 任意 | grpc | grpc、またはConnectプロトコルで呼び出すconnect |
addr | String | 必須 | - | gRPCサーバーのホストとポート。protocol: connectではサービスを提供しているURL |
service | String | 必須 | - | 完全修飾のサービス名 |
method | String | 必須 | - | メソッド名 |
body | String、Object、List | 任意 | "" | リクエストメッセージ(JSON)。オブジェクトはJSONにシリアライズされます。リストはリクエストのストリームのメッセージです。ストリーミングのメソッドを参照 |
metadata | Object | 任意 | {} | リクエストメタデータ(HTTPのヘッダーに相当) |
timeout | String | 任意 | 30s | 接続とリフレクションによる解決を含めた、呼び出しの制限時間。"10s"のようなGoのduration形式。それ以外の値はエラー |
tls | Boolean | 任意 | false | TLSを使う |
insecure | Boolean | 任意 | false | 証明書の検証をスキップする |
cert_file | String | 任意 | - | mTLS用のクライアント証明書 |
key_file | String | 任意 | - | mTLS用のクライアント鍵 |
ca_file | String | 任意 | - | サーバー検証に使うCA証明書 |
codec | String | 任意 | json | Connectの呼び出しのエンコード。jsonかproto。protocol: connectのときだけ |
max_messages | Integer | 任意 | - | レスポンスのストリームがこの件数届いた時点で打ち切る。ストリーミングのメソッドを参照 |
proto | Objectまたはfalse | 任意 | - | filesに挙げた.protoファイルと呼び出しを照合し、違反があればステップを失敗させます。falseはジョブのdefaultsが求める照合を外します。.protoファイルによる検証を参照 |
レスポンスオブジェクト
呼び出しの後、resに応答とgRPCのステータスが入ります。
| プロパティ | 型 | 説明 |
|---|---|---|
res.body | Any | レスポンスメッセージ。JSON形式から解析したオブジェクトで、フィールド名はcreatedAtのようなlowerCamelCase |
res.rawbody | String | 解析前のJSON形式のレスポンスメッセージ |
res.status_code | String | gRPCのステータスコード(OK、NOT_FOUNDなど) |
res.status_message | String | ステータスメッセージ |
res.metadata | Object | サーバーが送ったヘッダーとトレーラー。各キーの最初の値が入り、同じ名前ならトレーラーが優先 |
rt.duration | String | ラウンドトリップ時間(例: "1.2ms") |
rt.sec | Float | ラウンドトリップ時間(秒) |
status | Integer | ステータスがOKのとき0、それ以外は1 |
req | Object | 送信したリクエスト |
res.violations | Array | 呼び出しのうち.protoファイルが許可しないもの。すべて許可されていれば空です。protoを指定したときだけ入ります |
res.messages | Array | レスポンスをストリーミングするメソッドの全レスポンス。それぞれres.bodyと同じくオブジェクトです。そのようなメソッドの場合だけ入ります |
res.complete | Boolean | レスポンスのストリームを、呼び出しが打ち切ったのではなくサーバーが終えたか。レスポンスをストリーミングするメソッドの場合だけ入ります |
res.contract | Object | .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;
}
}