マイクロサービスを連携してみよう
はじめに
前回は、Spring Bootを使ったマイクロサービスの基本的な開発について説明し、OpenShift上でサービスを稼働する方法を紹介しました。
今回は、複数のマイクロサービスを連携する方法を解説します。
マイクロサービスの連携方法
マイクロサービスでは、複数のサービスを連携することで業務システムを構成します。サービスの連携方法には「同期通信による連携」と「非同期通信による連携」の2つの方法があります。
同期通信による連携
同期通信による連携では、RESTやgRPCなどを使って同期的に連携します。
RESTによる同期通信では、呼び出されるサービスはRESTインタフェースのAPIを公開し、呼び出し元のサービスは、公開されたAPIのクライアントを実装しAPIを呼び出します。
RESTによる同期通信の開発では、REST APIのクライアント開発や既存システムのAPI化を支援するツール、ミドルウェア、クラウドサービスが充実しており、開発が容易という特徴があります。一方、呼び出し元のサービスは、直接呼び出し先サービスのAPIを参照するためサービス間の結合度は高くなり、サービス開発の独立性は非同期通信に比べて低くなります。サービス間の開発独立性よりも、開発利便性を優先したい場合に向く連携方法です。
また、サービスの数が膨大な場合やAPIを外部公開する場合は3scaleなどのAPI管理を導入することで、APIの配布、制御、分析といった運用を効率化できます。
非同期通信による連携
非同期通信による連携では、メッセージキュー製品などの非同期通信ミドルウェアを経由して、サービス間を間接的に連携します。
非同期通信では、呼び出し元のサービスと呼び出し先のサービスを間接的に連携するためサービス間の結合度は低くなり、サービス開発の独立性を高めることができます。一方、非同期処理では結果の即時性が担保されないため、それを考慮した業務ロジックの実現が必要になります。
また、メッセージやサービス間の通信に順序がある場合に処理が複雑になりやすい、メッセージキューが単一障害点になる可能性がある、といった運用面での配慮も必要になります。
ここでは、オーソドックスな同期APIによる連携について、API管理を使用しないシンプルな構成で紹介します。
同期APIによるサービスの連携
同期APIによるサービスの連携では、接続先URL、オペレーション(HTTPメソッド)、送受信データ(パラメータ、リクエストボディ、レスポンス)といったAPIのインタフェースを定義し、APIの利用者へ公開します。利用者は公開されたインタフェースに沿ってAPIを呼び出します。
APIで連携するサービスの開発方法
APIで連携するマイクロサービス開発では、まずAPIで呼び出されるサービスを開発し、そのAPI仕様を公開します。次に、API仕様に沿って呼び出すクライアントとして呼び出し元サービスを開発し、サービス間の連携を実現します。
呼び出し先サービスの代表的な開発方法には、次の2つがあります。
- トップダウンでの呼び出し先サービスの開発
最初にAPI仕様を設計します。作成したインタフェースを基にAPIを提供するサービスを実装します。開発したサービスのAPI仕様を利用者へ公開します。トップダウン方式の開発は、新規にサービスを開発する場合やウォーターフォール開発に向いています。 - ボトムアップでの呼び出し先サービスの開発
最初に、実装した新規サービスや既存サービスから公開する機能を選択します。選択したサービス実装からツールを利用してAPI仕様を生成します。生成したサービスのAPI仕様を利用者へ公開します。ボトムアップ方式の開発は、既存サービスの機能を公開する場合やサービスに加えた改修を随時反映するアジャイル開発に向いています。
APIを記述するための標準規格
OpenAPI Specification
マイクロサービス開発でAPI仕様を定義するには、OpenAPI Specificationを使用します。OpenAPI SpecificationはREST APIのインタフェースを記述するための標準規格で、OpenAPI Initiativeにより推進されています。OpenAPI Specificationの2020年7月時点の最新バージョンは3.0.3です。
OpenAPI Specificationに則ったAPI仕様ドキュメント(OpenAPIドキュメント)は、JSONまたはYAML形式で記述します。OpenAPI Specificationはサービスを実装するプログラム言語に依存しないフォーマットとなっています。
今回作成するマイクロサービス
前回作成したサービス(Sample)をベースに、2つのサービスが連携するマイクロサービスを作成してみましょう。
前回は、サービス(Sample)をシンプルな単独のサービスとして開発しましたが、他のサービスからAPIを呼び出せるように、APIを公開できる形に拡張します。トップダウンによるサービス(Sample)の開発、ボトムアップによるサービス(Sample)の開発をそれぞれ紹介します。また、サービス(Sample)を呼び出す、呼び出し元サービス(Caller)も新たに開発し、サービスの連携をしてみましょう。
開発するマイクロサービスを下図に示します。
このサービス(Caller)では、受け取ったデータが妥当かチェックし、問題がなければサービス(Sample)を呼び出してデータベースにデータを登録します。
トップダウンでの
呼び出し先サービスの開発
トップダウンによる開発では、サービス実装よりも先にAPI仕様を記述したOpenAPIドキュメントを作成します。OpenAPIドキュメントからソースコードを自動生成し、呼び出し先のサービスを開発します。
API仕様を記述したOpenAPIドキュメントを作成
はじめに、サービスのAPI仕様を定義したOpenAPIドキュメントを作成します。OpenAPIドキュメントはSwagger Editorで記述します。Swagger EditorはWebブラウザベースでOpenAPIドキュメントを記述できるエディタです。リアルタイムに表示されるSwagger UI形式のプレビューを確認しながら、YAML形式でOpenAPIドキュメントを記述できます。インストール不要で利用できるオンライン版が公開されており、手軽に試すこともできます。
サービス(Sample)のOpenAPIドキュメントは次の通りです。
openapi: 3.0.3 info: title: サンプル description: APIのサンプルです。 version: '1.0' servers: - url: 'http://sample.samplesystem.example.com:8080' description: 公開するAPIのURL tags: - name: Sample description: Sample Operations paths: /api/sample/apply: post: tags: - Sample summary: ユーザ情報登録 description: ユーザ情報を登録します。 operationId: insertSampleEntity requestBody: description: 登録するユーザ情報 required: true content: application/json: schema: $ref: '#/components/schemas/RequestResource' responses: '201': description: 登録されたユーザID content: '*/*': schema: $ref: '#/components/schemas/ResponseResource' components: schemas: RequestResource: title: RequestResource description: ユーザ情報 type: object required: - user_name - store_id - store_name properties: user_id: type: integer description: ユーザID format: int32 readOnly: true example: 1 user_name: maxLength: 255 minLength: 1 type: string description: ユーザ名 example: 日立太郎 store_id: type: integer description: ストアID format: int32 example: 1111 store_name: maxLength: 255 minLength: 1 type: string description: ストア名 example: ショップA ResponseResource: title: ResponseResource description: 登録結果 type: object properties: user_id: type: integer description: ユーザID format: int32 example: 1 error_code: type: integer description: エラーコード format: int32 example: -1 error_message: maxLength: 255 minLength: 0 type: string description: エラーメッセージ example: システムエラー
OpenAPIドキュメントからソースコードを生成
次に、OpenAPIドキュメントからソースコードを作成します。OpenAPIドキュメントからソースコードを作成するツールとしてはSwagger Codegenが有名ですが、最新のOpenAPI Specification 3.0への対応が遅れています。OpenAPI Specification 3.0に則ったOpenAPIドキュメントからソースコードを生成する場合はOpenAPI Generatorの利用をおすすめします。OpenAPI GeneratorはSwagger Codegenからフォークされたツールで、OpenAPI Specification 3.0に対応しています。
OpenAPI GeneratorはOpenAPIドキュメントからサービスのスタブやクライアントのプログラムコードを自動生成でき、Java、JavaScript、C#、Python、Go、Rubyなどの主要な言語やライブラリに対応しています。
OpenAPI Generatorを使って、Spring Bootに対応するソースコードを生成します。ソースコードを生成するコマンド実行例は次の通りです。
java -jar openapi-generator-cli-4.3.1.jar generate -g spring -i api-docs.json -c config.json -o generated-source
-gオプションには、Spring Bootのソースコードを生成するためにspringを指定します。-iオプションには、入力となるOpenAPIドキュメントを指定します。-cオプションには、OpenAPI Generatorの設定ファイルを指定します。設定ファイルの例は次の通りです。
{ "groupId" : "com.example.samplesystem", "artifactId" : "sample", "artifactVersion" : "1.0.0", "basePackage" : "com.example.samplesystem.sample", "apiPackage" : "com.example.samplesystem.sample.api", "configPackage" : "com.example.samplesystem.sample.configuration", "modelPackage" : "com.example.samplesystem.sample.model", "dateLibrary" : "java8-localdatetime", "java8" : true, "library": "spring-boot", "interfaceOnly": true }
interfaceOnlyオプションを指定すると、APIのインタフェースとなるソースコードのみを生成し、実装クラスを生成しません。そのため、API仕様を変更した際にソースコードを再生成しても、実装クラスを上書きしてしまうことはありません。interfaceOnlyオプションを使用するとビルドパイプラインでの自動化に組み込むことができ、CIを実現できます。
コマンドを実行すると、-oオプションで指定したディレクトリにJavaインタフェース、Model、Configurationなどのソースコードが生成されます。APIを提供するためのApiApiインタフェースの例は次の通りです。
@Validated @Api(value = "api", description = "the api API") public interface ApiApi { … @ApiOperation(value = "ユーザ情報登録", nickname = "insertSampleEntity", notes = "ユーザ情報を登録します。", response = ResponseResource.class, tags={ "Sample", }) @ApiResponses(value = { @ApiResponse(code = 201, message = "登録されたユーザID", response = ResponseResource.class) }) @RequestMapping(value = "/api/sample/apply", produces = { "*/*" }, consumes = { "application/json" }, method = RequestMethod.POST) default ResponseEntity<ResponseResource> insertSampleEntity(@ApiParam(value = "登録するユーザ情報" ,required=true ) @Valid @RequestBody RequestResource requestResource) { … } }
生成したソースコードを使って
マイクロサービスを実装
OpenAPI Generatorで生成したソースコードを活用し、業務ロジックの実装を追加してサービス(Sample)を作成します。Controller層やService層などサービスの開発は前回で解説した手順に従ってください。ApiApiインタフェースを実装したControllerの例は次の通りです。
@RestController public class SampleController implements ApiApi { … @Override @PostMapping public ResponseEntity<ResponseResource> insertSampleEntity(@Valid @RequestBody RequestResource requestResource) { … } }
ApiApiインタフェースを実装し、@RestControllerアノテーションを設定してSampleControllerクラスを作成します。オーバーライドしたinsertSampleEntityメソッドに@PostMappingアノテーションを設定し、さらに実装を追加します。
ここまでで、トップダウンによる呼び出し先サービスの開発は完了です。
ボトムアップでの
呼び出し先サービスの開発
ここからは、サービスを実装した後にOpenAPIドキュメントを作成する、ボトムアップでの開発について説明します。
ボトムアップでの開発では、まずサービスを開発します。このサービスの開発も前回で解説した手順に従ってください。開発したサービスからOpenAPIドキュメントを作成する方法は様々ですが、今回はSpringFoxを使用します。
SpringFoxは、Spring Frameworkを使用したサービスのソースコードからOpenAPIドキュメントを生成するJavaライブラリです。APIの実装クラスにSwagger Coreのアノテーションを記述することでOpenAPIドキュメントを自動生成できます。
2020年7月にリリースされた最新のSpringFox 3.0では、OpenAPI Specification 3.0に対応したほか、Spring WebFluxやSpring Integrationにも対応しています。
SpringFoxを使用して生成するOpenAPIドキュメントにAPI仕様の説明を出力するための定義を、サービス(Sample)に追加していきます。
SpringFoxライブラリを依存関係に追加
はじめに、APIを公開するサービスのpom.xmlを編集し、springfox-boot-starterモジュールを依存関係に追加します。
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-boot-starter</artifactId> <version>3.0.0</version> </dependency>
SpringFox 2.xまではspringfox-swagger2モジュールとspringfox-swagger-uiモジュールを使用していましたが、SpringFox 3.0では代わりにspringfox-boot-starterモジュールを使用します。
SpringFox用Configurationクラスの実装
次に、Configurationクラスを実装し、サービス全体に関するAPI仕様の情報を定義します。
@Configuration public class SwaggerConfig { @Bean public Docket customDocket() { return new Docket(DocumentationType.OAS_30) .select() .paths(PathSelectors.regex("^\\/api.*")) .build() .useDefaultResponseMessages(false) .apiInfo(apiInfo()) .tags(new Tag("Sample", "Sample Operations")); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("サンプル") .description("APIのサンプルです。") .version("1.0") .build(); } }
クラス宣言に@Configurationアノテーションを定義します。なお、SpringFox 2.xまでは、@EnableSwagger2アノテーションも定義する必要がありましたが、SpringFox 3.0では不要です。
Controllerクラスの変更
Controllerクラスにアノテーションを追加し、APIごとのオペレーション、レスポンス、パラメータといった説明を定義します。
@Api(tags = "Sample") @RestController @RequestMapping("/api/sample/apply") public class SampleController { @ApiOperation(value = "ユーザ情報登録", notes = "ユーザ情報を登録します。" ) @ApiResponses(value = { @ApiResponse(code = 201, message = "登録されたユーザID")}) @PostMapping @ResponseStatus(HttpStatus.CREATED) public ResponseResource insertSampleEntity(@Validated @RequestBody RequestResource requestResource) { … } }
@Api、@ApiOperation、@ApiResponse、@ApiParamアノテーションなどを使って説明情報を定義します。
Modelクラスの変更
Modelクラスにアノテーションを追加し、APIのリクエスト、レスポンスに対応する説明を定義します。
@ApiModel(description = "ユーザ情報の登録結果") @Data @JsonPropertyOrder({ "user_id", "error_code", "error_message" }) @JsonInclude(JsonInclude.Include.NON_NULL) public class ResponseResource { @ApiModelProperty(value = "ユーザID", example = "1") @Size(min = Integer.MIN_VALUE, max = Integer.MAX_VALUE) @JsonProperty("user_id") private Integer id; … }
@ApiModel、@ApiModelPropertyアノテーションなどを設定します。
OpenAPIドキュメントの公開
最後に、OpenAPIドキュメントを公開します。Spring Bootアプリケーションを起動すると、次の2つの形式でOpenAPIドキュメントが自動的に公開されます。
- Swagger UI形式
Swagger UI形式のWebドキュメントが次のURLで公開されます。
http://<サービスのドメイン名>/swagger-ui/index.html
- JSON形式
JSON形式のOpenAPIドキュメントが次のURLで公開されます。
http://<サービスのドメイン名>/v3/api-docs
Swagger UI形式でOpenAPIドキュメントを閲覧した例は次の通りです。
これで、ボトムアップでの呼び出し先サービスの開発は完了です。
呼び出し元サービスの開発
呼び出し先のサービスを開発したのち、OpenAPIドキュメントを使って呼び出し元のサービスを開発します。
OpenAPIドキュメントから
クライアントのソースコードを生成
はじめに、APIのOpenAPIドキュメントからAPIクライアントのソースコードを作成します。ソースコードの作成はOpenAPI Generatorを使います。ソースコードを生成するコマンド実行例は次の通りです。
java -jar openapi-generator-cli-4.3.1.jar generate -g java --library resttemplate -i api-docs.json -c config.json -o generated-source
-gオプションには、Javaクライアントのソースコードを生成するためにjavaを指定します。--libraryオプションには、Spring BootのRestTemplateを使ってAPI呼び出しするソースコードを生成するためにresttemplateを指定します。-iオプションには入力となるOpenAPIドキュメントを指定します。コマンドを実行すると、-oオプションで指定したディレクトリにソースコードが生成されます。
呼び出し元サービスを実装
OpenAPI Generatorで生成したAPIクライアントのソースコードは、Spring Bootアプリケーションから呼び出せるライブラリとなっています。生成したソースコードを活用し、APIを呼び出して連携する実装を追加して呼び出し元のサービス(Caller)を作成します。
@Service public class CallerService { /* サービス(Sample)のURLのベースパス */ @Value("${sample_api.base_path}") private String basePath; public ServiceResponse apply(ServiceRequest serviceRequest) throws CallerServiceException { try { … /* APIクライアントの生成 */ SampleApi sampleApi = new SampleApi(); ApiClient apiClient = sampleApi.getApiClient(); apiClient.setBasePath(basePath); /* リクエストの作成 */ RequestResource requestResource = new RequestResource(); requestResource.setUserName(serviceRequest.getUserName()); requestResource.setStoreId(serviceRequest.getStoreId()); requestResource.setStoreName(serviceRequest.getStoreName()); /* サービス(Sample)の呼び出し */ ResponseResource responseResource = sampleApi.insertSampleEntity(requestResource); … } catch (Exception e) { /* エラー処理 */ CallerServiceException ex = new CallerServiceException(); ex.setErrorCode(ServiceResponse.SERVICE_ERROR); throw ex; } } … }
OpenAPI GeneratorによりSampleApiクラス、ApiClientクラス、ModelであるRequestResourceクラスやResponseResourceクラスなどが生成されます。サービス(Caller)ではSampleApiインスタンスを生成し、呼び出し先サービス(Sample)のURLのベースパスをApiClientインスタンスに設定してからinsertSampleEntityメソッドを呼び出すことで、サービス(Sample)を呼び出します。
これで、呼び出し元サービスの開発は完了です。
サービスの実行
それでは、サービスを実行して連携してみましょう。ここではcurlコマンドを使ってサービス(Caller)を呼び出します。
curl http://caller-caller-project.apps.xxxx-xxxxx-cluster.xxxxx.xxxxx.com/api/caller/apply \ -v -X POST -H "Content-Type:application/json" -d "{\"user_name\":\"Taro\", \"store_id\":1111, \"store_name\":\"Shop_A\"}" …
サービス(Caller)が呼び出されると、OpenAPIドキュメントから生成したAPIクライアントを使用してサービス(Sample)を呼び出します。サービス(Caller)がサービス(Sample)を呼び出した際のHTTPリクエストメッセージは次の通りです。
POST /api/sample/apply HTTP/1.1 Accept: */* Content-Type: application/json User-Agent: Java-SDK Host: sample-sample-project.apps.xxxx-xxxxx-cluster.xxxxx.xxxxx.com Connection: keep-alive Content-Length: 73 {"user_id":null,"user_name":"Taro","store_id":1111,"store_name":"Shop_A"}
連携により呼び出されたサービス(Sample)は、JSON文字列を含むHTTPレスポンスメッセージをサービス(Caller)へ返します。
HTTP/1.1 201 Content-Type: application/json Transfer-Encoding: chunked Date: Wed, 22 Jul 2020 01:29:10 GMT Keep-Alive: timeout=60 Connection: keep-alive d {"user_id":1} 0
サービス(Caller)が実行されると、curlコマンドは実行結果としてJSON文字列を出力します。
{"user_id":1}
おわりに
今回はマイクロサービスの連携について解説しました。ここまではサービスやAPIの開発に絞って紹介しましたが、実際にマイクロサービスの利点を活かすためには、コンテナ管理ツールと連携したCI/CDによるサービス開発の実現が必要です。
次回は、コンテナ管理ツールを使用したCI/CDについて解説します。