Mercari Engineering Blog

We're the software engineers behind Mercari. Check out our blog to see the tech that powers our marketplace.

gRPCと手動テスト

この記事はMERPAY TECH OPENNESS MONTHの5日目の記事です。

merpayでNFC決済のmicroservice (nfc-service) を開発担当している @Hiraku です。メルペイのバックエンドシステムは、各microserviceが主にgRPCを主な通信プロトコルとして用意しています。当然、各チームはgRPCサーバーを開発しています。この記事では、ちょっとした開発の日常をお見せしたいと思います。

とあるエンドポイントの場合

そもそもgRPCサーバーだけで、iOS/Androidのバックエンドができるのか?と思う方がいるかもしれません。
下図は、典型的なリクエスト経路を示しています。

f:id:escape_artist:20190523113714p:plain

  1. merpay-gateway ... 主にプロトコルの変換を担う
  2. merpay-api ... BFF(Backend for Frontend)的な役割を担い、他のmicroserviceを集約する
  3. nfc-service ... ロジックとDBを担当

この記事を書いている時点では、メルペイの機能部分はHTTPSに直接Protocol Buffersのバイナリを流して通信を行っており、そこの経路はgRPCではありません。merpay-gatewayがgRPCへのプロトコル変換を行ってくれるため、以降のサービスはgRPCが話せれば十分機能を果たせるようになっています。

私のチームが担当するのは一番奥底の nfc-service ですので、gRPCだけ話せばいいわけです。

作ったら、動かしてみたい…!

私は昔mercariで、JSONを返すWebAPI (いわゆるmercari-api)を作っていました。当時、開発したものを試すときは素朴に curl コマンドを使ってリクエストをして、 jq コマンドで結果を整形して眺めていました。

# リクエストイメージ
$ curl -H "Authorization: bearer $TOKEN" -d 'format=details' http://localhost:9999/user | jq .
{
  "name": "foo",
  ...
}

人によってはHTTPieなどの便利なclientを使っていたり、QAエンジニアだとPostmanを標準的に使ってシナリオを書いています。JSON over HTTP はWebAPIとしてありふれた形式ですので、手軽にテストするためのツールがたくさん揃っていますよね。

「作ったら、動かしたい」

これはエンジニアとして当然の欲求だと思います。
gRPCサーバーの場合も同様で、curlは使えませんが手軽にgRPCを試すためのコマンドラインのクライアントがいくつか存在します。

https://github.com/grpc-ecosystem/awesome-grpc#tools-cli

例えばkazegusuri/grpcurlだと、こんな風にリクエストできます。(公式のREADMEから引用)

$ echo '{"Message": "hello"} | grpcurl --insecure call localhost:8080 test.EchoService.Echo
{"Message":"hello"}

protobufのバイナリではなくJSONでリクエストでき、レスポンスもJSON形式に整形してくれるため、とてもお手軽ですね。 curlを使っていた部分をこういったツールに修正するだけで、「作ったものを動かしてみる」が対応できます。

認証や依存サービスが話を難しくする

ところがgRPCを話せても、実際にAPIを試すまでには2つ大きなハードルがあります。

1つ目は、認証です。 弊社では基本的に全てのmicroserviceはリクエストごとに認証処理をしています。認証はユースケースによって様々な種類がありますが、トークンをmetadataに付与してリクエストし、サーバーはそのトークンを検証しています。開発環境でも同じで、正当なトークンを付与しないことにはUNAUTHENTICATED(=16)のエラーになってしまいます。なお、トークン発行はauthority-serviceと呼ばれるサービスのみが可能です。

開発環境であれば任意のトークンを発行することはできるのですが、認証ごとに別のトークンが必要です。user本人の直接のリクエストを期待している箇所と、別のmicroserviceからバッチ処理で叩かれることを想定する箇所では、認証の状態も別々に設計されているわけですね。

認証を使い分けているのは正しいのですが、いざAPIを試そうとしたとき、「あれ?どういうトークンが必要なんだったっけ…?」となりがちです。この煩雑さが1つ目のハードルです。

f:id:escape_artist:20190523114033p:plain

2つ目は、依存する他のサービスです。 メルペイはメルカリに依存したシステムですので、メルカリのAPIを使って特定の状況を作り出さないとテストできないことがあります。

メルカリAPIは先程も少し触れましたが JSON over HTTPS なWebAPIですので、テストシナリオを作ると結局HTTPクライアントが必要になります。gRPCのクライアントだけでは、テストが完結できないのです。

QAエンジニアの場合: grpc-http-proxy

QA/Automationチームでは、これまでPostmanを使ってシナリオテストを作っていました。
gatewayの外側からのテストは当然として、各microservice単体でも可能な限りシナリオを書きたいところです。しかし、各サービスは公式にはgRPCのインターフェースしか提供していません。どうやってテストすれば良いのでしょうか…。

メルカリのmicroservice-platformでは、この問題を解決するべく grpc-http-proxy というコンポーネントが動いています。このproxyを通すと、gRPCのReflectionインターフェースを使って"いい感じに"あらゆるgRPCサービスをJSONでリクエストできるようにしてくれます。

