KongでAPI Gatewayを試す


# Kongってなに？

https://jp.konghq.com/

マルチクラウド環境でのマイクロサービスやAPIを１つのエントリポイントで管理運用することができる、APIゲートウェイプラットフォームです。

Amazon API GatewayやGoogleのCloud Endpoints、API Gatewayとかの類ですね。

ここでいうKongは主にKong Gatewayのことを指していますが、他にもKong Meshとか色々あります。

内部にはNginx/Cassandra/PostgreSQLとかが使われている。

Enterprise版とOSS版があるのですが、ここではOSS版を利用していきます。

## 準備

OSS版を各種環境でインストールする方法があります。

https://konghq.com/install/#kong-community

macOSの場合

```bash
$ brew tap kong/kong
$ brew install kong
$ kong version
2.7.0

$ mkdir kong
$ kong config init
```

いったんDB-less modeにします。

```bash
$ export KONG_DATABASE=off
$ kong start
Kong started
$ kong stop
Kong stopped
```

ulimitで警告がでるようならulimit -n 4096などで変更しておきます。

ex) dockerの場合

```bash
$ git clone https://github.com/Kong/docker-kong
$ cod docker-kong/compose
$ docker-compose up
```

### 構成

APIのアプリケーションを3つ用意してKong Gatewayを利用します。

```
internet
└── kong-gateway (:8000, :8001)
   ├── node-web (:3002)
   ├── rails-web (:3000)
   └── rust-web (:3001)

```



### RailsでAPI作成

```bash
$ bundle exec rails new rails-web -T -d sqlite3 --api
$ bundle exec rails g scaffold User name:string email:string
```

```bash
$ vim db/seeds.rb
3.times { User.create name: Faker::Name.name, email: Faker::Internet.email }

$ bundle exec rails db:migrate db:seed
```

レスポンスを確認

```bash
$ bundle exec rails foreman start
$ curl -s http://localhost:3000/users | jq
```

```json
[
  {
    "id": 1,
    "name": "Augustine Leannon",
    "email": "lang.heller@hudson.co",
    "created_at": "2021-12-25T07:13:32.878Z",
    "updated_at": "2021-12-25T07:13:32.878Z"
  },
  {
    "id": 2,
    "name": "Octavio Monahan",
    "email": "thaddeus@bauch.org",
    "created_at": "2021-12-25T07:13:32.883Z",
    "updated_at": "2021-12-25T07:13:32.883Z"
  },
  {
    "id": 3,
    "name": "Elsie Thiel",
    "email": "philip.barton@mayer.net",
    "created_at": "2021-12-25T07:13:32.887Z",
    "updated_at": "2021-12-25T07:13:32.887Z"
  }
]
```



### RustでAPI作成

```bash
$ cargo new rust-web
```

```toml:Cargo.toml
[dependencies]
actix-web = "3.3.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
```

```rust:src/main.rs
use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Post {
    title: String,
    content: String,
}

#[get("/posts")]
async fn index_post() -> impl Responder {
    HttpResponse::Ok().json(Post {
        title: String::from("post title"),
        content: String::from("post description content"),
    })
}

#[post("/posts")]
async fn create_post(post: web::Json<Post>) -> impl Responder {
    println!("{:?}", post);
    HttpResponse::Ok().body("ok")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new().service(index_post).service(create_post))
        .bind("127.0.0.1:3001")?
        .run()
        .await
}
```

起動してレスポンスを確認する

```bash
$ cargo run
$ curl -s http://localhost:3001/posts | jq
```

```json
{
  "title": "post title",
  "content": "post description content"
}
```



### NodeでAPI作成

```bash
$ mkdir node-web
$ cd node-web
$ yarn init
$ yarn add express
```

```js:app.js
const express = require('express')
const app = express()
app.get('/messages', (req, res) => {
  const content = [
    { subject: 'Unforgiven', body: `Here's looking at you, kid.` },
    { subject: 'The Princess Bride', body: 'I love the smell of napalm in the morning.' },
    { subject: 'The Third Man', body: 'You talking to me?' },
  ]
  res.json(content)
})

app.listen(3002, () => console.log('Listening on port 3002'))

```

レスポンスを確認

```bash
$ node app.js
$ curl -s http://localhost:3002/messages | jq
```

```json
[
  {
    "subject": "Unforgiven",
    "body": "Here's looking at you, kid."
  },
  {
    "subject": "The Princess Bride",
    "body": "I love the smell of napalm in the morning."
  },
  {
    "subject": "The Third Man",
    "body": "You talking to me?"
  }
]
```



### kongの設定

先程kong initしたときにできたkong.ymlを編集します。

```yml:kong.yml
services:
  - name: rails-web
    url: http://localhost:3000
    routes:
      - name: user-routes
        paths:
          - /api/rails

  - name: rust-web
    url: http://localhost:3001
    routes:
      - name: post-routes
        paths:
          - /api/rust
  - name: node-web
    url: http://localhost:3002
    routes:
      - name: message-routes
        paths:
          - /api/node

