- 対象の環境
- Kubernetes 1.4.x(筆者の環境は 1.4.6)
- AWS
AWS 上で稼動する Kubernetes (k8s) で LoadBalancer 型の Service を作ると、自動的に入り口となる ELB (Elastic Load Balancer) を立ち上げてくれてとても便利なのだが、AWS では当たり前に出来ていた sticky session が、この構成だとどうも実現できないらしい。
k8s で新規に開発しようという場合なら、sticky session が必要な実装はなるべく避けるのかもしれないが、稼働中の Web アプリを k8s に移行したいとなった場合に sticky session が必要になるケースもあるのではないだろうか。
というわけで、k8s on AWS における sticky session の実現方法について調べてみた。
諸悪の根源 kube-proxy
まず、単に ELB の sticky session を有効にしたらどうなるだろうか?
[ELB] = sticky => [Node] => [kube-proxy] => [Pod]
ELB から kube-proxy までが、Service の内部構造である。まず ELB が選択するのは k8s クラスタの各ノードである。ELB が sticky に設定されている場合、ノードの選択が sticky になるだけで、その先の Pod の選択は kube-proxy に委ねられている。
もう少し詳しく見てみると、
こんな感じで、ELB が sticky になってても意味はなく、Pod の選択を行う kube-proxy が sticky にならないといけない。
Service の sessionAffinity 機能
実は k8s の Service (kube-proxy) には sessionAffinity という機能がある。これを有効にすれば IP アドレスベースの stickiness を実現できるらしいのだが、なんと type が LoadBalancer の場合はこの機能を利用出来ないという。
ClientIP value for sessionAffinity is not supported when service type is LoadBalancer.
IPアドレスが隠れちゃう問題
では、LoadBalancer 型を利用するのはやめて、NodePort 型の Service にして、sessionAffinity を有効にし、自前で ELB を立てたらどうなるか?
その場合も、ELB がクライアントの IP アドレスを隠してしまうので sessionAffinity は正しく動作しない。現状の kube-proxy は、ELB がサポートしている X-Forwarded-For
ヘッダや、proxy protocol を解釈しないので、[ELB] → [kube-proxy]
の構成だとどう頑張っても sticky session を実現できないことになる。LoadBalancer 型の Service が sessionAffinity をサポートしない所以だ。
Proxy protocol のサポートについては多くのリクエストがあるようなので、将来的にはサポートされる可能性が高い。もしサポートされれば IP アドレスベースの stickiness については標準構成で利用出来るようになる。
解決策 1) ELB と kube-proxy の間に nginx を立てる
kube-proxy は X-Real-IP
ヘッダを解釈するらしい。これを利用して、ELB と kube-proxy の間に nginx を立てて、そこで X-Real-IP
ヘッダを追加するという手段が紹介されていた(ちなみに、ELB は X-Real-IP
を追加してくれない)。
以下のような構成になる。
[ELB] = proxy protocol => [nginx] = X-Real-IP => [kube-proxy]
解決策 2) Nginx Ingress Controller
解決策 1) も悪くないように思えるが、IP アドレスベースの stickiness だと Web アプリとしてはあんまり嬉しくないので、なんとか諸悪の根源である kube-proxy をバイパスする方法がないだろうかということで見つけたのが、最近サポートされ始めたばかりの Ingress を使う方法だ。
Ingress とは、Service に対する柔軟なルーティングやロードバランシング機能を提供する k8s では比較的新しいリソースで、機能的には LoadBalancer 型の Service を切り出して、独立したリソースにしたような感じである。
Ingress を利用するためには、まず Ingress Controller というものを k8s 内に立ち上げておく必要がある。Ingress Controller の実体は proxy server であり、その proxy server に適用するルールのセットを Ingress リソースと呼ぶ。
Ingress Controller には色々な実装が提供されていて、その中でも k8s の公式実装と思われる Nginx Ingress Controller が sticky session をサポートするために kube-proxy をバイパスするらしい。まさに今回のニーズにぴったしな実装だと言える。
The NGINX ingress controller does not uses Services to route traffic to the pods. Instead it uses the Endpoints API in order to bypass kube-proxy to allow NGINX features like session affinity and custom load balancing algorithms. It also removes some overhead, such as conntrack entries for iptables DNAT.
Nginx Ingress Controller のセットアップ
実際に Nginx Ingress Controller をセットアップする手順は以下の通り。必要なマニフェストファイル一式は、cotoami-infraのリポジトリ に置いてある。
1) まず、ルーティング先が見つからなかった場合にリクエストを送るための、デフォルトのバックエンドサービスを立てる。このサービスは単に 404 を返すだけである。
$ kubectl create -f https://raw.githubusercontent.com/cotoami/cotoami-infra/master/kubernetes/ingress/default-backend.yaml
2) Ingress Controller を作る前に、ConfigMap による設定を先に作っておく。
$ kubectl create -f https://raw.githubusercontent.com/cotoami/cotoami-infra/master/kubernetes/ingress/nginx-load-balancer-conf.yaml
この ConfigMap の中に、sticky session を有効にする設定 enable-sticky-sessions: "true"
が含まれている。
3) 設定が登録出来たら、主役の Ingress Controller を作る。
$ kubectl create -f https://raw.githubusercontent.com/cotoami/cotoami-infra/master/kubernetes/ingress/ingress-controller.yaml
上のマニフェストファイル ingress-controller.yaml
では、Ingress Controller に外部からアクセス出来るように、LoadBalancer 型の Service をくっつけてある。これで、ELB 経由で Ingress Controller にアクセス出来るようになる。
(※) Controller本体は、公式の例に倣って ReplicationController で作成しているが、Daemonset で作ることも出来る。
以下のように Service の一覧を見ると、nginx-ingress-controller へアクセスするための Service が登録されているのが分かる。
$ kubectl get svc ... nginx-ingress-controller 100.66.116.210 a5902e609eed6... 80/TCP 20s ...
a5902e609eed6...
と省略表示されているのが ELB の DNS name である。試しにこのアドレスにアクセスしてみると、
default backend - 404
という感じで、先ほど立ち上げておいたデフォルトサービスの応答が得られるはずだ。
4) Ingress をテストするための、テスト用 Web アプリを立ち上げる。
Ingress 経由でアクセスする Web アプリを立ち上げる。ここでは、HTTPやサーバーに関する情報を表示するだけの http-debug-server を立ち上げてみる。
$ kubectl create -f https://raw.githubusercontent.com/cotoami/cotoami-infra/master/kubernetes/ingress/http-debug-server.yaml
マニフェストファイルにあるように、Nginx Ingress Controller 経由でアクセスする場合、Service の type は ClusterIP
にしておく。
5) Ingress リソースを登録する
最後にルーティングルールを登録する。
$ kubectl create -f https://raw.githubusercontent.com/cotoami/cotoami-infra/master/kubernetes/ingress/ingress.yaml
このルールでは、ホスト debug.example.com
に対するアクセスを、http-debug-server
Service にルーティングするように設定している。以下のように、curl コマンドでアクセスすると、デバッグ用の情報がレスポンスとして返されるはずだ。
$ curl <ELB-DNS-name> -i -H 'Host: debug.example.com'
sticky session をテストするためには cookie が必要になるので、以下のようにしてレスポンスヘッダーを表示させる。
$ curl <ELB-DNS-name> -I -H 'Host: debug.example.com'
以下のような感じで、ヘッダーに cookie の情報が含まれているので、
... Set-Cookie: route=acfc2f2f4d692d7a90b9797428615e29d3936e95; Path=/; HttpOnly ...
この cookie をリクエストに含めて送信する。
$ curl -b route=acfc2f2f4d692d7a90b9797428615e29d3936e95 <ELB-DNS-name> -H 'Host: debug.example.com' -s | python -mjson.tool
レスポンスの JSON データが出力されると思うが、hostname
のところに注目して欲しい。何度リクエストを送っても、同じホスト名が返ってくるはずだ。試しに-b
オプションを外して送信すると、送信するたびにホスト名が変わる。
大分ややこしい感じになったが、以上が Ingress で sticky session を実現するための手順である。
解決策 3) service-loadbalancer
その他、k8s が提供している service-loadbalancer を使うことで sticky session を実現する事例が紹介されていた。