Software Design 2024年7月号 連載「レガシーシステム攻略のプロセス」第3回 API Gatewayとサービスメッシュによるリクエスト制御

Software Design 2024年7月号 連載「レガシーシステム攻略のプロセス」第3回 API Gatewayとサービスメッシュによるリクエスト制御

はじめに

技術評論社様より発刊されているSoftware Designの2024年5月号より「レガシーシステム攻略のプロセス」と題した全8回の連載が始まりました。

ZOZOTOWNリプレイスでは、段階的にシステムを置き換えるというアプローチによってマイクロサービス化を進めています。第3回は、マイクロサービス化の要となったAPI Gatewayの自社開発の話を中心に紹介します。

目次

はじめに

ZOZOTOWNは2004年から運営を開始し、オンプレミス環境で動くモノリシックなシステムとして10年以上の間アーキテクチャを変えずに拡大してきました。ビジネスが順調に成長する一方で、古いシステムはスケーラビリティの限界、モノリシックなシステムの保守コストの増大、使用技術の陳腐化など多くの課題に直面していました。それらの課題を抜本的に解決すべく、ZOZOでは2017年からZOZOTOWNのマイクロサービス化を進めています。

この記事は、シリーズ連載の第3回として、ZOZOTOWNにおける段階的なマイクロサービス化に向けたアプローチと、その戦略の要となるZOZO API Gatewayを自社開発した話をします。一般的なマイクロサービス化に関する説明は割愛します。

前提として、ZOZOTOWNの旧システムはオンプレミス環境、移行先の新システムはクラウド環境にあります。また、本記事において「ZOZO API Gateway」という表現は、ZOZOで自社開発しているシステムを指します。Amazon Web Servicesなど他社のプロダクトにAPI Gatewayという名称が含まれるプロダクトについては、異なるシステムもしくはサービスであることがわかるよう、正式名称で記載します。

API Gateway自社開発の動機

マイクロサービス化に向けたアプローチ

ZOZOTOWNのマイクロサービス化のためのアプローチとして、ZOZOではストラングラーフィグパターンを採用することを決めました。ストラングラーフィグパターンとは、リプレイス対象のシステムの機能を段階的に新しいマイクロサービスに置き換えていき、すべての機能を置き換え最終的に移行元のシステムを停止する戦略です。

ストラングラーフィグパターンには、フロントエンドにインターフェースを提供し、バックエンドの状態を隠蔽するファサードが必要です。ファサードが各マイクロサービスへの動的なルーティング機能を持ち、リクエストの送信先を徐々に新システムに切り替えていくことで、段階的なシステム移行を実現します。ZOZOTOWNの規模を考えると、マイクロサービスに移行して完全に旧システムを停止できるまでに、数年もしくはそれ以上の時間が必要と考えられました。その間に断続的に行われる置き換え作業をシームレスに実現するためのファサードとして、ZOZO API Gatewayを開発することを決定しました。リプレイス初期のシステム構成を簡略化すると、図1のようになります。

図1 ストラングラーフィグパターンによる旧システムから新システムへの処理の切り替え

また、ZOZO API Gatewayはファサードであると同時に、API Gatewayパターンで定義されるAPI Gatewayの役割の一部も担います。フロントエンドに単一エントリポイントを提供することに加え、バックエンドに対してサービス横断的な機能も提供します。API GatewayパターンではAPI GatewayはBFF(Backends for Frontends)として振る舞う前提で語られることがありますが、ZOZOではそれらを別のシステムとして開発しています。BFFは複数のマイクロサービスからのレスポンスの集約や、ビジネスロジックに関するサービス横断的な機能の実現を担当しており、連載第6回で紹介予定です。

リプレイスが完了した時点でのシステム構成は図2のようになることを想定しています。

図2 旧システムからの切り替え後の理想的なシステム構成

API Gatewayの自社開発を決めた理由

ZOZOでは、API Gatewayをスクラッチで開発することを選択しました。自社開発という意思決定に至った決定的なメリットとして、開発の柔軟性と、動作の軽量さを重視した技術選定ができたことが挙げられます。

開発の柔軟性

マイクロサービス化を決定した当初、巨大化していたZOZOTOWNの機能や仕様をすべて洗い出すことは困難でした。そのため、リプレイスの具体的な進め方やサービス横断的に必要となる機能の要件が固まりきっていない状態でリプレイスプロジェクトをスタートせざるを得ませんでした。API Gatewayの仕様追加・変更に手間がかかる状態だと、バックエンドの開発を進められない状況を作り出し、API Gatewayがプロジェクト全体のボトルネックになるリスクがありました。

