DMMの API-Gatewayを構築した話 (第一話)

こんにちは、DMMのプラットフォーム事業本部の基盤開発グループの恩田です。プラットフォーム事業本部では、DMMの各事業を支える共通のAPIやGUI部分を提供しています。その基盤を開発しているのが我々基盤開発グループとなります。昨今では特に APIの品質強化や運用の自動化に取り組んでいます。

さて、我々が手掛けた DMMの第二世代 API-Gateway（Gen2-GW）が稼働して1年以上が経ちました。基盤選定から開発・テストに至る全行程で苦労させられたこのプロダクトですが、そろそろ実績も出来てきたので、制作秘話や簡単な内部構造を共有したいと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/375665/0effd88e-5834-9984-dde6-3fd714c5bb67.png)

## 新型 API-Gatewayは何を改善したのか
以降は、Gen2-GWの改善点と改善方法を述べるスタイルで進めたいと思いますが、忙しい人のためにざっとリストアップしてみました。今回はこの冒頭にあたる機能面を説明してゆきたいと思います。

- **APIの推奨仕様を独自のRPCスタイルから標準的なRESTスタイルに変えた**
- **APIの仕様は新旧同時にサポートした**
- **旧来からの独自機能も暫定でサポートした**
- レイテンシとスループットを底上げしてビジネスの拡大に備えた
- 水平スケーリングや自動復旧で拡張性・可用性を上げメンテナンスフリーに近づけた
- デプロイ自動化により運用効率を上げ、改善スピードを加速させた
- 可観測性を上げて問題の解消スピードを上げた
- 従来では不可能な大量のログ管理・分析を可能にした

### 機能面での改善

#### 1. 独自のAPI形式への互換性を残しつつ 標準的なRESTfulAPIに対応 
詳細は後述しますが、社内では旧来の独自API形式が未だに多く利用されています。実は旧来のAPI-Gateway（Gen1-GWと呼びます）はこの形式に依存しており、（そこまでは良いのですが）API-Gatewayの仕様上独自形式から抜け出せないという状況でした。これは結構厳しいです。かくいう僕も入社して初めてAPIを作成した際、RESTfulに設計したAPIが適用できず、泣く泣く独自形式に合わせた悲しい過去があります。

| 種別 | Method |URL |
| --- | --- | --- |
| Expected | GET | `https://xxxxxx/user-service/v1/users/001` |
| Actual | POST | `https://xxxxxx/user-service/v1/getUser` |
#####  標準への舵取り
他の人には同じ思いをさせたくない…という思いがあったわけではありませんが、この独自性を解消したらDMMのAPIをRESTfulに誘導できます。現行仕様を踏襲すれば波風立たないのは百も承知ですが、この課題を見てみぬフリをすると組織の成長を阻害します。という訳でココは本格的に対応することにしました。今後のAPI戦略にも関わってきますし重要です。

また独自のAPI形式をRESTfulにすると良いことが沢山あります。例えばURL, HTTP-Method, StatusCodeがHTTPの仕様に基づいた利用法となるので、**周辺システム**（例えばProxyやログ分析基盤）が**正しい状況判断をできるように**なります。具体的には Methodの仕様に基づいて冪等性や安全性を判断できたり、StatusCodeに基づいてリクエスト単位の状況を判断できたりといった形です。当たり前の状態を作っているだけではありますが、こういう細かい改善は後々効果が出てきます。

##### 新旧2形式を同時サポート
更には、**新旧の並列稼働も実現**しました。完全に新規の仕様となると移行が大事になるので、そこを考慮して独自形式とRESTfulAPI形式の双方を扱える仕様にします。こうすることで旧式にしか対応していないクライアントでも改修せずに利用することができます。この辺りは細く設定可能にし、API毎に既定の挙動を設定しつつも、リクエスト毎に挙動を指定できるようにしています。

> （改善）新旧APIの互換性を持たせて並列稼働可能にした

この分野における 新旧GWの比較も下の表に纏めておきます。

| # | 項目 | Gen1 | Gen2 | 
| --- | --- | --- | --- |
| 1 | URL | 独自形式 | 自由形式 |
| 2 | PathParam | ✕ | ○ |
| 3 | Method | POST前提 | 全てサポート |
| 4 | ResponseBody | 固定要素あり | 自由 |


#### 2. 従来からの独自機能は引き続きサポート
一般的に API-Gatewayはそれほど多くの機能をもたせるものではありませんが、やはりリクエストが集約される箇所には機能を追加したくなる様です（効率的に見えるんでしょうね）気づけば Gen1-GWには様々な独自機能が追加されていました。

責務的にココじゃない感が半端ないので、本音を言えば直ぐにでも削除したい訳ですが、それらをパワープレイで除外するとそれに依存していたAPIが次々に障害を起こして大変なことになります。こういう点が横断プロダクトの辛いところですが、当面は機能を搭載して互換性を維持しなければなりません。そういう訳で Gen2-GWにも独自機能は搭載されています。

