TT Lab
はじめる
学ぶ 学習パス コース

EAI 中間層をつくる

門番は部屋の中の仕事をしない

TT Labで続きを見る

一言でいうと

APIゲートウェイは、HTTP APIの前に立ち、誰が(認証)、どれだけ(クォータ)、どのバージョンで(ルーティング)呼び出すかを1か所で決めます。EAIハブが電文を翻訳してシステム間をつなぐとすれば、ゲートウェイは業務ロジックを知らないまま、呼び出しのゲートキーパーの役割だけを担います。製品(Kong、Apigeeなど)を使っても、nginxで作っても、やっていることの原理は同じです。

なぜ必要なのか

銀行が、提携先やフィンテックに照会APIを公開したとします。最初は、APIサーバーが直接キーを確認し、呼び出し数を数え、古いバージョンのリクエストを新しいバージョンへ振り向けていました。APIサーバーが3台に増えると、3か所のキー確認コードが少しずつ違ってきました。ある提携先がバグで毎秒数千件を送りつけると、すべての提携先がまとめて遅くなりました。v1を廃止しようとしたところ、誰がまだv1を使っているのか、誰にもわかりませんでした。

この3つ(認証、呼び出し量の制限、バージョン)は、業務と無関係に、すべてのAPIで同じように必要です。そのためAPIサーバーごとに実装せず、前段の1か所にまとめます。逆に、ゲートウェイに業務ルール(残高確認、限度額計算)を入れ始めると、ゲートウェイが2つ目のアプリケーションサーバーになってしまいます。ゲートキーパーは、身元を見てドアを開けるだけで、部屋の中の仕事はしません。

どう動くのか

このラボは、製品なしでnginxの標準モジュールを使って原理を作ります。公式ドキュメントのディレクティブだけを使います。

認証: APIキーをコンシューマー名に。mapは、リクエストの値(ここではX-API-Keyヘッダー、変数$http_x_api_key)を別の変数に変換する表です。キー → コンシューマー名の表をファイルに置いてincludeします。表にないキーは既定値(空の値)になり、空のコンシューマーは401で返します。アップストリームにはコンシューマー名だけをX-Consumerで渡し、キー自体は渡しません。proxy_set_headerの値を空文字列にすると、そのヘッダーはアップストリームに渡されません。秘密はゲートウェイで止まり、アップストリームのログにキーが漏れません。(APIキーは呼び出すプログラムを識別するだけで、ユーザーを認証しません。ユーザー単位の権限が必要ならトークンを使います。このコースの範囲外です。)

クォータ: コンシューマーごとのレート制限(rate limit)。limit_reqモジュールは、ドキュメントの表現どおり「リーキーバケット(leaky bucket)」方式でリクエストの処理速度を制限します。limit_req_zone $consumer zone=… rate=5r/sでコンシューマー名をキーにしてゾーンを作ると、制限がコンシューマーごとに別々にかかります(キーをIPにすると、1つのNATの内側のすべての提携先が、1つのバケットを分け合います)。ドキュメントによれば、キーが空のリクエストはカウントされません。burstは、瞬間的に集中したリクエストを何件まで行列に並べるか、nodelayは、並べたリクエストを遅らせずにすぐ処理するかを決めます。上限を超えたリクエストは、既定で503を受け取りますが、limit_req_status 429で変更します。429 Too Many Requestsは、RFC 6585が「送ったリクエストが多すぎる」という意味で定義したステータスコードなので、呼び出し元は「サーバーの具合が悪い(503)」と「自分が送りすぎた(429)」を区別できます。

バージョン: パスとヘッダー。パスにバージョンを入れる方式(/v1/…、/v2/…)は、目に見えて、キャッシュやログでの区別も簡単です。ヘッダーで選ぶ方式(X-API-Version: 2)は、パスを変えません。このラボは両方を受け付けます。バージョンなしの/api/…はヘッダーで選び、ヘッダーがなければv1です。そして、廃止するバージョンにはSunsetヘッダー(RFC 8594)を付けて、「このリソースは、この時刻以降は応答しなくなる可能性がある」と知らせます。値はHTTPの日付形式です。すべてのレスポンスに付いているので、呼び出し元のログやモニタリングが自分で気づきます。お知らせメールより確実です。

リクエストID。呼び出し元がX-Request-IDを送ってきたらそのまま渡し、なければゲートウェイが作ります。nginxの$request_idは、ドキュメントによれば、ランダムな16バイトを16進数で表した一意の識別子です。このIDを、アクセスログにコンシューマーとステータスと一緒に残せば、提携先が「昨日14時に429を受け取った」と言ってきたとき、その行をすぐ見つけられます(モジュール7のGUIDと同じ考え方を、HTTPの境界に適用したものです)。

タイムアウト。アップストリームが止まると、ゲートウェイの接続も一緒に止まります。proxy_read_timeoutは、アップストリームからの2回の読み取りの間に待つ最大時間で、超えるとゲートウェイが504を返します。既定値は60秒ですが、呼び出し元が10秒で諦めるAPIなら、ゲートウェイが60秒も抱え込む理由はありません(モジュール4の「内側ほど短く」)。

EAIハブとの違い。ハブは電文を翻訳し(形式・コード)、同期・非同期を切り替え、複数のシステムを組み合わせます。ゲートウェイはHTTPリクエストをほぼそのまま通過させながら、統制だけを行います。現場では両方が一緒にあります。外部の提携先 → ゲートウェイ → ハブ → 勘定系です。

現場での姿

1つ目は、IPでクォータをかけてしまうミスです。大手の提携先がNATの内側で複数のサービスを動かし、お互いの上限を食い合います。コンシューマーの識別が先です。2つ目は、上限超過を503で返す設定です。呼び出し元のリトライロジックが「サーバー障害」と見て、さらに強くリトライします。3つ目は、アップストリームのログにAPIキーが残ることです。ログ収集システムに提携先のキーが平文で溜まります。4つ目は、バージョン廃止のお知らせをメールだけで行うことです。担当者が替わった提携先は知らないまま、廃止の日に障害を迎えます。

次のラボですること

アップストリームのフィクスチャ(v1・v2、受け取ったヘッダーを映すAPI)を起動し、nginxの設定nginx.confをステップごとに育てます。パスプロキシ、APIキー認証とコンシューマー名の受け渡し、コンシューマーごとのクォータと429、ヘッダーによるバージョンルーティングとSunset、リクエストIDとアクセスログ、アップストリームのタイムアウトの順です。採点ツールは、設定ファイルをコピーして自分のポートで新しく起動し、リクエストを流してみます。