そこで、開発をすばやく柔軟に進められる状態にしておくために、自社開発が妥当と判断しました。既存のOSSやAPI Gatewayの機能を提供するクラウドサービスの使用も検討したものの、機能拡張のハードルの高さから開発スピードの低下が懸念されました。実際にプロジェクト開始後には、技術スタックやアーキテクチャがまったく異なる新旧両システムを連携させる役割をZOZO API Gatewayが担うことになり、会社特有の事情を考慮した開発も発生しました。結果として自社開発という選択は、開発コストとシステム保守の負担の軽減につながったと感じています。

動作の軽量さを重視した技術選定

ZOZO API Gatewayは、リプレイス完了時にはZOZOTOWNのほぼすべてのリクエストを通すことになります。そのため、レイテンシが低いこと、また障害発生時にも早急に復旧できる必要がありました。したがって、OSSやマネージドなクラウドサービスを使用して充実した多くの機能を付与できることよりも、軽量かつ起動が容易で、チューニングしやすいアプリケーションを作るメリットのほうが大きいと判断しました。ZOZOではサーバサイドのアプリケーション開発にJavaもしくはGoの採用を推奨しており、とくにGoは上記の条件を満たしていました。

ZOZO API Gatewayが担う機能

ZOZO API Gatewayの主な機能は次のとおりです。

  • ルーティング
  • ユーザー認証
  • クライアント認証
  • トレースIDの発行
  • リトライ
  • リクエストタイムアウト
  • 加重ルーティング
  • レートリミット

稼働を開始した当初は上記の機能をすべて提供していましたが、Istioを導入するにあたって責務の分担をし、現在はリクエストタイムアウト、リトライ、加重ルーティング、レートリミットの機能をIstioが担っています。この節ではZOZO API Gatewayの機能を説明し、Istioについての詳細は次節で説明します。

ルーティング

ルーティングは、受け取ったリクエストを検証し、設定されたマイクロサービスもしくはオンプレミスシステムに転送する機能です。マイクロサービスの増加に伴いルーティングの設定も増えるため、ZOZO API Gatewayのアプリケーションとは切り離し、YAMLファイルで宣言的に設定をしています。

ルーティングを設定するためのYAMLファイルはroutes.yaml(リスト1)とtarget_groups.yaml(リスト2)の2つがあります。この例では、リクエストのパスが正規表現で^/sample/(.+)$に一致した場合、targetAに転送します。また転送時には、$1がキャプチャグループのマッチした部分に置き換えられます。この設定を必要に応じてYAMLに記述し、ルーティングを実現しています。

 - from:
     path: ^/sample/(.+)$
   to:
     destinations:
       - target_group: targetA
         path: /$1

リスト1 routes.yaml

 targetA:
   targets:
     - host: sample.example.com
       port: 8080”

リスト2 target_groups.yaml

ユーザー認証

ユーザー認証は、トークンを用いてリクエスト送信者が誰であるかを確認する機能です。ただしログイン時に認証するのはマイクロサービスの1つであるID基盤サービスで、このサービスでログイン時にトークンを発行します。ZOZO API GatewayではID基盤APIを用いてリクエストに付与されたトークンを検証し、ユーザーの認証情報が正しいことを確認します。

クライアント認証

ZOZOTOWNのAPIは一般公開していません。そのためリクエストできるクライアントを制限しています。リクエストする際はトークンをヘッダに付与してもらうことで、ZOZOTOWNのネイティブアプリや社内サービスなどのクライアントを識別し、クライアントを認証します。クライアント認証後は、クライアントを識別できるヘッダを伝搬し、バックエンドの処理で認可に利用しています。

リクエストを許可するクライアントは、routes.yamlのclientsで設定できます。リスト3の設定では、targetAにリクエストできるクライアントはserviceAだけに制限できます。トークンの詳しい設定方法については、セキュリティ上の理由から割愛します。

 - from:
     path: ^/sample/(.+)$
     clients:
       - serviceA
   to:
     destinations:
       - target_group: targetA
         path: /$1”

リスト3 clientsを指定したroutes.yaml

トレースIDの発行

