オンプレ Kubernetes に MetalLB を入れて EdgerouterX と BGP mode で接続し LoadBalancer 構築

## はじめに
オンプレ Kubernetes に、Service の type: Loadbalancer を可能にするため、 
ベアメタル環境で Loadbalancer が構築可能な MetalLB を導入したのでその内容を記載する
また、MetalLB は BGP でプールIP を広報できるため、BGP が利用できコスパが高い EdgerouterX を外部ルータとして利用したのでそちらの内容も合わせて記載する

## MetalLB とは

https://metallb.universe.tf/

 - ベアメタル環境向け Loadbalancer 実装
 - Loadbalancer で付与する IP をクラスタ外部に広報する方法として、2つのモードがある
    - Layer2 mode (ARP/NDP) 
        - ノード接続セグメントと同じセグメントにプールIPを作成して ARP(IPv4)/NDP(IPv6) で広報する
    - BGP mode
        - BGP で外部ルータとピアリングをし Loadbalancer で使用するプール IP を広報する
 - 現在はベータ版 [[成熟度](https://metallb.universe.tf/concepts/maturity/)]

今回利用 Version: **v0.8.3** (2020.03.01 時点で最新)

## EdgeRouter X (ER-X) とは
Vyatta ベースの OS で動く Router
[Amazon で ¥9,999 (2020.03.01時点)](https://www.amazon.co.jp/Ubiquiti-Networks-ER-X-Edgerouter-ER-X%EF%BC%88%E6%97%A5%E6%9C%AC%E5%9B%BD%E5%86%85%EF%BC%89/dp/B010MZFH5A) でとてもコスパがいい


 - Vyatta からフォークされ開発されている EdgeOS で動作 (Vyatta, VyOS と似た操作・設定)
 - WebUI 有り。ある程度なら Web で操作・設定が可能
 - Routing Protocol は様々なプロトコルに対応
    - Static Routes
    - OSPF/OSPFv3
    - RIP/RIPng
    - BGP (with IPv6 Support)
    - IGMP Proxy
 - Security 機能として ACL/Zone Base Firewall および NAT 機能
 - その他にも色々機能あり[[データシート](https://dl.ubnt.com/datasheets/edgemax/EdgeRouter_X_DS.pdf)]
 - 最大消費電力 5W
 - ベンダー： Ubiquiti Networks

実機写真
![スクリーンショット 2020-03-01 22.52.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326183/1118f9f9-ffbe-610c-3840-4697fc09d824.png)

OS Version は後述するが、2020.03.01 時点で最新の **v2.0.8** に Upgrade している


## Kubernetes クラスタ環境

kubespray で構築した Kubernetes クラスタ。(intel nuc + ESXi + CentOS7 + kubespray)
Version : **v1.16.3**
詳細構成は下記記事の環境。
https://qiita.com/suzuyui/items/e7531fe5e1e84c061b23

ノードのIPは 192.168.129.0/24 のうち、192.168.129.25-28,35-38 を使用している。

## 構成図
今回の、 Metallb リソースや Network 構成を整理把握するため、概要を下記に記載する。
(左側のオレンジ・紫付近を Netowrk , 右側の K8s cluster 内が Kubernetes リソースをメインに記載)

![metallb2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326183/e4c684df-39de-21bd-63f8-ff0f39271ed8.png)


 - Kubernetes リソース (青色箇所)
    - Controler は Deployment で通常 1 Pod 構築される
    - Speaker は DaemonSet で tolerations も設定され、Master/Worker 全ノードに構築される
    - Controler / Speaker は SA を持ち rules が適応されている
    - Speaker は PodSecurityPolicy が適応される
 - BGP 構成 (オレンジ・紫箇所)
    - EdgeRouterX と MetalLB 間は eBGP で接続 (iBGP でも可)
    - MutiPaths 設定で接続し ECMP でのルーティングを実施する
    - config詳細は後述する
    - 今回は、EdgeRouterX 側を AS:65001, MetalLB 側を AS:65002, LoadBalancer へのIPプールアドレスは 192.168.131.0/24 とした

また、**DaemonSet** は下記の記載がある。

 - `hostnetwork: true`
    - Pod はノードのネットワークに属することができる。これは PodSecurityPolicy で許可されている。

概要図の設定は下記に分かれる。(赤色で設定範囲を囲っている)

![metallb_config2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326183/f2ad8db8-5d6d-ddf4-b1c5-60841f1b5046.png)


上記図の設定 3 つは下記

 - Edgerouter Config : Edgerouter で設定する Config範囲 (BGP 設定を抜粋して Edgerouter X 設定の際に記載する)
 - metallb.yaml: metallb 導入時に使用するyaml
 - metallb-config.yaml: metallb の設定をするユーザ側で設定する config


## 実行手順
下記の順番で導入し確認を実施する。

1. MetalLB インストール
2. MetalLB Configuration
3. Edgerouter X OS Upgrade
4. Edgerouter X Configuration
5. Loadbalancer 利用テストアプリでの動作確認

参考：
https://metallb.universe.tf/
https://github.com/metallb/metallb

コマンドは 2020.03.01 時点の記載となる。

## 1. MetalLB インストール

MetalLB を公式サイトの manifest : **metallb.yaml** をダウンロードして apply して導入する。

```
$ curl -OL  https://raw.githubusercontent.com/google/metallb/v0.8.3/manifests/metallb.yaml
$ kubectl apply -f metallb.yaml
namespace/metallb-system created
podsecuritypolicy.policy/speaker created
serviceaccount/controller created
serviceaccount/speaker created
clusterrole.rbac.authorization.k8s.io/metallb-system:controller created
clusterrole.rbac.authorization.k8s.io/metallb-system:speaker created
role.rbac.authorization.k8s.io/config-watcher created
clusterrolebinding.rbac.authorization.k8s.io/metallb-system:controller created
clusterrolebinding.rbac.authorization.k8s.io/metallb-system:speaker created
rolebinding.rbac.authorization.k8s.io/config-watcher created
daemonset.apps/speaker created
deployment.apps/controller created
```

適応すると、controller が 1 Pod と、各ノードに speaker Pod が構築される

```
$ kubectl get po -n metallb-system -o wide
NAME                          READY   STATUS    RESTARTS   AGE     IP               NODE            NOMINATED NODE   READINESS GATES
controller-65895b47d4-5tpkx   1/1     Running   0          6h39m   10.233.92.184    k8sworker02     <none>           <none>
speaker-4db6t                 1/1     Running   0          6h39m   192.168.129.26   k8smaster03     <none>           <none>
speaker-ftz2p                 1/1     Running   0          6h39m   192.168.129.38   k8scalicorr02   <none>           <none>
speaker-kntts                 1/1     Running   0          6h39m   192.168.129.25   k8smaster01     <none>           <none>
speaker-pgnll                 1/1     Running   0          6h39m   192.168.129.28   k8scalicorr01   <none>           <none>
speaker-rjc4n                 1/1     Running   0          6h39m   192.168.129.27   k8sworker01     <none>           <none>
speaker-tzhmv                 1/1     Running   0          6h39m   192.168.129.37   k8sworker03     <none>           <none>
speaker-v2xz7                 1/1     Running   0          6h39m   192.168.129.35   k8smaster02     <none>           <none>
speaker-vp7rp                 1/1     Running   0          6h39m   192.168.129.36   k8sworker02     <none>           <none>
```

## 2. MetalLB Configuration

MetalLB の設定をする ConfigMap を作成する。
今回は BGP mode で設定する。

 - my-asn: k8s-cluster 向けに `65002` とした
 - peer-ana: EdgeRouterX 側のAS番号。今回はeBGPのため別のAS `65001` とした
 - peer-address: EdgeRouterX 側のアドレス
 - protocol: `bgp` とする
 - addresses: loadbalancer で実際に付与するアドレスプールアドレス

```yaml:metallb-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  namespace: metallb-system
  name: config
data:
  config: |
    peers:
    - my-asn: 65002
      peer-asn: 65001
      peer-address: 192.168.129.254
    address-pools:
    - name: my-ip-space
      protocol: bgp
      addresses:
      - 192.168.131.0/24

```

上記を apply する

```
$ kubectl apply -f metallb-config.yaml
configmap/config created
```

## 3. Edgerouter X OS Upgrade

EdgerouterXのOS Upgrade を実施する。
EdgeOS が v1.10 以降であれば実施不要。

### Upgrade 理由
Edgerouter では Version により BGP ECMP 構成でメモリリークがあるらしく、
コミュニティではv1.10以降は大丈夫との報告があるので、念のため最新版にバージョンアップグレードを実施する。

#### 参考コミュニティ情報
https://community.ui.com/questions/eBGP-multipath-memory-leak-EdgeRouter-Pro-v1-9-7hotfix-1/937739a1-1e12-4be5-8a56-d4d0aaaf39d8

上記抜粋
`eBGP multipath memory leak / EdgeRouter Pro v1.9.7+hotfix.1`
`Since updating to 1.10, I have not encountered the memory leak problem.`


#### OS Download
OSは下記でダウンロード可能。ER-Xと記載のあるものをダウンロードする。

https://www.ui.com/download/edgemax

<img width="1414" alt="スクリーンショット 2020-02-29 14.01.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326183/748b0a8a-e6b8-e90b-3979-edba97282402.png">


#### OS Upgrade
WebUI でログインして、Systemタブを開き「Upgrade System Image」でダウンロードした OS を Upload する。
Upload が終わると、Upgrade を続けるかなど聞かれるのでビューに従い進める。(rebootすることになる)

<img width="1387" alt="スクリーンショット 2020-02-29 14.04.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326183/5dc334ff-2cbc-3475-6e82-595c7329ea84.png">


Upgradeが終わるとログインし直しとなり、左上の ver 表記が変わる。

Upgrade前
![スクリーンショット 2020-02-29 14.17.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326183/e9390299-cde0-98e2-379b-90b46d1772b6.png)

Upgrade後
![スクリーンショット 2020-02-29 14.46.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326183/5697420a-8111-8d06-676a-fc8c2a4eab8a.png)

(2020.03.01 時点で最新の **v2.0.8** に Upgrade が完了している)

## 4. Edgerouter X Configuration

EdgeRouterX へ今回のための BGP の設定を追加します。

 - maximum-paths ebgp: 最大のパス数を 32 にする (これはノード数以上であれば良い)
 - peer-group: k8scluster01 として BGP ネイバーをグルーピングする
    - default-originate: デフォルトルートを広報する
    - remote-as: k8s側のAS番号
 - neighbor: k8sノードのIP分設定する (下記では 8IP)

```edgerouter-bgp.config
set protocols bgp 65001 maximum-paths ebgp 32
set protocols bgp 65001 neighbor 192.168.129.25 peer-group k8scluster01
set protocols bgp 65001 neighbor 192.168.129.26 peer-group k8scluster01
set protocols bgp 65001 neighbor 192.168.129.27 peer-group k8scluster01
set protocols bgp 65001 neighbor 192.168.129.28 peer-group k8scluster01
set protocols bgp 65001 neighbor 192.168.129.35 peer-group k8scluster01
set protocols bgp 65001 neighbor 192.168.129.36 peer-group k8scluster01
set protocols bgp 65001 neighbor 192.168.129.37 peer-group k8scluster01
set protocols bgp 65001 neighbor 192.168.129.38 peer-group k8scluster01
set protocols bgp 65001 parameters router-id 192.168.129.254
set protocols bgp 65001 peer-group k8scluster01 default-originate
set protocols bgp 65001 peer-group k8scluster01 remote-as 65002
```

適応

```
$ configure
#[edit]
# 上記のコマンドを投入
[edit]
# commit
[edit]
# save
Saving configuration to '/config/config.boot'...
Done
[edit]
# exit
exit
$ 
```

BGP ネイバー確認

```
$ show ip bgp summary   
BGP router identifier 192.168.129.254, local AS number 65001
BGP table version is 12
2 BGP AS-PATH entries
0 BGP community entries
32  Configured ebgp ECMP multipath: Currently set at 32
1  Configured ibgp ECMP multipath: Currently set at 1

Neighbor                 V   AS   MsgRcv    MsgSen TblVer   InQ   OutQ    Up/Down   State/PfxRcd
192.168.129.25           4 65002  415        418      12      0      0  03:12:02               2
192.168.129.26           4 65002  415        420      12      0      0  03:12:02               2
192.168.129.27           4 65002  412        418      12      0      0  03:12:02               2
192.168.129.28           4 65002  415        420      12      0      0  03:12:02               2
192.168.129.35           4 65002  412        418      12      0      0  03:12:02               2
192.168.129.36           4 65002  412        417      12      0      0  03:12:02               2
192.168.129.37           4 65002  412        418      12      0      0  03:12:02               2
192.168.129.38           4 65002  415        420      12      0      0  03:12:02               2

Total number of neighbors 8

Total number of Established sessions 8
```

ルーティング確認

```
$ show ip route
Codes: K - kernel, C - connected, S - static, R - RIP, B - BGP
       O - OSPF, IA - OSPF inter area
       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2
       E1 - OSPF external type 1, E2 - OSPF external type 2
       > - selected route, * - FIB route, p - stale info

IP Route Table for VRF "default"
〜〜〜
中略
〜〜〜
B    *> 192.168.131.0/32 [20/0] via 192.168.129.38, eth4.100, 00:10:42
     *>                  [20/0] via 192.168.129.37, eth4.100, 00:10:42
     *>                  [20/0] via 192.168.129.36, eth4.100, 00:10:42
     *>                  [20/0] via 192.168.129.35, eth4.100, 00:10:42
     *>                  [20/0] via 192.168.129.28, eth4.100, 00:10:42
     *>                  [20/0] via 192.168.129.27, eth4.100, 00:10:42
     *>                  [20/0] via 192.168.129.26, eth4.100, 00:10:42
     *>                  [20/0] via 192.168.129.25, eth4.100, 00:10:42
```
全ての Neighbor からのルーティングがベストパス ( **\*>** ) となっていることが確認できる。
Service(Type: Loadbalancer) へアドレスプールからアサインされた後に /32 単位で広報されるようで、
今回は LoadBalancer Service が 1 つ動いていたので 1 IP (ここでは192.168.131.0/32) が上記では広報された。

## 5. Loadbalancer 利用テストアプリでの動作確認
テストアプリケーションでloadbalancer service を通して外部からアクセスできるようになったことを確認する。

テストは [metallb の github](https://github.com/metallb/metallb/tree/main/manifests) にあった Manifest dir 内の tutorial を元にしている。

```yaml:testlb.yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1
        ports:
        - name: http
          containerPort: 80

---
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  ports:
  - name: http
    port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: nginx
  type: LoadBalancer
```

上記テスト Deployment, Service を apply する

```
$ kubectl apply -f testlb.yaml 
deployment.apps/nginx created
service/nginx created
$ kubectl get po
NAME                     READY   STATUS    RESTARTS   AGE
nginx-5f78746595-877sq   1/1     Running   0          10s
```

kubectl get svc で `EXTERNAL-IP` に IP がアサインされていることを確認する

```
$ kubectl get svc
NAME         TYPE           CLUSTER-IP     EXTERNAL-IP     PORT(S)        AGE
kubernetes   ClusterIP      10.233.0.1     <none>          443/TCP        77d
nginx        LoadBalancer   10.233.7.180   192.168.131.1   80:30706/TCP   14s
```

Edgeroute X 側で `show ip route` を実施し、上記 EXTERNAL-IP が追加広報されていることを確認する

```
$ show ip route
Codes: K - kernel, C - connected, S - static, R - RIP, B - BGP
       O - OSPF, IA - OSPF inter area
       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2
       E1 - OSPF external type 1, E2 - OSPF external type 2
       > - selected route, * - FIB route, p - stale info

IP Route Table for VRF "default"
〜〜〜
中略
〜〜〜
B    *> 192.168.131.0/32 [20/0] via 192.168.129.38, eth4.100, 04:23:57
     *>                  [20/0] via 192.168.129.37, eth4.100, 04:23:57
     *>                  [20/0] via 192.168.129.36, eth4.100, 04:23:57
     *>                  [20/0] via 192.168.129.35, eth4.100, 04:23:57
     *>                  [20/0] via 192.168.129.28, eth4.100, 04:23:57
     *>                  [20/0] via 192.168.129.27, eth4.100, 04:23:57
     *>                  [20/0] via 192.168.129.26, eth4.100, 04:23:57
     *>                  [20/0] via 192.168.129.25, eth4.100, 04:23:57
B    *> 192.168.131.1/32 [20/0] via 192.168.129.38, eth4.100, 00:01:30
     *>                  [20/0] via 192.168.129.37, eth4.100, 00:01:30
     *>                  [20/0] via 192.168.129.36, eth4.100, 00:01:30
     *>                  [20/0] via 192.168.129.35, eth4.100, 00:01:30
     *>                  [20/0] via 192.168.129.28, eth4.100, 00:01:30
     *>                  [20/0] via 192.168.129.27, eth4.100, 00:01:30
     *>                  [20/0] via 192.168.129.26, eth4.100, 00:01:30
     *>                  [20/0] via 192.168.129.25, eth4.100, 00:01:30
```

外部からブラウザで EXTERNAL-IP にアサインされた IP へアクセスできることを確認する

![スクリーンショット 2020-03-01 21.10.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/326183/911dac61-4bf3-8ea1-4e0a-a664cfe402b6.png)

以上、外部から直接アクセスできる IP まで作成完了することを確認できた。



## その他. Kubespray
kubespray でも MetalLB がサポートされている模様。(本構成は kubespray で構築した環境で実施したが、下記では実施してない)

https://github.com/kubernetes-sigs/kubespray/tree/master/contrib/metall

また、L2mode を使用する場合は、`kube_proxy_strict_arp`で デフォルト `false` から `true` に変更する必要がありそう。

```yaml:k8s-cluster.yml(該当箇所抜粋)
# configure arp_ignore and arp_announce to avoid answering ARP queries from kube-ipvs0 interface
# must be set to true for MetalLB to work
# kube_proxy_strict_arp: false
kube_proxy_strict_arp: true
```

## 追記(2020.03.20). IPアドレス共有 allow-shared-ip
現在 (2020.03.20 時点)、Kubernetes は Loadbalancer にて[**複数のプロトコル (TCP/UDP) を同じ IP で使用することを許可されてない**](https://github.com/kubernetes/kubernetes/issues/23880)
(TCP/UDP 両方指定の type: Loadbalancer を作成すると `cannot create an external load balancer with mix protocols` とエラーが出る)

Metallb では、`allow-shared-ip` を使用することで、複数の Loadbalancer Service に同じ IP を指定することが可能になる。

指定方法は、Service (type: Loadbalancer) に、`metadata` として下記のように `metallb.universe.tf/allow-shared-ip` 追記する。
※下記例の share-service-name は共通にする名前(変数)。同じ annotation の Service が同じ IP が振られる(振れる)

```yaml
metadata:
  annotations:
    metallb.universe.tf/allow-shared-ip: "share-service-name"
```

`allow-shared-ip` を使用することで `type: LoadBalancer` を、
**TCP/UDP を同じ IP で Service 提供**できたり、
**違う Deployment (違う selector での 別Service) のアプリケーションを同じ IP でポートで分けて提供**できたりする。

Metallb 公式: https://metallb.universe.tf/usage/#ip-address-sharing

## おわりに
オンプレ環境でも Service type: Loadbalancer が作成できるようになった。
外部への IP (EXTERNAL-IP) も確保できるようになったので、今後のサービスはこれで作成していく予定。