一方で、これらは可能なら今すぐ削りたい機能です。そのため独自機能は疎に結合させ、処理レイヤーを一つ削るだけで、影響なく簡単に廃棄できる設計にしてあります。**簡単に削れる・簡単に棄てられる**という特性も長い目で見るととても大事です。

> （改善）レガシー機能はサポートしつつ Disposableにして将来に備えた

| # | 項目 | Gen1 | Gen2 | 
| --- | --- | --- | --- |
| 1 | 独自機能サポート | ○ | ○ (廃棄可能) |

#### 3. API-Gatewayの一般的な機能を漏れなくサポート

ここからは負債とは関係ない API-Gateway一般に期待される機能の話です。ゼロからAPI-Gatewayを設計される方はこのあたりを実現することになると思います。もちろんGen2-GWもこれらの機能を実現させており、Gen1の不足部分を補う形で強化させています。以下に具体的な強化ポイントを載せておきます。

| # | 項目 | Gen1 | Gen2 | 
| --- | --- | --- | --- |
| 1 | Routing | 独自のURL設計のみサポート | RESTfulなURLもサポート |
| 2 | Traffic Handling | 数種類の機能をサポート | 一般機能は全てサポート |
| 3 | Authorization | OAuth2に対応 | OAuth2に対応 (キャッシュ機能付き) |
| 4 | Logging | サーバー内に1週分を記録 | クラウド上に1月分を記録 |

> （改善）基本機能も漏れなく強化した

続けて、各項目について掘り下げつつ、API-Gatewayが必要とされている一般機能に関して再確認してゆきつつ、改善点を確認してゆきたいと思います。

##### 3.1. Routing
API-Gatewayは Clientに**単一の窓口**を提供します。これはClientにとってサービスディスカバリーとして機能しますが、API-Gatewayにとっては Routingの責務を負うことも意味します。そうなると大量のリクエストを膨大な定義に基づいてRoutingする必要が出てきます。こう書くと大変そうに聞こえますが、実際のところ技術的な難しさはあまり無いのが救いです。

唯一気をつける点は、RESTfulなPath設計をサポート可能にすることで、よく問題になるのがPathパラメータまわりです。この辺りはURL設計のポリシーを満足させられるエンジンを選んでおかなければなりません。例えば `/v1/resources/:id`と `/v1/resources/customMethod`では最後の要素が2系統の定義で衝突しますが（`/v1/resources/hello`の`hello`がidかcustomMethodか判別しづらい）、エンジンが上手いことRoutingしてくれないと厳しいです。

##### 3.2. Traffic Handling
次に API-Gatewayの責務として主要なものは **Traffic制御**です。いわゆるHTTPの Request / Responseにおける通信の制御となります。この制御が適当だと、APIのクライアントへの応答が過度に遅れてしまったり、不調なバックエンドAPIに対しリクエストを激しく送り続けてしまったり、それらのリクエストが滞留してAPI-Gateway自体に負荷がかかり API全体の不調を招いてしまったりします。さて、具体的なTraffic制御内容ですが、主に次の様なものです。

| # | 機能 | Cli保護 | Srv保護 | 処理内容 |
| --- | --- | --- | --- | --- |
| 1 | Timeout | ○ | △ | 一定時間以上応答の無いリクエストをキャンセル |
| 2 | Rate Limiting | - | ○ | Client単位 または全体の流量を API単位で制限 |
| 3 | Circuit Breaking | ○ | ○ | 不調なAPIが回復するまで GWがClientのリクエストを即座に失敗させる |
| 4 | Firewall | - | ○ | 不要なアクセス元や攻撃の発生源を遮断 |

