2023年年末にk3sを使ってRaspberry Pi 4と10年位前に買った超古いノートPCで自宅Kubernetesクラスタに構築した。構築はめっちゃくちゃ楽だった(数分間で終わる)が、k8s標準のものではなく、addonsや新しいOSSを試す際に謎のバグが多発してdebugも大変だったので、改めてkubeadmを使って構築することにした。クラスタが崩壊したときの復旧を楽にするために、手順を残しておく。もし検索エンジや生成AIの参考リンクなどでこのページにたどり着いた方がいたら、何かの参考になれば嬉しい。

手順

  1. container runtime (containerd)をインストール
  2. kubeadm, kubelet, kubectlをインストール
  3. kubeadm initでcontroller planeを構築
  4. kubeadm joinでnodeをクラスタに追加

当たり前のことだが、container runtimeとkubeadm, kubelet, kubectlはcontroller planeとworker nodeの両方にインストールする必要がある。

環境

Raspberry Pi 4

  • CPU: Cortex-A72, aarch64, 4 cores
  • Memory: 8GiB
  • OS: Debian GNU/Linux 12 (bookworm)

Mini PC

  • CPU: Intel(R) N97, x86_64, 4 cores
  • Memory: 8GiB
  • OS: Ubuntu 24.04.2 LTS

古いノートPC

  • CPU: Intel(R) Pentium(R) 3805U @ 1.90GHz, 2 cores
  • Memory: 8GiB
  • OS: Ubuntu 22.04.5 LTS

Raspberry Piをcontrol planeにして、残り2台をworkerにする。

container runtime (containerd)をインストール

他のcontainer runtimeでも良いが、無難にcontainerdを使うことにする。今後はCRI-Oで遊んでみるかもしれないが、Docker Engineはたぶん使わない。

基本的にcontainerdの Getting Started を参考してcontainerdをインストールする。ただ、control planeとworker nodeで複数回実行するため、簡単なsnippetを用意した。 また、クラスタはCNI pluginsが必要なので、aptを利用せずに、直接バイナリをダウンロードする。

Step 1: Installing containerd

CONTAINERD_VERSION=2.1.1
CLI_ARCH=amd64
if [ "$(uname -m)" = "aarch64" ]; then CLI_ARCH=arm64; fi
curl -L --fail --remote-name-all https://github.com/containerd/containerd/releases/download/v${CONTAINERD_VERSION}/containerd-${CONTAINERD_VERSION}-linux-${CLI_ARCH}.tar.gz{,.sha256sum}
sha256sum --check containerd-${CONTAINERD_VERSION}-linux-${CLI_ARCH}.tar.gz.sha256sum
sudo tar xzvfC ./containerd-${CONTAINERD_VERSION}-linux-${CLI_ARCH}.tar.gz /usr/local
rm -f containerd-${CONTAINERD_VERSION}-linux-${CLI_ARCH}.tar.gz{,.sha256sum}

containerd -v

サービスを有効化して起動する。

curl -L --fail --remote-name https://raw.githubusercontent.com/containerd/containerd/main/containerd.service
sudo mv containerd.service /etc/systemd/system/containerd.service
sudo systemctl daemon-reload
sudo systemctl enable --now containerd
systemctl status containerd.service

Step 2: Installing runc

curl -L --fail --remote-name https://github.com/opencontainers/runc/releases/download/v1.3.0/runc.${CLI_ARCH}
sudo install -m 755 runc.${CLI_ARCH} /usr/local/sbin/runc
rm -f runc.${CLI_ARCH}

Step 3: Installing CNI plugins

CNI_PLUGINS_VERSION=1.7.1
CLI_ARCH=amd64
if [ "$(uname -m)" = "aarch64" ]; then CLI_ARCH=arm64; fi
curl -L --fail --remote-name-all https://github.com/containernetworking/plugins/releases/download/v${CNI_PLUGINS_VERSION}/cni-plugins-linux-${CLI_ARCH}-v${CNI_PLUGINS_VERSION}.tgz{,.sha256}
sha256sum --check cni-plugins-linux-${CLI_ARCH}-v${CNI_PLUGINS_VERSION}.tgz.sha256
sudo mkdir -p /opt/cni/bin
sudo tar Cxzvf /opt/cni/bin cni-plugins-linux-${CLI_ARCH}-v${CNI_PLUGINS_VERSION}.tgz
rm -f cni-plugins-linux-${CLI_ARCH}-v${CNI_PLUGINS_VERSION}.tgz{,.sha256}

