マイクロサービスのコントラクト、アドレス指定、API

リージョン ID

REGION_ID は、アプリの作成時に選択したリージョンに基づいて Google が割り当てる省略形のコードです。一部のリージョン ID は、一般的に使用されている国や州のコードと類似しているように見える場合がありますが、このコードは国または州に対応するものではありません。2020 年 2 月以降に作成されたアプリの場合、REGION_ID.r は App Engine の URL に含まれています。この日付より前に作成されたアプリの場合、URL のリージョン ID は省略可能です。

詳しくは、リージョン ID をご覧ください。

App Engine でマイクロサービスが互いを呼び出すには、一般に HTTP ベースの RESTful API を使用します。また、タスクキューを使用してバックグラウンドでマイクロサービスを呼び出すことも可能です。その場合、ここで説明する API の設計原則が適用されます。マイクロサービス ベースのアプリケーションの安定性、セキュリティ、パフォーマンスを確保するため、特定のパターンに従うことが重要です。

強力なコントラクトの使用

マイクロサービス ベースのアプリケーションの最も重要な側面の一つは、それぞれのマイクロサービスを完全に独立した形でデプロイできることです。この独立性を実現するには、各マイクロサービスで、バージョニングされ、明確に定義されたクライアント(他のマイクロサービス)とのコントラクトを提供する必要があります。 バージョニングされた特定のコントラクトに依存している他のマイクロサービスが存在しないことが明らかになるまでは、各マイクロサービスはこれらのバージョニングされたコントラクトに従わなければなりません。他のマイクロサービスが、以前のコントラクトを必要とするコード バージョンにロールバックしなければならなくなる場合に備え、貴社の非推奨ポリシーやサービス終了ポリシーにこの点を含めることが重要です。

堅牢でバージョニングされたコントラクトの管理は、安定したマイクロサービス ベースのアプリケーションを提供したいと考える組織にとって最も大きな課題でしょう。開発チームは、互換性を損なう変更と互換性を損なわない変更の違いを明確に理解し、どのような場合に新しいメジャー リリースが必須になるかを把握しておく必要があります。また、古いコントラクトをいつどのように終了できるかを理解することも必要です。開発チームは、マイクロサービス コントラクトの変更を周知させるために、利用非推奨やサービス終了の通知方法を含めた適切な伝達手段を採用する必要があります。このような取り組みは困難に思えるかもしれませんが、取り組みを開発文化に取り入れることで、長期的には開発の速度と品質に大幅な向上がもたらされます。

マイクロサービスのアドレス指定

サービスとコード バージョンは直接アドレス指定できます。そのため、既存のコード バージョンと並行して新しいコード バージョンをデプロイしたり、新しいコードをサービスのデフォルトの提供バージョンにする前にテストしたりすることも可能です。

App Engine の各プロジェクトにはデフォルト サービスがあり、各サービスにはデフォルト コード バージョンがあります。プロジェクトのデフォルト バージョンのデフォルト サービスに対処するには、次の URL を使用します。
https://PROJECT_ID.REGION_ID.r.appspot.com

user-service というサービスをデプロイする場合、そのサービスのデフォルトの提供バージョンにアクセスするには、次の URL を使用します。

https://user-service-dot-my-app.REGION_ID.r.appspot.com

banana というデフォルト以外の別のコード バージョンを user-service サービスにデプロイする場合、そのコード バージョンに直接アクセスするには、次の URL を使用します。

https://banana-dot-user-service-dot-my-app.REGION_ID.r.appspot.com

cherry というデフォルト以外の別のコード バージョンを default サービスにデプロイする場合、そのコード バージョンにアクセスするには、次の URL を使用します。

https://cherry-dot-my-app.REGION_ID.r.appspot.com

App Engine では、デフォルト サービスのコード バージョン名にはサービス名と同じ名前は使用できないというルールが適用されます。

特定のコード バージョンを直接アドレス指定するのは、スモークテストを行う場合と、A/B テスト、ロール フォワード、ロールバックを円滑化する場合に限定する必要があります。クライアントのコードでは、デフォルト サービスまたはその他の特定のサービスのデフォルトの提供バージョンのみをアドレス指定するようにします。


https://PROJECT_ID.REGION_ID.r.appspot.com

https://SERVICE_ID-dot-PROJECT_ID.REGION_ID.r.appspot.com

このようなアドレス指定の方法により、クライアントを変更せずに、バグ修正を含むマイクロサービスの新しいバージョンをデプロイできます。

API バージョンの使用

次に示すように、すべてのマイクロサービス API の URL には、メジャー API バージョンを含める必要があります。

/user-service/v1/

このメジャー API バージョンにより、マイクロサービスのどの API バージョンが呼び出されたかがログに明確に記録されます。さらに重要なのは、メジャー API バージョンでは URL が変わるため、古いメジャー API バージョンと並行して新しいメジャー API バージョンを提供できることです。

