マイクロサービス アーキテクチャでは、サービス間のすべてのデータ交換がメッセージまたは API 呼び出しを通じて行われるため、適切な API 設計が必要です。 効率的な API は、 頻繁な入出力 (I/O) を防ぐのに役立ちます。 独立したチームはサービスを設計するため、サービスを更新するときに他のサービスが中断されないように、API セマンティクスとバージョン管理スキームを明確に定義する必要があります。
次の 2 種類の API を区別する必要があります。
- クライアント アプリケーションが呼び出すパブリック API
- サービス間通信用のバックエンド API
これら 2 つの種類の要件は異なります。 パブリック API は、ブラウザー アプリケーションやネイティブ モバイル アプリケーションなどのクライアント アプリケーションと互換性がある必要があります。 ほとんどのパブリック API では、HTTP 経由で REST が使用されます。 ただし、バックエンド API はネットワーク パフォーマンスを考慮する必要があります。 サービスの粒度によっては、サービス間通信によってネットワーク トラフィックが多すぎる可能性があります。 サービスはすぐに I/O バインドになる可能性があるため、シリアル化速度やペイロード サイズなどの考慮事項がより重要になります。 REST over HTTP の一般的な代替手段としては、gRPC リモート プロシージャ コール (gRPC)、Apache Avro、Apache Thrift などがあります。 これらのプロトコルは、バイナリ シリアル化をサポートし、HTTP と比較して効率を向上させます。
考慮事項
API を実装する方法を決定するときは、次の要因を考慮してください。
REST とリモート プロシージャ コール (RPC): REST スタイルのインターフェイスと RPC スタイルのインターフェイスの間のトレードオフを検討してください。
REST は、ドメイン モデルを直感的に表現する方法を提供するリソースをモデル化します。 これは、進化性を促進する HTTP 動詞に基づいて統一されたインターフェイスを定義します。 べき等性、副作用、応答コードの明確に定義されたセマンティクスが含まれています。 REST ではステートレス通信も適用され、スケーラビリティが向上します。
RPC では、操作またはコマンドに重点を置いています。 RPC インターフェースはローカルメソッド呼び出しに似ているため、API が過度に冗長になる可能性があります。 ただし、RPC では、おしゃべりな通信は必要ありません。 その結果を回避するには、インターフェイスを慎重に設計する必要があります。
RESTful インターフェイスの場合、ほとんどのチームは JSON 経由で REST over HTTP を選択します。 RPC スタイルのインターフェイスの場合、一般的なフレームワークには gRPC、Avro、Thrift が含まれます。
効率: 速度、メモリ、ペイロード サイズの観点から効率を考慮してください。 通常、gRPC ベースのインターフェイスは、HTTP 経由の REST よりも高速です。
インターフェイス定義言語 (IDL): IDL を使用して、API のメソッド、パラメーター、および戻り値を定義します。 IDL は、クライアント コード、シリアル化コード、および API ドキュメントを生成できます。 API テスト ツールでは、IDL が使用されます。 gRPC、Avro、Thrift などのフレームワークは、独自の IDL 仕様を定義します。 REST over HTTP には標準の IDL 形式はありませんが、一般的な選択肢は OpenAPI (以前の Swagger) です。 正式な定義言語を使用せずに HTTP REST API を作成することもできますが、コードの生成とテストの利点は失われます。
シリアル化: 通信経路上でオブジェクトをシリアル化する方法を選択します。 オプションには、JSON などのテキストベースの形式と、プロトコル バッファーなどのバイナリ形式が含まれます。 バイナリ形式は、テキストベースの形式よりも高速です。 ただし、JSON では、ほとんどの言語とフレームワークで JSON シリアル化がサポートされるため、より広範な相互運用性が提供されます。 一部のシリアル化形式では、固定スキーマまたはコンパイル済みスキーマ定義ファイルが必要です。 このような場合は、この手順をビルド プロセスに組み込む必要があります。 詳細については、「 メッセージ エンコードのベスト プラクティス」を参照してください。
フレームワークと言語のサポート: ほぼすべてのフレームワークと言語で HTTP がサポートされています。 Avro、gRPC、Thrift には、C++、C#、Java、およびPython用のライブラリが用意されています。 Thrift と gRPC も Go をサポートしています。
互換性と相互運用性: gRPC などのプロトコルを選択する場合は、パブリック API とバックエンドの間にプロトコル変換レイヤーが必要になる場合があります。 ゲートウェイはその機能を実行できます。 サービス メッシュを使用する場合は、サービス メッシュとのプロトコルの互換性を確認します。 たとえば、Linkerd には HTTP、Thrift、および gRPC のサポートが組み込まれています。
バイナリ プロトコルのパフォーマンス上の利点が必要な場合を除き、REST over HTTP を使用します。 REST over HTTP は特別なライブラリを必要とせず、呼び出し元がサービスと通信するためにクライアント スタブを必要としないため、最小限の結合を作成します。 REST エコシステムには、RESTful HTTP エンドポイントのスキーマ定義、テスト、監視をサポートするためのツールが含まれています。 HTTP はブラウザー クライアントでも機能するため、クライアントとバックエンドの間にプロトコル変換レイヤーは必要ありません。
HTTP よりも REST を選択した場合は、開発プロセスの早い段階でパフォーマンスとロード テストを行い、シナリオに適したパフォーマンスが得られます。
RESTful API の設計
RESTful API の設計には、次のリソースが役立ちます。
次の要因について検討します。
内部実装の詳細を公開したり、内部データベース スキーマをミラー化したりする API は避けてください。 API はドメインをモデル化し、サービス間のコントラクトとして機能する必要があります。 コードのリファクタリングやデータベース スキーマの変更を行う場合ではなく、新しい機能を追加する場合にのみ API を変更する必要があるのが理想的です。
モバイル アプリケーションやデスクトップ Web ブラウザーなど、さまざまな種類のクライアントでは、異なるペイロード サイズや対話パターンが必要になる場合があります。 フロントエンドのバックエンド パターンを使用して、クライアントごとに個別のバックエンドを作成することを検討してください。 各バックエンドは、そのクライアントに最適なインターフェイスを公開します。
副作用を引き起こす操作の場合は、冪等にし、
PUTメソッドとして実装することを検討してください。 この方法により、安全な再試行が可能になり、回復性が向上します。 詳細については、「 サービス間通信」を参照してください。HTTP メソッドは非同期セマンティクスを持つことができます。メソッドはすぐに応答を返しますが、サービスは非同期的に操作を実行します。 その場合、メソッドは HTTP 202 応答コードを返す必要があります。 このコードは、要求が処理のために受け入れられたが、まだ処理されていないことを示します。 詳細については、「 非同期 Request-Reply パターン」を参照してください。
汎用データ アクセス API: OData と GraphQL に関する考慮事項
REST API は、リソースを公開するための構造化されたアプローチを提供しますが、一部のシナリオでは、より柔軟なデータ アクセス パターンが必要です。 OData や GraphQL などのクエリ指向 API には、クライアントが必要なデータを正確に指定できる代替手段が用意されています。 この方法では、過剰フェッチを減らし、パフォーマンスを向上させる可能性があります。 これらの種類の API は、読み取り操作に優先順位を付けます。 作成、更新、削除などの変更操作は実装が複雑になる場合がありますが、さまざまなフレームワークでこれらの操作を効果的に管理できます。
汎用データ アクセス API を検討する場合
次の状況では、汎用データ アクセス パターンを使用します。
クライアントにはさまざまなデータ要件があり、その結果、多くの特殊な REST エンドポイントまたは特殊な動作が発生します。
複数のデータ エンティティにわたる複雑なクエリ、フィルター処理、並べ替え操作をサポートする必要があります。
オーバーフェッチは、特にモバイルまたは帯域幅に制約のあるクライアントの場合、パフォーマンスに大きな問題があります。
次の状況では、汎用データ アクセス API を使用しないでください。
マイクロサービス アーキテクチャでは、厳密なサービス境界とドメインカプセル化が強調されています。
データ アクセス パターンとセキュリティ ポリシーをきめ細かく制御する必要があります。
API は主に、単純な作成、読み取り、更新、削除 (CRUD) 操作または明確に定義されたビジネス ワークフローをサポートします。
REST は、ネットワーク パフォーマンスとペイロードの要件を既に満たしています。
セキュリティ要件では、攻撃対象領域を最小限に抑えるために、明示的なエンドポイント定義が必要です。
チームには、クエリ言語の実装と最適化の経験がありません。
REST を DDD パターンにマップする
エンティティ、集計、値オブジェクトなどのパターンは、ドメイン モデル内のオブジェクトの制約を定義します。 多くのドメイン駆動設計 (DDD) の説明では、コンストラクターやプロパティ ゲッターやセッターなどのオブジェクト指向 (OO) 言語の概念を使用して、これらのパターンについて説明します。 たとえば、 値オブジェクト は不変である必要があります。 OO プログラミング言語では、コンストラクターの値を割り当て、プロパティを読み取り専用にすることで、この制約を適用します。
export class Location {
readonly latitude: number;
readonly longitude: number;
constructor(latitude: number, longitude: number) {
if (!Number.isFinite(latitude) || latitude < -90 || latitude > 90) {
throw new RangeError('latitude must be between -90 and 90');
}
if (!Number.isFinite(latitude) || longitude < -180 || longitude > 180) {
throw new RangeError('longitude must be between -180 and 180');
}
this.latitude = latitude;
this.longitude = longitude;
}
}
これらのコーディング プラクティスは、従来のモノリシック アプリケーションを構築する上で重要な役割を果たします。 大規模なコード ベースでは、多くのサブシステムが Location オブジェクトを使用する可能性があるため、オブジェクトは正しい動作を強制する必要があります。
リポジトリ パターンには、別の例が用意されています。 このパターンにより、アプリケーションの他の部分がデータ ストアに直接読み取りまたは書き込みを行わないようにします。
マイクロサービス アーキテクチャでは、サービスは同じコード ベースまたはデータ ストアを共有しません。 代わりに、API を介して通信します。 たとえば、スケジューラ サービスは、ドローン サービスにドローンに関する情報を要求する場合があります。 ドローン サービスは、コードを使用して内部ドローン モデルを定義します。 ただし、スケジューラはこれらの詳細に直接アクセスできません。 代わりに、スケジューラはドローン エンティティの 表現 (HTTP 応答の JSON オブジェクトなど) を受け取ります。
この例は、航空機および航空宇宙産業に適切に適用されます。
スケジューラ サービスは、ドローン サービスの内部モデルを変更したり、ドローン サービスのデータ ストアに書き込んだりすることはできません。 そのため、ドローン サービスを実装するコードは、従来のモノリスのコードと比較して、露出する面積が小さくなります。 ドローン サービスで Location クラスが定義されている場合、そのクラスのスコープは制限されます。他のサービスはクラスを直接使用しません。
これらの理由から、このガイダンスは戦術的な DDD パターンに関連するコーディングプラクティスにあまり焦点を当てません。 ただし、REST API を使用して多くの DDD パターンをモデル化できます。
次の例は、REST の概念が一般的な DDD コンストラクトとどのように一致するかを示しています。
集約は、REST内のリソースに自然と結びつきます。 たとえば、配信 API では、配信集計をリソースとして公開できます。
集計では、整合性の境界が定義されます。 集約に対する操作が集約を不整合な状態にしてはいけません。 クライアントが集計の内部状態を操作できるようにする API は作成しないでください。 代わりに、リソースとして集計を公開する粒度の粗い API を優先してください。
エンティティには一意の ID があります。 REST では、リソースは URL の形式で一意の識別子を持ちます。 エンティティのドメイン ID に対応するリソース URL を作成します。 URL からドメイン ID へのマッピングは、クライアントに対して不透明である可能性があります。
集約の子エンティティには、ルート エンティティから到達できます。 アプリケーション状態 (HATEOAS) の原則のエンジンとしてハイパーメディアに従う場合、親エンティティの表現のリンクを介して子エンティティに到達できます。
値オブジェクトは変更できません。 更新を行うには、値オブジェクト全体を置き換えます。 REST では、
PUT要求またはPATCH要求を通じて更新プログラムを実装します。リポジトリを使用すると、クライアントはコレクション内のオブジェクトのクエリ、追加、または削除を行うことができます。 リポジトリは、基になるデータ ストアの詳細を抽象化します。 REST では、コレクションは、コレクションに対してクエリを実行したり、新しいエンティティをコレクションに追加したりするためのメソッドを含む個別のリソースにすることができます。
API を設計するときは、モデル内のデータだけでなく、ドメイン モデルがどのように表現されるかを考えてください。 また、ビジネス操作とデータに対する制約も考慮してください。
| DDD の概念 | REST に相当する | Example |
|---|---|---|
| Aggregate | Resource | { "1":1234, "status":"pending"... } |
| アイデンティティ | URL | https://delivery-service/deliveries/1 |
| 子エンティティ | Links | { "href": "/deliveries/1/confirmation" } |
| 値オブジェクトを更新する |
PUT または PATCH |
PUT https://delivery-service/deliveries/1/dropoff |
| リポジトリ | 徴収 | https://delivery-service/deliveries?status=pending |
API のバージョン管理
API は、サービスと、そのサービスのクライアントまたはコンシューマーとの間のコントラクトとして機能します。 API の変更により、API に依存する外部クライアントまたはマイクロサービスが中断される可能性があります。 行った API 変更の数を最小限に抑えます。 基になる実装の変更では、多くの場合、API を変更する必要はありません。 ただし、ある時点で、既存の API を変更する必要がある新機能や新機能を追加する必要がある可能性があります。
可能な場合は、API の変更に下位互換性を持たせるようにします。 たとえば、モデルからフィールドを削除しないようにします。 この変更により、フィールドが存在することを期待するクライアントが機能停止する恐れがあります。 クライアントは応答で認識できないフィールドを無視する必要があるため、フィールドを追加しても互換性は損なわれません。 ただし、サービスは、新しいフィールドを省略した古いクライアントからの要求を処理する必要があります。
API コントラクトでのバージョン管理をサポートします。 破壊的な API の変更を導入する場合は、新しい API バージョンを導入してください。 以前のバージョンを引き続きサポートし、クライアントが呼び出すバージョンを選択できるようにします。 バージョン管理を行う 1 つの方法は、両方のバージョンを同じサービスで公開することです。 もう 1 つのオプションは、2 つのバージョンのサービスをサイド バイ サイドで実行し、HTTP ルーティング規則に基づいて要求を一方または他方のバージョンにルーティングすることです。
図には 2 つの部分があります。 左側には、2 つのバージョンをサポートするサービスが表示されます。 v1 クライアントと v2 クライアントはどちらも 1 つのサービスを指します。 右側には、サイドバイサイド デプロイメントが表示されます。 v1 クライアントは v1 サービスを指し、v2 クライアントは v2 サービスを指します。
複数のバージョンでは、開発者の時間、テスト、運用のオーバーヘッドの観点からコストが追加されます。 古いバージョンはできるだけ早く非推奨にしてください。 内部 API の場合、API を所有するチームは他のチームと連携して、新しいバージョンへの移行を支援できます。 ここでは、チーム間のガバナンス プロセスが役立ちます。 外部 (パブリック) API は、特に外部またはネイティブのクライアント アプリケーションが API を使用する場合に、API バージョンの非推奨化が困難になる可能性があります。
サービス実装が変更されたら、変更にバージョンのタグを付けます。 このバージョンでは、エラーのトラブルシューティングに役立つ重要な情報が提供されます。 この方法では、呼び出されるサービスのバージョンがわかっているため、根本原因分析がサポートされます。 サービス バージョンに セマンティック バージョン管理を 使用することを検討してください。 セマンティックバージョニングではMAJOR.MINOR.PATCH形式が使用されます。 ただし、クライアントは、メジャーバージョン番号で API を選択するか、マイナーバージョン間に重要ではあるが互換性を壊さない変更がある場合はマイナーバージョンを選択する必要があります。 たとえば、クライアントは API のバージョン 1 とバージョン 2 のいずれかを選択できますが、バージョン 2.1.3 は選択しないでください。 そのレベルの粒度を許可すると、サポートするバージョンが多すぎるリスクがあります。
詳細については、「 RESTful Web API のバージョン管理を実装する」を参照してください。
べき等演算
最初の呼び出し後に副作用を増やさずに複数回呼び出すことができる場合、操作は冪等です。 べき等性は、アップストリーム サービスが操作を複数回安全に呼び出すのを可能にするため、便利な回復性戦略として機能します。 詳細については、「 分散トランザクション」を参照してください。
HTTP 仕様では、 GET、 PUT、および DELETE メソッドはべき等である必要があることを示しています。
POST メソッドがべき等であるとは限りません。
POST メソッドが新しいリソースを作成する場合、この操作がべき等性があるという保証は通常ありません。 この仕様は、冪等性を次のように定義しています。
要求メソッドが べき等 と見なされるのは、そのメソッドを使用した複数の同一要求のサーバーに対する意図した効果が、そのような 1 つの要求の効果と同じである場合です。 (RFC 7231)
新しいエンティティを作成するときの PUT セマンティクスと POST セマンティクスの違いを理解します。 どちらの場合も、クライアントは要求本文でエンティティの表現を送信します。 ただし、URI (Uniform Resource Identifier) の意味は異なります。
POSTメソッドの場合、URI はコレクションなどの新しいエンティティの親リソースを表します。 たとえば、新しい配信を作成するには、URI が/api/deliveries。 サーバーはエンティティを作成し、/api/deliveries/39660などの新しい URI を割り当てます。 この URI は、応答のLocationヘッダーで返されます。 クライアントが要求を送信するたびに、サーバーは新しい URI を持つ新しいエンティティを作成します。PUTメソッドの場合、URI はエンティティを識別します。 既存のエンティティにその URI がある場合、サーバーは既存のエンティティを要求のバージョンに置き換えます。 その URI を使用するエンティティがない場合は、サーバーによって作成されます。 たとえば、クライアントがPUTにapi/deliveries/39660要求を送信するとします。 配信リソースがその URI を使用しない場合、サーバーは新しい URI を作成します。 クライアントが同じ要求を再度送信すると、サーバーは既存のエンティティを置き換えます。
配信サービスでは、次のコードを使用して PUT メソッドを実装します。
[HttpPut("{id}")]
[ProducesResponseType<Delivery>(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<IActionResult> Put([FromBody]Delivery delivery, string id)
{
logger.LogInformation("In Put action with delivery {Id}: {@DeliveryInfo}", id, delivery.ToLogInfo());
try
{
var internalDelivery = delivery.ToInternal();
// Create the new delivery entity.
await deliveryRepository.CreateAsync(internalDelivery);
// Create a delivery status event.
var deliveryStatusEvent = new DeliveryStatusEvent { DeliveryId = delivery.Id, Stage = DeliveryEventType.Created };
await deliveryStatusEventRepository.AddAsync(deliveryStatusEvent);
// Return HTTP 201 (Created)
return CreatedAtRoute("GetDelivery", new { id= delivery.Id }, delivery);
}
catch (DuplicateResourceException)
{
// This method mainly creates deliveries. If the delivery already exists, update it.
logger.LogInformation("Updating resource with delivery id: {DeliveryId}", id);
var internalDelivery = delivery.ToInternal();
await deliveryRepository.UpdateAsync(id, internalDelivery);
// Return HTTP 204 (No Content)
return NoContent();
}
}
ほとんどの要求では新しいエンティティが作成されるため、メソッドは作成が成功すると予測し、CreateAsync をリポジトリオブジェクトで呼び出します。 その後、リソースを更新することで、重複リソースの例外がメソッドによって処理されます。
次のステップ
クライアント アプリケーションとマイクロサービスの境界で API ゲートウェイを使用する方法について説明します。