sshota0809のブログ

技術に関する備忘録など

Kong: DB-less and Declarative Configuration まとめ + Kubernetes 上で運用する上での Tips

TL;DR

Kong の設定あれこれを備忘録も兼ねてまとめる。

  • Kong の DB-less mode における 主にトラフィック制御に関する Configuration まとめ(v1.x 系を対象)

概要

Kong とは

Nginx ベースのオープンソースAPI Gateway。セキュリティやロギング、認証やトラフィック制御を行える。 Nginx ベースなのでパラメータチューニング等がしやすい。 プラグインがもともと豊富なので色々とやれることが多い。(もちろん自前で実装もできる。)

github.com

DB-less mode

Kong は自身の Configuration を DB に格納して管理をする DB mode と ローカルの Configuration を読み込む DB-less mode が存在する。 DB mode の場合、Kong の管理 API を通して設定を行うことになる。 DB-less mode の場合、YAML 形式で各設定を定義する。

DB の運用コストを無くしたい、Kubernetes 上で動かすことを考えるとなるべくステートレスにしたいという思いがあるので、弊社環境では DB-less mode を採用している。

ちなみに、Admin API のドキュメントは下記となる。

https://docs.konghq.com/1.3.x/admin-api/

設定

Object

Kong には Object という概念があり、各 Object の設定によってリクエストに対して最終的なルーティング先や、Plugin の適用をするか否かが一意に決定する。 トラフィック制御をする上で重要な基本的な Object は下記となる。

  • Route Object

受信したトラフィックのルーティング条件。 protocols, methods, hosts, paths, headers の条件でマッチ条件を設定可能。

  • Service Object

どの Route にマッチしたか一意に決定できた後に、そのリクエストをどの上流サーバにプロキシするかの設定。 なので、Route Object は Service Object に紐づくイメージとなる。

  • Upstream Object

仮に Kubernetes 上でルーティングを行うのであれば Service オブジェクトにおけるプロキシ先の設定は [service resource name].[name space name].cluster.local 等にしておけば、後は Kubernetes のレイヤーで各 POD への負荷分散が行われる。 しかし、Kubernetes 内のリソースではなく、VM などにプロキシするときはプロキシ先の VM 死活監視やそもそもプロキシ先の一覧を定義しなくてはならない。 それを定義できるのが Upstream Object。

Service Object には上記のようにプロキシ先の FQDN( ex. [service resource name].[name space name].cluster.local )を定義することもできるし、Upstream Object を紐付けることもできる。 Upstream Object に紐付けた場合には、紐付けた Upstream Object の定義に従ってプロキシされる。

設定例

Route + Service Object

まずは簡単な設定例として下記条件のルーティング定義を設定する。

  • Protocol: https / http, Host: hoge.example.compath: /fuga/ のリクエストをルーティング
  • fuga-svc.fuga-ns.cluster.local:80 にプロキシ
services:
- name: fuga
  host: fuga-svc.fuga-ns.cluster.local
  port:80
  protocol: http
  routes:
  - name: fuga
    paths:
    - /fuga/
    hosts: 
    - hoge.example.com
    preserve_host: true
    protocols: 
    - https
    - http

YAML で記載できるのもあって非常に設定が見やすい。 preserve_host: true の設定はドキュメントにも記載されているが、プロキシ先のホストに通信するときにリクエストとして受けた Host ヘッダを引き継ぐかの設定となる。 お好みで設定すれば良い。

複数の Service Object を定義する場合は下記のように配列で記載すれば良い。

services:
- name: fuga
  host: fuga-svc.fuga-ns.cluster.local
  port:80
  protocol: http
  routes:
  - name: fuga
    paths:
    - /fuga/
    hosts: 
    - hoge.example.com
    preserve_host: true
    protocols: 
    - https
    - http
- name: fuga2
  host: fuga2-svc.fuga-ns.cluster.local
  port:80
  protocol: http
  routes:
  - name: fuga2
    paths:
    - /fuga2/
    hosts: 
    - hoge.example.com
    preserve_host: true
    protocols: 
    - https
    - http