ストラングラーフィグパターンによりリクエストが新旧システムに振り分けられ、さらに複数のマイクロサービスを通る場合もあるため、リクエストの追跡が困難になりました。そこでZOZO API Gatewayで独自のトレースIDを発行し、転送先へ伝搬しました。新旧システムで受け取ったトレースIDをログに出力することで、サービスを横断したリクエストを追跡できます。

Istio導入以前に必要だった機能

リトライ、リクエストタイムアウト、加重ルーティングの機能は現在Istioに置き換えられており、ZOZO API Gatewayではすでに使われていないため、簡潔に紹介します。いずれもtarget_groups.yamlで設定しました。

リトライ

リトライは、リクエストが何かしらのエラーで失敗した場合に再試行する機能です。次のパラメータを用いて、転送先ごとに設定できます。

  • max_try_count:リクエストを試行する最大回数
  • retry_cases:リトライする条件(サーバエラーやリクエストタイムアウト時など)
  • retry_non_idempotent:冪等でないHTTPメソッド(POST、PATCH)に対するリトライ設定
  • retry_to:リトライ先の指定

リクエストタイムアウト

リクエストに対するタイムアウトの設定も、リトライと同じく、転送先ごとに設定できます。

  • connect_timeout:1リクエストあたりのTCPコネクション確立までの間のリクエストタイムアウト値(ミリ秒単位)
  • read_timeout:1リクエストあたりのリクエスト開始からレスポンスボディを読み込み終わるまでの間のリクエストタイムアウト値(ミリ秒単位)
  • idle_conn_timeout:データが送受信されなかった場合にコネクションを維持する時間(ミリ秒単位)
  • max_idle_conns_per_host:1ホストあたりに保持するアイドル状態のコネクションの最大数

加重ルーティング

新旧システムへのリクエスト比率の重みを設定できます。この比率を調整することで、旧システムから新システムへの移行の際にカナリアリリースを実現しました。リスト4の設定では、old-systemへ80%、new-systemへ20%の割合でリクエストされます。

targetA:
   targets:
     - host: old-system
       port: 8080
       weight: 4
     - host: new-system
       port: 8081
       weight: 1”

リスト4 加重ルーティングを適用したtarget_groups.yaml

ZOZO API Gatewayの運用

Istioの導入

連載第2回(本誌2024年6月号)でお伝えしたとおり、ZOZOTOWNのマイクロサービスが稼働するKubernetesクラスタ「プラットフォーム基盤」では、オープンソースのサービスメッシュであるIstioを導入しています。導入の背景は、新たなトラフィック制御の要件です。リプレイスが進み、新しくマイクロサービスが他サービス(クラスタ外のサービスを含む)を呼び出す通信要件が発生しました。当時マイクロサービス起点のZOZO API Gatewayを経由しない通信では、一貫したトラフィック制御機能がありませんでした。そのため、各マイクロサービスは必要に応じて独自に機能を追加する非効率的な状況に陥っていました。そこで、プラットフォーム基盤で一貫したトラフィック制御機能の提供が課題となりました。この課題に対し、ZOZOでは3つの案を検討しました。

  • 案1:マイクロサービス起点の通信でもZOZO API Gatewayを介し、ZOZO API Gatewayのトラフィック制御機能を使う
  • 案2:トラフィック制御機能を提供する共通ライブラリを作成し各マイクロサービスに組み込む
  • 案3:サービスメッシュを導入し、マイクロサービスを変更することなくサイドカーパターンでプロキシを注入して透過的にトラフィック制御機能を追加する

案1はZOZO API Gatewayへの負荷が大きく、スケールによるコスト増加が現実的ではありませんでした。また、案2はライブラリの作成やそのメンテナンス、ライブラリをマイクロサービスに組み込み逐次更新するといった手間が発生します。さらに、その作業の担当をどうするか、といった課題も挙がりました。最終的に、案3のサービスメッシュが最も現実的であるという結論に至りました。ZOZOでは次のような理由からIstioを選定し、2020年後半から検証を進め、導入を決めました。

  • サービスメッシュを実現するOSSの中で利用実績が多い
  • 分散トレーシングに利用しているDatadogが、Istioとのインテグレーションをサポートしている

Istioの導入により、マイクロサービスのPodにはistio-proxyというサイドカーコンテナが注入されます。マイクロサービス起点の通信はすべてistio-proxyをプロキシとして経由することになり、ZOZO API Gatewayを経由せずにIstioの設定による一貫したトラフィック制御ができるようになりました(図3)。

図3 Istioによるトラフィック制御

