Kubernetesクラスターを「クラウド↔自宅ネットワーク」間で作る（前編）

## 背景

Kubernetesの勉強用に、クラウド上で２つのVMを作りクラスター(Masterノード、Workerノード)の構築をしていた。が、勉強用でたまにしか使わないのに2つのVMぶんの料金が毎月掛かってもったいないので、Workerノードは家にあるLinuxマシンに作って、Masterノードのクラウドと繋げられるといいなあと思い始めた。
このようなモチベーションから試行錯誤した試みを本稿でまとめる。

## 実現したい構成

:::note info
2022/12/20 追記
本記事は「クラスターの２ノードは別々のプライベートネットワークに属する前提」で書いており、かなり七面倒くさいネットワーク設定を行っている。
これは[Tailscale](https://tailscale.com/)というP2PのVPNサービスを使えばより簡単に解決できることを確認した。Tailscaleを用いたクラスター構築方法を以下のQiitaにまとめたため、特別な理由がなければそちらを見ていただくことをお勧めする。
:::

https://qiita.com/showchan33/items/7500bcb73b10be437e49


下図が実現したい構成。
クラウド上のVMと自宅のLinuxマシンは公開IPでインターネットを経由しないとつながらないので、ここをどのように実現するかが一番のポイントとなる。

![network1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/370519/87559efc-88fb-fcb1-faeb-a1027dc9671d.png)


k8sクラスターを構築するマシン、ネットワークの情報を念のため記載

* クラウドサービス側
    * Masterノードにしたいマシン
        * ホスト名 : k8s-master
        * OS : Ubuntu 18.04
        * 公開IP : 20.30.40.50/24
        * プライベートIP : 10.0.0.4/24
* 自宅ネットワーク側
    *  ルーター（HG8045Q）
        * OS : 不明（Linux系？)
        * 公開IP : 60.70.80.90/24
        * プライベートIP : 192.168.1.1/24 （今後は登場しないがご参考まで）
 
    * Workerノードにしたいマシン
        * ホスト名 : k8s-worker
        * OS : Debian 9
        * 公開IP : **なし（ルーター経由でしかインターネット接続できない）**
            * ここに対処するための工夫が後々必要となる
        * プライベートIP : 192.168.1.4/24

## Kubernetesのクラスター構築

今回は最も一般的な(?)kubeadmを使って構築する。基本的な手順は以下サイトに基づくが、幾つかカスタマイズが必要なので後ほど説明する。

* kubeadmのインストール
    * https://kubernetes.io/ja/docs/setup/production-environment/tools/kubeadm/install-kubeadm/

### (1) セキュリティ設定（ポート解放）