Route + Service + Upstream Object

次に Upstream Object を利用したルーティングルールの例を紹介する。 Upstream Object は特にヘルスチェックの定義が癖が強いので後ほど解説を行う。

こちらも簡単な設定例として下記条件のルーティング定義を設定する。

  • Protocol: https, Host: hoge.example.compath: /fuga/ のリクエストをルーティング
  • Protocol: httpsserver1.example.local:443server2.example.local:443 いずれかにプロキシ
  • プロキシ先サーバのヘルスチェックパスは /healthcheck とする
upstreams:
- name: fuga-upstream
  healthchecks:
    active:
      type: https
      https_verify_certificate: false
      http_path: "/healthcheck"
      timeout: 1
      unhealthy:
        interval: 1
        tcp_failures: 3
        http_failures: 3
        http_statuses:
        - 400
        - 401
        - 402
        - 403
        - 404
        - 405
        - 406
        - 407
        - 408
        - 429
        - 500
        - 501
        - 502
        - 503
        - 504
        - 505
      healthy:
        interval: 1
        successes: 3
        http_statuses:
        - 200
        - 301
        - 302
  targets:
    - target: server1.example.local:443
      weight: 100
    - target: server2.example.local:443
      weight: 100
services:
- name: fuga
  host: fuga-upstream
  protocol: https
  routes:
  - name: fuga
    paths:
    - /fuga/
    hosts: 
    -  hoge.example.com
    preserve_host: true
    protocols: 
    - https

Service Object の host には Upstream Object の名前を定義する。 Upstream Object では、下記のような内容の定義を行う。(下記は一例なのでそのすべてではない。)

  • targets

プロキシ先のサーバ一覧。サーバ毎に weight が指定できる。

  • healthchecks

プロキシ先サーバの死活監視設定。ヘルスチェックの方法には activepassive ヘルスチェックが実装されている。

active ヘルスチェックは一般的なヘルスチェックの設定。tcphttp のレイヤーでそれぞれ死活監視ができる。

passive ヘルスチェックはサーキットブレーカー。実通信のレスポンスステータスの割合等の情報によって死活監視を行う。

  • algorithm

上記例では設定していないが、負荷分散アルゴリズムも設定可能。

ヘルスチェックの定義について

ヘルスチェックの設定はすべて optional となっている。 つまり明示的に設定をしない限りはヘルスチェックは Up 状態になり続ける。

どういうことかというと、下記のように healthy の設定はしているが unhealthy の設定はしていない定義を例に取る。

upstreams:
    - name: fuga-upstream
      healthchecks:
        active:
          type: https
          https_verify_certificate: false
          http_path: "/healthcheck"
          timeout: 1
          healthy:
            interval: 1
            successes: 3
            http_statuses:
            - 200
            - 301
            - 302
...

この場合、healthy.http_statuses で定義をしてあるステータスコード以外のものが返却された場合でも、ヘルスチェックは Down にならない。

どうやら、Kong は Up / Down のステータス管理をヘルスチェックのカウントアップによって行っている。

デフォルト Up の状態からステータスが始まるヘルスチェックに失敗すると healthchecks.active.unhealthy.http_failures に対するカウントが溜まっていく。これが、定義したカウント数に達すると Down 状態遷移する。

Down の状態で引き続きヘルスチェックを行い、ヘルスチェックに成功すると healthchecks.active.healthy.successes に対するカウント溜まっていく。これも同様に、定義したカウント数に達すると Up に状態遷移する。

しかし、healthchecks.active.unhealthy.http_failures のデフォルト値は 0 となっているため、明示的に設定しない限りはカウントがいくら溜まっても Down の状態に遷移しない。

これは tcp においても同様で、healthchecks.active.unhealthy.tcp_failures のデフォルト値は 0 となっている。

