Edited at

Kubernetes の Service type LoadBalancer を手作業で作る

More than 1 year has passed since last update.

ベアメタルで Kubernetes を運用している皆さん、外部からどうやって Kubernetes 上の Service に接続しようかと困っていませんか?具体的にいうと Service type LoadBalancer が無いので困っていませんか?

Kubernetes の Service type LoadBalancer に対応していないインフラストラクチャ上で type LoadBalancer な Service を作成するとこんな感じで、

$ kubectl get svc

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx LoadBalancer 10.254.0.220 <pending> 80:30692/TCP 11s

いつまでたっても EXTERNAL-IP が <pending> のままでロードバランサーが作られることはありません。というのも Kubernetes の中でロードバランサーを作成し、Service の EXTERNAL-IP をロードバランサーの IP アドレスに設定する、という処理を担当する controller が動いていないからです。(いないから。

そこで今回のこの記事の趣旨は、いないなら手作業でやってしまえば良いじゃない!です。手順は以下。


  1. 前準備

  2. Pod/Service の作成

  3. ロードバランサーの作成

  4. Service へロードバランサーの情報を設定


前準備

とりあえずテスト用に lbtest という名前の Namespace と、後々の作業のために lbtest 上の default サービスアカウントに cluster-admin 権限を与えておきます。

$ cat << EOF | kubectl create -f -

---
kind: Namespace
apiVersion: v1
metadata:
name: lbtest
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: lbtest
subjects:
- kind: ServiceAccount
namespace: lbtest
name: default
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
EOF


Pod/Service の作成

テスト用に nginx が動いている Pod と、その Pod にアクセスするための、type: LoadBalancerService を作成します。

$ cat << EOF | kubectl create -f -

---
apiVersion: v1
kind: Pod
metadata:
name: nginx
namespace: lbtest
labels:
app: nginx
spec:
containers:
- name: nginx-container
image: nginx
ports:
- containerPort: 80
---
kind: Service
apiVersion: v1
metadata:
name: nginx
namespace: lbtest
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
type: LoadBalancer
EOF

作ったサービスを確認すると、案の定 EXTERNAL-IP が <pending> のままですね。

$ kubectl get svc -n lbtest

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx LoadBalancer 10.254.0.220 <pending> 80:30692/TCP 11s


ロードバランサーの作成

ちなみに、Kubernetes の type: LoadBalncerService のロードバランサーがどうやってPod に対してロードバランシングを行なっているかというと、

一番簡単な説明が Tim Hockin が Google Cloud Next '17 で行なった The ins and outs of networking in Google Container Engine and Kubernetesというプレゼンを見るのが早いのだけれども、すごい大雑把にいうとこんな感じ。

Service type LoadBalancer

つまりロードバランサーは ServiceNodePort にバランシングを行えば良いということになる。先ほど作った Service30692NodePort で待ち受けているようなので、、

Node の IP Address を確認し、

$ kubectl get nodes

NAME STATUS ROLES AGE VERSION
172.18.201.121 Ready <none> 23d v1.9.2
172.18.201.122 Ready <none> 23d v1.9.2
172.18.201.123 Ready <none> 23d v1.9.2

以下のような HAProxy の設定を書いて HAProxy を起動する。

$ cat << EOF > haproxy.cfg

global
maxconn 256

defaults
mode http
timeout client 120000ms
timeout server 120000ms
timeout connect 6000ms

listen http-in
bind *:80
server server1 172.18.201.121:30692
server server2 172.18.201.122:30692
server server3 172.18.201.123:30692
EOF
$ docker run -d --name haproxy \
-p 80:80 \
-v $(pwd)/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro \
haproxy:1.8

localhost に対して curl を叩いて見ると、ロードバランサーがちゃんと動作していることがわかります。

$ curl 127.0.0.1:80                                                                                                      (cluster/lbtest)

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>


Service へロードバランサーの情報を設定

さて、細かいことを気にしなければ(笑) これで Kubernetes の Service に対して外部のロードバランサーが設定できたことになるのですが、

$ kubectl get svc -n lbtest

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx LoadBalancer 10.254.0.220 <pending> 80:30692/TCP 2h

やっぱり Service の EXTERNAL-IP が <pending> になったままになっているのが気になる人が出てくる気もします。ので、ここを修正します。

まず、lbtest Namespace の default サービスアカウントのアクセストークンをどうにかしてとってきます。

$ TOKEN=$(kubectl describe secret \

$(kubectl get secrets | grep default | cut -f1 -d ' ') | \
grep -E '^token' | \
cut -f2 -d':' | tr -d '\t' | tr -d ' ')

このアクセストークンが有効かどうかちょっと確認してみましょう。

$ curl -k https://${KUBERNETES_API_ENDPOINT}/api/v1/namespaces \

--header "Authorization: Bearer $TOKEN"
{
"kind": "NamespaceList",
"apiVersion": "v1",
"metadata": {
"selfLink": "/api/v1/namespaces",
"resourceVersion": "3765645"
... (中略)
}%

良さそうです。

ちょっと調べたところ、Kubernetes の Service のステータス (EXTERNAL-IP の情報を含んでいる属性)の情報を書き換えるには単純に Service の該当属性を書き換えるだけではダメらしく、service/status サブリソースを書き換える必要があるようです。

とりあえず現状の service/status をとってきます。

$ curl -k --header "Authorization: Bearer $TOKEN" \

https://${KUBERNETES_API_ENDPOINT}/api/v1/namespaces/lbtest/services/nginx/status \
> nginx-status.json

そしてとってきた情報の中の、/status/loadBalancer の項目を修正します。

$ diff -u nginx-status.json.old nginx-status.json

--- nginx-status.json.old 2018-02-23 14:01:55.000000000 +0900
+++ nginx-status.json 2018-02-23 14:01:44.000000000 +0900
@@ -31,7 +31,7 @@
},
"status": {
"loadBalancer": {
-
+ "ingress": [ { "ip": "127.0.0.1" } ]
}
}
}

ここの ip にはロードバランサーのアドレスを設定します。それではこの情報を使って Service を更新しましょう!

$ curl -k --header "Authorization: Bearer $TOKEN" \

https://${KUBERNETES_API_ENDPOINT}/api/v1/namespaces/lbtest/services/nginx/status \
-X PUT -d @nginx-status.json -H 'content-type:application/json'

これで Service の情報は更新されました。

$ kubectl get svc -n lbtest

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx LoadBalancer 10.254.0.220 127.0.0.1 80:30692/TCP 2h

ちゃんと更新されてますね、素晴らしい!これであなたのベアメタルなKubernetesクラスターでも何も気にすることなく Service type: LoadBalancer を使いたい放題ですね!やった!


補足

この記事は MetalLB のソースコードを読んで冗談で書いたものです。今までは特に詳細を気にすることなく、CloudProvider インタフェースの LoadBalancer() を実装しなくちゃいけない (must) なのかと思ってましたがそんなことなかったんですね。ちゃんと確認するもんです。


補足2

上記を自動化すれば一応使い物になる Service type: LoadBalancer controller ができるかも?