Kong Ingress ControllerでKongの設定を管理する
これは日立グループ OSS Advent Calendar 2019の20日目の記事。
Kubernetesクラスタ上で動くKongの設定管理にKong Ingress Controllerを使う、というのを試してみた話。
Kongとは、の説明から、Kong Ingress Controllerを実際に動かしてみるところまで書く。
API Gatewayパターン
私が担当しているIT運用最適化サービスのAI for IT Operationsというサービスプラットフォームは、マイクロサービスアーキテクチャを採用していて、Kubernetesの上で動く。
商品形態としては、基本的にはお客様毎に別々のKubernetesクラスタを作り、お客様毎にアプリケーションのデプロイ設定をチューニングし、Helmでデプロイして提供するというものになっている。
このプラットフォーム上では、様々なビジネスドメインのアプリケーションが動き、それぞれがREST APIやWeb GUIを提供するんだけど、それらの独立性を保ちつつ、統一的なユーザ体験を提供したり、リソース消費を抑えたり、重複開発を防いだりする工夫をアーキテクチャに施している。
その一つがAPI Gatewayパターン。
このパターンでは、ユーザからの全リクエストをひとつのマイクロサービスで受け付けることで、認証やTLS終端といった共通的な処理を集約したり、内部のマイクロサービスのAPIを隠蔽したりする。
AI for IT OperationsではAPI GatewayにKongを使っている。
Kong
KongはKong社によるOSSのリバースプロキシ。
(Enterprise版もある。)
GitHubで2万4000以上のスターを集めている人気なOSSで、コミュニティが大きく開発も活発に行われている。
OpenRestyというnginxとLua言語によるWebプラットフォームを使って作られていて、高機能で高性能で柔軟で拡張性に富んでいる。
ルーティングやアクセス制御など、ほとんどの設定はPostgreSQLやCassandraのDBに保存され、KongのREST API(Admin API)で管理できる。
(Kong 1.1.0からは宣言的な設定ファイルによるDBレスモードもサポートされている。)
Admin APIで管理できる主なリソースにはService、Route、Pluginがある。
ServiceはKongの裏で動いて実際にAPIを実装するバックエンドサービスを表し、主にリクエストのルーティング先を設定するために使う。
RouteはひとつのServiceに紐づくリソースで、主にそのServiceにルーティングするリクエストの条件を設定するために使う。
PluginはServiceやRouteに紐づけて、それらによってルーティングされるリクエストやそのレスポンスに様々な制御を設定するために使う。
PluginはKong社やコミュニティによっていろいろなものが提供されていて、ベーシック認証、LDAP連携、アクセス制御リスト、Prometheus連携、レスポンスの加工など、いろいろできる。
PluginもLua製なので、既存のPluginをちょっと改造ということもできるし、新しいのを作るのも割と簡単にできるようになっている。
Kongの設定処理における課題
AI for IT Operationsでは、KongのデータストアにはPostgreSQLを使い、それらをKubernetesクラスタにDeploymentでデプロイし、NodePortのServiceをKong Podに紐づけ、ユーザからのリクエストをKongで受けられるようにしている。
前述したように、お客様毎にデプロイ設定をチューニングする必要があり、つまりKongの設定もお客様毎に異なる。
そのため、可変部分をHelmのChart変数にしておいて、デプロイ時にKubernetesのJobを実行し、Kongの初期設定をするようにしている。
JobにはKongのAdmin APIを呼ぶ処理を書くわけだけど、その記述が命令的になってしまっているのが改善したい点。
Kubernetesの宣言的な世界観とずれているし、設定の変更や削除のための作りこみがやや煩雑になるという課題がある。
IngressとIngress Controller
そこの改善に使えるKubernetesの機能がIngressとIngress Controllerというもの。
IngressはL7のロードバランサとか、HTTP(S)のアクセスポイントを公開する仕組みとか、ふんわりした表現で説明されることが多いけど、端的に言ってしまえば、リバースプロキシの設定を表現するKubernetesリソースだ。
kind: IngressなAPIがKubernetesにビルトインされていて、そのマニフェストには汎用的なリバースプロキシの設定を書けて、それをKubernetesに登録できるようになっている。
ただ、Kubernetes自体はリバースプロキシの機能を備えているわけではないので、Ingressリソースを登録してもそれだけでは何もおこらない。
Ingressリソースを活用するにはIngress Controllerが必要になる。
Ingress Controllerは、Ingressリソースの作成や変更を監視するコントローラサービスと、そのリソース定義に従って動くリバースプロキシを組み合わせたもの。
このリバースプロキシのアクセスポートは、NodePortタイプかLoadBalancerタイプのServiceによってKubernetesクラスタの外部に公開して、ユーザからアクセスできるようにする。
Ingress Controllerをデプロイし、Ingressリソースを登録することで、ユーザからのリクエストをリバースプロキシで受けて捌くことができるようになる。
因みにLoadBalancer Serviceは、Kubernetesクラスタ外部のロードバランサとNodePort Serviceを制御して、ユーザからロードバランサへのリクエストをNodePort Service経由でいい感じにPodにルーティングしてくれるもの。
つまり、外部のロードバランサとそれを制御するコントローラサービスがなければ使えないServiceで、普通はGKEとかのマネージドKubernetes環境でしか使わない。
(MetalLBとか使えばオンプレでも使えるけど。)
Kong Ingress Controller
Ingress Controllerには、リバースプロキシの実装ごとにさまざまな実装がある。
そのひとつがKong Ingress Controller。
Kong Ingress Controllerはリバースプロキシとして(当然ながら)Kongを使うもので、そのコントローラサービスはIngressリソースの定義を読んで、その通りの設定になるようにKongのAdmin APIを呼ぶ。
Ingressリソースはリバースプロキシの汎用的な定義なので、高機能なKongの設定を表現するには全然足りない。
なので、Kong Ingress Controllerは足りない分をCustom Resourcesで補っている。
Custom Resourceは簡単に言えばKubernetesのAPIの拡張。
Kong Ingress ControllerはKongIngress、KongPluginなどのCustom Resource(詳細は後述)を提供し、Kongの細かい設定を表現できるようにしている。
Kong Ingress Controllerを使えば、Kongの設定をKubernetesのAPIで宣言的にできるようになるので、前述した課題が解消できる。
Kong Ingress Controllerが扱うKubernetesリソース
Kong Ingress Controllerが扱うIngressリソースやCustom Resourceについてまとめる。
- 
Ingress
Kubernetesの組み込みリソース。
ルーティングするリクエストの条件と、ルーティング先の(Kubernetesの)Serviceなどを定義する。
Kong Ingress Controllerはその定義からKongのRouteとServiceを設定する。 - 
KongIngress
Kong Ingress ControllerのCustom Resource。
Ingressと組み合わせて使い、Ingressだけでは表現できないRoute設定を記述する。
Ingressのannotationにconfiguration.konghq.comというキーでKongIngressの名前を指定することで紐づけられる。 - 
KongPlugin
Kong Ingress ControllerのCustom Resource。
KongのPlugin設定を定義するためのもの。
IngressかServiceのannotationにplugins.konghq.comというキーでKongPluginの名前を指定することで紐づけられる。
global: "true"というラベルを付けたKongPluginを作れば、そのPluginは全リクエストに適用される。 
他にKongConsumerとKongCredentialという認証情報を扱うCustom Resourceがある。
けど今回使わないので詳細は省略する。
Kong Ingress Controllerの公式マニフェストを読む
Kong Ingress Controller 0.6.2の公式のマニフェストには以下のリソースが定義されている。
- 
Namespace
kongという名前の名前空間。以下のすべてのリソースが属する。 - 
CustomResourceDefinition (4つ)
KongIngress、KongPluginなどのCustom Resourceの定義。
 - 