:::note warn
[Tailscale](https://tailscale.com/)などで構築したプライベートネットワークが使える場合は不要の手順
:::

双方で、インターネットに対して以下のポートを開放する必要あり

**23/03/03 追記**
以下に示すポートは筆者の試行錯誤で記載したもの。 Kubernetes利用時に使うポートはkubernetes.ioの[必須ポートの確認](https://kubernetes.io/ja/docs/setup/production-environment/tools/kubeadm/install-kubeadm/#%E5%BF%85%E9%A0%88%E3%83%9D%E3%83%BC%E3%83%88%E3%81%AE%E7%A2%BA%E8%AA%8D)に記載されているので、素直にこちらの内容に従うほうが間違いないはず。

* Masterノード（クラウド側）
    * 以下のポート（6443だけだが…）を、各クラウドサービスのセキュリティ設定で通信許可するようにする
    * 尚、kube-apiserverのポート番号6443はデフォルト値で別の値にカスタマイズも可能

| 通信許可するIP | プロトコル | ポート番号 | 通信の方向 | 用途 |
| --- | --- | --- | --- | --- |
| 60.70.80.90<br>(自宅LANの公開IP) | TCP | 6443 | Inbound | Workerノードがkube-apiserverを介して通信するのに利用 |

* Workerノード（自宅LAN側）
    * 以下のポート（10250だけだが…）を、自宅ルーターのWeb管理ツールなどを使って通信許可するようにする。（他のルーターでもおそらく同じように設定できるはず？）
    * 筆者宅のルーターはHG8045Qを使っており、[こちらのサイト](https://www.akakagemaru.info/port/hg8045q-portfw.html)などを参考にするとインターネットからの通信を受け入れ可能にできる
    * 注意すべきは、実際にポート番号10250でサービス提供するのは「公開IPをもつルーターではなく、自宅LAN内にあるマシン」だということ。つまり、「インターネットから`20.30.40.50:10250`で受けたリクエスト」を「WorkerノードにするマシンのIP:ポート(=`192.168.1.4:10250`)」に転送する設定が必要となる。

| 通信許可するIP | プロトコル | ポート番号 | 通信の方向 | 用途 |
| --- | --- | --- | --- | --- |
| 20.30.40.50<br>(クラウドサーバの公開IP) | TCP | 10250 | Inbound | MasterノードがWorkerノード内のPodにアクセスする際に必要 |

### (2) kubeletサービス起動時のノードIP指定

kubeadmを使ってクラスター構築する前に、各ノードが`kubeadm init`や`kubeadm join`コマンドでKubernetesクラスターに仲間入りする際に使うIPアドレス（ノードのIP）を明示的に指定する。ノードIPが他のノードから認識できないIPに設定されてしまうとクラスターが正常に構築できなくなってしまうので注意が必要である。
適切でない例として、例えば２つのノードが公開IPを介してしかアクセスできないのに、ノードIPがプライベートIPで設定されてしまう等。逆にプライベートネットワークでつながれるのに、公開IPがノードIPに設定されてしまうのも（一概には言えないが）よろしくないかも。

具体的には各マシンに`/etc/default/kubelet`を作成して、以下のように設定

:::note warn
[Tailscale](https://tailscale.com/)などで構築したプライベートネットワークが使える場合はそのIPを記載すればいいはず
:::

* Masterノード（クラウド側）
    * ``--node-ip``にクラウドサーバの公開IPを設定する

```txt:/etc/default/kubelet
KUBELET_EXTRA_ARGS=--node-ip=20.30.40.50
```

* Workerノード（自宅LAN側）
    * ``--node-ip``に自宅LAN内のプライベートIPを指定する

```txt:/etc/default/kubelet
KUBELET_EXTRA_ARGS=--node-ip=192.168.1.4
```

Workerノード側のIPは、先ほど説明した内容と矛盾した値になっている。「公開IP経由でノードがつながるのだから、自宅LANルーターの公開IP（``60.70.80.90``）をWorkerの``--node-ip``に設定するべきではないか？」と思われただろう。ところが公開IPを指定しても、``kubeadm join``コマンドでクラスターにWorkerノード追加後に、kube-apiserverでWorkerノードの公開IPが反映されない。具体的には、以下のようにWorkerノードのINTERNAL-IPが`<none>`となってしまう。

```
user@k8s-master$ kubectl get node -o wide
NAME         STATUS   ROLES    AGE   VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE ...
k8s-master   Ready    master   8d    v1.18.8   20.30.40.50   <none>        Ubuntu 18.04.3 LTS             ...
k8s-worker   Ready    <none>   23h   v1.18.8   <none>        <none>        Debian GNU/Linux 9 (stretch)   ...
```

推測だが、``--node-ip``に指定して有効となるIPは、そのマシンに各ネットワークインターフェースで付与されているIPのみの模様。例えば筆者のWorkerノードとなる自宅LAN内のマシンの付与IPを調べると、有効なIPは以下のように``192.168.1.4``と``172.17.0.1``であり、自宅LANのルーターに付与されている公開IPは出てこない。

```
user@k8s-worker$ ip a | egrep -e ^[1-9] -e "inet "
...
4: wlp2s0: ...
    inet 192.168.1.4/24 brd 192.168.1.255 scope global dynamic wlp2s0
5: docker0: ...
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
```

そこで既に述べたように、自宅LAN内のマシンでの設定ファイル``/etc/default/kubelet``には``--node-ip=192.168.1.4``を指定する。

これらの設定を行った後、Masterノードとなるクラウドサーバでは``kubeadm init``、Workerノードとなる自宅LAN内のマシンでは``kubeadm join``で始まるコマンドを実行する。

* クラウドサーバでのクラスター追加＆Masterノード生成コマンド

```
user@k8s-master$ sudo kubeadm init \
  --pod-network-cidr=10.128.0.0/16 \        # Pod内ネットワーク範囲(好きなように設定可能)
  --apiserver-advertise-address=20.30.40.50 # クラウドサーバの公開IP
```

* 自宅LAN内のマシンでのWorkerノード追加コマンド
    * tokenとhashは``kubeadm init``で最後に出力されるものをコピペすればOK

```
user@k8s-worker$ sudo kubeadm join 20.30.40.50:6443 --token xxxxxxxx... \
    --discovery-token-ca-cert-hash sha256:yyyyyyyy...
```

ここまでやると、２つのマシンでKubernetesクラスターが一応、出来上がる。


```
user@k8s-master$ kubectl get node -o wide
NAME         STATUS   ROLES    AGE   VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE ...
k8s-master   Ready    master   8d    v1.18.8   20.30.40.50   <none>        Ubuntu 18.04.3 LTS             ...
k8s-worker   Ready    <none>   23h   v1.18.8   192.168.1.4   <none>        Debian GNU/Linux 9 (stretch)   ...
```

だがまだ問題が残っている。Kubernetesクラスターは「WorkerノードのIPアドレスは``192.168.1.4``だ」と思っており、Workerノード内のPodに``kubectl exec``コマンド等でアクセスする際に、このIPを使おうとしてしまう。``192.168.1.4``はWorkerノードの自宅LAN内でしか使えないアドレスなので、当然アクセスできない。

## クラウドサーバ(Masterノード)のNAT設定

:::note warn
[Tailscale](https://tailscale.com/)などで構築したプライベートネットワークが使える場合は不要
:::

MasterノードからWorkerノードにアクセス可能にするために、宛先IP``192.168.1.4``へのリクエストをWorkerノードの自宅LANの公開IP``60.70.80.90``に変換する。これをNAT(Network Address Transform)という。（実際は自宅LAN内でまた``192.168.1.4``に変換されるので、あまりスマートなやり方ではないのだが…）
NATを実現する方法はいくつかあるが、ここではiptablesコマンドによる設定を以下に紹介する。

```
user@k8s-master$ sudo iptables -t nat -A OUTPUT \
                               -p tcp -d 192.168.1.4 -j DNAT \
                               --to-destination 60.70.80.90
```

ちなみに、上記の設定を消したい場合は`-A`の部分を `-D` に変換してコマンド実行すればOK。（24/06/04 間違って `-X`と書いていたので修正）

NATが上手くできて、Workerノード内のPodにアクセスできるかどうかを確認する。まずは、kube-systemのNamespaceにできているPodを確認。

```

user@k8s-master$ kubectl get pod -n kube-system -o wide
NAME                                   READY   STATUS    RESTARTS   AGE     IP             NODE         NOMINATED NODE   READINESS GATES
coredns-66bff467f8-djn4w               1/1     Running   0          9d      10.128.117.2   k8s-master   <none>           <none>
coredns-66bff467f8-gf842               1/1     Running   0          9d      10.128.117.3   k8s-master   <none>           <none>
etcd-k8s-master                        1/1     Running   0          9d      20.30.40.50    k8s-master   <none>           <none>
kube-apiserver-k8s-master              1/1     Running   0          9d      20.30.40.50    k8s-master   <none>           <none>
kube-controller-manager-k8s-master     1/1     Running   5          9d      20.30.40.50    k8s-master   <none>           <none>
kube-proxy-p6bwq                       1/1     Running   1          2d      192.168.1.4    k8s-worker   <none>           <none>
kube-proxy-xdtzc                       1/1     Running   0          9d      20.30.40.50    k8s-master   <none>           <none>
kube-scheduler-k8s-master              1/1     Running   5          9d      20.30.40.50    k8s-master   <none>           <none>
```

確認すると、`kube-proxy-p6bwq`はWorkerノードに出来ていることがわかる。上手く行っていれば、以下のようにWorkerノードのPod(コンテナ)にアクセス可能。

```
user@k8s-master$ kubectl exec kube-proxy-p6bwq -n kube-system -it -- /bin/sh
# date
Tue Dec  8 14:06:51 UTC 2020
# 
```

## 残る課題

:::note warn
（くどいですが）[Tailscale](https://tailscale.com/)などで構築したプライベートネットワークが使える場合は課題にはならない
:::

実はもう一つ課題が残っている。現状だとKubernetesのPod同士でノードをまたいだ通信ができない。その方法については[後編](https://qiita.com/showchan33/items/abac0221e2b0ca30f3fb)に記載。