🐈‍⬛

Proxmox上にkube-vipでcontrol-plane・Service LB両対応のHAクラスタを構築する。

2024/08/17に公開

この記事を書いた理由

k8sでHAを完結させたいと思い、しらべてみたところ、kube-vipという存在と出会ったが、
なぜだか皆推奨されているDaemonSetでのデプロイ方式を書いていなかった。
そして、書かれていたstatic podのマニフェストファイルを、kubeadm実行前にmanifestsディレクトリの中に入れて、initを実行するという手法が取られていたが、最新バージョンではこの方式は対応していないようである。
そのため、この記事で改めて今週リリースされたばかりのK8s 1.31.0を使い、いい感じのクラスタを構築する。
実動作を確認したいため、以下のpalworldというゲームサーバーをデプロイして試してみる。
https://github.com/thijsvanloef/palworld-server-docker/blob/main/k8s/readme.md

目標

CEPH領域をPVで利用できるようにして、
試しにPalworldサーバーが動いたら成功とする。

前提知識

・コントロールプレーンは冗長化に最低3台いるよ。
・デフォルトでは、control-planeは管理が仕事なので、control-planeとして登録したnodeに実際に走らせたいPODはデプロイされないよ。
(今回の例では、試しにpalworldのゲームサーバーをデプロイしてみたが、このようなPODを指す)
・K8sのService type LoadBalancerは、クラウドサービスを使うことを前提に作られており、オンプレで再現するには、MetalLBやCiliumのようなCNIのIPAM機能を有効化する必要があるよ。
(例えばAWSにデプロイすれば、AWS ELBと勝手に連携して、IPを振ってくれる。)

なぜProxmoxでCEPH?Rook使えよ。

確かに、Rookでマニフェストファイルを書けばk8sだけでストレージを完結させることができる。
が、果たしてストレージに使ったHDDなどのパスを記述したマニフェストファイルにどれほどの価値があるのか?という話である。
それを考えれば、proxmoxでディスクのwipeなどもGUIでわかりやすくできるので、
proxmox上でやった方がいいと判断した。
もしあなたがお金持ちで、k8sのために新品の同じ構成のサーバーを複数台購入したのであれば、
Rookのほうがいいと思う。

Kube-vipをLBで使うメリット

  • ARPを使える。BGPはなんかうまく動作しないこと[1]がある!
    • BGPは、仮想IPアドレスのプール[2]を作って、LBを建てたら勝手に割り振られるスタイル。
      こっちの方がクラウドサービスの仕様に近いが、オンプレで立てる以上ルーターがDHCPでランダムに定義されている環境の方が多いだろうし、
      (IPアドレスは割り振られるが、アクセスできない。)
      一方、ARPなのであれば、LB毎に使いたいIPを宣言すればいい。
      こっちの方が大抵の環境において堅牢ではないだろうか?
  • kube-vipだけで完結できる。
    • 現状、ciliumのipamも、MetalLBも直接コントロールプレーンのHAに対応していない。
    • そのため、k8sで完結させたい場合、kube-vipは必須。

物理構成(ざっくり)

ノード CPU メモリ IP 備考
pve 12コア 64G 192.168.1.100 GPU付きタワー型サーバー 中古で7万
pve2 8コア 16G 192.168.1.101 Amazonで3万円だった中古のデスクトップPC
pve3 6コア 32G 192.168.1.102 最近飽きたので使ってなかった元ゲーミングPC

注意: Ciliumはマルチアーキテクチャ非対応なので、ラズパイ等を使う場合はarmで統一する必要がある。

さて、問題はVMをどう割り振るかだが、
まずcontrol-plane 3台は確定している。
2台以下だと、どれかが停止した場合、クラスタが崩壊してしまう。
ので、VMでそれぞれのノードに16Gのcontrol-plane VMを配置することにする。
つまりpve2は16Gしかないので、control-planeしか入らない。
ワーカーノードが2台しかないので、別にcontrol-planeのメモリは8Gでも全然いいと思うが、ゆとりを持って16Gにしている。

そして、余った領域にワーカーノードを押し込むことにする。
CPUはオーバーコミットして、それぞれのVMで全てのコアを利用することにする。
最終的な構成はこうなった。
pve2にもワーカーノードを作りたくなった時ように、k8s-worker-2の名はは空けておく。
ちなみにOSは全てubuntu22.04