/user-service/v1/
/user-service/v2/

URL にマイナー API バージョンを含める必要はありません。マイナー API バージョンには定義上、互換性を損なう変更をデプロイしないためです。むしろマイナー API バージョンを URL に含めると、URL の数が大幅に増え、クライアントが新しいマイナー API バージョンに移行できるかが不確実になります。

この記事では、常にメインブランチが App Engine にデプロイされる、継続的インテグレーションおよびデリバリー環境を想定しています。この記事での「バージョン」には、2 つの異なるコンセプトがあります。

  • コード バージョン。App Engine サービスのバージョンに直接マッピングされ、メインブランチの特定の commit タグを表します。

  • API バージョン。API の URL に直接マッピングされ、リクエスト引数の形状、レスポンス ドキュメントの形状、API の動作を表します。

またこの記事では、コードのデプロイ 1 回で、1 つの API の古いバージョンと新しいバージョンの両方が共通のコード バージョンに実装されることを想定しています。たとえば、デプロイされているメインブランチで /user-service/v1//user-service/v2/ の両方が実装されるとします。この方法により、新しいマイナー バージョンやパッチ バージョンを導入する場合に、コードで実際に実装される API バージョンに関係なく、2 つのコード バージョンの間でトラフィックを分割できます。

/user-service/v1//user-service/v2/ を別々のコードブランチで開発することで、両方のバージョンがコードのデプロイ 1 回で同時に実装されないようにすることもできます。App Engine でもこのモデルを採用できますが、トラフィックを分割するにはメジャー API バージョンをサービス名自体に含める必要があります。 たとえば、クライアントが次の URL を使用するとします。

http://user-service-v1.my-app.REGION_ID.r.appspot.com/user-service/v1/
http://user-service-v2.my-app.REGION_IDappspot.com/user-service/v2/