ClusterRole
Node、Pod、Ingress、Serviceなどのリソースや、上記Custom Resourceの参照権限等を与えるロール。
 - 
ServiceAccount
コントローラサービスに上記ClusterRoleを紐づけるためのアカウント名。
 - 
ClusterRoleBinding
コントローラサービスと上記ClusterRoleを紐づける定義。
 - 
ConfigMap
Kong(のnginx)にメトリクス取得やヘルスチェックのAPIを追加する定義ファイル。
 - 
Service (kong-proxy)
Kongのアクセスポートを外部公開するためのLoadBalancerタイプのService。
 - 
Service (kong-validation-webhook)
上記Custom Resourceの登録や変更時にバリデーションをできるようにするために、コントローラサービスのポートをクラスタ内部(のkube-apiserver)に公開するClusterIPタイプのService。
 - 
Deployment
Kongとコントローラサービスを起動する定義。
KongはDBレスモードで動かすようになっている。 
なんだかたくさんのリソースがあるけど、ひとつひとつ見ていくとそれほど難しくない。
Kong Ingress Controllerのデプロイ
いざデプロイ。
デプロイ先のKubernetesクラスタは最近CentOS 8でつくったやつで、Kubernetesのバージョンは1.16.0。
Kong Ingress Controllerは最新版の0.6.2。
Kong Ingress Controller公式のマニフェストは、kong-proxyというServiceがLoadBalancerタイプで使いにくいので、今回はNodePortに変える。
また、DeploymentのapiVersionがextensions/v1beta1になっていて古く、Kubernetes 1.16にデプロイできないので、apps/v1に直す。
修正差分は↓こんな感じ。
@@ -448,17 +448,18 @@
   ports:
   - name: proxy
     port: 80
     protocol: TCP
     targetPort: 8000