物理ノード ホスト名 メモリ IP
pve k8s-controller-1 16G 192.168.1.10
pve k8s-worker-1 48G 192.168.1.20
pve2 k8s-controller-1 16G 192.168.1.11
pve3 k8s-controller-3 16G 192.168.1.12
pve3 k8s-worker-3 16G 192.168.1.21

control-planeのVIPは192.168.1.50
palworldのVIPは192.168.1.241とする。(空いてたから適当)

CEPHの設定はこの記事の通りにやったらうまくいった。
この前仮想環境を立てた後だと、なぜかcephが自動でインストールできなかったので、早めにインストールだけしておくといいかもしれない。
https://www.tunamaguro.dev/articles/use-proxmox-ceph-from-k8s/

Proxmox上の画面はこんな感じ。(VM101だけ名前をつけ忘れた)

各VEでの作業

ついにUbuntuでの作業を開始する。
テンプレートを作るという方式の方が効率的だが、せいぜい5つしか作らないので、一個ずつやる。
$マークを省いて記述したので、以下の内容をコピペすれば良い。

全てのノードでやる作業

ネットワーク設定
sudo apt update && sudo apt -y upgrade

cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
sudo modprobe overlay
sudo modprobe br_netfilter
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF
sudo sysctl --system

swap無効化

これをやらないとkubeadmとランタイムがエラーを吐く

swap無効
sudo swapoff -a

再起動後もswapを無効にするようにする

/etc/fstab
- /swap.img      none    swap    sw      0       0\
+ #/swap.img      none    swap    sw      0       0

ランタイムのインストール

sudo apt install containerd
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/g' /etc/containerd/config.toml
sudo sed -i 's#sandbox_image = "registry.k8s.io/pause:3.6"#sandbox_image = "registry.k8s.io/pause:3.9"#g' /etc/containerd/config.toml
sudo systemctl restart containerd && systemctl status containerd

kubeadm kubectl kubeletインストール

sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl gpg jq
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.31/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.31/deb/ /' | sudo tee /etc/apt/sources.list.d/kubernetes.list

sudo apt-get update
sudo apt-get install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl

kube-vip対策

さて、あとはk8s-controller-1でkubeadm initして、
他のノードでjoinすれば終わりだーという状態までいける。
だが待ってほしい。このままだと、VIPが作れていないので、control-plane-endpointが固定されてしまう!
かといって、クラスタを構築してしまったら後から変更するのは至難の業だ。
しかし、kube-vipをDaemonsetで立てるには、クラスタを立ち上げないといけない。
これではマッチポンプではないか!という問題に対応する。
というえわけで、/etc/hostsにk8s-controllerという名前で登録しそれを参照するようにする。
これならIPアドレスが変わっても問題ないというわけだ。
しかも、複数のIPを同じ名前で登録できるので、後から編集する必要性すらない。

cat <<EOF | sudo tee -a /etc/hosts
192.168.1.10 k8s-controller-1
192.168.1.11 k8s-controller-2
192.168.1.12 k8s-controller-3
192.168.1.50 k8s-controller
192.168.1.10 k8s-controller
192.168.1.20 k8s-worker-1
192.168.1.21 k8s-worker-3
EOF

ちなみに192.168.1.10で仮にinitし、あとから立てたVIPを指定してもpre-flightで弾かれてしまう。

k8s-controller-1でのみやる作業

initでさっき設定したk8s-controllerというホスト名でendpointを指定する。
正常に終了すれば、コントロールプレーン用のjoinコマンド、ワーカー用のjoinコマンドが生成されるので、
ちゃんと保存しておくように。トークンと一緒に出力されるので、ネット上のコマンドで代替できない。
--control-planeというオプションがついている方が、コントロールプレーン用のコマンドだ。

sudo kubeadm init \
--control-plane-endpoint k8s-controller \
--pod-network-cidr=10.1.0.0/16 \
--upload-certs

kubectlを使えるようにする

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -gh) $HOME/.kube/config

kube-vipのインストール

クラスタを建てたら、早速kube-vipをインストールする。
まず適切に権限を割り振るため、RBAC設定をapplyする。

kubectl apply -f https://kube-vip.io/manifests/rbac.yaml

公式ドキュメントにマニフェストファイルを生成する方法があるので、その通りにやる。
ただ、公式ドキュメントではrootユーザーで実行しているが、kubectlは一般ユーザーで利用するので、
rootでマニフェストファイルを生成したのち、一般ユーザーに戻ってapplyする。

rootになる
sudo su

インターフェースのチェック

kube-vipに使うインターフェースを渡す必要がある。コマンドでどれを使うべきか調べておこう。