```

kong.ymlと同じ場所にkong.confを作成します。

```config:kong.conf
database = off
declarative_config = ./kong.yml
```

kongを起動します。

```bash
$ kong start -c kong.conf
Kong started
```

それぞれ設定したURLにkong経由でアクセスしてみます。

```bash
$ curl -s http://localhost:8000/api/rails/users | jq
```
```json
[
  {
    "id": 1,
    "name": "Augustine Leannon",
    "email": "lang.heller@hudson.co",
    "created_at": "2021-12-25T07:13:32.878Z",
    "updated_at": "2021-12-25T07:13:32.878Z"
  },
  {
    "id": 2,
    "name": "Octavio Monahan",
    "email": "thaddeus@bauch.org",
    "created_at": "2021-12-25T07:13:32.883Z",
    "updated_at": "2021-12-25T07:13:32.883Z"
  },
  {
    "id": 3,
    "name": "Elsie Thiel",
    "email": "philip.barton@mayer.net",
    "created_at": "2021-12-25T07:13:32.887Z",
    "updated_at": "2021-12-25T07:13:32.887Z"
  }
]

```

```bash
$ curl -s http://localhost:8000/api/rust/posts | jq
```
```json
{
  "title": "post title",
  "content": "post description content"
}

```

```bash
$ curl -s http://localhost:8000/api/node/messages | jq
```
```json
[
  {
    "subject": "Unforgiven",
    "body": "Here's looking at you, kid."
  },
  {
    "subject": "The Princess Bride",
    "body": "I love the smell of napalm in the morning."
  },
  {
    "subject": "The Third Man",
    "body": "You talking to me?"
  }
]

```

それぞれKong Gatewayを通してAPIが取得できました。

### プラグインを設定してみる

https://docs.konghq.com/hub/

色々ありますので今回は以下のプラグインを設定してみます。中にはEnterpriseでないと利用できないものもあるのでOSSでの利用ができそうなものをピックアップしました。

- Authentication
  - JWT
- Traffic Control
  - Proxy Cache
  - Rate Limitting

#### キャッシュの設定

kong.yml

```yml:kong.yml
plugins:
  - name: proxy-cache
    service: node-web
    config:
      response_code:
        - 200
      request_method:
        - GET
        - HEAD
      content_type:
        - text/plain
        - application/json
      cache_ttl: 300
      strategy: memory
```

```bash
$ curl -i http://localhost:8000/api/node/messages
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 204
Connection: keep-alive
X-Cache-Key: 7c55750e1a2f5e73df7a0d9bf9892880
X-Cache-Status: Bypass
X-Powered-By: Express
ETag: W/"cc-gw3x3v8iu3ri0ITrNa7RwN1YRnI"
Date: Sat, 25 Dec 2021 09:00:22 GMT
X-Kong-Upstream-Latency: 1
X-Kong-Proxy-Latency: 2
Via: kong/2.7.0

[{"subject":"Unforgiven","body":"Here's looking at you, kid."},{"subject":"The Princess Bride","body":"I love the smell of napalm in the morning."},{"subject":"The Third Man","body":"You talking to me?"}]
```

X-Cache-* のあたりが追加されています。


#### レートリミットの設定

```yml:kong.yml
plugins:
  - name: rate-limiting
    service: rust-web
    config:
      second: 5 # 秒間5回
      minute: 10 # 分間10回
      hour: 10000 # 1時間10,000回
      policy: local
      fault_tolerant: true
      hide_client_headers: false
      redis_ssl: false
      redis_ssl_verify: false
```

初回アクセス時

```
$ curl -i http://localhost:8000/api/rust/posts
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 59
Connection: keep-alive
RateLimit-Limit: 5
RateLimit-Remaining: 4
RateLimit-Reset: 1
X-RateLimit-Limit-Second: 5
X-RateLimit-Limit-Minute: 10
X-RateLimit-Limit-Hour: 10000
X-RateLimit-Remaining-Hour: 9999
X-RateLimit-Remaining-Second: 4
X-RateLimit-Remaining-Minute: 9
date: Sat, 25 Dec 2021 09:07:08 GMT
X-Kong-Upstream-Latency: 1
X-Kong-Proxy-Latency: 2
Via: kong/2.7.0

{"title":"post title","content":"post description content"}
```

制限がかかった状態

```
curl -i http://localhost:8000/api/rust/posts
HTTP/1.1 429 Too Many Requests
Date: Sat, 25 Dec 2021 09:07:36 GMT
Content-Type: application/json; charset=utf-8
Connection: keep-alive
RateLimit-Limit: 10
RateLimit-Remaining: 0
RateLimit-Reset: 24
Retry-After: 24
X-RateLimit-Limit-Second: 5
X-RateLimit-Limit-Minute: 10
X-RateLimit-Limit-Hour: 10000
X-RateLimit-Remaining-Hour: 9990
X-RateLimit-Remaining-Second: 5
X-RateLimit-Remaining-Minute: 0
Content-Length: 41
X-Kong-Response-Latency: 1
Server: kong/2.7.0

