Kubernetes の Service type LoadBalancer を手作業で作る
Kubernetes の Service type LoadBalancer を手作業で作る
ベアメタルで 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         23d       v1.9.2
172.18.201.122   Ready         23d       v1.9.2
172.18.201.123   Ready         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 ができるかも?