ZOZO API GatewayとIstioの責務の整理と機能分担

ZOZO API Gatewayはネットワークの入口でのみトラフィック制御機能を提供します。一方Istioは、メッシュネットワーク全体にトラフィック制御機能を提供します。IstioではVirtualServiceというカスタムリソースを用いて、各マイクロサービスでトラフィック制御をリスト5のように定義します。

apiVersion: networking.istio.io/v1beta1
 kind: VirtualService
 metadata:
     name: virtualservice
 spec:
     hosts:
     - microservice
     http:
     - route:
           - destination:
               host: microservice.ns.svc.cluster.local
               subset: subset
       retries:
           attempts: 1
           perTryTimeout”
           perTryTimeout: 3s
           retryOn: 5xx
       timeout: 4s

リスト5 virtualservice.yaml

リスト5の場合、メッシュネットワーク内でマイクロサービスへHTTPリクエストを行ったとき、3秒でリクエストタイムアウトとなります。HTTPステータスコードで5xx(リクエストタイムアウト504を含む)が返却された場合、1回リトライします。リトライを含むリクエスト全体で4秒経過するとクライアントにリクエストタイムアウトが返ります。ZOZO API Gatewayをメッシュネットワークに追加する際、トラフィック制御を両方設定しているとトラフィック制御が二重で行われてしまう状態となります。その場合、意図しないトラフィック制御により正常なリクエストができずZOZOTOWNの画面が表示されないなどの事象を引き起こす可能性があります。この課題に対し、ZOZOでは次の方針で重複する機能の責務を整理しました。

  • 一貫した機能提供のため、メッシュネットワーク全体に関わる機能はIstioの責務とする
  • 入口のGatewayレイヤーでのみ必要となる機能はZOZO API Gatewayの責務とする

この責務に従い、各機能を表1のとおり分担することにしました。

機能 ZOZO API Gateway Istio
ルーティング
ユーザー認証
クライアント認証
トレースIDの発行
リトライ
リクエストタイムアウト
加重ルーティング
レートリミット

表1 責務整理後における機能の分担結果

ZOZO API Gatewayで不要となる機能はサービスメッシュへの追加と併せて削除していきました。結果、意図しないトラフィック制御などの問題が発生することなく、プラットフォーム基盤全体にIstioを導入できました。このように、Istioの導入でZOZO API Gatewayは責務を変え、Gatewayレイヤーで必要な機能のみ提供する形になりました。

監視

リプレイスプロジェクトではDatadog APMを使用し、分散トレーシングを実現しています。ZOZO API Gatewayでは、traceのspanタグにクライアント識別子を追加することでリクエスト元を判断できるようにしており、エラー発生時の可観測性を向上させています。ZOZO API GatewayはZOZOTOWNのリクエストのほぼすべてを処理する前提で開発していることから、このようなしくみによってすべてのサービスのエラーを検知し、分析できます。

監視の運用については特有の難しさもあります。どのサービスが発したエラーであれ、システム異常の可能性があることに変わりはないため、ZOZO API Gateway開発チームでは、検知したエラーにはほぼすべて対応します。チームはアラートを受けて、エラーが発生したサービスの通知が流れるSlackチャンネルを確認したり、担当チームにヒアリングを行ったり、事情を考慮してZOZO API Gateway側でのエラー通知の基準を調整したりなどの意思決定をします。現状、マイクロサービスが障害を起こす頻度は低く、監視で対応するケースは激しいスパイクアクセスを処理する機能を提供するサービスに起因した避けられないエラーの通知がほとんどです。しかし、安全な運用のために、ZOZO API Gatewayを開発するチームは関連するマイクロサービスの性質についてもある程度幅広く知っておく必要があります。

ZOZO API Gateway本番稼働開始から3年を経て

ZOZO API Gatewayは2020年4月に稼働を開始し、2024年時点で3年以上の間本番環境で動いています。本記事の締めくくりとして、この節ではリプレイスプロジェクトにおける具体的な成果を紹介します。以降の説明で使用されるデータは、すべて2024年4月上旬時点のものです。

ストラングラーフィグパターンの功績

