KINTO Technologies Advent Calendar 2021 - Qiita の24日目の記事です。
はじめに
本記事では、マイクロサービスのBFF(Backend For Frontend)の実装方式を検討した時にPoCで行ったGraphQL Meshの構築例を記載しています。
この記事で伝えたいこと
RESTインタフェースベースのマイクロサービスに、Spring Cloud GatewayとGraphQL Meshを利用すれば、既存RESTインタフェースに加えてGraphQLインタフェースにも対応できるBFFがどれほど簡単に構築できるかについて共有できればと思います。
システム概要図
なぜSpring Cloud Gateway?
- Spring Boot
現在マイクロサービスのバックエンドがSpring Bootを使っており、BFFもSpringエコシステムを使って構築するつもりでしたので、Spring Cloud系のフレームワークを使うことにしました。
- マルチクラウド問題
BFFの役割として、AWSのマネジメントサービスも検討していましたが、サービスの海外展開でマルチクラウド環境に対応する必要もあり、コンテナベースでAPI Gatewayを構築することにしました。
- 認証・認可のカスタマイズ
BFFで認証・認可も行う予定でして、海外展開で各国のシステムに合わせてカスタマイズの必要性もあり、標準+柔軟な対応ができる構成にしたかったです。Spring Security等のエコシステムを利用できることもメリットになると思いました。
なぜGraphQL Mesh?
- API Composition問題
システムがマイクロサービスになり、各APIレスポンスをマージするユースケースが想定されていて、API Composition方針を検討しましたが、既存REST APIではユースケースごとのAPIを新しく作成する必要がありました。それでAPI Compositionはもちろん、リクエストやレスポンスの柔軟な対応ができるGraphQLインタフェースを検討し、GraphQL Meshを利用することにしました。
- 構築が一番簡単
GraphQLインタフェースで既存マイクロサービスをラップする方法は多くあると思いますが、その中でGraphQL Meshはopenapiの設定だけで構築できて一番楽でした。
実装
Microservice
マイクロサービスは、OpenAPI Generatorを使ってAPI定義のYAMLファイルからソースコードを出力する想定です。今回の検証では、SubscriptionとVehicleというマイクロサービスをGETメソッドのみ実装することにします。
※本記事ではBFFの実装がメインですので、マイクロサービスの詳細実装内容については割愛します。
- OpenAPI設定
Subscription Microservice
openapi: 3.0.3
info:
title: Subscription API
description: This is a subscription api for test.
version: 1.0.0
servers:
- url: 'http://localhost:8091/'
description: Local development env
tags:
- name: Subscriptions
description: test subscription api
paths:
/subscriptions:
get:
tags:
- Subscriptions
summary: Get subscription list
description: |
<li>Returns subscription list from subscription table sorting with creating datetime.
operationId: listSubscription
parameters:
- in: query
name: count
description: max count
required: true
schema:
type: integer
- in: query
name: customerId
description: customer id
required: false
schema:
type: string
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/SubscriptionResponse'
'400':
$ref: '#/components/responses/BadRequest'
default:
$ref: '#/components/responses/Default'
security:
- BearerAuth: []
components:
responses:
BadRequest:
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
Default:
description: Unexpected Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
schemas:
ApiError:
type: object
properties:
errors:
type: array
items:
type: object
properties:
code:
type: string
description: error code
message:
type: string
description: error message
SubscriptionResponse:
type: object
properties:
id:
type: string
customerId:
type: string
mailAddress:
type: string
name:
type: string
createdAt:
type: string
format: date-time
createdBy:
type: string
modifiedAt:
type: string
format: date-time
modifiedBy:
type: string
version:
type: integer
format: int32
securitySchemes:
BearerAuth:
type: http
scheme: bearer
Vehicle Microservice
openapi: 3.0.3
info:
title: Vehicle API
description: This is a vehicle api for test.
version: 1.0.0
servers:
- url: 'http://localhost:8092/'
description: Local development env
tags:
- name: Vehicles
description: test vehicle api
paths:
/vehicles:
get:
tags:
- Vehicles
summary: Get vehicle list
description: |
<li>Returns vehicle list from vehicle table sorting with creating datetime.
operationId: listVehicles
parameters:
- in: query
name: count
description: max count
required: true
schema:
type: integer
- in: query
name: customerId
description: customer id
required: false
schema:
type: string
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/VehicleResponse'
'400':
$ref: '#/components/responses/BadRequest'
default:
$ref: '#/components/responses/Default'
security:
- BearerAuth: []
components:
responses:
BadRequest:
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
Default:
description: Unexpected Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
schemas:
ApiError:
type: object
properties:
errors:
type: array
items:
type: object
properties:
code:
type: string
description: error code
message:
type: string
description: error message
VehicleResponse:
type: object
properties:
id:
type: string
customerId:
type: string
model:
type: string
color:
type: string
createdAt:
type: string
format: date-time
createdBy:
type: string
modifiedAt:
type: string
format: date-time
modifiedBy:
type: string
version:
type: integer
format: int32
securitySchemes:
BearerAuth:
type: http
scheme: bearer
Spring Cloud Gateway
Spring Initializrのデフォルトソースベースで、以下の設定のみ変更します。
- 設定
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-gateway:3.0.5'
}
```
```application.yml
spring:
cloud:
gateway:
routes:
- id: graphql
uri: http://localhost:4000
predicates:
- Path=/graphql
filters:
- SetPath=/graphql
- id: subscription
uri: http://localhost:8091
predicates:
- Path=/subscriptions
filters:
- SetPath=/subscriptions
- id: vehicle
uri: http://localhost:8092
predicates:
- Path=/vehicles
filters:
- SetPath=/vehicles
```
### GraphQL Mesh
GraphQL MeshのDocker Imageは、以下をベースにしました。
https://github.com/onelittlenightmusic/graphql-mesh-docker
- GraphQL Mesh設定
```..meshrc.yaml
sources:
- name: Fake API
handler:
openapi:
source: ./openapi.yml
baseUrl: http://host.docker.internal:8080
serve:
hostname: 0.0.0.0
※GraphQL Meshは、ローカルPCのDocker上で動いていて、Spring Cloud GateはローカルPCで動いているため、hostをhost.docker.internalに設定しています。
- Dockerfile
FROM hiroyukiosaki/graphql-mesh:latest-all-alpine
COPY .meshrc.yaml ./.meshrc.yaml
COPY openapi.yml ./openapi.yml
- OpenAPI設定
Microservice API
openapi: 3.0.3
info:
title: Microservice API
description: This is a microservice api for test.
version: 1.0.0
servers:
- url: 'http://localhost:8080/'
description: Local development env
tags:
- name: Subscriptions
description: test subscription api
- name: Vehicles
description: test vehicle api
paths:
/subscriptions:
get:
tags:
- Subscriptions
summary: Get subscription list
description: |
<li>Returns subscription list from subscription table sorting with creating datetime.
operationId: listSubscription
parameters:
- in: query
name: count
description: max count
required: true
schema:
type: integer
- in: query
name: customerId
description: customer id
required: false
schema:
type: string
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/SubscriptionResponse'
'400':
$ref: '#/components/responses/BadRequest'
default:
$ref: '#/components/responses/Default'
security:
- BearerAuth: []
/vehicles:
get:
tags:
- Vehicles
summary: Get vehicle list
description: |
<li>Returns vehicle list from vehicle table sorting with creating datetime.
operationId: listVehicles
parameters:
- in: query
name: count
description: max count
required: true
schema:
type: integer
- in: query
name: customerId
description: customer id
required: false
schema:
type: string
responses:
'200':
description: OK
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/VehicleResponse'
'400':
$ref: '#/components/responses/BadRequest'
default:
$ref: '#/components/responses/Default'
security:
- BearerAuth: []
components:
responses:
BadRequest:
description: Bad Request
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
Default:
description: Unexpected Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
schemas:
ApiError:
type: object
properties:
errors:
type: array
items:
type: object
properties:
code:
type: string
description: error code
message:
type: string
description: error message
SubscriptionResponse:
type: object
properties:
id:
type: string
customerId:
type: string
mailAddress:
type: string
name:
type: string
createdAt:
type: string
format: date-time
createdBy:
type: string
modifiedAt:
type: string
format: date-time
modifiedBy:
type: string
version:
type: integer
format: int32
VehicleResponse:
type: object
properties:
id:
type: string
customerId:
type: string
model:
type: string
color:
type: string
createdAt:
type: string
format: date-time
createdBy:
type: string
modifiedAt:
type: string
format: date-time
modifiedBy:
type: string
version:
type: integer
format: int32
securitySchemes:
BearerAuth:
type: http
scheme: bearer
※openapi.ymlは、マイクロサービスのsubscription.ymlとvehicle.ymlをマージしたものです。
- Docker image作成
PS C:\dooboo\test\custom-images\graphql-mesh> docker build . --tag graphql-mesh
[+] Building 0.2s (8/8) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 156B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/hiroyukiosaki/graphql-mesh:latest-all-alpine 0.0s
=> [1/3] FROM docker.io/hiroyukiosaki/graphql-mesh:latest-all-alpine 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 225B 0.0s
=> CACHED [2/3] COPY .meshrc.yaml ./.meshrc.yaml 0.0s
=> [3/3] COPY openapi.yml ./openapi.yml 0.0s
=> exporting to image 0.1s
=> => exporting layers 0.0s
=> => writing image sha256:b9e2f32d6c20cb5340de6cafa9e2f72b663fc95150809002fe1b08bd463a2acf 0.0s
=> => naming to docker.io/library/graphql-mesh 0.0s
Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
- GraphQL Mesh 実行
PS C:\dooboo\test\custom-images\graphql-mesh> docker run --name graphql-mesh -p 4000:4000 graphql-mesh
yarn run v1.22.5
$ mesh dev
🕸️ - Server: Generating Mesh schema...
🕸️ - Server: Serving GraphQL Mesh: http://0.0.0.0:4000
- GraphQL 起動確認
http://localhost:4000 にアクセスし、以下のGraphiQL画面で設定されたAPIが表示されればOKです。
結果
実行環境
アプリケーション | 実行環境 | ポート |
---|---|---|
Spring Cloud Gateway | ローカルPCのIDE | 8080 |
GraphQL Mesh | Docker | 4000:4000 |
Subscription microservice | ローカルPCのIDE | 8091 |
Vehicle microservice | ローカルPCのIDE | 8092 |
REST
RESTの検証は、swagger editorのサイトで上記openapi.yml定義にて、
http://localhost:8080
のSpring Cloud Gatewayを直接実行しました。
GraphQL
http://localhost:8080/graphql にアクセスして、APIを実行してみます。
こちらも正常に実行されることを確認できました。
おわりに
Spring Cloud GatewayとGraphQL Meshを使って、既存マイクロサービスにBFFを追加することで、RESTインタフェースとGraphQLインタフェースを両方簡単に対応することができました。実際にBFFを運用する場合は、認証・認可もBFF側で行う想定でして、Spring Security + Spring Sessionを利用して対応することもできると思いますので、汎用的な使い方もできると思いました。
当社では、トヨタ車のサブスク「KINTO」等の企画/開発を行っており、エンジニアを募集中です。
KINTO Technologies コーポレートサイト