+    nodePort: 30080
   - name: proxy-ssl
     port: 443
     protocol: TCP
     targetPort: 8443
   selector:
     app: ingress-kong
-  type: LoadBalancer
+  type: NodePort
 ---
 apiVersion: v1
 kind: Service
 metadata:
   name: kong-validation-webhook
@@ -470,11 +471,11 @@
     protocol: TCP
     targetPort: 8080
   selector:
     app: ingress-kong
 ---
-apiVersion: extensions/v1beta1
+apiVersion: apps/v1
 kind: Deployment
 metadata:
   labels:
     app: ingress-kong
   name: ingress-kong
Kongのアクセスポートを外部公開するNodePortのポート番号は30080にしてある。
修正したマニフェストをkubectl applyしたら普通に起動した。
$ kubectl get po -n kong
NAME                            READY   STATUS    RESTARTS   AGE
ingress-kong-65fffbc76b-gvqmf   2/2     Running   2          10m
Kongコンテナが立ち上がらないとコントローラサービスが起動中に落ちるので、最初数回CrashLoopBackOffするのがちょっとダサい。
Kongのアクセスポート(i.e. NodePort)に向かってGETリクエストを送ると、まだKongに何のルーティング設定もないのでno Route matched with those valuesというメッセージが返ってくる。
(KubernetesノードのIPアドレスは192.168.1.200)
$ NODE_IP=192.168.1.200
$ curl http://${NODE_IP}:30080/
{"message":"no Route matched with those values"}
ともあれ、動いていることは確認できた。
Ingressを試す
Ingressを登録して、Kongの設定を作ってみる。
まず、Kongのルーティング先のバックエンドサービスとして、リクエストの内容を返してくるだけのechoサービスをデプロイするため、以下のマニフェストをkubectl applyする。
apiVersion: v1
kind: Service
metadata:
  name: echo
spec:
  selector:
    app: echo
  ports:
  - port: 8080
    protocol: TCP
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: echo
  template:
    metadata:
      labels:
        app: echo
    spec:
      containers:
      - image: gcr.io/kubernetes-e2e-test-images/echoserver:2.2
        name: echo
        ports:
        - containerPort: 8080