上記パラメータを設定しない( = 0 )の時、例えばプロキシ先サーバの 443 ポートが Listen しておらず connection refused のエラーが発生するような状態であっても Down に状態遷移しないため注意が必要。(http レイヤーのみのヘルスチェックを定義していても tcp レイヤーで失敗した場合のエラーを検出できない)

なので、Upstream Object ではきちんと明示的にヘルスチェックの設定定義をすることが大事になってくる(と認識している)。

HTTPS リダイレクトの実装例

ドキュメントを確認すると Route Object に対して https_redirect_status_code というパラメータを記載できることがわかる。

https://docs.konghq.com/1.3.x/admin-api/#add-route

ドキュメントの記載は下記の通り。

The status code Kong responds with when all properties of a Route match except the protocol i.e. if the protocol of the request is HTTP instead of HTTPS. Location header is injected by Kong if the field is set to 301, 302, 307 or 308. Defaults to 426.

説明の通り、着信したリクエストが protocols 以外マッチしていた場合 HTTPS のリダイレクトを返すことができるというもの。

例えば、下記のような定義をしたとする。

services:
- name: fuga
  host: fuga-svc.fuga-ns.cluster.local
  port:80
  protocol: http
  routes:
  - name: fuga
    paths:
    - /fuga/
    hosts: 
    - hoge.example.com
    preserve_host: true
    protocols: 
    - https
    https_redirect_status_code: 301

この場合、http://hoge.example.com/fuga/ にアクセスした場合、この Route Object は protocols パラメータに https のみ指定されているため、それ以外のプロトコルはルーティングしないが、逆にそれ以外の条件はこの Route Objectにマッチしている。

その場合、Kong はリクエストに対して Location: https://hoge.example.com/fuga/301 リダイレクト を返却する。

また、Kong の前段に SSL アクセラレータが存在し、そこで SSL の終端を行っている場合も Kong はそれを判断できる。

下記ドキュメントを参考にすると、trusted_ips から着信したリクエストに関しては X-Forwarded-Proto: https が HTTP ヘッダに入っていれば、protocols: https として扱われる。

https://docs.konghq.com/1.3.x/proxy/#restricting-the-client-protocol-httphttps-grpcgrpcs-tcptls

なので、SSL アクセラレータでヘッダの挿入 + trusted_ips の設定に SSL アクセラレータの IP を記載することで、Kong の前段で SSL を終端しており SSL アクセラレータから Kong への通信が http であっても、リダイレクトを返さずにリクエストを処理することができる。

Kong の各種パラメータチューニングについて

ここが個人的には Kong の大好きな部分。Kong は Nginx がベースとなっているため、Nginx の知識さえあれば学習コストもかからず各種パラメータのチューニングが可能となっている。

下記ドキュメントに記載されているように、Kong の設定に Nginx のパラメータを定義することで、定義に基づくディレクティブに設定が追加される。

https://docs.konghq.com/1.3.x/configuration/#injecting-individual-nginx-directives

また、config ファイルに記載するだけでなく環境変数でもパラメータを渡すことができる。

https://docs.konghq.com/1.3.x/configuration/#environment-variables

Kubernetes 上で DB-less mode の Kong を運用する上で

DB-less mode の Kong を Kubernetes 上で運用する場合、Configuration ファイルを Kong の Pod に渡す手段は色々あると思う。 現在、特に機密情報等は保持しないことから弊環境では Config Map で渡している。

この場合、Kong の設定を変更( = Config Map の更新)を行った場合、Kong の Pod を一度 restart することで新しい Configuration ファイルを読み込んだ Kong を起動させている。(正確には kubectl rollout restart コマンドで rollout している。)

Kong に限ったことではないが、既存の既存セッションが存在する場合は Pod を再起動してしまうと予期せぬエラーをクライアント側に返却してしまうため、注意が必要だ。その場合、だいたいの場合 graceful shutdown を行うと思う。

Kong にも(ベースが Nginx だし)、graceful shutdown を実施する kong quit コマンドがあるため、これを prestop に設定する戦略にしている。

https://docs.konghq.com/1.3.x/cli/#kong-quit

