KubernetesでのIngress経由のSession Sticky


マイクロサービスではアプリケーションはステートレスであるのが望ましいですが、既存アプリケーションをなるべくリファクタリングせずKubernetesで動かしたい場合など、Kubernetes環境でもSession Stickyを使いたい場合はあると思います。

MinikubeやIBM Cloud Privateなど、Ingress Controllerとして[NGINX Ingress Controller](https://github.com/kubernetes/ingress-nginx)を使用している環境では、NGINX Ingress Controllerの[Session Sticky機能](https://github.com/kubernetes/ingress-nginx/tree/master/docs/examples/affinity/cookie)を使って、Session Sticky（Session Affinity）が行えます。セッションを使うWebアプリケーションをMinikubeにデプロイし、Session Stickyができることを確認してみます。

## MinikubeでのIngressの利用

MinikubeでIngressのaddonが有効になっているかを確認し、有効でない場合は有効にします。

```
minikube addons list
minikube addons enable ingress
```

## セッションを使うアプリケーションの準備

セッションを使うWebアプリケーションと、そのアプリケーションをデプロイ済みのLibertyコンテナを準備します。今回はアプリがデプロイ済みの`sotoiwa540/sample:1.0`というLibertyイメージを使用します。

アプリケーションの作成方法は以下の記事が参考になります。

- [EclipseでJavaアプリを書いて、Libertyコンテナで、Kubernetesで公開するまでの話し (その1)](https://qiita.com/MahoTakara/items/1f2d160aeb129eec50ce)

## アプリケーションのデプロイ

`sotoiwa540/sample:1.0`のLibertyコンテナのPodをデプロイするマニフェストを作成します。

```yaml:liberty-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: liberty
spec:
  selector:
    matchLabels:
      app: liberty
  replicas: 3
  template:
    metadata:
      labels:
        app: liberty
    spec:
      containers:
      - name: liberty
        image: sotoiwa540/sample:1.0
        imagePullPolicy: Always
        ports:
        - containerPort: 9080
```

マニフェストを適用します。

```shell
kubectl apply -f liberty-deployment.yaml
```

確認します。

```shell-session
$ kubectl get po
NAME                      READY   STATUS    RESTARTS   AGE
liberty-f68456cbc-dzrzq   1/1     Running   0          64s
liberty-f68456cbc-gbgjm   1/1     Running   0          64s
liberty-f68456cbc-m5fbr   1/1     Running   0          64s
$
```

## Serviceの作成

Serviceのマニフェストを作成します。

```yaml:liberty-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: liberty
spec:
  type: ClusterIP
  selector:
    app: liberty
  ports:
  - protocol: TCP
    port: 9080
```

マニフェストを適用します。

```
kubectl apply -f liberty-service.yaml
```

確認します。

```shell-session
$ kubectl get svc
NAME         TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
kubernetes   ClusterIP   10.96.0.1      <none>        443/TCP    4m10s
liberty      ClusterIP   10.102.34.78   <none>        9080/TCP   68s
$
```

## Ingressの作成

Ingressのマニフェストを作成します。

MinikubeではなくIBM Cloud Privateの場合は、 Nginx Ingress Controllerの設定として`--annotations-prefix=ingress.kubernetes.io`設定されているため、アノテーションのキー名は`nginx.ingress.kubernetes.io/hogehoge`ではなく`ingress.kubernetes.io/hogehoge`であることに注意。

(参考)
[Enable Ingress Controller to use a new annotation prefix](https://www.ibm.com/support/knowledgecenter/en/SSBS6K_2.1.0.3/getting_started/known_issues.html#new_prefix)

```yaml:liberty-ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: liberty
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - host: sample.example.com
    http:
      paths:
      - path: /app1
        backend:
          serviceName: liberty
          servicePort: 9080
```

マニフェストを適用します。

```
kubectl apply -f liberty-ingress.yaml
```

```shell-session
$ kubectl get ing
NAME      HOSTS                ADDRESS     PORTS   AGE
liberty   sample.example.com   10.0.2.15   80      7s
$
```

Minikubeの場合はMinikubeのIPを確認します。

```
$ minikube ip
192.168.99.113
$
```

`/etc/hosts`に`192.168.99.113  sample.example.com`を追記します。

```
sudo vi /etc/hosts
```

## 動作確認（Stickyなし）

ブラウザを開き、以下のURLでIngress経由でアプリケーションにアクセスします。

http://sample.example.com/app1/sample/session

incrementボタンを押すことでセッションのカウンターを増やすことができます。ボタンを連打し、セッションが維持されていないので、カウントが増えていかないことを確認します。

<img width="727" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/86449/0221f12b-f101-b92b-3143-ea39ee29688d.png">

curlでアクセスしてレスポンスヘッダを確認します。`JSESSIONID`のみがSet-Cookieされています。

```shell-session
$ curl -v -c cookie.dat http://sample.example.com/app1/sample/session?increment=increment 2>&1 | grep -i "set-cookie:"
< Set-Cookie: JSESSIONID=0000w76PPbo4dXi5lAo03nzhSLM:e5aed9a9-19cb-41df-b170-6d6aa0791700; Path=/; HttpOnly
$
```

`cookie.dat`に`JSESSIIONID`が保存されていることが確認できます。

```shell-session
$ cat cookie.dat
# Netscape HTTP Cookie File
# https://curl.haxx.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

#HttpOnly_sample.example.com	FALSE	/	FALSE	0	JSESSIONID	0000w76PPbo4dXi5lAo03nzhSLM:e5aed9a9-19cb-41df-b170-6d6aa0791700
$
```

## Session Stickyの有効化

Ingressを一度削除します。

```shell
kubectl delete -f liberty-ingress.yaml
```

以下のドキュメントを参考に、IngressでのActive Cookie Session Affinityを有効にするためのアノテーションを追加します。Active Cookieなので、IngressがCookieを付加するようになるはずです。

https://github.com/kubernetes/ingress-nginx/tree/master/docs/examples/affinity/cookie

`liberty-ingress-sticky.yaml`を作成します。

Kubernetes環境がMinikubeではなくIBM Cloud Privateの場合は`nginx.ingress.kubernetes.io/hogehoge`ではなく`ingress.kubernetes.io/hogehoge`であることに注意。

```yaml:liberty-ingress-sticky.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: liberty
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/affinity: "cookie"
    nginx.ingress.kubernetes.io/session-cookie-name: "route"
    nginx.ingress.kubernetes.io/session-cookie-hash: "sha1"
spec:
  rules:
  - host: sample.example.com
    http:
      paths:
      - path: /app1
        backend:
          serviceName: liberty
          servicePort: 9080
```

マニフェストを適用します。

```
kubectl apply -f liberty-ingress-sticky.yaml
```

## 動作確認（Stickyあり）

アプリケーションにアクセスします。

http://sample.example.com/app1/sample/session

incrementボタンを連打します。今度はセッションが維持されているのでカウンターが増えていくことが確認できます。

<img width="720" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/86449/f36c81db-19ea-f543-6e5a-4990e8fa7ab9.png">

curlでアクセスしてレスポンスヘッダを確認します。`JSESSIONID`に加えて`route`がSet-Cookieされています。

```shell-session
$ curl -v -c cookie.dat http://sample.example.com/app1/sample/session?increment=increment 2>&1 | grep -i "set-cookie:"
< Set-Cookie: route=62d26518514d05ee42de6a48017d492202d11606; Domain=sample.example.com; Path=/app1; HttpOnly
< Set-Cookie: JSESSIONID=00003DHvBFe2s00bLutVDflq3It:e5aed9a9-19cb-41df-b170-6d6aa0791700; Path=/; HttpOnly
$
```

cookie.datを確認すると、`JSESSIONID`に加えて、Ingressがセットした`route`というCookieが確認できます。

```shell-session
$ cat cookie.dat
# Netscape HTTP Cookie File
# https://curl.haxx.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

#HttpOnly_.sample.example.com	TRUE	/app1	FALSE	0	route	872ea48729dcfeef62390b62963ddcc20e5bbf9b
#HttpOnly_sample.example.com	FALSE	/	FALSE	0	JSESSIONID	00008gyAfvIxqgTLiaTHXqMXRDQ:e5aed9a9-19cb-41df-b170-6d6aa0791700
```

## Ingress ControllerのHA構成

Ingress ControllerがHA構成の場合、最初にCookieを発行したIngress Controllerとは違うIngress Controllerにリクエストが割り振られたとしたら、ちゃんとStickyが効くのかを確認します。

MinikubeではIngress ControllerはHA構成になっていないので、ここでは、簡易的に検証するため、単にIngress ControllerのPodを再起動することによりIngress Controllerが保持している情報をクリアしてみて、そのあともStickyが有効になるのかを確認します。

Podを削除して再作成させます。

```shell-session
$ kubectl get po -n kube-system | grep ingress
nginx-ingress-controller-6958898f8f-2tc59   1/1     Running   0          2m29s
$ kubectl delete po -n kube-system nginx-ingress-controller-6958898f8f-2tc59
pod "nginx-ingress-controller-6958898f8f-2tc59" deleted
$ kubectl get po -n kube-system | grep ingress
nginx-ingress-controller-6958898f8f-4bbvv   0/1     Running   0          13s
$
```

先ほどのブラウザでアクセスし、セッションが維持されることを確認します。

<img width="730" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/86449/ce56bdd9-4384-4469-e5d2-04ea1b63a7db.png">

何度か再作成を繰り返してみても、セッションは維持されます。ですので、Ingress ControllerがHA構成の場合、その前段にいる負荷分散装置からIngress Controllerに対しては、アフィニティーを効かせなくても大丈夫と思われます。

（補足）
IBM Cloud Private v3.1.0に付属のバージョンのNGINX Ingress ControllerではCookieの値は単に`<PodのIP>:<ポート>`を指定のアルゴリズム（sha1）でハッシュしただけのものなので、`echo -n '172.17.0.7:9080' | shasum -a 1`のようにしたものと一致したのですが、[この変更](https://github.com/kubernetes/ingress-nginx/commit/9e639f97888ead927ab918210f410e9b787cc270#diff-8f83606fdd5dc8d2406cda71b9609762)で変わったっぽいです。他にも[アフィニティー関連の修正は多そう](https://github.com/kubernetes/ingress-nginx/commits/master/rootfs/etc/nginx/lua/balancer/sticky.lua)なので、バージョンによる挙動の違いには注意した方がよさそうです。

|環境|イメージ|
|---|---|
|minikube v0.32.0|quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.21.0|
|IBM Cloud Private v3.1.0|mycluster.icp:8500/ibmcom/nginx-ingress-controller-amd64:0.16.2|

# 参考リンク

- [Minikube 用の入口の構成](https://www.ibm.com/support/knowledgecenter/ja/SS5PWC/front_end_config_minikube_task.html)
- [kubernetes/ingress-nginx Annotations](https://github.com/kubernetes/ingress-nginx/blob/master/docs/user-guide/nginx-configuration/annotations.md)