f:id:escape_artist:20190523114114p:plain

  • microserviceを直接叩きたい場合はgrpc-http-proxyを経由
  • mercari-apiなら直接HTTP
  • gatewayを経由した場合も直接HTTP

こうやって、全経路をJSON over HTTPSで試せるようになっており、シナリオはこれまで通りPostmanを使って記述できています。すばらしいですね。

私の場合

grpc-http-proxyはとても便利なのですが、これ自体もソフトウェアコンポーネントのため、まれに障害が発生することがありますし、microservice側が設定を間違えていることもあります。
バックエンドエンジニアとしては、サービスがうまく動いていないとき、どこに障害が起きているのか瞬時に切り分ける力が求められます。なので、grpc-http-proxyにだけ依存してはダメで、直接gRPCで叩く経路、grpc-gatewayを経由した経路など、可能な限りの経路を直接確認できる方がよいことになります。

最初の頃は「トークンを取得して、パラメータを調べて、サーバーのドメインをドキュメントから探して、リクエスト!」と必要になるたびに実行していたのですが…。はい。とてもやってられないので、よく使う部分をスクリプト化しておくことにしました。

nfc-serviceで用意したスクリプト

gRPCとHTTPが話せれば良いので、どんなプログラミング言語で書いても良いのですが、QA, クライアントエンジニアなど様々な人が参照する可能性があるため、 bashスクリプト で書くことにしました。 プロジェクトのgitリポジトリに tools/manualtest というディレクトリを切って管理しています。

残念ながらinternalのAPIは公開できないので、スクリプトは雰囲気だけになるのですが、紹介したいと思います。

.
├── .env
├── .env.sample
├── .env.sh
├── .token.sh
├── README.md
├── direct
...
│   └── nfcapi-Ping.sh
├── gateway
...
│   └── nfcapi-Ping.sh
├── grpc-http-proxy
...
│   └── nfcapi-Ping.sh
├── json
│   └── Ping.json
└── login.sh

.env には環境変数で各種サーバーのドメインが書いてあります。

スクリプト使い方は単純で、まずは以下のように login.sh を実行します。メールアドレスとパスワードの入力が求められ、完了するとトークンが .token.sh に書き出されます。 .token.sh は.gitignoreしてあるので、誤ってコミットする危険はありません。

$ bash login.sh
Email: test@example.com
Pass: 

login.shの中身は以下のような感じです。

#!/bin/bash

set -eu
cd $(dirname ${BASH_SOURCE:-$0})

source ./.env.sample
if [[ -f ./.env ]]; then
    source ./.env
fi

read -p "Email: " email
read -sp "Password: " pass

# (ログインしてトークンにする処理色々)

echo TOKEN1=$TOKEN1 > .token.sh
echo TOKEN2=$TOKEN2 >> .token.sh
...

書き出された.token.shをsourceすれば環境変数として各トークンが使えますので、あとは各エンドポイントを叩くスクリプトを書くだけです。

grpc直接だったらこんな感じです。ここではkazegusuri/grpcurlを使っています。

#!/bin/bash

set -eu
cd $(dirname ${BASH_SOURCE:-$0})
source ../.token.sh

cat '{}' |
    grpcurl call --insecure \
    -H "Authorization: bearer $TOKEN1" \
    $GRPC_HOST \
    grpc_service_name.grpc_method_name |
    jq .

grpc-http-proxyを経由する場合はこんな感じになります。

#!/bin/bash

set -eu
cd $(dirname ${BASH_SOURCE:-$0})
source ../.token.sh

curl --verbose \
    "$GHP/grpc_service_name/grpc_method_name" \
    -H "Grpc-Metadata-Authorization: bearer $TOKEN1" \
    -H "Content-Type: application/json" \
    -d '{}' | jq .

あとは必要に応じてリクエストパラメータのサンプルを用意しておけば、とっさに使える便利なスクリプトの出来上がりです。

Hello, worldからはじめよう

もちろん、こういう泥臭いスクリプトは本番環境では使えません。本番では主にDatadogのtraceを元に障害の原因究明をします。また、開発中もユニットテストを書きながら、テスト駆動で書きますので直接gRPCのリクエストを試す機会はそこまで多くありません。

しかしサービスの新規開発で、開発環境を構築しているような段階だと、こういった"Hello, world"に近い単純なスクリプトがとても役立ちます。依存するサービスのアドレスの間違いや、SpannerなどのGCPの設定のtypoなど、実行環境に依存した問題もあります。ユニットテストやコンパイラが見つけてくれない問題は、自分の力で根気強く調べるしかないのです。

microservicesアーキテクチャを支える新しい技術はたくさん出ています。しかし「作って、動かす」という根本的なところから開発作業に向き合ってみるのもまた良いのではないでしょうか。

MERPAY TECH OPENNESS MONTHですが、次回は5月27日(月)に yagi5 さんによる、「What is AML?」です。お楽しみに!