: O. Yuanying

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 が動いていないからです。(いないから。

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

  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というプレゼンを見るのが早いのだけれども、すごい大雑把にいうとこんな感じ。

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

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 ができるかも?