kubeadm, kubelet, kubectlをインストール

kubeadmのドキュメント は親切に書かれているので、実行するだけで良い。

インストールした後、systemctl status kubelet kubeletが起動しない問題が起きているので、journalctl -xeu kubeletでログを確認する。

[ERROR Swap]: running with swap on is not supported. Please disable swap

なので、swapを無効にする。

sudo swapoff -a

swapを無効化したくない場合は、 このissue を参考にして、kubeletの設定を変更する

echo "KUBELET_EXTRA_ARGS=--fail-swap-on=false" | sudo tee /etc/default/kubelet

kubeadm initでcontroller planeを構築

ドキュメント

kubeadm initを実行する

まずはcontrol planeになるnode(例えば192.168.1.232) に入って、 host名を設定する。

echo "192.168.1.232   cluster-endpoint" | sudo tee -a /etc/hosts

flagからクラスタ作成時の設定を指定すると、再実行時にきにめんどくさくなるので、kubeadm initを実行する前に reference を参考にして、設定ファイルkubeadm-init.yamlを作成しておく。

apiVersion: kubeadm.k8s.io/v1beta4
caCertificateValidityPeriod: 87600h0m0s
certificateValidityPeriod: 8760h0m0s
certificatesDir: /etc/kubernetes/pki
clusterName: spbro
controllerManager: {}
controlPlaneEndpoint: cluster-endpoint
dns: {}
encryptionAlgorithm: RSA-2048
etcd:
  local:
    dataDir: /var/lib/etcd
imageRepository: registry.k8s.io
kind: ClusterConfiguration
kubernetesVersion: 1.33.0
networking:
  dnsDomain: cluster.local
  serviceSubnet: 10.96.0.0/12
  podSubnet: 10.244.0.0/16
proxy: {}
scheduler: {}

今回は3箇所しか変更していない

  • networking.podSubnetはCNI pluginに依存するので、Flannelを使う場合は10.244.0.0/16を指定する。他のrangeを設定した場合、Flannel設定を変更する必要がある
  • clusterName defaultのクラスタ名kubernetesspbroに変更する。(余談:spbro = Space Brothers = 宇宙兄弟)
  • controlPlaneEndpoint さっきhostsに設定したcluster-endpointを指定する

初期化を実行する

sudo kubeadm init --config=kubeadm-init.yaml --upload-certs

以下のようなメッセージが出力されたら初期化は成功。

Your Kubernetes control-plane has initialized successfully!

portの利用状況を確認すると、kube関連のportがLISTEN状態になっていることがわかる。

$sudo ss -tulnp | grep kube

tcp   LISTEN 0      4096                     127.0.0.1:10248      0.0.0.0:*    users:(("kubelet",pid=1512560,fd=20))
tcp   LISTEN 0      4096                     127.0.0.1:10249      0.0.0.0:*    users:(("kube-proxy",pid=1512652,fd=9))
tcp   LISTEN 0      4096                     127.0.0.1:10259      0.0.0.0:*    users:(("kube-scheduler",pid=1512422,fd=3))
tcp   LISTEN 0      4096                     127.0.0.1:10257      0.0.0.0:*    users:(("kube-controller",pid=1512520,fd=3))
tcp   LISTEN 0      4096                             *:10256            *:*    users:(("kube-proxy",pid=1512652,fd=10))
tcp   LISTEN 0      4096                             *:10250            *:*    users:(("kubelet",pid=1512560,fd=18))
tcp   LISTEN 0      4096                             *:6443             *:*    users:(("kube-apiserver",pid=1512425,fd=3))

kubectlコマンドを使えるようにする

kubeconfigを設定して、kubectlコマンドを使えるようにする。

# control plane node内で実行
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
$kubectl get po -A