Kubernetes の Pod が終了するとき、Service Resource からのエンドポイントから削除されるのと、prestop の実行は並列で走るため、どちらが先に実行されるかわからない。そのため、場合によっては prestop によって graceful shutdown が開始しているのにも関わらず、Service Resource 経由でリクエストがその Pod にルーティングされてしまうことがある。(この場合、Kong は新規リクエストを受けれないためエラーとなる。)

そのため、-w オプションを利用し意図的に数秒 Delay を入れることで Serive Resource から Pod のエンドポイントが確実に削除されてから、graceful shutdown を開始するように設定をしている。

また、graceful shutdown の設定に合わせて Pod に SIGTERM が送られるまでの猶予時間 deletionGracePeriodSeconds も合わせてチューニングしている。

なお、Pod の終了戦略については下記記事が詳しく解説されているのでぜひ一読していただければと思います。

qiita.com

上記設定を表現した Kubernetes のマニュフェストファイルは以下となる。

apiVersion: apps/v1
kind: Deployment
...
spec:
...
  template:
    metadata:
      labels:
        app: kong
    spec:
      containers:
...
        lifecycle:
          preStop:
            exec:
              command: ["/usr/local/bin/kong", "quit", "-t", "300", "-w", "3"]
...
      terminationGracePeriodSeconds: 303
...

このようにして、安全にクライアントからのリクエストを犠牲にせず安全に rollout を実行している。

おわりに

備忘録も含め、Kong の基本的な設定から運用の Tips までまとめた。 今回はルーティング制御の話がメインだったが、API Gateway としてロギングな認証等本当に様々なことができるのでぜひ使ってみていただけたらと思います。

vSphere環境でCloud-initを使って楽に仮想マシンの初期化を行う

TL;DR

会社の業務で vSpehre 環境の構築を行ったが、仮想マシンのセットアップを楽に行いたく Cloud-init を採用した話

  • cloud-init-vmware-guestinfo を利用することで仮想マシンmatadatauserdata を渡すことができる github.com

前提

vSphere 上の仮想マシンリソースはすべて terraform を利用して構成管理を行っている。 そのため、今回実現したい要件をまとめると以下の通りとなる。

  • vSphere上の仮想マシンmatadatauserdata を渡すことで Cloud-init でそれを使って仮想マシンの初期化処理を実施する
  • terraform 上で metadatauserdata の管理や仮想マシンへの配布を行いたい。

アプローチ

vSphere上の仮想マシンを Cloud-init を利用して初期化する

こちらに関しては、少し調べたところ VMware 社が公開している Cloud-init 用の Datasource があったため、こちらを利用することにしました。

github.com

詳しい仕様に関しては README を見ていただければと思いますが、仮想マシンリソースの extraconfig として各種データを定義することで 、そのデータをこの Datasource が読み込んで Cloud-init が走るという仕様になっています。

参考: https://github.com/vmware/cloud-init-vmware-guestinfo#configuration

terraform 上で metadata や userdata を定義および仮想マシンに配布する

上記の Datasource を利用し extraconfig に各種データを定義することで、Cloud-initを利用できることがわかりました。 なので、次は terraform を利用して extraconfig にデータを渡す方法を確認します。

これは、terraformvsphere_virtual_machine モジュールのドキュメントを確認したところ、extra_config オプションを定義することで可能でした。

www.terraform.io

実装

Datasource のインストール

今回は、下記をインストールしたマシンを 仮想マシンイメージ として vSphere 上に定義をし、それを利用して仮想マシンを作成する方針にしました。なので、好きな OS イメージを利用して 仮想マシンイメージ の元になるイメージを作成していきます。

好きな OS イメージで仮想マシンを立ち上げたら、Cloud-init をインストールします。今回、自分は CentOS 7.7 を利用することにしたので下記の通りです。

 yum -y install cloud-init

次に cloud-init-vmware-guestinfo の README に従って Datasource のインストールを行います。 今回は、シェルスクリプトを利用してインストールを行います。