ファサードが存在することにより、バックエンドの開発が完全にフロントエンドから切り離され、リプレイスの戦略に大きな柔軟性を生みました。フロントエンドはAPIのホストが新旧システムのどちらであるかを意識する必要はなく、ZOZO API Gatewayの設定を変更するだけで新システムでの処理に切り替えることができます。これは、たとえばオンプレミスDBからマイクロサービスDBへのデータ移行時に役立ちます。リプレイスプロジェクトでは、ZOZOTOWNの稼働を止めずにデータを移行するために、書き込みを行うAPIとデータ取得のAPIを異なるタイミングでリリースします。ファサードがなければリクエストの切り替えに都度フロントエンドの修正が必要ですが、ストラングラーフィグパターンではフロントエンドのチームがバックエンドの事情を考慮する必要はありません。データ移行戦略の詳細についてはテックブログ*1をご覧ください。

プロジェクト全体としても、マイクロサービス開発組織のスケールを実現できました。現在20以上のサービスの開発が同時に進んでおり、ZOZOTOWNを止めることなくリプレイスを進めています。エンジニアの採用も積極的に行い、リプレイスプロジェクトに携わるエンジニアの数は150人程度に増えました。

自社開発のコストメリット

ZOZOTOWNのアクセス量の多さから、Amazon API Gatewayのような従量課金の料金体系のサービスを使用するよりも、自社でサービスを開発したほうが安くなるのではないかというもくろみが開発当初からありました。実際にコストメリットがあるのか、試算してみます。ここでは、Amazon API Gatewayサービスと、ZOZO API Gatewayが稼働しているAmazon EC2(以下EC2)の料金を用います。

執筆時点のAmazon API Gatewayの料金体系では、最初の3億リクエストは100万件当たり1.29USD、それを超えた分は100万件当たり1.18USDかかります。一方、ZOZO API GatewayはAmazon Elastic Kubernetes Service上で稼働しているため、そのEC2ノードにかかるコストが主な利用料金となります。リプレイス完了後にZOZO API Gatewayがどの程度のアクセスを処理することになるかは現時点では明言できませんが、たとえば月間のリクエスト数を10億とした場合、それぞれの概算コストは表2のようになります。リクエスト数はアプリケーションの設計にもよりますが、試算のとおり、月間10億リクエスト程度であれば、ZOZO API GatewayはAmazon API Gatewayと比べて60%程度のコストでアクセスをさばくことができると言えます。

Amazon API Gateway ZOZO API Gateway
試算 3億✕$1.29/100万
+ (10億-3億)✕$1.18/100万
省略
(弊社実績とEC2の料金体系からの概算)
コスト(月額) $1,213 $734

表2 Amazon API GatewayとZOZO API Gatewayの概算コスト比較

実際にZOZO API Gatewayが処理するアクセス数はより多く、よりZOZO API Gatewayにコストメリットがある状態です。またZOZO API GatewayはEC2のリザーブドインスタンスを契約するなどの工夫により、試算した金額感よりもさらにコストを抑えることができています。そのため、当初のもくろみどおりZOZO API Gatewayの自社開発はマネージドサービスの従量課金と比較してコストメリットがあることがわかります。

一方で、Amazon API GatewayにはAPIの作成、保守運用などに必要な豊富な機能が備わっています。ZOZOTOWNでのユースケースでは自社開発が結果的にコスト抑制にもつながりましたが、初期開発コストを抑えてより一般的な機能を実現したい場合や、アプリケーションの要件によっては、Amazon API Gatewayを採用するほうが妥当な可能性があります。

まとめ

ZOZOTOWNではストラングラーフィグパターンによるマイクロサービス化を進めており、その戦略の要となるファサードとしてZOZO API Gatewayを開発しました。ZOZO API Gatewayは一般的なルーティングなどの役割を担い、Istioの導入によってリクエストタイムアウトやリトライなどの一部機能を分担し、開発規模は比較的小さく保っています。プロジェクトの進行に伴い、一部新旧システムの仕様の差を吸収する役割を担うことになったものの、スクラッチから開発したことにより柔軟に開発を進めることができました。ZOZOTOWNのケースでは、API Gatewayの自社開発はリプレイスプロジェクトのスムーズな進行に大きく寄与し、コスト低減にも役立っています。


本記事は、技術本部 ECプラットフォーム部 会員基盤ブロックの富永 良子、吉江 守弘と、技術本部 SRE部 プラットフォームSREブロック ブロック長の亀井 宏幸によって執筆されました。

本記事の初出は、Software Design 2024年7月号 連載「レガシーシステム攻略のプロセス」の第3回「API Gatewayとサービスメッシュによるリクエスト制御」です。


ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。

corp.zozo.com

カテゴリー