動いた。
$ kubectl get po
NAME                    READY   STATUS    RESTARTS   AGE
echo-588b79f67f-xxn56   1/1     Running   0          14h
このPodはechoというService(以下echoサービス)に紐づいていて、そのServiceのポートは8080に設定されている。
試しにそのServiceにGETリクエストを投げてみる。
$ curl http://$(kubectl get svc echo -o jsonpath='{.spec.clusterIP}'):8080
Hostname: echo-588b79f67f-xxn56
Pod Information:
        -no pod information available-
Server values:
        server_version=nginx: 1.12.2 - lua: 10010
Request Information:
        client_address=10.32.0.1
        method=GET
        real path=/
        query=
        request_version=1.1
        request_scheme=http
        request_uri=http://10.0.185.110:8080/
Request Headers:
        accept=*/*
        host=10.0.185.110:8080
        user-agent=curl/7.61.1
Request Body:
        -no body in request-
ちゃんとレスポンス返ってきた。
このechoサービスへルーティングするIngressを作るため、以下のマニフェストをkubectl applyする。
/echoにアクセスするとechoサービスの8080ポートに送るという内容。
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: echo
spec:
  rules:
  - http:
      paths:
      - path: /echo
        backend:
          serviceName: echo
          servicePort: 8080
これでKongにechoサービスを表すServiceとそこへのRouteが設定されたはず。
Kongコンテナの8444ポートでAdmin APIが公開されていて、そこにアクセスするとKongの設定が見れるので見てみる。
$ kubectl exec -it -n kong ingress-kong-65fffbc76b-gvqmf -c proxy -- curl -k https://localhost:8444/routes | jq
{
  "next": null,
  "data": [
    {
      "strip_path": true,
      "tags": null,
      "updated_at": 1575845092,
      "destinations": null,
      "headers": null,
      "protocols": [
        "http",
        "https"
      ],
      "created_at": 1575845092,
      "snis": null,
      "service": {
        "id": "dac20c80-66d8-59e2-8f57-66e046e18d45"
      },
      "name": "default.echo.00",
      "preserve_host": true,
      "regex_priority": 0,
      "id": "e97bc643-59d4-53c1-83e2-87169eaa28d5",
      "sources": null,
      "paths": [
        "/echo"
      ],
      "https_redirect_status_code": 426,
      "methods": null,
      "hosts": null
    }
  ]
}
$ kubectl exec -it -n kong ingress-kong-65fffbc76b-gvqmf -c proxy -- curl -k https://localhost:8444/services | jq
{
  "next": null,
  "data": [
    {
      "host": "echo.default.svc",
      "created_at": 1575845092,
      "connect_timeout": 60000,
      "id": "dac20c80-66d8-59e2-8f57-66e046e18d45",
      "protocol": "http",
      "name": "default.echo.8080",
      "read_timeout": 60000,
      "port": 80,
      "path": "/",
      "updated_at": 1575845092,
      "client_certificate": null,
      "tags": null,
      "write_timeout": 60000,
      "retries": 5
    }
  ]
}
できてた。
KongのNodePortの/echoにアクセスしてみる。
$ NODE_IP=192.168.1.200
$ curl http://${NODE_IP}:30080/echo
Hostname: echo-588b79f67f-xxn56
Pod Information:
        -no pod information available-
Server values:
        server_version=nginx: 1.12.2 - lua: 10010
Request Information:
        client_address=10.32.0.5
        method=GET
        real path=/
        query=
        request_version=1.1
        request_scheme=http
        request_uri=http://192.168.1.200:8080/
Request Headers:
        accept=*/*
        connection=keep-alive
        host=192.168.1.200:30080
        user-agent=curl/7.61.1
        x-forwarded-for=10.32.0.1
        x-forwarded-host=192.168.1.200
        x-forwarded-port=8000
        x-forwarded-proto=http
        x-real-ip=10.32.0.1
Request Body:
        -no body in request-