参考: https://github.com/vmware/cloud-init-vmware-guestinfo#installing-on-other-linux-distributions

curl -sSL https://raw.githubusercontent.com/vmware/cloud-init-vmware-guestinfo/master/install.sh | sh -

シェルスクリプトの内容に関してですが、主に下記を行っています。

  • Datasource のインストール
  • インストールした Datasource を利用するための config ファイルの配置

dsname として VMwareGuestInfo という値が定義されており、それを config ファイル上で指定しています。

datasource_list: [ "VMwareGuestInfo" ]

https://github.com/vmware/cloud-init-vmware-guestinfo/blob/master/DataSourceVMwareGuestInfo.py#L102 https://github.com/vmware/cloud-init-vmware-guestinfo/blob/master/99-DataSourceVMwareGuestInfo.cfg#L13

ここまで実施したらインストールは完了なので、このイメージ状態のイメージを利用して 仮想マシンイメージ を作成してください。

terraform で仮想マシンリソースを定義

GitHub に参考となる雛形のようなリポジトリを作成したので、それを利用して説明をします。

github.com

ディレクトリ構成は下記の通りとなっています。

なお、GCP を普段利用しているため backend 等の設定は GCP のものになっていますが適宜変更をしてください。

今回、ポイントとなるのは 仮想マシンイメージとして先程作成したイメージ(この場合は template-centos7.7-cloud-init )を指定している点、extra_confg として metadatauserdata仮想マシンに渡している部分になります。

modules/common_centos77/main.tf

https://github.com/sshota0809/vsphere-vm-cloud-init/blob/master/modules/common_centos77/main.tf

data "vsphere_virtual_machine" "template" {
  name          = "template-centos7.7-cloud-init"
  datacenter_id = "${var.datacenter_id}"
}

...

data "template_file" "test_cloud_init_userdata" {
  template = "${file("${path.module}/cloud-init/userdata.yaml")}"
}

data "template_file" "test_cloud_init_metadata" {
  template = "${file("${path.module}/cloud-init/metadata.yaml")}"
  
  vars = {
    instance_name = "${var.hostname}"
    network_address = "${var.network_address}"
    network_gateway = "${var.network_gateway}"
  }
}

...

  extra_config = {
    "guestinfo.userdata"          = "${data.template_file.test_cloud_init_userdata.rendered}"
    "guestinfo.metadata"          = "${data.template_file.test_cloud_init_metadata.rendered}"
  }

上記のように定義したテンプレートファイルを仮想マシンの定義に設定しています。また、このテンプレートファイルは下記の通り別ファイルで定義しています。

modules/common_centos77/cloud-init/metadata.yaml

https://github.com/sshota0809/vsphere-vm-cloud-init/blob/master/modules/common_centos77/cloud-init/metadata.yaml

instance-id: ${instance_name}
local-hostname: ${instance_name}
network:
  version: 2
  ethernets:
    ens192:
      addresses:
        - ${network_address}
      gateway4: ${network_gateway}
      nameservers:
          addresses: [10.0.0.100, 10.0.0.101]

modules/common_centos77/cloud-init/userdata.yaml

https://github.com/sshota0809/vsphere-vm-cloud-init/blob/master/modules/common_centos77/cloud-init/userdata.yaml

#cloud-config

debug:
  verbose: true
  output: '/var/log/cloud-init-debug.log'

locale:       en_US.UTF-8
timezone:     Asia/Tokyo

今回はサンプルのため、対した設定はしていませんが Cloud-init の仕様に応じて色々設定を追加してみてください。

terraform の実行

あとは、init -> plan -> apply を実行すれば、仮想マシンリソースが vSphere上で立ち上げられ、Cloud-init が走り期待する設定がされます。

おわりに

仮想マシンの初期化はもちろん色々と選択肢がありますが、業務ではオンプレミス基盤だけでなくパブリックプラウドを利用していることもあり、どうせなら Cloud-init を利用する方針で統一したいという思いがあったため、今回はこのような方法を選択しました。