user-service-v1user-service-v2 のようにメジャー API バージョンがサービス名自体に含まれています。(このモデルではパスの /v1//v2/ の部分が冗長なので削除することもできますが、ログ分析ではこのままのほうが役立つ場合があります)。通常、このモデルを使用する場合は、メジャー API バージョンの変更時に新しいサービスがデプロイされるようにデプロイ スクリプトを更新する必要があります。そのため、必要な作業がやや増えます。また、1 つの App Engine アプリケーションで許可されているサービスの最大数にも注意してください。

互換性を損なう変更と互換性を損なわない変更

互換性を損なう変更と互換性を損なわない変更の違いを理解することは重要です。一般に、互換性を損なう変更は「引き算」的で、リクエストまたはレスポンス ドキュメントの一部を取り除く結果になります。ドキュメントの形状やキー名を変更すると、互換性を損なう変更を招く可能性があります。必須の引数を新しく増やすと、その変更によって常に互換性が損なわれます。マイクロサービスの動作が変わった場合も、互換性を損なう変更が生じることがあります。

互換性を損なわない変更は、多くの場合、「足し算」的です。オプションの新しいリクエスト引数の追加や、レスポンス ドキュメントの新しいセクションの追加は、互換性を損なわない変更です。互換性を損なわない変更にするには、転送時のシリアル化フォーマットの選択が重要です。JSON、プロトコル バッファ、Thrift など、シリアル化フォーマットの多くは互換性を損なわない変更が容易で、シリアル化解除の際に予期しない余分な情報が自動的に無視されます。動的言語で、余分な情報はシリアル化解除されたオブジェクト内に表われるだけです。

サービス /user-service/v1/ 用の次の JSON 定義について考えてみます。

{
  "userId": "UID-123",
  "firstName": "Jake",
  "lastName": "Cole",
  "username": "jcole@example.com"
}

次のような互換性を損なう変更を行う場合、サービスのバージョンを /user-service/v2/ に変更する必要があります。

{
  "userId": "UID-123",
  "name": "Jake Cole",  # combined fields
  "email": "jcole@example.com"  # key change
}

ただし、次のような互換性を損なわない変更を行う場合、新しいバージョンは不要です。

{
  "userId": "UID-123",
  "firstName": "Jake",
  "lastName": "Cole",
  "username": "jcole@example.com",
  "company": "Acme Corp."  # new key
}

互換性を損なわない新しいマイナー API バージョンのデプロイ

App Engine では、新しいマイナー API バージョンをデプロイするときに、古いコード バージョンと並行して新しいコード バージョンをリリースできます。App Engine ではデプロイ済みの任意のバージョンを直接アドレス指定できますが、デフォルトの提供バージョンは 1 つのバージョンのみです。サービスごとにデフォルトの提供バージョンが存在することに注意してください。次の例では、apple という古いコード バージョン(これがデフォルトの提供バージョンです)と並行して、banana という新しいコード バージョンをデプロイします。デプロイするのは互換性を損なわないマイナーな API の変更であるため、マイクロサービスの URL はどちらのコード バージョンも同じ /user-service/v1/ です。

App Engine には、トラフィックを apple から banana へ自動的に移行するメカニズムがあります。この機能を利用するには、新しいコード バージョン banana をデフォルトの提供バージョンとして設定します。新しいデフォルトの提供バージョンを設定すると、新しいリクエストは apple にルーティングされず、すべて banana にルーティングされます。このようにして、クライアント マイクロサービスに影響を与えることなく、新しいマイナーまたはパッチ API バージョンを実装する新しいコード バージョンにロール フォワードできます。

エラー発生時にロールバックするには、上記のプロセスを逆に行います。この例では、デフォルトの提供バージョンを元の apple に戻します。新しいリクエストはすべて以前のように古いコード バージョンにルーティングされ、banana にはルーティングされなくなります。ただし、処理中のリクエストは完了するまで処理が続行されます。

App Engine には、特定の割合のトラフィックのみを新しいコード バージョンに割り振る機能もあります。このプロセスは一般にカナリア リリース プロセスと呼ばれ、App Engine ではこのメカニズムのことをトラフィック分割と呼びます。新しいコード バージョンには、トラフィックの 1%、10%、50%、または任意の割合を割り振ることができます。この割合は徐々に調整することもできます。たとえば、新しいコード バージョンを 15 分間導入し、徐々にトラフィックを増やしながら、ロールバックが必要となるような問題が発生しないか確認できます。このメカニズムを使用して、2 つのコード バージョンの A/B テストも実行できます。その場合、トラフィック分割を 50% に設定し、2 つのコード バージョンのパフォーマンスとエラー率の特性を比較して、想定どおり改善されていることを確認します。

次の図は、Google Cloud コンソールでのトラフィック分割の設定を示しています。

Google Cloud コンソールでのトラフィック分割の設定

互換性を損なう新しいメジャー API バージョンのデプロイ

互換性を損なうメジャー API バージョンをデプロイする場合のロール フォワードとロールバックのプロセスは、互換性を損なわないマイナー API バージョンの場合と同じです。ただし、通常はトラフィック分割や A/B テストは行いません。互換性を損なう API バージョンは /user-service/v2/ のように新しくリリースされる URL であるためです。もちろん、古いメジャー API バージョンの基盤となる実装を変更した場合は、トラフィック分割を使って、古いメジャー API バージョンが正常に機能し続けるかをテストすることをおすすめします。

重要な点として、新しいメジャー API バージョンをデプロイする場合、古いメジャー API バージョンもまだ提供されている可能性を考慮する必要があります。たとえば /user-service/v1/ のリリース時に、まだ /user-service/v2/ が提供されていることがあります。これは、独立したコードリリースを行う際に念頭におくべき点です。古いコード バージョンへのロールバックが必要になる可能性があるマイクロサービスを含め、他のすべてのマイクロサービスが古いメジャー API バージョンを必要としていないことを確認した後でのみ、そのメジャー API バージョンの提供を終了できます。

具体的な例として、user-service という別のマイクロサービスに依存する web-app というマイクロサービスがあるとします。user-service では、firstNamelastNamename という 1 つの項目にまとめるなど、基盤となる実装を一部変更する必要があり、この変更によって現在 web-app が使用している古いメジャー API バージョンをサポートできなくなるとします。したがって、user-service は古いメジャー API バージョンの提供を終了する必要があります。

この変更を実現するには、3 つの異なるデプロイを行う必要があります。

  • まず、user-service は、/user-service/v1/ をサポートしたまま /user-service/v2/ をデプロイする必要があります。このデプロイでは、下位互換性をサポートするために一時的なコードの書き込みが必要になる場合があります。マイクロサービス ベースのアプリケーションでは、このようなケースはよくあります。

  • 次に web-app は、依存関係を /user-service/v1/ から /user-service/v2/ に変更するように更新されたコードをデプロイする必要があります。

  • 最後に user-service の担当チームは、web-app/user-service/v1/ が必要なくなったこと、および web-app をロールバックする必要がないことを確認した後、古くなった /user-service/v1/ エンドポイントとそのサポートのための一時的なコードを削除するコードをデプロイできます。

このような作業は煩わしく思えるかもしれませんが、マイクロサービス ベースのアプリケーションでは不可欠なプロセスであり、独立した開発リリース サイクルを実現できるのはこのプロセスのおかげです。重要な点として、このプロセスは相互依存性が高いように見えますが、上記の各ステップは個別のタイムラインで実行することが可能で、ロール フォワードとロールバックは 1 つのマイクロサービスのスコープ内で発生します。決まっているのはステップの順序のみで、各ステップは数時間、数日、あるいは数週間かけて実行できます。

次のステップ