Kubernetes the hard way on Raspberry Pi

![PXL_20210725_085200448.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39615/90cd31ca-5978-4eac-fcbd-6aacae510ab4.jpeg)

以下の動機でKubernetes the hard way on Raspberry Piを実施。その際のメモ。

* CKAの勉強を兼ねて
* いざという時に備えて家に１つクラスタを持っておきたい

# Key takeaway

* 事前に注意しておきたいハマる（かもしれない）ポイントが分かる（私の場合、環境設定で躓いた）
    * 本記事内では公式ドキュメントにある手順について１つずつ追うことはしない
* Controller & Worker 両roleを持つノードをセットアップする方法が分かる（公式ドキュメント等に手順見当たらない）
* Metallbを使ってLoad Balancerを配備するところまでカバー

# 環境

* ハードウェア
    * Controller & Worker
        * Raspberry Pi 4 Model B 8GB RAM
    * Worker
        * Raspberry Pi 4 Model B 8GB RAM
        * Raspberry Pi 3 Model B 1GB RAM
* OS
    * Ubuntu Server 20.04.2 LTS on Raspberry Pi
* 作業環境
    * Ubuntu 18.04(WSL2) on Windows 10 21H1

連休前に思い立ち、フリマサイトで本体や冷却ファン等を買い足した。

# 参考にしたサイト

