ベアメタルで 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 が動いていないからです。(いないから。
そこで今回のこの記事の趣旨は、いないなら手作業でやってしまえば良いじゃない!です。手順は以下。
- 前準備
- Pod/Service の作成
- ロードバランサーの作成
- 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: LoadBalancer
な Service
を作成します。
$ 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: LoadBalncer
な Service
のロードバランサーがどうやってPod
に対してロードバランシングを行なっているかというと、
一番簡単な説明が Tim Hockin が Google Cloud Next '17 で行なった The ins and outs of networking in Google Container Engine and Kubernetesというプレゼンを見るのが早いのだけれども、すごい大雑把にいうとこんな感じ。
つまりロードバランサーは Service
の NodePort
にバランシングを行えば良いということになる。先ほど作った Service
は 30692
の NodePort
で待ち受けているようなので、、
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 ができるかも?