🌮

外部API連携のスタンスや技術面の考慮事項をまとめてみた

2024/07/09に公開

はじめに

株式会社tacomsでCTOやってる井上です!

https://tacoms-inc.com/

tacomsは複数デリバリーサービスを1台のタブレットにまとめたり、様々なPOSレジ会社と連携してオペレーションをシームレスにする「Camel」というプロダクトを提供している会社でして、プロダクトの属性上めちゃくちゃ連携している会社数が多いのが特徴です。(様々な属性の連携パートナーを合わせるとなんとざっくり30以上!!!)

また、tacomsではPublicAPIも作っているため、連携先のAPIに合わせて開発するだけでなく、連携先に使ってもらう汎用APIの開発にも取り組んでいます。

外部APIを使う側、作る側の双方を開発してきた経験から、連携開発に取り組む上でのスタンスや考慮事項をまとめてみました。少しでも皆さんのお役に立てると幸いです!

外部API連携の定義

おおよそ一般的な認識だと思いますが、外部APIを 「連携先の会社が開発したAPI」 と定義します。
外部APIには大きく分類すると、webhookのように送信してもらうパターンと、自分が送信するパターンの2つがあると思いますが、本記事では特に分類せずどちらも外部API連携と呼称します。

連携開発する上でのスタンス🤝

連携先のAPI仕様書は一緒に作り上げるもの

外部APIは連携先の内部実装が分からないこともあり、API仕様書が唯一の指針となります。
しかし、連携用のAPI仕様書を100%正確に作るのは難しいです。型自動生成などで実装していけばスキーマレベルでは正確なものになりますが、パラメータの説明やパラメータ同士の依存関係などは、どうしても人が書いていくことになるためミスがない状態にするのは難しいでしょう。
上記背景から、双方が歩み寄りながらAPI仕様書作り上げるぞ! ぐらいのスタンスが理想だなと思っています。
API仕様書が間違ってますという指摘を繰り返すだけでなく、実際に検証環境のAPIを使ってAPI仕様書との差分を確認した上で、ドキュメントの修正提案など双方がFBを繰り返しながら、仕様書を作り上げていけると良いなと感じてます。

連携パートナーへのリスペクトを忘れずに

連携先の会社は、開発開始のタイミングから実際に店舗様で運用いただくまで、長い付き合いとなるパートナーであり双方が円滑にコミニュケーションを取れる関係性であることは重要だと考えています。
運用が始まると双方でエラー調査した上で、エラー原因の切り分けをしたりと、ピリピリする場面も出てくるのは事実ですが、、、、どんな状況においても円滑にコミニュケーション取れる関係性であることが大事かなと思います。

システムとしての正しさを過剰に求めない

RFCに準拠してない、PATCHとPOSTの使い方が異なる、無理やりなシステム設計でPublicAPIを利用しようとしてるケースなど、大小様々な粒度で気になることがあったりします。。。😭😭
ただ、連携開発においては、あくまで連携をした先にお客さんの価値提供を最大化することがゴールなので、そのゴールに影響しない細かい指摘はなるべく避けるようにしています。ただ、明らかに将来の負債になってしまう場合や開発メンバーの認知負荷が上がってしまう場合などは、継続的な価値提供を毀損するリスクがあるためその限りではありません。

連携開発における技術的な考慮事項🧐

連携開発を進める上で、技術的に気をつけないといけないポイントは言語関係なくあると思います。実際に踏んだミスも踏まえていくつか考慮事項を書いてみました。

STG / PRDの環境差分

STGとは何だったのか!? そうツッコミたくなる気持ちはあれど、連携先企業の環境差分は起こり得る前提で考えるのが良いと思っています。
STGでOKだからPRDもOKというスタンスではなく、本番では必ず段階的にリリースしましょうという話は社内で大事にしてます。(特に配達員など実世界に絡む機能のAPIは挙動が変わりがちです🥹🥹)

認証・認可と秘匿情報管理

事業者ごと認証・認可なのか、アカウントごと認可なのか