* [kelseyhightower/kubernetes-the-hard-way: Bootstrap Kubernetes the hard way on Google Cloud Platform. No scripts.](https://github.com/kelseyhightower/kubernetes-the-hard-way)
    * 基本的には本家を読み進めるがGCP環境を前提とした手順になっているのでRaspberry Pi環境向けに読み替える必要がある箇所について、次のリファレンスを参考にさせてもらった。
* [Nek0trkstr/Kubernetes-The-Hard-Way-Raspberry-Pi](https://github.com/Nek0trkstr/Kubernetes-The-Hard-Way-Raspberry-Pi)
* [home-kubernetes-2020/how-to-create-cluster-logical-hardway at master · CyberAgentHack/home-kubernetes-2020](https://github.com/CyberAgentHack/home-kubernetes-2020/tree/master/how-to-create-cluster-logical-hardway)

# ハマったとこ

## DHCP無効化

ノードのStatic IP設定方法がひと昔前の方法から変わっていてやや躓いたのでメモ。Netplanというものを初めて知る。各Worker上のPOD CIDRへのルーティングもNetplanを使って簡単に不揮発化できた。

* https://superuser.com/questions/1302474/stop-dhcp-when-i-get-static-ip-ubuntu-server

## Worker node上のswapの恒久的な無効化

swapが有効だとkubeletが動かない記載は何度か見ていたので、 `sudo swapoff -a` で無効化はしていたが、これだと起動のたびに無効化が必要だったらしい。Worker node起動後、 `kubectl get node`でノードが `NotReady`だったので各種コンポーネントのログをチェックしていたら、kubeletのユニットログにswapをoffしろのメッセージを発見して気づく。以下のコマンドで恒久的にswapを無効化する。

```
sudo systemctl disable dphys-swapfile
```
* 参考
    * [Permanently disable swap on Raspbian Buster - Raspberry Pi Forums](https://www.raspberrypi.org/forums/viewtopic.php?t=244130)



## Hostnameが重要

当初、controllerにraspberry piを1台、workerを残りの2台に割り当てたが、controllerもworkerとして動かしたく、controllerにworker向けのセットアップを追加で実施した。Controller向けに設定済みノードに対して、他のWorkerノードと同様のセットアップを施してみたがkubeletが動かない。kubeletのログを見ると、controller向けに命名していたhostnameをkubeletが混同して使っていて、API Serverにアクセスできないようで、エラーになっていた。そもそも、`kube-controller`や`kube-worker1`のようにノードのロールをHostnameに使っていたのが良くなかった（rpi1,rpi2みたいにしておいた方がいいと思われる）。

以下にWorker 3台構成が正しく動くまでの紆余曲折した構成の変遷を記載しておく。


### Worker 2台構成（Hardway開始当初の構成）

| HW | Node role | Hostname |
|:-:|:-:|:-:|
|Raspberry Pi 1| Controller | kube-controller |
|Raspberry Pi 2| Worker | kube-worker1 |
|Raspberry Pi 3| Worker | kube-worker2 |

### Worker 3台構成（3台目のWorker(kubelet)が動かなかった時の構成）

| HW | Node role | Hostname |
|:-:|:-:|:-:|
|Raspberry Pi 1| Controller & Worker | kube-controller |
|Raspberry Pi 2| Worker | kube-worker1 |
|Raspberry Pi 3| Worker | kube-worker2 |

### Worker 3台構成（3台のWorker構成が無事動いた時の構成）

| HW | Node role | Hostname |
|:-:|:-:|:-:|
|Raspberry Pi 1| Controller & Worker | kube-worker0 |
|Raspberry Pi 2| Worker | kube-worker1 |
|Raspberry Pi 3| Worker | kube-worker2 |

# 読み替えた部分

## Bootstrapping the etcd Cluster

etcdは、arm64版のバイナリがリリースされているのでそれを使う

## Bootstrapping the Kubernetes Control Plane

* 各種コンポーネントのバイナリはarm64版に読み替え。wgetするURLはamd64をarm64に置換
* kube-apiserverの起動引数は本家サイトの最新版とRaspberry Pi向けサイトの情報をマージしたものを利用

```
cat <<EOF | sudo tee /etc/systemd/system/kube-apiserver.service
[Unit]
Description=Kubernetes API Server
Documentation=https://github.com/kubernetes/kubernetes

[Service]
ExecStart=/usr/local/bin/kube-apiserver \\
  --advertise-address=${INTERNAL_IP} \\
  --allow-privileged=true \\
  --apiserver-count=1 \\
  --audit-log-maxage=30 \\
  --audit-log-maxbackup=3 \\
  --audit-log-maxsize=100 \\
  --audit-log-path=/var/log/audit.log \\
  --authorization-mode=Node,RBAC \\
  --bind-address=0.0.0.0 \\
  --client-ca-file=/var/lib/kubernetes/ca.pem \\
  --enable-admission-plugins=NamespaceLifecycle,NodeRestriction,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota \\
  --etcd-cafile=/var/lib/kubernetes/ca.pem \\
  --etcd-certfile=/var/lib/kubernetes/kubernetes.pem \\
  --etcd-keyfile=/var/lib/kubernetes/kubernetes-key.pem \\
  --etcd-servers=https://${CONTROLLER0_IP}:2379 \\
  --event-ttl=1h \\
  --encryption-provider-config=/var/lib/kubernetes/encryption-config.yaml \\
  --kubelet-certificate-authority=/var/lib/kubernetes/ca.pem \\
  --kubelet-client-certificate=/var/lib/kubernetes/kubernetes.pem \\
  --kubelet-client-key=/var/lib/kubernetes/kubernetes-key.pem \\
  --runtime-config='api/all=true' \\
  --service-account-key-file=/var/lib/kubernetes/service-account.pem \\
  --service-account-signing-key-file=/var/lib/kubernetes/service-account-key.pem \\
  --service-account-issuer=https://${INTERNAL_IP}:6443 \\
  --service-cluster-ip-range=10.32.0.0/24 \\
  --service-node-port-range=30000-32767 \\
  --tls-cert-file=/var/lib/kubernetes/kubernetes.pem \\
  --tls-private-key-file=/var/lib/kubernetes/kubernetes-key.pem \\
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
```

## Bootstrapping the Kubernetes Worker Nodes

runcとcontainerdは本家にarm64版のリリースがなかったのでLaunchpadからarm64版debパッケージをダウンロードして利用した。

* Download runc package for arm64 from launchpad
    * https://launchpad.net/~ubuntu-security/+archive/ubuntu/ppa/+build/21509545
* Download containerd package for arm64 from launchpad
    * https://launchpad.net/ubuntu/+source/containerd/1.4.4-0ubuntu1~20.04.2/+build/21335709


# metallbのセットアップ

公式ドキュメントに従って特に躓くこともなく設定できた。

* [MetalLB, bare metal load-balancer for Kubernetes](https://metallb.universe.tf/installation/)

まず以下のマニフェストをインストールする。

```
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.10.2/manifests/namespace.yaml
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.10.2/manifests/metallb.yaml
```

metallb用のconfigmapをapplyするまでアイドル状態とのこと。以下のconfigmapをapplyする。あらかじめホームネットワークのDHCPリース範囲を確認し、使われる予定のないIPアドレスの範囲を確認しておく（私の場合、192.168.2.100-192.168.2.200がDHCPのリース範囲内だった）。

```
{
  cat << EOF | kubectl apply -n metallb-system -f -
  apiVersion: v1
  kind: ConfigMap
  metadata:
    namespace: metallb-system
    name: config
  data:
    config: |
      address-pools:
      - name: default
        protocol: layer2
        addresses:
        - 192.168.2.2-192.168.2.99
EOF
}
```

以上である。以下のサンプルdeploymentで動作確認だけしておく。

```
{
  kubectl create namespace kube-verify
  cat <<EOF | kubectl create -f -
  apiVersion: apps/v1
  kind: Deployment
  metadata:
    name: kube-verify
    namespace: kube-verify
    labels:
      app: kube-verify
  spec:
    replicas: 3
    selector:
      matchLabels:
        app: kube-verify
    template:
      metadata:
        labels:
          app: kube-verify
      spec:
        containers:
        - name: nginx
          image: nginx:1.21.1
EOF
}
```

このdeploymentをLoadBalancer typeでサービス公開する。

```
kubectl expose deployment kube-verify -n kube-verify --type=LoadBalancer --target-port=80 --port=80
service/kube-verify exposed
kubectl get service kube-verify -n kube-verify
NAME          TYPE           CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
kube-verify   LoadBalancer   10.32.0.205   192.168.2.2   80:31865/TCP   4s
```

configで指定した範囲内のIPアドレスがさっそく払い出されていることがわかる。 http://192.168.2.2 にアクセスするとクラスタ上のサービスにアクセスできることがわかる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/39615/1781466a-df81-8ff6-398c-c37c4d83dc04.png)

# 最後に

Kubernetes the hard wayは思っていたより、簡単にできた。オリンピックを観ながら設定していたのでミスが色々あったが、そのおかげでトラブルシューティングをやるチャンスができ、理解をより深めることができたと思う。物理的な構築にはあまりこだわらなかったが、あまり広げて散らかすと家庭内からクレームがでる恐れがあるので、それなりにこじんまりさせることも重要と思う。