Kubernetesでセッションを維持する

ログインが必要なタイプのWebアプリケーションをKubernetesで動かす際,何も考えずにPodを複数動かしてしまうと,正常にセッション管理ができない場合がある.

例えば,ログインしたという情報をWebアプリケーション側で保持しておく場合を考える.
1. DeploymentでPodのreplicaを3つ作る(Pod A,Pod B,Pod C)
2. クライアントがServiceを経由してPod Aにアクセスする.
3. クライアントはPod Aでログインする
4. ログインを終えたクライアントが,次の画面に遷移するために新しいリクエストを送る
5. リクエストを受け取ったServiceが,Pod A以外のPod(例えばPod B)にリクエストを振り分ける
6. Pod Bではクライアントがログインしたという記録がないので,再度ログイン画面に飛ばされる
7. 同様に,ログイン -> 次のPodに振り分けられるというのが続いてしまう

このような問題を解決するため,Serviceを作成する時に sessionAffinityClientIP に設定する.
ClientIP にすることで,クライアントのIPアドレスを考慮しながらPodへリクエストを流してくれるので,ステートフルなアプリケーションもKubernetes上で実行できる.

文章で説明するのは得意ではないので,実際にテスト.
例として,GrafanaをKubernetesで動かしてみる.
DeploymentとServiceを次のように作成する.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: grafana-deployment
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: grafana
    spec:
      containers:
        - name: grafana
          image: grafana/grafana
          ports:
            - name: grafana-port
              containerPort: 3000
              protocol: TCP
  selector:
    matchLabels:
      app: grafana
---
apiVersion: v1
kind: Service
metadata:
  name: grafana-service
spec:
  ports:
    - name: grafana
      port: 3000
      protocol: TCP
      targetPort: grafana-port
  selector:
    app: grafana
  type: NodePort
  sessionAffinity: ClientIP

上のマニフェストを用いて作成.

$ kubectl create -f grafana.yml

sessionAffinityClientIP に設定しているので,正常にログインができるはず.

Docker Engine API試用

前に少し気になっていたDocker Engine APiを使ってみたので,それについて.

特に複雑なことはせず,引数に与えたイメージを削除するというプログラムを作ってみた.
ただ単純に削除するだけではつまらないので,何世代分保存しておくか,というのをオプションで指定できるようにした.

コードは mas9612/docker-tools/image-remove に置いてある.
あまりきれいなコードではないのでご注意ください.

Client.ImageList() メソッドでローカルにあるイメージの一覧が取得できるが, filter でイメージ名を指定できなさそうだったので,愚直にfor文で1つ1つ確認している.
アルゴリズムは得意ではないので,良い方法があれば教えてください…

images, err := client.ImageList(ctx, types.ImageListOptions{})
if err != nil {
    log.Fatalf("[ERROR] client.ImageList(): %s\n", err)
}

for _, image := range images {
    for _, repotag := range image.RepoTags {
        repository := strings.Split(repotag, ":")
        if repository[0] == *imageName {
            imageInfos = append(imageInfos, imageInfo{
                ID:      image.ID,
                Created: image.Created,
                Name:    repotag,
            })
        }
    }
}

削除対象のイメージをリスト出来たら,それを作成日時でソートし,指定した世代分は残してそれ以外を Client.ImageRemove() メソッドで削除している.
デフォルトでは,イメージ名にマッチしたもの全てを削除するようになっているのでお気をつけください.

if *generation > len(imageInfos) {
    *generation = len(imageInfos)
}
removeOptions := types.ImageRemoveOptions{
    Force: *force,
}
for _, image := range imageInfos[*generation:] {
    _, err := client.ImageRemove(ctx, image.ID, removeOptions)
    if err != nil {
        log.Fatalf("[ERROR] client.ImageRemove(): %s\n", err)
    }
    fmt.Printf("Image %s was deleted.\n", image.Name)
}

やっている事自体は簡単なので,ドキュメントと見比べて頂ければわかると思います.