Spring Cloudでマイクロサービスを構成する（４）：API Gateway編


Spring Cloudシリーズの第4回です。

### 概要

今回は「Spring Cloud Gateway」を使ってAPI Gatewayを立てることにします。

![services.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534936/7b785297-c11d-4f81-fbe5-7146cdc50eba.png)

Spring Cloundファミリーでは従来、「[Zuul](https://github.com/Netflix/zuul/wiki)」が使われていましたが、Zuulはメンテナンスモードとなり、「WebFlux＆Nettyベースの」「Spring Gloud Gateway」が代替プロダクトとなっています。

API GatewayはAPIの前段に建てるリバースプロキシとして働きます。
API Gatewayの役割としては、クライアントとAPIの間に入り、API側に持ち込みたくない関心事を前段でさばくことにあります。

下の図は、API Gateway向けの別プロダクト「[KrakenD](https://www.krakend.io/)」のドキュメントから拝借しています。
![krakend_gateway.pnt.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534936/e4197361-3e99-675a-9c25-504b81baa82d.png)

フィルタリング、入出力の返還、APIの集約、ログや統計情報取得、認証回り（SSLを解いたり、トークンをチェックしたり付加したり）、Throtting（流量制御でいいのかな？）といったところが代表例です。

個人的には２つを重要視していて、SpirngベースのSpring Cloud Gatewayには期待しています。

* APIの前段に構えるので、パフォーマンスが良いこと。
    * 並行してアクセス数を多くさばける必要あり。
* カスタマイズ性に富んでいること。
    * API Gatewayに機能追加がしやすいこと。（やりたいことがシンプルにできる）

### 環境

以下が動く環境を前提とします。

* docker & docker-compose
* JDK11

[前回までの投稿](https://qiita.com/takayoshi-shiraki/items/11068313e1d9c6b6d4c5)の続きですので、前回のゴール地点のソースを手元に用意し、それをベースとします。


### 作業ステップ

本記事では以下のステップで進めていきます。

1. API Gatewayのひな型作成
1. 実装（アノテーションの付加と設定ファイルの記述）
1. Dockerイメージ化とdocker-componentへの追加


### ステップ1：API Gatewayのひな型作成

[Spring Initializr](https://start.spring.io/) の出番です。

**Artifact** と **Name** だけ`gateway`に指定し、あとは
**Dependencies** に`Gateway`と `Eureka Client`、`Spring Cloud LoadBalancer`、`SpringBoot Actuator`を追加しましょう。
ActuatorはGatewayに必須ではありませんが、何かと必要になるものなので追加しておきます。

| Dependencies         | 説明                    |
|:---------------------|:------------------------|
| Gateway | Spring Cloud Gateway を導入するために追加 |
| Eureka Discovery Client | Eurekaからサービスのリストを取得するために追加 |
| Spring Cloud Loadbalancer | クライアントサイド・ロードバランスのライブラリを追加 |
| Spring Boot Actuator | Spring Boot の内部情報を参照するために追加 |

![gateway.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/534936/7f572b89-f6f1-bafd-9c9c-3865f90b5b77.png)


あとは`GENERATE`するとスケルトンのgateway.zipがダウンロードできます。
　

### ステップ2：実装（アノテーションの付加と設定ファイルの記述）

今回もあまりやることがありません。２つだけ。

まず１つ目。
Applicationクラスにアノテーションを１つ付けます。
これでService Discoveryを参照できるようになります。

```java:gateway/src/main/java/com/example/gateway/GatewayApplication.java
package com.example.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient; // ★追加する

@SpringBootApplication
@EnableDiscoveryClient // ★追加する
public class GatewayApplication {

	public static void main(String[] args) {
		SpringApplication.run(GatewayApplication.class, args);
	}
}

```

やること2つ目。設定ファイル「application.yml」を書きます。
※`src/main/resources/application.properties`は不要になるので、削除してください。

```yml:gateway/src/main/resources/application.yml
server:
  port: ${PORT:5000}

spring:
  main:
    banner-mode: "off"
  application:
    name: gateway

  cloud:
    gateway:
      routes:
        - id: account
          uri: lb://ACCOUNT-API
          predicates:
            - Path=/api/**
          filters:
          - RewritePath=/api(?<segment>/?.*), $\{segment}
    loadbalancer:
      ribbon:
        enabled: false

management:
  endpoints:
    web:
      exposure:
        include: "*"

eureka:
  instance:
    prefer-ip-address: false
  client:
    registerWithEureka: false
    serviceUrl:
      defaultZone: ${DISCOVERY:http://localhost:8761/eureka}
```

`Routes`の箇所がルーティングの定義です。
今回は１つだけRouteの定義をしており、以下を指定しています。

* ServiceDiscoveryから「ACCOUNT-API」サービスのインスタンスリストを取得し、負荷分散してリクエストする。(uri)
* リクエストされたパス「/api/xxx」 に対し、アクセス先パスを「/xxx」に書き換えてサービスへリクエストする。(predicates/filters)

また、`eureka.client.serviceUrl.defaultZone` には、Service Discovery(Eurekaサーバ)のURLを指定します。


### ステップ3：Dockerイメージ化とdocker-componentへの追加

Dockerイメージを作成してdocker-composeで操れるようにしましょう。
前回のディレクトリ構造に以下のように`gateway`ディレクトリを配置します。

その上で、Dockerfileを作成し、とdocker-compose.ymlに追記します。

```text
spring-msa
├── account
│   └── ・・・
├── db
│   └── init
├── docker-compose.yml   <-- ★追記する
├── gateyway            <-- ★配置する
│   └── Dockerfile   <-- ★作成する
└── sd      
    └── ・・・
```

```text:discovery/Dockerfile
FROM openjdk:11-jdk-slim
ADD target/gateway-*.jar /gateway.jar
ENV CONTAINER_NAME=localhost \
    PORT=5000 \
    OPTS_ARGS=''

ENTRYPOINT ["java", "-jar", "/gateway.jar", "${OPTS_ARGS}"]
```

```text:docker-compose.yml
version: '3'
・・・
services:
  db:
    ・・・
  adminer:
    ・・・
  gateway:
    image: spring-msa/gateway
    container_name: gw
    build:
      context: ./gateway
      dockerfile: Dockerfile
    environment:
      - CONTAINER_NAME=gw
      - PORT=5000
      - DISCOVERY=http://sd:3001/eureka/,http://sd2:3002/eureka/
    ports:
      - "5000:5000"
  sd:
    ・・・
  account:
    ・・・
```

これで準備OK.
mvn wrapperでJARを作ったあとに、Dockerイメージを作成します。

```console
$ cd discovery
$ .mvnw clean package
$ cd ..
$ docker-compose build gw
```

これでdocker-composeでgwを起動・停止できるようになりました。

```console
(全サービスを起動)
$ docker-compose up -d
(GWの停止)
$ docker-compose stop gw
(全サービスの停止と破棄)
$ docker-compose down
(全サービスの停止とVolumeを含めた破棄)
$ docker-compose down -v
```

コンテナの5000ポートをホストの5000ポートにbindしているので、
http://localhost:5000/api/info でアクセスできます。

下記のように繰り返し/api/infoを呼ぶと、Service Discoveryが返すサービスインスタンスのリストを使って
呼び先のACCOUNT-APIが負荷分散されているのがわかると思います。
組み込まれているロードバランサのデフォルトのアルゴリズムがRoundRobinなため、順番にリクエスト先が振られている状態です。

```console
$ curl localhost:5000/api/info
{"containerName":"account","port":"9001","hostAddress":"172.18.0.9","hostName":"c1f0f98a3859"}
$ curl localhost:5000/api/info
{"containerName":"account2","port":"9002","hostAddress":"172.18.0.7","hostName":"27d40318d8bc"}
$ curl localhost:5000/api/info
{"containerName":"account3","port":"9003","hostAddress":"172.18.0.8","hostName":"7c2abf523dcf"}
$ curl localhost:5000/api/info
{"containerName":"account","port":"9001","hostAddress":"172.18.0.9","hostName":"c1f0f98a3859"}
$ curl localhost:5000/api/info
{"containerName":"account2","port":"9002","hostAddress":"172.18.0.7","hostName":"27d40318d8bc"}
$ curl localhost:5000/api/info
{"containerName":"account3","port":"9003","hostAddress":"172.18.0.8","hostName":"7c2abf523dcf"}
```



何かがおかしければdockerのログをみてください。

```console
$ docker-compose logs -f gw
```


### さいごに

ここまでのソースを以下に置いています。

https://github.com/Piecemeal-Technology-Inc/spring-msa/tree/add_gateway


ここまで来てようやく土台ができつつあります。

これからやってみたいこととしては、API Gatewayに機能追加を試すことです。
ロードバランスのアルゴリズムをカスタマイズしたり、APIに負荷をかけすぎないようにアクセス流量制御を追加してみたいところです。

と、なると、
その前にもう少し準備をしておく必要があります。

* どこのAPIサービスにリクエストが行って１リクエストの中でどこにどれだけ時間がかかっているのか。
* リクエスト数はそれぞれのサービスでどれだけさばけているのか。
* そもそも負荷をかけたときにサービスはどのくらい余裕があるのか。

などなど、アクセスのトレースや、コンテナ／サービスのモニタリングをができる状態で負荷テストツールで負荷をかけられるようにしておきたいところです。

次回からはその辺を進めていきたいと思います。

ではでは。