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

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

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

```bash
$ 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` 権限を与えておきます。

```bash
$ 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` を作成します。

```bash
$ 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>` のままですね。

```bash
$ 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](https://www.youtube.com/watch?v=y2bhV81MfKQ)というプレゼンを見るのが早いのだけれども、すごい大雑把にいうとこんな感じ。

![Service type LoadBalancer](https://qiita-image-store.s3.amazonaws.com/0/9958/e15e2e65-ce6a-afbc-3a24-c54c87755a2e.png)


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

`Node` の IP Address を確認し、

```bash
$ 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 を起動する。

```bash
$ 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 を叩いて見ると、ロードバランサーがちゃんと動作していることがわかります。

```bash
$ 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` に対して外部のロードバランサーが設定できたことになるのですが、

```bash
$ 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` サービスアカウントのアクセストークンをどうにかしてとってきます。

```bash
$ TOKEN=$(kubectl describe secret \
  $(kubectl get secrets | grep default | cut -f1 -d ' ') | \
  grep -E '^token' | \
  cut -f2 -d':' | tr -d '\t' | tr -d ' ')
```

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

```bash
$ 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` をとってきます。

```bash
$ curl -k --header "Authorization: Bearer $TOKEN" \
  https://${KUBERNETES_API_ENDPOINT}/api/v1/namespaces/lbtest/services/nginx/status \
  > nginx-status.json
```

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

```diff
$ 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` を更新しましょう！

```bash
$ 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` の情報は更新されました。

```bash
$ 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](https://metallb.universe.tf) のソースコードを読んで冗談で書いたものです。今までは特に詳細を気にすることなく、`CloudProvider` インタフェースの `LoadBalancer()` を実装しなくちゃいけない (must) なのかと思ってましたがそんなことなかったんですね。ちゃんと確認するもんです。

## 補足2

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