NAMESPACE     NAME                          READY   STATUS    RESTARTS      AGE
kube-system   coredns-674b8bbfcf-fhsgf      0/1     Pending   0             18m
kube-system   coredns-674b8bbfcf-q8xr5      0/1     Pending   0             18m
kube-system   etcd-apo                      1/1     Running   2             19m
kube-system   kube-apiserver-apo            1/1     Running   0             19m
kube-system   kube-controller-manager-apo   1/1     Running   3 (19m ago)   19m
kube-system   kube-proxy-q8j8f              1/1     Running   0             18m
kube-system   kube-scheduler-apo            1/1     Running   2             19m

自分の開発環境からもアクセスできるようにする。

echo "192.168.1.232   cluster-endpoint" | sudo tee -a /etc/hosts

configをコピーして、ローカルにすでに存在するkube configとマージする。

# 開発環境で実行
scp root@<control-plane-host>:/etc/kubernetes/admin.conf ~/.kube/admin.conf
cp ~/.kube/config ~/.kube/config.backup
KUBECONFIG=~/.kube/config.backup:~/.kube/admin.conf kubectl config view --flatten >> ~/.kube/config

Pod network add-onをインストールする

Pod間通信は標準機能でサポートされていないので、自らPod network add-onをインストールするまでに、CoreDNSがPending状態になっている。

選択肢 は結構あるが、今回は比較的にシンプルなFlannelを使う。ただし、FlannelはNetwork Policyをサポートしていないので、別途インストールする必要がある。

kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml

数分後、CoreDNSがPendingからRunningに変わる。

$kubectl get po -A
NAMESPACE      NAME                          READY   STATUS    RESTARTS   AGE
kube-flannel   kube-flannel-ds-slvsr         1/1     Running   0          97s
kube-system    coredns-674b8bbfcf-b6dlp      1/1     Running   0          15m
kube-system    coredns-674b8bbfcf-t4g49      1/1     Running   0          15m
kube-system    etcd-apo                      1/1     Running   3          15m
kube-system    kube-apiserver-apo            1/1     Running   0          15m
kube-system    kube-controller-manager-apo   1/1     Running   0          15m
kube-system    kube-proxy-l758c              1/1     Running   0          15m
kube-system    kube-scheduler-apo            1/1     Running   4          15m

[オプション] control plane nodeに他のpodをスケジュールできるようにするために、node-role.kubernetes.io/control-planeのtaintを削除する。

kubectl taint nodes --all node-role.kubernetes.io/control-plane-

余談:Ciliumを試してみたが、Raspberry Pi 4では動かなかった

もともとFlannelではなく、 Cilium を使いたかったが、kernelバージョンの問題でRaspberry Pi 4ではcilium-envoyが起動しなかった。

MmapAligned() failed - unable to allocate with tag (hint, size, alignment) - is something limiting address placement?
FATAL ERROR: Out of memory trying to allocate internal tcmalloc data (bytes, object-size); is something preventing mmap from succeeding

kernelを再コンパイルしたあと、tcmalloc をビルドすればいけそうではあるが、一旦断念した https://github.com/envoyproxy/envoy/issues/23339

この方 とまさに同じ状況だった

kubeadm joinでnodeをクラスタに追加

最後は、この一行でクラスタにworker nodeをクラスタに追加する

sudo kubeadm join cluster-endpoint:6443 --token xxxxx.xxxxxxxxxxxxx --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxx

tokenとca-cert-hashは、kubeadm initを実行したときに出力されるやつを使えば良い。 もし以下のようなエラーが出た場合は、kubeadm token create --print-join-commandを実行して、再度取得する

I0527 10:59:10.955411    5742 token.go:250] [discovery] Retrying due to error: could not find a JWS signature in the cluster-info ConfigMap for token ID "xxxxx"

worker nodeをクラスタにjoinした後、flannelのDaemonSetに作られたpodから以下のエラーが出た。

cannot stat /proc/sys/net/bridge/bridge-nf-call-iptables: No such file or directory sysctl: cannot stat /proc/sys/net/bridge/bridge-nf-call-ip6tables: No such file or directory

調べてみると、iptablesからのpacket filtering rulesを適用するために、br_netfilter kernel moduleを有効にする必要があることがわかった。

modprobe br_netfilter
sysctl -p /etc/sysctl.conf

終わり!!!

$kubectl get nodes
NAME    STATUS   ROLES           AGE   VERSION
apo     Ready    control-plane   3d    v1.33.1
mutta   Ready    <none>          17h   v1.33.1