どの単位で認証・認可を行うのかによって、トークンの保持方法が変わってきます。
事業者単位である場合、tacomsの視点から見ると保持するトークンは1つのみとなるので、AWSのSSM Parameter StoreやSecretManagerなどの選択肢が出てくるでしょう。ただ、アカウント単位となってしまった場合は、店舗の増加に伴い保持するトークンが増えてしまうため、SSMなどのサービスでは管理が煩雑となってしまうので、DBでの管理が選択肢に上がるでしょう。

DBでトークンを保持する場合どのように秘匿情報を管理するか

パスワードを不可逆暗号してDBに登録するのは当たり前になっているように、秘匿情報をDBに持つのは極力控えるというのは一般的な話だと思います。ただやむ終えずDBでトークンを保持しないといけないとなった場合は、以下のような対策が考えられます。

  • AWS KMSを活用し秘密鍵の管理を不要化。SDK経由でEncrypt/Decryptすることで安全に可逆暗号化(詳しい実装の話はこの記事では触れません。。! 別で書くかもですmm)
  • AES暗号で可逆暗号化する実装を行い、秘密鍵をSSM Parameter StoreやSecretManagerで管理

よりセキュアなのはKMSを利用する方針ですが、実装が複雑になるというトレードオフがあるので、求められるセキュリティ要件に応じて使い分けるのが良いかなと思っています。

https://docs.aws.amazon.com/ja_jp/kms/latest/developerguide/concepts.html
https://qiita.com/kj1/items/16b2b341a2991185054f

リクエストタイムアウトの設定

タイムアウトを何秒で設定して良いか連携先とすり合わせる

外部APIをリクエストする時に欠かせない設定がリクエストタイムアウトです。
もし、リクエストタイムアウトを設定しなかった場合、連携先の障害が原因で30s〜60sレスポンスを待機するプロセスが大量発生して、自社サーバーのCPU・メモリを枯渇させることに繋がるため、一定のタイミングでリクエストは切るべきだと考えています。

外部APIをリクエストする時、皆さんは何秒でタイムアウトを設定しているでしょうか?
一般的にAPIのレイテンシーにおいて、2sとか3sを超えることはあまり想定されないことだと思います。自分も10sぐらいで設定しておけば問題ないんじゃないかと思っていましたが、連携先の負荷が増大したタイミングで一瞬10sを超えてしまうレイテンシーが発生することがあり、タイムアウト設定が問題になったことがありました。

連携先にレスポンスタイムのSLOがある場合は、その指標に従って実装すること。もしSLOがない場合はタイムアウトを何秒で設定して良いか、事前に連携先とすり合わせておくことが大事です

外部APIとDBのデータ整合性担保

「外部API経由でリソースを更新し、自社のDBでもデータを更新する」 というケースにおいて、整合性を担保するのは重要な論点です。
どのように整合性を担保するかについては以下の2パターンが考えられると思います。

パターン1) DBのトランザクションを活用し、トランザクションスコープ内でAPIリクエスト。APIリクエスト成功後にDBも更新しトランザクションをコミット
パターン2) 先にAPIリクエスト、その後にDB更新とした上でDB更新が失敗した場合、APIリクエストで前の実行結果に戻す

Atomicに処理することを考えた場合パターン1にするのが一番確実でしょう。しかし、外部APIに障害が起きてレスポンスタイムが遅延した場合、DBのトランザクションが長くなりロックの時間が長くなってしまうというデメリットがあります。そのため、このパターン1を取る場合は外部APIのリクエストタイムアウトであったり、リトライ回数などをなるべく小さくコントールする必要があるでしょう。外部APIに引っ張られて、DBのロック時間が長くなりデッドロックなどを誘発してしまう事態は避けなければいけません。

パターン2については、パターン1のようにDBのトランザクションを張らないためロックの時間が長くなることはありません。ただ、処理が失敗した後のAPIリクエストが失敗したらどうする?という問題があるため、Atomicな処理が担保されてるとは言い切れないでしょう。

パターン1、パターン2双方にトレードオフがあるため、外部APIのレスポンスタイムや求められる整合性レベルに応じて使い分ける必要がありそうです🧐

リトライと冪等性