ip address

結果

2: ens18: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether bc:24:11:a9:c1:45 brd ff:ff:ff:ff:ff:ff
    altname enp0s18
    inet 192.168.1.10/24 brd 192.168.1.255 scope global ens18
       valid_lft forever preferred_lft forever
    inet6 2409:10:b3c0:5400:be24:11ff:fea9:c145/64 scope global dynamic mngtmpaddr noprefixroute
       valid_lft 2591570sec preferred_lft 604370sec
    inet6 fe80::be24:11ff:fea9:c145/64 scope link
       valid_lft forever preferred_lft forever

いっぱい出てきたが、192.168.1.10/24が割り当てられているens18が対象だ。
さっき書いた通り、コントロールプレーンのVIPは192.168.1.50にするので、それを公式のコマンドに落とし込む。

export VIP=192.168.1.50
export INTERFACE=ens18

KVVERSION=$(curl -sL https://api.github.com/repos/kube-vip/kube-vip/releases | jq -r ".[0].name")

alias kube-vip="ctr image pull ghcr.io/kube-vip/kube-vip:$KVVERSION; ctr run --rm --net-host ghcr.io/kube-vip/kube-vip:$KVVERSION vip /kube-vip"

マニフェスト生成

公式ドキュメントのコマンドはマニフェストを生成するだけなので、生成結果をapplyする必要がある。
ので、kube-vip-daemon.yamlとして保存しておく。名前はなんでもいい。
もしどうしてもMetalLBなどを別で使いたいのであれば、競合してしまうので、
servicesオプションを消すこと。

kube-vip manifest daemonset \
    --interface $INTERFACE \
    --address $VIP \
    --inCluster \
    --taint \
    --controlplane \
    --services \
    --arp \
    --leaderElection > kube-vip-daemon.yaml
一般ユーザーに戻る
exit
kubectl apply -f kube-vip-daemon.yaml

VIPが正常に動作しているかテスト。

これに応答があれば、kube-vipのインストールは完了だ。

ping 192.168.1.50

こうなればおk

PING 192.168.1.50 (192.168.1.50) 56(84) bytes of data.
64 bytes from 192.168.1.50: icmp_seq=1 ttl=64 time=1.39 ms
64 bytes from 192.168.1.50: icmp_seq=2 ttl=64 time=0.935 ms
64 bytes from 192.168.1.50: icmp_seq=3 ttl=64 time=0.974 ms
64 bytes from 192.168.1.50: icmp_seq=4 ttl=64 time=0.996 ms

他のノードでjoin

ここまでうまくいけば、あとはjoinするだけ。
さっき生成されたコマンドに、sudoをつければいい。

k8s-controller-2・3で実行
sudo kubeadm join k8s-controller --token <TOKEN> \
	--discovery-token-ca-cert-hash <token-hash> \
	--control-plane --certificate-key <certificate-key>
k8s-worker-1・3で実行
sudo kubeadm join k8s-controller --token <TOKEN> \
	--discovery-token-ca-cert-hash <token-hash> \

cliumインスコ

cilium公式ドキュメントのコピペ

k8s-controller-1
CILIUM_CLI_VERSION=$(curl -s https://raw.githubusercontent.com/cilium/cilium-cli/main/stable.txt)
CLI_ARCH=amd64
if [ "$(uname -m)" = "aarch64" ]; then CLI_ARCH=arm64; fi
curl -L --fail --remote-name-all https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VERSION}/cilium-linux-${CLI_ARCH}.tar.gz{,.sha256sum}
sha256sum --check cilium-linux-${CLI_ARCH}.tar.gz.sha256sum
sudo tar xzvfC cilium-linux-${CLI_ARCH}.tar.gz /usr/local/bin
rm cilium-linux-${CLI_ARCH}.tar.gz{,.sha256sum}

cilium install

クラスタがちゃんと構築できてるか確認

ちょっと時間を置いてから実行する。

k8s-controller-1
kubectl get nodes
cilium status

こうなってれば完璧

NAME               STATUS   ROLES           AGE   VERSION
k8s-controller-1   Ready    control-plane   19h   v1.31.0
k8s-controller-2   Ready    control-plane   19h   v1.31.0
k8s-controller-3   Ready    control-plane   19h   v1.31.0
k8s-worker-1       Ready    <none>          19h   v1.31.0
k8s-worker-3       Ready    <none>          19h   v1.31.0
    /¯¯\
 /¯¯\__/¯¯\    Cilium:             OK
 \__/¯¯\__/    Operator:           OK
 /¯¯\__/¯¯\    Envoy DaemonSet:    OK
 \__/¯¯\__/    Hubble Relay:       disabled
    \__/       ClusterMesh:        disabled

Deployment             cilium-operator    Desired: 1, Ready: 1/1, Available: 1/1
DaemonSet              cilium-envoy       Desired: 5, Ready: 5/5, Available: 5/5
DaemonSet              cilium             Desired: 5, Ready: 5/5, Available: 5/5
Containers:            cilium             Running: 5
                       cilium-operator    Running: 1
                       cilium-envoy       Running: 5
Cluster Pods:          8/8 managed by Cilium

最終段階 デプロイ!!!!!

palworldをデプロイして、実際に動くか検証する。

管理しやすいように、namespaceを作っておく

kubectl create ns palworld-server
git clone https://github.com/thijsvanloef/palworld-server-docker
cd palworld-server-docker/k8s
vim pvc.yaml

CEPHはさっきのサイトどうりに構築し、PVも立てている。
cephのrdbを使用するように設定する。

pvc.yaml
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  labels:
    app: palworld-server
  name: palworld-server-datadir
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 12Gi
+ storageClassName: ceph-rbd

kubevipでLoadBalancerにIPを割り振ってもらうには、
次のように記述する。

service.yaml
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: palworld-server
  name: palworld-server
+ annotations:
+   kube-vip.io/loadbalancerIPs: "192.168.1.241"
spec:
  ports:
    - name: server
      port: 8211
      protocol: UDP
      targetPort: server
    - name: query
      port: 27015
      protocol: UDP
      targetPort: query
  selector:
    app: palworld-server
  type: LoadBalancer
kubectl apply -f pvc.yaml -f configmap.yaml -f secret.yaml -f service.yaml -f deployment.yaml -n palworld-server

コンテナを作るのに結構時間がかかる
以下のコマンドで、時々様子を見るといい。

kubectl -n palworld-server get all

以下のようになればOKだ。

NAME                                   READY   STATUS    RESTARTS   AGE
pod/palworld-server-5746656875-2wbdg   1/1     Running   0          15h

NAME                      TYPE           CLUSTER-IP    EXTERNAL-IP     PORT(S)                          AGE
service/palworld-server   LoadBalancer   10.99.63.50   192.168.1.241   8211:30743/UDP,27015:31000/UDP   17h

NAME                              READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/palworld-server   1/1     1            1           17h

NAME                                         DESIRED   CURRENT   READY   AGE
replicaset.apps/palworld-server-5746656875   1         1         1       17h

ちゃんとEXTERNAL-IPが割り振られているようだ。素晴らしい。
ちゃんと192.168.1.241にアクセスできるか、クラスタに無関係のMacbookで調べてみる。

UDPポートスキャン
sudo nmap -sU 192.168.1.241 -p 8211
Starting Nmap 7.94 ( https://nmap.org ) at 2024-08-17 16:03 JST
Nmap scan report for 192.168.1.241
Host is up (0.0026s latency).

PORT     STATE         SERVICE
8211/udp open|filtered unknown
MAC Address: BC:24:11:2F:EE:A7 (Unknown)

Nmap done: 1 IP address (1 host up) scanned in 0.31 seconds

Openとなってる。最後に、PalworldがプレイできればOKだ。

実験は成功だ!
Proxmoxのダッシュボードを見てみよう。

うん。間違いなくpve3のワーカーノードで展開されている。
ゲームサーバーはステートフルなので、複数サーバーでPODを展開はできないが、pve3が非常停止しても別のワーカーノードで自動復旧してくれるというわけだ。

CEPHはこんな感じ。こっちも問題ない。
poolのsizeを3倍にしているので、
実際に入っているゲームデータは4G程度だろう。

終わりに

k8sの記事を読んでいて、これ一本読めば十分に使えるオンプレk8sクラスタが組めるなという記事がなかったため、こうして記事にさせていただいたが、一枚の記事にギチギチに詰め込みすぎたなという反省もある。
そのため、この終わりにまで読み切った人はそこまで多くないのでは?とすら思っている。
あまりにも情報量が多いので、Youtubeで噛み砕いた動画も出してみようかなと考えている。

脚注
  1. 実際筆者の環境ではMetalLBやCiliumのようなBGPを採用した方式は正常に動作しなかった。 ↩︎

  2. 例:192.168.1.200-192.168.1.210 ↩︎

Discussion