{
  "message":"API rate limit exceeded"
}
```



#### JWTの設定

```yml:kong.yml
plugins:
  - name: jwt
    service: rails-web
    config:
      secret_is_base64: false
      run_on_preflight: true
consumers:
  - username: hime-chan
    custom_id: abcd123
jwt_secrets:
  - consumer: hime-chan
    algorithm: HS256
    key: key1234
    secret: hogehoge1234
```
keyやsecretは`$ head /dev/urandom | hexdump | sha256sum`とかで生成しておくとよいです。
DBを利用しているならAPIからユーザを作成できます(今回はDB-lessなので必要ないです)

```bash
$ curl -d "username=hime-chan&custom_id=abcd123" http://localhost:8001/consumers
```

設定した情報の確認

```bash
$ curl -s http://localhost:8001/consumers/hime-chan/jwt | jq
```
```json
{
  "data": [
    {
      "key": "key1234",
      "created_at": 1640426398,
      "rsa_public_key": null,
      "algorithm": "HS256",
      "secret": "hogehoge1234",
      "tags": null,
      "consumer": {
        "id": "b84d0924-e7b6-5c17-bfb3-c4eacce71cca"
      },
      "id": "d6f2fe3c-d64a-5577-beac-d35e04b23d8b"
    }
  ],
  "next": null
}
```

ヘッダにJWTがない状態でアクセス

```bash
$ curl -i http://localhost:8000/api/rails/users
HTTP/1.1 401 Unauthorized
Date: Sat, 25 Dec 2021 09:29:56 GMT
Content-Type: application/json; charset=utf-8
Connection: keep-alive
Content-Length: 26
X-Kong-Response-Latency: 1
Server: kong/2.7.0

{"message":"Unauthorized"}
```

適当なJWTでアクセス

```bash
curl -i http://localhost:8000/api/rails/users -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'
HTTP/1.1 401 Unauthorized
Date: Sat, 25 Dec 2021 10:03:48 GMT
Content-Type: application/json; charset=utf-8
Connection: keep-alive
Content-Length: 42
X-Kong-Response-Latency: 0
Server: kong/2.7.0

{"message":"No mandatory 'iss' in claims"}
```

https://jwt.io でTokenを作成します。

payloadに

```json
{
  "iss": "key1234",
  "name": "Hanako",
  "user_id": 100
}
```

VERIFY SIGNATUREの部分に設定したsecret `hogehoge1234`を設定して生成します。


作成したTokenをつけてAPI呼び出し

```bash
$ curl -i http://localhost:8000/api/rails/users -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJrZXkxMjM0IiwibmFtZSI6IkhhbmFrbyIsInVzZXJfaWQiOjEwMH0.roghKCqrODUgni7MM8RM6XU-9lpozDysctKqWkhgOXE'
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 0
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: strict-origin-when-cross-origin
Vary: Accept
ETag: W/"f3fb8adbc998b02efefb6493f1f588d7"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: e6fce311-e8d4-4d56-b14f-bc167be07e1d
X-Runtime: 0.019405
Server-Timing: start_processing.action_controller;dur=0.11962890625, sql.active_record;dur=0.701904296875, instantiation.active_record;dur=0.044921875, process_action.action_controller;dur=1.932861328125
X-Kong-Upstream-Latency: 25
X-Kong-Proxy-Latency: 2
Via: kong/2.7.0

[{"id":1,"name":"Augustine Leannon","email":"lang.heller@hudson.co","created_at":"2021-12-25T07:13:32.878Z","updated_at":"2021-12-25T07:13:32.878Z"},{"id":2,"name":"Octavio Monahan","email":"thaddeus@bauch.org","created_at":"2021-12-25T07:13:32.883Z","updated_at":"2021-12-25T07:13:32.883Z"},{"id":3,"name":"Elsie Thiel","email":"philip.barton@mayer.net","created_at":"2021-12-25T07:13:32.887Z","updated_at":"2021-12-25T07:13:32.887Z"}]

```



他に気になったプラグインは

- Key Authentication
- OAuth 2.0 Authentication
- Session
- ACL
- IP Restriction
- Datadog
- Prometheus
- GraphQL
- gRPC
- Logging関連

。。。いっぱいあるなぁ

## 所感

とにかく色々な会社で開発されたシステムがそれぞれ提供する独自のAPIを一元管理できるようになるのは大きなところだと思います。
認証一つとってもそれぞれのAPIで実装するのではなくKongで吸収できるという効率化もはかれそう。モニタリングやロギングも管理できますね。
プラグインも多数あり、これはOSSというコミュニティの特色なのかも。

kongのプラグインはLuaやGoで書けるので、利用するサービスにあったプラグインがなければ自前で作ることも可能ですね。

ただ、大手プロバイダによるマネージドサービスではないので、そういった部分ではProduction環境でのコストはかかりそう。
digitaloceanに構築したり、Dockerのコンテナとして実行する、Kubernetesで運用する、など色々と工夫が必要そうです。

今後はKong Meshなんかも試してみたいです。