Kong経由でechoサービスにアクセスできた模様。
Request HeadersにKongが追加したであろうx-forwarded-forとかがあるのがわかる。
Request Informationを見ると、real pathが/になっている。
これは、KongのRouteのstrip_path設定がデフォルトでtrueなので、curlで送ったURLパスの/echoをKongが切り捨てるため。
この設定はIngressでは変えられないので、そういうのを変えたい場合にはKongIngressが必要になる。
KongIngressを試す
前節で作ったRouteのstrip_pathをfalseにすべく、KongIngressを作ってみる。
まず以下のKongIngressのマニフェストをkubectl applyする。
apiVersion: configuration.konghq.com/v1
kind: KongIngress
metadata:
  name: keep-path
route:
  strip_path: false
で、このKongIngressを前節で作ったIngressに紐づけるため、次のコマンドでconfiguration.konghq.comというannotationをIngressに追加する。
$ kubectl patch ingress echo -p '{"metadata":{"annotations":{"configuration.konghq.com":"keep-path"}}}'
これでKongのRoute設定のstrip_pathが変わったはず。
見てみる。
$ kubectl exec -it -n kong ingress-kong-65fffbc76b-gvqmf -c proxy -- curl -k https://localhost:8444/routes | jq '.data[].strip_path'
false
ちゃんとfalseになっている。
KongのNodePortの/echoにアクセスしてみる。
$ NODE_IP=192.168.1.200
$ curl -s http://${NODE_IP}:30080/echo | grep 'real path'
        real path=/echo
送ったURLパスが切り捨てられず、real pathが/echoになるようになった。
KongPluginを試す
最後にKongPluginでCorrelation IDプラグインを有効にしてみる。
これは、適用されたリクエストのHTTPヘッダに、リクエスト毎にユニークなUUIDを付けるプラグイン。
プラグインを有効にする対象としてServiceやRouteなどを指定することができるが、今回は全リクエストに適用するglobalにする。
以下のマニフェストをkubectl applyする。
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
  name: correlation-id
  labels:
    global: "true"
config:
  header_name: Global-Request-ID
plugin: correlation-id
labelsにglobal: "true"を設定するのがポイント。
これでKongにPlugin設定が作られるはず。
KongのAdmin APIでPluginを取得して確認してみる。
$ kubectl exec -it -n kong ingress-kong-65fffbc76b-gvqmf -c proxy -- curl -k https://localhost:8444/plugins | jq
{
  "next": null,
  "data": [
    {
      "created_at": 1575852780,
      "config": {
        "echo_downstream": false,
        "header_name": "Global-Request-ID",
        "generator": "uuid#counter"
      },
      "id": "65d138a5-b9b3-5559-84d6-c459f3cc456a",
      "service": null,
      "enabled": true,
      "tags": null,
      "consumer": null,
      "run_on": "first",
      "name": "correlation-id",
      "route": null,
      "protocols": [
        "grpc",
        "grpcs",
        "http",
        "https"
      ]
    }
  ]
}
できてた。
実際にリクエストを送ってみる。
$ NODE_IP=192.168.1.200
$ curl -s http://${NODE_IP}:30080/echo
Hostname: echo-588b79f67f-xxn56
Pod Information:
        -no pod information available-
Server values:
        server_version=nginx: 1.12.2 - lua: 10010
Request Information:
        client_address=10.32.0.5
        method=GET
        real path=/echo
        query=
        request_version=1.1
        request_scheme=http
        request_uri=http://192.168.1.200:8080/echo
Request Headers:
        accept=*/*
        connection=keep-alive
        global-request-id=caad53f7-2e95-483d-8515-79a45b6a52d3#2
        host=192.168.1.200:30080
        user-agent=curl/7.61.1
        x-forwarded-for=10.32.0.1
        x-forwarded-host=192.168.1.200
        x-forwarded-port=8000
        x-forwarded-proto=http
        x-real-ip=10.32.0.1
Request Body:
        -no body in request-
Request Headersにglobal-request-idとしてUUIDが挿入されるようになった。
今回のまとめ
KubernetesクラスタにKong Ingress Controllerをデプロイし、Ingress、KongIngress、KongPluginによりKongの設定ができることを確認した。
Kong Ingress Controllerを使えば、Kongの設定をKubernetesのAPIで宣言的に管理できるようになり、いろいろ捗りそう。

