K8s on Vagrant, kube-keepalived-vip を利用したサービスIP設定


k8sクラスタの外部へサービス提供用するには、ノードの停止に備えてVIPを、代わりノードが引き取る機能が必須になります。そこで、kube-keepalived-vipを利用してVIPを持つ方法について検証したメモです。


この記事は、k8s on Vagrant の7回目です。

1. [Kubenetes v1.10 クラスタをVagrantで構築したメモ](https://qiita.com/MahoTakara/items/28cd766d0447140b7ae3)
1. [K8s on Vagrant, Node障害時の振る舞いについての検証記録](https://qiita.com/MahoTakara/items/9b106664492303e1ba01)
1. [K8s on Vagrant, ダッシュボードのセットアップ](https://qiita.com/MahoTakara/items/b9b1d4bdc5a3e0d6ddfb)
1. [K8s on Vagrant, NFS 永続ボリュームの利用メモ](https://qiita.com/MahoTakara/items/c07b1b3418fc9b7130f6)
1. [K8s on Vagrant, NGINX Ingress Controller の利用](https://qiita.com/MahoTakara/items/cdfa379f2280a58fdd6d)
1. [K8s on Vagrant, Workerノードの追加と削除](https://qiita.com/MahoTakara/items/42a1ec9022fafab252e0)
1. [K8s on Vagrant, kube-keepalived-vip を利用したサービスIP設定](https://qiita.com/MahoTakara/items/9b6e6a328ddc0d1aeee9)


# KeepAlivedが必要な理由

k8sのサービスのタイプNodePortでポートを開くと、ノードのIPアドレスで、デフォルトで30000-32767の範囲でポートを開き、k8sクラスタ内のサービスを外部へ提供できる様になります。 k8sのサービスへのリクエストは、kube-proxyによって、他のノード上のポッドへリクエストが分散されます。しかし、この方法では、ノードにアサインされたパブリックのIPアドレスが、公開用IPアドレスとなるために、何らかの理由でノードが停止した場合に、他のノードが生きていて、それぞれ、kube-proxyが動作していて負荷分散できる状態でも、サービスが停止することになります。

そこで次の図の様に、フロントに冗長化されたロードバランサーのアプライアンス等を配置して、これらのロードバランサーにサービス提供用の代表IPアドレス(VIP)を持たせる事で、各ノードへ負荷分散でき、ノードの一つが停止した場合でも、サービスを継続することができます。　また、ロードバランサーの障害時には、バックアップ機がVIPを引き取りますから、継続してサービスを利用することができます。 

しかし、フロントの Load Balancer と kube-proxy の両方で負荷分散するのは、ちょっと、勿体無い感じがするのですが kube-proxy は、ポッド・ネットワーク上のポッドへの負荷分散になりますから、同じノード上の複数のポッドのIPアドレス、そして、異なるノード上のポッドのIPアドレス群へ、均等にリクエスを分散する働きがあります。　一方、Load Balancer の負荷分散は、ノードのIPアドレス単位になります。　この事から、kube-proxyの方が、より全てのポッドへ、均等にリクエストを分散させる、と理解できると思います。

![スクリーンショット 2018-04-28 13.32.54.png](https://qiita-image-store.s3.amazonaws.com/0/42829/01f79f8b-2ef7-a3c1-59ba-6d38647f2162.png "スクリーンショット 2018-04-28 13.32.54.png")

専用のロードバランサーは、専用に開発されたカスタムLSIなどによって、物理サーバーで負荷分散したのでは、とても達成できない高い性能を得る事ができます。 しかし、その様なケースは、むしろ特殊なケースかもしれません。 一般的なアプリケーションでは、仮想サーバーなどでも十分処理できるリクエスト数に留まる事が多いと思われます。 もちろん、専用にロードバランサーを設置すると、相応のコストが発生することも、忘れてはいけません。

それでは、「ノードにVIPを付与して、kube-proxyで負荷分散させて、ノード障害時には、他のノードがVIPを引き継ぐ様にしようよ」、と考えると思います。　例えば、次の図の様に、クラスタを構成するノード間で、VIPを引き取れる様にするだけで、ニーズに答える事ができます。

![スクリーンショット 2018-04-28 13.33.03.png](https://qiita-image-store.s3.amazonaws.com/0/42829/c09ed28a-becf-9fb3-62b7-f4bf5b8fce81.png "スクリーンショット 2018-04-28 13.33.03.png")

これを実現するのが、kube-keepalived-vip https://github.com/kubernetes/contrib/tree/master/keepalived-vip で、今回の記事で検証を進める対象です。


## アーキテクチャ

kube-keepalived-vipは、デーモンセットとして、ワーカーノードに配置されるポッドです。 これらのポッドは、VRRPを利用して相互に稼働を確認して、メンバーのノードの中からVIPを受け持つノード決定します。 VIPを受け守るノードが停止した場合は、代わりのノードが決定され、VIPを引き継がれることになります。

ConfigMap vip-configmap には、VIPとしてアサインするIPアドレス、アプリケーションのネームスペース名とサービス名が登録され、kube-keepalived-vipは、このvip-configmapを読み込んで、ノードにVIPを設定して、トラフィックを指定のサービスへ転送します。

模擬のアプリケーションのコンテナ echoheader は、 httpリクエストを送ると、リクエストのヘッダー情報を応答のボディに入れて返すアプリケーションです。 サービス echoheader を登録することで、kube-proxyに指示を送り、サービスのIPアドレスへ送られたリクエストが、ポッド・ネットワークのポッドのIPアドレスへ転送します。

![スクリーンショット 2018-04-28 18.19.51.png](https://qiita-image-store.s3.amazonaws.com/0/42829/4cf6f2ae-0d87-6b72-f48d-8120e264eee1.png "スクリーンショット 2018-04-28 18.19.51.png")

# 検証

## ネームスペースの登録

ネームスペース vip を作成して、ネームスペースを切り替えます。

~~~
$ kubectl create ns vip
$ kubectl config set-context vip --namespace=vip --cluster=kubernetes --user=kubernetes-admin
$ kubectl config use-context vip
~~~


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

参考にしたGitHub kubernetes/[kube-keepalived-vip](https://github.com/kubernetes/contrib/tree/master/keepalived-vip) では、`Kind: ReplicationController` を利用していましたが、1.7以降では、デプロイメント・コントローラを利用して、レプリケーション・コントローラの利用が推奨されています。 このために、下記のYAMLファイルでは `Kind: Deployment` に変更しています。

次のYAMLファイルを `kubectl apply -f echoheaders-deploy.yaml` により適用します。　これにより、サービス、デプロイメント、ポッドが作成されます。

~~~echoheaders-deploy.yaml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: echoheaders
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: echoheaders
    spec:
      containers:
      - name: echoheaders
        image: k8s.gcr.io/echoserver:1.4
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: echoheaders
  labels:
    app: echoheaders
spec:
  type: NodePort
  ports:
  - port: 80
    nodePort: 30302
    targetPort: 8080
    protocol: TCP
    name: http
  selector:
    app: echoheaders
~~~


## サービスアカウント作成

~~~
$ kubectl create sa kube-keepalived-vip

$ $ kubectl get sa
NAME                  SECRETS   AGE
default               1         5h
kube-keepalived-vip   1         7s
~~~



## RBACの設定

ClusterRoleを作成して、ネームスペース vip の サービスアカウント kube-keepalived-vip とバインドします。


~~~rbac.yaml 
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: kube-keepalived-vip
rules:
- apiGroups: [""]
  resources:
  - pods
  - nodes
  - endpoints
  - services
  - configmaps
  verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: kube-keepalived-vip
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: kube-keepalived-vip
subjects:
- kind: ServiceAccount
  name: kube-keepalived-vip
  namespace: vip
~~~


## コンフィグマップの設定

コンフィグマップに、VIPとなるIPアドレス、ネームスペース、サービス名を設定します。 これは、kube-keepalived-vipから参照されます。

~~~vip-configmap.yaml 
apiVersion: v1
kind: ConfigMap
metadata:
  name: vip-configmap
data:
  192.168.1.99: vip/echoheaders
~~~
  
  

## デーモンセットのデプロイ

次のYAMLファイルを適用すると、k8sクラスタの各ワーカーノード上に、デーモンセットのポッドとして、kube-keepalived-vipのコンテナが起動します。 起動が完了すると、コンフィグマップに指定した IPアドレスをVIPをアクセスすることで、指定のサービスへ繋がる様になります。

~~~vip-daemonset.yaml 
apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
  name: kube-keepalived-vip
spec:
  template:
    metadata:
      labels:
        name: kube-keepalived-vip
    spec:
      hostNetwork: true
      serviceAccount: kube-keepalived-vip
      containers:
        - image: k8s.gcr.io/kube-keepalived-vip:0.11
          name: kube-keepalived-vip
          imagePullPolicy: Always
          securityContext:
            privileged: true
          volumeMounts:
            - mountPath: /lib/modules
              name: modules
              readOnly: true
            - mountPath: /dev
              name: dev
          # use downward API
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
          # to use unicast
          args:
          - --services-configmap=vip/vip-configmap
          # unicast uses the ip of the nodes instead of multicast
          # this is useful if running in cloud providers (like AWS)
          #- --use-unicast=true
          # vrrp version can be set to 2.  Default 3.
          #- --vrrp-version=2
      volumes:
        - name: modules
          hostPath:
            path: /lib/modules
        - name: dev
          hostPath:
            path: /dev
      nodeSelector:
        type: worker
~~~
  
## テスト

`kubectl get po -o wide`で、ポッドのリストを取得すると、NODEの欄から、それぞれ異なるノードにポッドが起動している事がわかる。
また、NodePortサービスとして起動しているので、ノードのIPアドレスとVIPで比較する事ができる。

~~~
$ kubectl get po -o wide
NAME                           READY     STATUS    RESTARTS   AGE       IP             NODE
echoheaders-68c5c54f96-fc6qm   1/1       Running   0          4h        10.244.1.5     k8s2x
kube-keepalived-vip-hgwwd      1/1       Running   0          4h        192.168.1.73   k8s3x
kube-keepalived-vip-lrz7b      1/1       Running   0          4h        192.168.1.74   k8s4x
kube-keepalived-vip-pghmt      1/1       Running   0          4h        192.168.1.72   k8s2x

$ kubectl get svc -o wide
NAME          TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE       SELECTOR
echoheaders   NodePort   10.244.127.167   <none>        80:30302/TCP   4h        app=echoheaders
~~~


### VIPでNodePortをアクセス

~~~
$ curl http://192.168.1.99:30302/;echo
CLIENT VALUES:
client_address=10.244.3.0
command=GET
real path=/
query=nil
request_version=1.1
request_uri=http://192.168.1.99:8080/

SERVER VALUES:
server_version=nginx: 1.10.0 - lua: 10001

HEADERS RECEIVED:
accept=*/*
host=192.168.1.99:30302
user-agent=curl/7.47.0
BODY:
-no body in request-
~~~


### VIPでアクセス

~~~
$ curl http://192.168.1.99/;echo
CLIENT VALUES:
client_address=10.244.3.0
command=GET
real path=/
query=nil
request_version=1.1
request_uri=http://192.168.1.99:8080/

SERVER VALUES:
server_version=nginx: 1.10.0 - lua: 10001

HEADERS RECEIVED:
accept=*/*
host=192.168.1.99
user-agent=curl/7.47.0
BODY:
-no body in request-
~~~


## ノード停止時の継続性のテスト

VIPを持っているノード（仮想サーバー）を強制終了して、サービスの継続を確認した記録です。

実験の結果、以下の２つのcurl コマンドの時間差は３秒であった。
１回目のcurlコマンドを実行した直後に、仮想サーバーをシャットダウンした。
２回目のcurlコマンドに対する応答は、VIPを引き継いだノードからの応答となる。

`client_address=`のIPアドレスは、VIPを持っているノードのポッドネットワークのIPアドレスなので、この値が変わることで、切り替わった事を判別する事ができる。 kube-keepalived-vip の切り替わり時間は、この実験結果から、およそ３秒とみなして良いと考えられる。

~~~
imac:~ maho$ date; curl http://192.168.1.99/;echo
2018年 4月28日 土曜日 22時21分34秒 JST
CLIENT VALUES:
client_address=10.244.2.0  <---- 切り替え前のノード この後停止
command=GET
real path=/
query=nil
request_version=1.1
request_uri=http://192.168.1.99:8080/

SERVER VALUES:
server_version=nginx: 1.10.0 - lua: 10001

HEADERS RECEIVED:
accept=*/*
host=192.168.1.99
user-agent=curl/7.54.0
BODY:
-no body in request-

imac:~ maho$ date; curl http://192.168.1.99/;echo
2018年 4月28日 土曜日 22時21分37秒 JST
CLIENT VALUES:
client_address=10.244.3.0  <----切り替え後のノードからの応答
command=GET
real path=/
query=nil
request_version=1.1
request_uri=http://192.168.1.99:8080/

SERVER VALUES:
server_version=nginx: 1.10.0 - lua: 10001

HEADERS RECEIVED:
accept=*/*
host=192.168.1.99
user-agent=curl/7.54.0
BODY:
-no body in request-
~~~


            
## 問題点

* kube-keepalived-vipが参照する vip-configmap に定義されたサービスが稼働していないと、keepalivedの設定ファイルを生成できず、クラッシュループを繰り返し、その間、VIPを失ってしまいます。 このため、vip-configmapに定義するのは、実際のアプリケーションではなく、headerechoの様な、keepalivedを維持するためのものとするのが良い様に思います。
* 今回は、 [Kubenetes v1.10 クラスタをVagrantで構築したメモ](https://qiita.com/MahoTakara/items/28cd766d0447140b7ae3)で作ったVagrantfileから、172.42.42.0のネットワークアドレスを除いて、192.168.1.0/24をポッドネットワークを通すルートとして利用する構成にしました。理由は、keepalived.confを直接設定するのではなく、kube-keepalived-vipが自動生成したものをkeepalivedは利用します。 その際に、トラッキングするべきネットワークインタフェースを指定できないために、意図しないネットワークにVIPを付与するという問題が発生してしまったためです。 改善するには、GO言語で書かれたkube-keepalived-vipのソースコードに手を加える必要があります。


## まとめ

kube-keepalived-vipには、問題もありますが、少々コードに手を加えるか、環境を工夫することで、本番でも利用できると思います。 それから、NGINX Ingressコントローラーと組み合わせて、利用することで、VIP, ロードバランス、TLS終端、仮想ホストなどの機能を利用できる様になると考えれます。


# 参考資料

1.「GitHub kubernetes/contrib」, < https://github.com/kubernetes/contrib/tree/master/keepalived-vip >, 2018/4/28