Proxmox上にkube-vipでcontrol-plane・Service LB両対応のHAクラスタを構築する。
この記事を書いた理由
k8sでHAを完結させたいと思い、しらべてみたところ、kube-vipという存在と出会ったが、
なぜだか皆推奨されているDaemonSetでのデプロイ方式を書いていなかった。
そして、書かれていたstatic podのマニフェストファイルを、kubeadm実行前にmanifestsディレクトリの中に入れて、initを実行するという手法が取られていたが、最新バージョンではこの方式は対応していないようである。
そのため、この記事で改めて今週リリースされたばかりのK8s 1.31.0を使い、いい感じのクラスタを構築する。
実動作を確認したいため、以下のpalworldというゲームサーバーをデプロイして試してみる。
目標
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を宣言すればいい。
こっちの方が大抵の環境において堅牢ではないだろうか?
- BGPは、仮想IPアドレスのプール[2]を作って、LBを建てたら勝手に割り振られるスタイル。
- 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が自動でインストールできなかったので、早めにインストールだけしておくといいかもしれない。
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とランタイムがエラーを吐く
sudo swapoff -a
再起動後もswapを無効にするようにする
- /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する。
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をつければいい。
sudo kubeadm join k8s-controller --token <TOKEN> \
--discovery-token-ca-cert-hash <token-hash> \
--control-plane --certificate-key <certificate-key>
sudo kubeadm join k8s-controller --token <TOKEN> \
--discovery-token-ca-cert-hash <token-hash> \
cliumインスコ
cilium公式ドキュメントのコピペ
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
クラスタがちゃんと構築できてるか確認
ちょっと時間を置いてから実行する。
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を使用するように設定する。
---
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を割り振ってもらうには、
次のように記述する。
---
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で調べてみる。
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で噛み砕いた動画も出してみようかなと考えている。
Discussion