Spring Cloud GatewayとGraphQL Meshで、既存マイクロサービスのBFF (REST+GraphQL) を簡単に構築できる。


[KINTO Technologies Advent Calendar 2021 - Qiita](https://qiita.com/advent-calendar/2021/kinto-technologies) の24日目の記事です。


# はじめに

本記事では、マイクロサービスのBFF(Backend For Frontend)の実装方式を検討した時にPoCで行ったGraphQL Meshの構築例を記載しています。


# この記事で伝えたいこと

RESTインタフェースベースのマイクロサービスに、Spring Cloud GatewayとGraphQL Meshを利用すれば、既存RESTインタフェースに加えてGraphQLインタフェースにも対応できるBFFがどれほど簡単に構築できるかについて共有できればと思います。

#### システム概要図

![API_GATEWAY概要図6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62308/0168b929-0de4-1e8c-b9d7-147222c30655.png)


# なぜSpring Cloud Gateway?

- Spring Boot

現在マイクロサービスのバックエンドがSpring Bootを使っており、BFFもSpringエコシステムを使って構築するつもりでしたので、Spring Cloud系のフレームワークを使うことにしました。

- マルチクラウド問題

BFFの役割として、AWSのマネジメントサービスも検討していましたが、サービスの海外展開でマルチクラウド環境に対応する必要もあり、コンテナベースでAPI Gatewayを構築することにしました。

- 認証・認可のカスタマイズ

BFFで認証・認可も行う予定でして、海外展開で各国のシステムに合わせてカスタマイズの必要性もあり、標準＋柔軟な対応ができる構成にしたかったです。Spring Security等のエコシステムを利用できることもメリットになると思いました。

https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/

# なぜGraphQL Mesh?

- API Composition問題

システムがマイクロサービスになり、各APIレスポンスをマージするユースケースが想定されていて、API Composition方針を検討しましたが、既存REST APIではユースケースごとのAPIを新しく作成する必要がありました。それでAPI Compositionはもちろん、リクエストやレスポンスの柔軟な対応ができるGraphQLインタフェースを検討し、GraphQL Meshを利用することにしました。

- 構築が一番簡単

GraphQLインタフェースで既存マイクロサービスをラップする方法は多くあると思いますが、その中でGraphQL Meshはopenapiの設定だけで構築できて一番楽でした。

https://www.graphql-mesh.com/docs/recipes/as-gateway


# 実装

### Microservice

マイクロサービスは、OpenAPI Generatorを使ってAPI定義のYAMLファイルからソースコードを出力する想定です。今回の検証では、SubscriptionとVehicleというマイクロサービスをGETメソッドのみ実装することにします。
※本記事ではBFFの実装がメインですので、マイクロサービスの詳細実装内容については割愛します。

- OpenAPI設定

<details><summary>Subscription Microservice</summary><div>

```subscription.yml
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
```
</div></details>

<details><summary>Vehicle Microservice</summary><div>

```vehicle.yml
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
```
</div></details>

https://github.com/OpenAPITools/openapi-generator


### Spring Cloud Gateway

Spring Initializrのデフォルトソースベースで、以下の設定のみ変更します。

https://start.spring.io/


- 設定

````build.gradle
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

```Dockerfile
FROM hiroyukiosaki/graphql-mesh:latest-all-alpine
COPY .meshrc.yaml ./.meshrc.yaml
COPY openapi.yml ./openapi.yml
```

- OpenAPI設定

<details><summary>Microservice API</summary><div>

```openapi.yml
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

```
</div></details>


※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です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62308/b018b160-26a0-fd31-58de-b6633057fd4e.png)



# 結果

### 実行環境

|アプリケーション  |実行環境  |ポート  |
|---|---|---|
|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を直接実行しました。

https://editor.swagger.io/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62308/2608ed8f-08df-d10d-e287-65c39641e0bc.png)
正常に実行されることを確認できました。


### GraphQL

http://localhost:8080/graphql にアクセスして、APIを実行してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/62308/5a779ed6-17c5-ad14-12cb-976e2606be8d.png)

こちらも正常に実行されることを確認できました。



# おわりに

Spring Cloud GatewayとGraphQL Meshを使って、既存マイクロサービスにBFFを追加することで、RESTインタフェースとGraphQLインタフェースを両方簡単に対応することができました。実際にBFFを運用する場合は、認証・認可もBFF側で行う想定でして、Spring Security + Spring Sessionを利用して対応することもできると思いますので、汎用的な使い方もできると思いました。


当社では、トヨタ車のサブスク「KINTO」等の企画/開発を行っており、エンジニアを募集中です。
[KINTO Technologies コーポレートサイト](https://www.kinto-technologies.com/)