k8sクラスタの外部へサービス提供用するには、ノードの停止に備えてVIPを、代わりノードが引き取る機能が必須になります。そこで、kube-keepalived-vipを利用してVIPを持つ方法について検証したメモです。
この記事は、k8s on Vagrant の7回目です。
- Kubenetes v1.10 クラスタをVagrantで構築したメモ
- K8s on Vagrant, Node障害時の振る舞いについての検証記録
- K8s on Vagrant, ダッシュボードのセットアップ
- K8s on Vagrant, NFS 永続ボリュームの利用メモ
- K8s on Vagrant, NGINX Ingress Controller の利用
- K8s on Vagrant, Workerノードの追加と削除
- K8s on Vagrant, kube-keepalived-vip を利用したサービスIP設定
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の方が、より全てのポッドへ、均等にリクエストを分散させる、と理解できると思います。
専用のロードバランサーは、専用に開発されたカスタムLSIなどによって、物理サーバーで負荷分散したのでは、とても達成できない高い性能を得る事ができます。 しかし、その様なケースは、むしろ特殊なケースかもしれません。 一般的なアプリケーションでは、仮想サーバーなどでも十分処理できるリクエスト数に留まる事が多いと思われます。 もちろん、専用にロードバランサーを設置すると、相応のコストが発生することも、忘れてはいけません。
それでは、「ノードにVIPを付与して、kube-proxyで負荷分散させて、ノード障害時には、他のノードがVIPを引き継ぐ様にしようよ」、と考えると思います。 例えば、次の図の様に、クラスタを構成するノード間で、VIPを引き取れる様にするだけで、ニーズに答える事ができます。
これを実現するのが、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アドレスへ転送します。
検証
ネームスペースの登録
ネームスペース 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 では、Kind: ReplicationController
を利用していましたが、1.7以降では、デプロイメント・コントローラを利用して、レプリケーション・コントローラの利用が推奨されています。 このために、下記のYAMLファイルでは Kind: Deployment
に変更しています。
次のYAMLファイルを kubectl apply -f 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 とバインドします。
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から参照されます。
apiVersion: v1
kind: ConfigMap
metadata:
name: vip-configmap
data:
192.168.1.99: vip/echoheaders
デーモンセットのデプロイ
次のYAMLファイルを適用すると、k8sクラスタの各ワーカーノード上に、デーモンセットのポッドとして、kube-keepalived-vipのコンテナが起動します。 起動が完了すると、コンフィグマップに指定した IPアドレスをVIPをアクセスすることで、指定のサービスへ繋がる様になります。
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を持っているノード(仮想サーバー)を強制終了して、サービスの継続を確認した記録です。
実験の結果、以下の2つのcurl コマンドの時間差は3秒であった。
1回目のcurlコマンドを実行した直後に、仮想サーバーをシャットダウンした。
2回目のcurlコマンドに対する応答は、VIPを引き継いだノードからの応答となる。
client_address=
のIPアドレスは、VIPを持っているノードのポッドネットワークのIPアドレスなので、この値が変わることで、切り替わった事を判別する事ができる。 kube-keepalived-vip の切り替わり時間は、この実験結果から、およそ3秒とみなして良いと考えられる。
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で構築したメモで作った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