Kong: DB-less and Declarative Configuration まとめ + Kubernetes 上で運用する上での Tips
TL;DR
Kong の設定あれこれを備忘録も兼ねてまとめる。
- Kong の DB-less mode における 主にトラフィック制御に関する Configuration まとめ(
v1.x
系を対象)
概要
Kong とは
Nginx ベースのオープンソースの API Gateway。セキュリティやロギング、認証やトラフィック制御を行える。 Nginx ベースなのでパラメータチューニング等がしやすい。 プラグインがもともと豊富なので色々とやれることが多い。(もちろん自前で実装もできる。)
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.com
のpath: /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.com
のpath: /fuga/
のリクエストをルーティングProtocol: https
でserver1.example.local:443
とserver2.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
プロキシ先サーバの死活監視設定。ヘルスチェックの方法には active
と passive
ヘルスチェックが実装されている。
active
ヘルスチェックは一般的なヘルスチェックの設定。tcp
と http
のレイヤーでそれぞれ死活監視ができる。
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 の終了戦略については下記記事が詳しく解説されているのでぜひ一読していただければと思います。
上記設定を表現した 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
を利用することで仮想マシンにmatadata
やuserdata
を渡すことができる github.com
前提
vSphere 上の仮想マシンリソースはすべて terraform
を利用して構成管理を行っている。
そのため、今回実現したい要件をまとめると以下の通りとなる。
- vSphere上の仮想マシンに
matadata
やuserdata
を渡すことで Cloud-init でそれを使って仮想マシンの初期化処理を実施する terraform
上でmetadata
やuserdata
の管理や仮想マシンへの配布を行いたい。
アプローチ
vSphere上の仮想マシンを Cloud-init を利用して初期化する
こちらに関しては、少し調べたところ VMware 社が公開している Cloud-init 用の Datasource
があったため、こちらを利用することにしました。
詳しい仕様に関しては README を見ていただければと思いますが、仮想マシンリソースの extraconfig
として各種データを定義することで 、そのデータをこの Datasource
が読み込んで Cloud-init が走るという仕様になっています。
参考: https://github.com/vmware/cloud-init-vmware-guestinfo#configuration
terraform 上で metadata や userdata を定義および仮想マシンに配布する
上記の Datasource
を利用し extraconfig
に各種データを定義することで、Cloud-initを利用できることがわかりました。
なので、次は terraform
を利用して extraconfig
にデータを渡す方法を確認します。
これは、terraform
の vsphere_virtual_machine
モジュールのドキュメントを確認したところ、extra_config
オプションを定義することで可能でした。
実装
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 に参考となる雛形のようなリポジトリを作成したので、それを利用して説明をします。
ディレクトリ構成は下記の通りとなっています。
/modules/
- 仮想マシンやそれに渡す
matadata
やuserdata
を定義
- 仮想マシンやそれに渡す
/resouce/
なお、GCP を普段利用しているため backend 等の設定は GCP のものになっていますが適宜変更をしてください。
今回、ポイントとなるのは 仮想マシンイメージとして先程作成したイメージ(この場合は template-centos7.7-cloud-init
)を指定している点、extra_confg
として metadata
と userdata
を仮想マシンに渡している部分になります。
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
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
#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 を利用する方針で統一したいという思いがあったため、今回はこのような方法を選択しました。