Traffic制御まで入るとだいぶAPI-Gatewayらしくなってきますが、この辺りまでなら Apache, Nginxの様なリバースプロキシサーバで代替が可能です。実際にOSSで有名な [Kong](https://konghq.com/kong/)は [Nginx](https://nginx.org/)を拡張して構築されています。

##### 3.3. Authorization
続いて 必要とされるのが**認可処理**です。API-Gatewayは主にAPIコールを対象とした Gatewayですので、直接的なユーザの認証は必要ありません。とはいえ全てのリクエストを通過させてしまうと不正なアクセスを遮断できません。

昨今は境界セキュリティに頼りきった時代では無くなって来ましたが、API-Gatewayといったリクエストの集約ポイントには、依然として不正アクセスを制御する役割が期待されます。例えば ZeroTrustセキュリティで有名な BeyondCorpの論文でも「Gatewayと呼ばれるProxy的な役割がAccessControlEngineの指示の下リクエストを通過させるか判断する」というデザインが採用されています。(参考: [BeyondCorp: Design to Deployment at Google](https://storage.googleapis.com/pub-tools-public-publication-data/pdf/44860.pdf)）

弊社でもAPIへのアクセスには OAuth2をベースとした認可の仕組みが導入されており、Gatewayはその指示のもとリクエストの通過・遮断を制御しています。その他の仕組みも併せて導入していますが、話が長くなりそうなので割愛します。

##### 3.4. Logging
最後に Loggingも API-Gatewayにとっては重要な機能です。API-Gatewayという**一点にリクエストが集約される**ので、標準化された形式でログを記録する箇所として最適なのです。実際、BackendのAPIが少々不調でも、API-Gateway側にログが残っていればそれをもとにリクエストの痕跡を確認することができます。

実際に運用してみると分かりますが、API-Gateway上できちんとロギングされていれば諸々の部署から調査依頼が入ってきます。それ自体は良いことなのですが、頻繁に調査を代行するのも大変なのでこのあたりのセルフサービス化も進めてゆく予定です。

詳細は次回の記事に書く予定ですが、ログ情報はクラウド上のログ基盤を経て分析可能としています。クラウドの力で可能になったログ集約とビッグデータ分析が、今後のデータ活用・分析におけるブレークスルーとなると考えています。

> （改善）Traffic制御機能の網羅率を従来より向上させ、大規模なログ情報も保存可能とした

### 機能実現の方法

上記の各機能を用意するにあたって、重視したのが「**OSSの活用**」と「**拡張の自由度**」です。つまり、API-Gatewayとしてのコア機能を充足させるためOSSを使って**工数を削減**しつつも、**自由に拡張する余地**を（充分に）残さなければなりません。特に後者は不十分だと対応できないことが増えてしまいます。ですので我々にとって重要なのがOSSにおける拡張の自由度なのです。

#### OSSの選びかた

さて、一般的に API-Gatewayの様なProxy系のOSSは Plugin形式での機能追加をサポートしています。より具体的には「多くのProxy系OSSが [タスクとフィルターのパターン](https://docs.microsoft.com/ja-jp/azure/architecture/patterns/pipes-and-filters)を適用して構成されており、そのフィルター部分を Pluginとして追加可能にすることで機能拡張を実現する」といった具合です。

多くの場合はこれで問題ないのですが、注意が必要なのはこれらが「Request単位」に特化した制御で、それ以上を想定していない点です。例えば「Routingエンジンに手を入れなければならない時」や「一部のコア機能を無効化したい場合」はどうすべきでしょうか。マイナーケースではありますが可能性がゼロでない以上、アーキテクトとして考えておかなければなりません。

> 弊社内では「取り敢えずやってみよう」「細かい部分は後で考えよう」という進め方は割と許容されていますが、この様な横断的な機能はそのノリで進めると後々行き詰まる可能性が残ります。システムの柔軟性をどの辺りまで担保するか、逆にどの辺りはFIXさせてしまって問題ないかというのをロジカルに判断してゆくのも横断システムの設計を考える上ではとても重要です。

また、DMMでは現在50以上の事業が存在しており、プラットフォーム事業本部内でも様々な案件が稼働しています。どの事業やどの案件が、どんなAPI-Gateway向けの新機能を要求するかは全く予想がつきません。もちろん全ての要求を実現する気はありませんが「実現できるけどやらない」と「実現できないからやれない」には大きな差があります。

さて、実際に Gateway系のOSS・サービスを見てゆくと、以下の様に分類することができます。

| # | 	種別	| 例	 | 評価 | 
| --- | --- | --- | --- |
| 1 | Managed-Service | [Apigee](https://cloud.google.com/apigee?hl=ja) | ・コアな変更が厳しそう<br>・流量によっては価格と性能のバランスがマッチしない場合も |
| 2 | Cloud-Based | [Amazon API-Gateway](https://aws.amazon.com/jp/api-gateway/) | ・コアな変更が厳しそう<br>・Cloud環境にロックイン | 
| 3 | k8s-Based-OSS | [Gloo Edge](https://www.solo.io/products/gloo-edge/) <br> [Ambassador](https://www.getambassador.io/) | ・k8sに強く依存 |
| 4 | OSS-Framework | [Kong](https://konghq.com/kong)<br>[TYK](https://tyk.io/) | ・Plugin の枠を越えた改修が厳しい<br>・有償版の機能には Cloudにより実現可能な部分も |
| 5 | OSS-Library | [Lura (KrakenD)](https://www.krakend.io/) | ・変更に制限が少なく機能追加が容易 <br> ・Framework化された [KrakenD-CE](https://github.com/devopsfaith/krakend-ce) も存在 |

##### 最終的な選定結果

この中で、我々が選択したのが KrakenD-CE となります。選択の理由は次のとおりです。

- Golang製で 高速・並列処理に期待できる
- Luraのライブラリ利用すら可能で変更の自由度が極度に高い (See: [Library-Usage](https://github.com/luraproject/lura#library-usage))
- Krakend-CEでのエンジンには[Gin Framework](https://gin-gonic.com/)を採用（高い稼働実績と分析・機能追加の容易さ）
- Cloud基盤やコンテナ基盤への制約が皆無
- Routing, Traffic制御といった基本機能が充足している 
    - [Timeout](https://www.krakend.io/docs/throttling/timeouts/#http-request-timeouts)
    - [Rate-Limit](https://www.krakend.io/docs/endpoints/rate-limit/)
    - [CircuitBreaker](https://www.krakend.io/docs/backends/circuit-breaker/)
    - (Firewall / WAFは Cloud基盤の責務と設計)
- APIの各種設定をファイル管理する形式なので、設定のGit管理が容易 
    - (別のデザインを採用した Kongは設定をマスターDB上で管理しAPIで更新・非同期で取得する方式 -> 設定の可視化より一元管理を優先)

> 結果的にこの決定には何度か救われた。具体的には次のような改修が必要となった
> - 不要な機能・Cloudで代替できる機能の無効化
> - HTTP-Clientの設定変更による性能向上
> - Backendに対するカナリヤリリースを実現するためのLB機能の拡張

###### [採用]

https://github.com/devopsfaith/krakend-ce

###### [コア部分で依存]

https://github.com/luraproject/lura

https://github.com/gin-gonic/gin

なお、krakenD-CE上での機能追加は基本的にフィルタ構造で行うことになります。フィルタの定義箇所は主に3種類です。基本的にはこれらのフィルタを追加・削除しながら、必要に応じて サーバー機能を提供するエンジン周りやバックエンドコールを行うクライアント周りに手を入れる形となります。

- `Handler`：エンジンのデフォルト実装となるgin-gonic上のフィルタ
- `Proxy`：krakenD上で、入力Pathを司る`Endpoint`毎のフィルタ
- `Backend`：krakenD上で、出力先サーバーを司る`Backend`ごとのフィルタ

既存のフィルタ定義は [krakend-ceプロジェクト](https://github.com/devopsfaith/krakend-ce)のルートに存在する各ファクトリの実装で確認できます。ここを追うと基本的な機能スタックが理解できます。

- [handler_factory.go](https://github.com/devopsfaith/krakend-ce/blob/master/handler_factory.go)
- [proxy_factory.go](https://github.com/devopsfaith/krakend-ce/blob/master/proxy_factory.go)
- [backend_factory.go](https://github.com/devopsfaith/krakend-ce/blob/master/backend_factory.go)

#### Gateway向けの言語選定

OSSの選定と深く絡むのですが、開発言語の選定もかなり重要なポイントです。極論を言ってしまえばどの言語を利用してもそれなりの API-Gatewayを組むことは可能ですが、言語によっては以下の問題が発生します。

- **型付け**が動的で複雑なシステムの構築に向かない
- **速度**や**並列化**に問題があり性能が伸びづらい
- ハイスペックな開発者の心に響かず**人員調達**に苦労する

これらをクリアする候補としてはJVM系の Kotlin/Scalaや Go言語が挙がりました。それぞれが静的型付けを持ち、Thread, Coroutine, Actor/Akka, Goroutineといった機能で並列実行もサポートしています。大きな違いは表現力豊かなJVM系の高級言語と、表現はベタ臭いが起動が高速でCloud適性のある Go言語といった印象です。

今回は**複雑なロジックを持たない**Gatewayを **Cloud基盤を前提に構築**するという想定だったため、短所が目立たず長所が活きる Golangを採用しました。

[採用]

https://github.com/golang/go

#### Cloudの選定

ここからが面白い部分なのですが、話が長くなりそうなので、次回に回したいと思います。。次回も頑張って書くのでさらっと目を通して頂けるとありがたいです。

## まとめ

今回は API-Gatewayの構築に関する機能面での課題・解決に関してご紹介しました。機能面での課題解決を下の図に纏めてみました。ポイントを整理したいと思います。

- 独自機能をサポートしするため、OSSは改造しやすいものを選びました
- GWの一般機能は OSSの活用で上手いことやり過ごしました
- 利用言語はGoに適性がありそうと判断しました
- ログ集約は次回以降でご紹介する予定です

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/375665/86d1bcd7-f991-237e-3688-5169ed675ffc.png)
次回は非機能面での課題・解決に関してクラウドを活用してゆくお話となります。また見てくださいね！

### 宣伝
PF事業本部では 他にもいろいろな記事を発信しています。良かったら他の記事にも目を通して雰囲気を感じ取ってください。
https://qiita.com/organizations/dmmcompf

またPFでは随時新しいメンバーを募集しています。私達とプラットフォームやAPIを改善していきませんか。（やりたいことが沢山あるんです）