サーバーでリトライするかクライアントからリトライしてもらうか

外部APIに依存した機能をクライアントが直接操作するというケースにおいて、非同期な要件を除くと、期待されているレスポンスタイムは1s以内であることがほとんどだと思います。
そのようなレスポンスタイムの制限がある中で、サーバーが何度もリトライしてしまうとレスポンスタイムが遅延しユーザー側の体験を損ねる可能性があります。そのため、同期的なレスポンスが求められるケースにおいてはサーバー側のリトライは最小限の回数にして、エラーになったらクライアント側に返した上で、クライアントの操作でリトライしてもらうのが良いと考えています。

ただし、決済などの冪等性を担保しなければいけないケースにおいては、サーバー側でリトライし続ける構成の方が実装しやすいこともあるので、クライアントからリトライするケースは冪等性が担保されてる前提と考えるのが良いと思います。

リトライ手法

サーバー側でリトライするケースにおいては、エクスポネンシャルバックオフで実装することが望ましいです。
指数関数的にリトライ間隔を伸ばしていくことで、リクエスト先の負荷を最小限にしつつ、リトライすることが出来ます。
相手先のシステムにクリティカルな問題が生じてる場合はリトライしても復活しないケースが多いですが、ネットワーク帯域の問題やサーバーの一時的なCPU上昇などにおいては、間隔を伸ばしながらリトライすることによって成功確率を高めることが出来ます。

https://codezine.jp/article/detail/10739

リトライ時の冪等性

APIの冪等性とは同じリソースに対して、同じ要求を何度繰り返しても、同じ結果になるということです。
一般的に、POSTリクエストは新規のリソースを登録するという性質上、冪等性が担保されてないと言えるでしょう。サーバー側で同じリクエストだと認識してもらえなければ、二重で課金されてしまうリスクを孕んでいます。

冪等性が担保されてないのであれば、クライアント側でエラーを判別してサーバーにリクエストすれば良いとも考えれるのですが、どのタイミングでエラーになったのかをクライアントは知り得ないため不可能です。
例えば、リクエストタイムアウトが発生した時、サーバー側に届く前なのか、サーバーの処理途中なのか、サーバーが処理した後なのか等、どのタイミングでタイムアウトしたのかをクライアントは判別できません。

上記の解決策としてidempotency-keyをリクエストヘッダーに含めようというものがIETFのドラフトで議論されています。
考え方としてはシンプルでリクエストごとにユニークなIDをクライアントから送付し、サーバー側で確認することでリトライされたリクエストが同一のリクエストであることを担保するというものです。

https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header-04

API連携する際は、連携先がどのレベルで冪等性を担保してるのか確認した上で、適切なリトライ実装をする必要があります。

リクエスト制限の考慮

連携先のAPIにはリクエスト制限がかかっているケースが多いと思います。IPアドレスベース、事業者ベース、アカウントベース(飲食店向けサービスなら店舗)など、様々な粒度で制限がかかっていることがあるでしょう。
また1時間あたりの制限であることもあれば、1分あたりの制限であることもあります。

規模が小さいうちはリクエスト制限が論点になることも少ないので、リクエスト制限の考慮に対して実装側で気を配る必要はないかもしれませんが、ある程度の規模になってくるとリクエストを制限しながら連携したい時が来るかもしれません。
この辺りはtacomsでもまだやれてないところですが、将来的には以下の図のようなフローになるのかも??というぼんやりなイメージを持っています🧐

おわりに

外部API連携が多いアプリケーションは不確実性が大きく、考慮事項も多くなる傾向があります。この記事では言及しませんでしたが、複数類似サービスと連携する時のデータモデリングや、アーキテクチャ設計なども気をつけないといけないポイントがいくつかあると考えています。

日頃、皆さんが外部APIと連携する上での参考にしていただけると嬉しいです!!!

記事の話でも、会社の話でも気になる部分あれば、雑談でも大丈夫なのでカジュアル面談お待ちしてます👏(笑)

https://tacoms-inc.com/#block-cddb337d21b847408a45f9ee69b14077

tacomsテックブログ

Discussion