3 台の Raspberry Pi で始める自宅 Kubernetes クラスタの構築


## はじめに

最近 Raspberry Pi を3台購入し、自宅に Kubernetes クラスタを構築しました。
この記事ではその体験記を共有します。

また、自宅 k8s を構築する際に参考になる記事になる事も目指しています。

![IMG_2925.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/638334/dbeb9fa4-53c1-0e25-c107-5499d4da67a1.jpeg)

## モチベーション

仕事で GCP 上で Kubernetes を使ったので、個人で Kubernetes クラスタを構築してさらに学習を深めたいと思いました。

実務ではクラウドサービスを使いますが、維持費が高額です。一日システムを立ち上げておくだけで数千円、一ヶ月では数万円かかってしまいます。

minikube などを用いてローカル環境で Kubernetes を動かすこともできますが、シミュレーター上の動作になってしまうので、どうせなら実際の環境に近いものを構築したいと思いました。

そこで、Raspberry Pi を用いることで、実際の環境に近い学習環境を構築してみることにしました。

## 全体の流れ

今回の流れは以下の通りです。

大まかに分けて、「準備編」「構築編」「動作確認編」に分けて行います。

**準備編**: k8s クラスタを構築するための準備を行います。

1. **必要部品の準備**: Raspberry Pi などの必要部品を準備します。
2. **技術選定**: コンテナランタイム、CNI、ロードバランサーなどの技術を選定します。
3. **ネットワーク設計**: ネットワーク構成を考え、IP アドレス範囲を決定します。

**構築編**: k8s クラスタを構築します。

1. **Raspberry Pi のセットアップ**: 各 Raspberry Pi に OS をインストールし、初期設定を行います。
2. **全てのノードの設定**: コンテナランタイムや必要なツールを導入します。
3. **マスターノードの設定**: クラスターを初期化し、マスターノードとして機能するよう設定します。
4. **ワーカーノードの設定**: ワーカーノードをクラスタに参加させます。
5. **k8s の初期設定**: CNI やロードバランサーを導入し、ネットワークを設定します。

**動作確認編**: k8s クラスタが正常に動作するか確認します。

1. **nginx のデプロイ**: nginx の pod をデプロイし、ロードバランサーを設定します。
2. **k8s の冗長性を確認**: ワーカーノードを一つ電源を切り、冗長性を確認します。

## 準備編

準備編では実際に手を動かす前に必要な部品の準備や技術選定、ネットワーク設計を行います。

### 1. 必要部品の準備

今回 Kubernetes クラスタ構築に使用した部品です。

Raspberry Pi 4 を使用しましたが、現在は Raspberry Pi 5 が発売されているため、そちらを使用することをお勧めします。

| 部品名               | 数量 | 備考                                                                                                      |
| -------------------- | ---- | --------------------------------------------------------------------------------------------------------- |
| Raspberry Pi 4 4GB   | 3台  | メモリについては 4GB を購入しました。他の体験記によりと 2GB だと k8s を動かすにはリソース不足なようです。 |
| SDカード 64GB        | 3枚  | 32GB でも十分だと思いますが、値段もさほど変わらなかったので、 64GB を購入しました。                       |
| Raspberry Pi 4用電源 | 3台  | 公式の物を購入しました。                                                                                  |
| Raspberry Pi用ケース | 1台  | 4台の Raspberry Pi をタワー型で収納できるケースが amazon で売っていたのでそれを購入しました。             |
| 電源タップ4口        | 1台  | サンワサプライ製の物を購入しました。                                                                      |

### 2. 技術選定

k8s を動かすためには最低限コンテナランタイム、CNI、ロードバランサーが必要になります。

- コンテナランタイム
  - コンテナを実行するためのランタイムです。containerd(Dockerに内蔵されているコンテナランタイム), CRI-O などがあります。
- CNI(Container Network Interface)
  - pod間の通信を行うためのインターフェースです。Calico, Flannel などがあります。
- ロードバランサー
  - クラスタ外からのアクセスを受け付けるために必要になります。

今回 Kubernetes クラスタ構築に使用した技術は以下の通りです。

| 技術       | 役割               | 理由                                                                                                                                                                                            |
| ---------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| kubernetes | クラスター管理     |                                                                                                                                                                                                 |
| CRI-O      | コンテナランタイム | Kubernetes に特化しているコンテナランタイムです。<br />最初は containerd を利用しましたが、 k8s で使用するためには設定を手動で変える必要があったので、k8s と相性の良い CRI-O に切り替えました。 |
| Flannel    | CNI                | シンプルな設定で MetalLB と動作出来るので採用しました。<br />最初は Calico を使用しましたが MetalLB と使用する場合に機能制限や各種設定の必要があったためこちらに切り替えました。                |
| MetalLB    | ロードバランサー   | ローカル環境で利用可能な唯一の選択肢だったため。                                                                                                                                                |

### 3. ネットワーク設計

ネットワーク構成は以下の通りです。ネットワーク設定は作業をスムーズに進めるために事前に決定しておくことが重要です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/638334/7bd5f220-4ac7-1117-14ef-b2360547ef22.png)

最初に Pod の IP アドレスやロードバランサーの IP アドレスを決めておくと、作業がスムーズに進みます。

今回は以下のように設定しました。

- Pod の IP アドレス範囲
  - ローカルネットワーク内で使用していない IP アドレスを選択する必要があります。
  - 今回は、10.244.0.0/16 を使用しました。家庭用ルーターは大抵の場合 192.168.x.x を使用しているため衝突することはないと思いますが、各自の環境に合わせて設定してください。
  - また、Flannel はデフォルトで 10.244.0.0/16 を使用するため、設定を変更する必要がなくスムーズです。
- ロードバランサーの IP アドレス範囲
  - ローカルネットワーク内で使用していない IP アドレスを選択する必要があります。
  - 今回は、192.168.0.250-192.168.0.254 を使用しました。
  - また、この IP アドレスは DHCP で割り当てられないように設定しておきました。

さらに、今回使用した Synology のルーターにはネットワーク分割機能があるため、ロードバランサーの IP アドレス範囲を分割したネットワーク内の IP アドレスに割り当てる必要がありました。
通常のルータの場合はこの点を考慮する必要はないです。

## 構築編

### 1. Raspberry Pi のセットアップ

#### a. Raspberry Pi に OS を入れる

[公式ツール](https://www.raspberrypi.com/software/)を用いて、Ubuntu Server LTS (64-bit)をSDカードにインストールします。

![image-20240217232101617.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/638334/2ddd803b-46db-6ab3-3e1d-369cf3b414f2.png)

インストール途中で以下の設定を行います。

- ホスト名
  - 固定の <ホスト名>.local で Raspberry Pi に接続できるように設定します。
    内部的には mDNS (Multicast DNS) が使用されています。
    ただし、ホスト名が重複すると接続に問題が生じるため、必ず重複しないように設定します。
  - 今回の環境では有効にするために `avahi-daemon` を起動する必要がありました。

```sh
sudo systemctl start avahi-daemon
sudo systemctl enable avahi-daemon  # システム起動時に自動起動するように設定
```

- ユーザー名とパスワード
  - デフォルトから変更することでセキュリティを向上させます。
- WiFi
  - 事前に設定しておくことで、立ち上げ後の手間を省きます。

#### b. Raspberry Pi を起動

次のコマンドで SSH 接続を行います。

```sh
ssh <ユーザー名>@<ホスト名>.local
```

### 2. 全てのノードの設定

#### a. コンテナランタイムの導入

Raspberry Pi にコンテナランタイムを導入します。

今回は CRI-O を使用します。

https://github.com/cri-o/packaging/blob/main/README.md/

#### b. kubernetes の導入

kubeadm、kubelet、kubectl をインストールします。
これらは、k8s のクラスターを構築するために必要なツールです。

https://kubernetes.io/ja/docs/setup/production-environment/tools/kubeadm/install-kubeadm/

### 3. マスターノードの設定

次のコマンドでクラスターを初期化します。

```sh
kubeadm init
```

### 4. ワーカーノードの設定

3. で表示されたコマンドを実行することで、ワーカーノードをクラスタに参加させます。

### 5. k8s の初期設定

k8s へ flannel や metallb の導入を行います。導入方法は変わる可能性があるため、具体的なコマンドはあえて記載しません。

参考にした公式ドキュメントは以下の通りです。

https://github.com/flannel-io/flannel

https://metallb.universe.tf/

## 動作確認編

### 1. nginx のデプロイ

k8s の導入が完了したら、nginx のデプロイを行います。

以下のnginx-deployment.yamlを作成します。

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.14.2
          ports:
            - containerPort: 80
```

デプロイの作成、確認は以下のコマンドで行います。

```sh
# 作成
kubectl apply -f nginx-deployment.yaml
# 動作確認
kubectl get pods -o wide --watch
```

次にnginx-service.yamlを作成します。

```yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  selector:
    app: nginx
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
  type: LoadBalancer
```

サービスの作成、確認は以下のコマンドで行います。

```sh
# 作成
kubectl apply -f nginx-service.yaml
# 動作確認
kubectl get services
```

これらのコマンドを実行することで、nginx の pod が 3 つ作成され、ロードバランサーが作成されます。

```sh
$ kubectl describe svc nginx-service
Name:                     nginx-service
Namespace:                default
Labels:                   <none>
Annotations:              metallb.universe.tf/ip-allocated-from-pool: first-pool
Selector:                 app=nginx
Type:                     LoadBalancer
IP Family Policy:         SingleStack
IP Families:              IPv4
IP:                       10.102.133.225
IPs:                      10.102.133.225
LoadBalancer Ingress:     192.168.0.5
Port:                     <unset>  80/TCP
TargetPort:               80/TCP
NodePort:                 <unset>  30205/TCP
Endpoints:                10.244.1.2:80,10.244.1.3:80,10.244.2.3:80
Session Affinity:         None
External Traffic Policy:  Cluster
Events:
  Type    Reason        Age                From                Message
  ----    ------        ----               ----                -------
  Normal  IPAllocated   10m                metallb-controller  Assigned IP ["192.168.0.5"]
  Normal  nodeAssigned  10m (x2 over 46m)  metallb-speaker     announcing from node "raspberrypi4-2" with protocol "layer2"
```

metallb が 192.168.0.5 の IP アドレスを割り当てていることがわかります。

```sh
curl http://192.168.0.5/
```

で nginx のデフォルトページが表示されれば成功です。

### 2. k8s の冗長性を確認

k8s の冗長性を確認するために、障害が発生した場合にどのように振る舞うかを確認します。
今回は物理的にワーカーノードを一つ電源を切ってみます。

実行前は以下のように raspberrypi4-2 と raspberrypi4-3 に pod が配置されています。

```sh
$ kubectl get pods -o wide
NAME                                READY   STATUS    RESTARTS   AGE     IP           NODE             NOMINATED NODE   READINESS GATES
nginx-deployment-77d8468669-n9rkw   1/1     Running   0          4h55m   10.244.2.3   raspberrypi4-3   <none>           <none>
nginx-deployment-77d8468669-snxpm   1/1     Running   0          4h55m   10.244.1.3   raspberrypi4-2   <none>           <none>
nginx-deployment-77d8468669-zm2nv   1/1     Running   0          4h55m   10.244.1.2   raspberrypi4-2   <none>           <none>
```

この状態で raspberrypi4-2 の電源を切ります。

```sh
sudo shutdown -h now
```

しばらく待つと、raspberrypi4-2 が NotReady になります。

```sh
$ kubectl get nodes
NAME             STATUS     ROLES           AGE     VERSION
raspberrypi4-1   Ready      control-plane   6h31m   v1.30.3
raspberrypi4-2   NotReady   <none>          6h20m   v1.30.3
raspberrypi4-3   Ready      <none>          6h16m   v1.30.3
```

pod の状態を確認すると、raspberrypi4-2 に配置されていた pod が raspberrypi4-3 に移行されていることがわかります。

```sh
$ kubectl get pods -o wide
NAME                                READY   STATUS        RESTARTS   AGE     IP           NODE             NOMINATED NODE   READINESS GATES
nginx-deployment-77d8468669-2b9nx   1/1     Running       0          15s     10.244.2.5   raspberrypi4-3   <none>           <none>
nginx-deployment-77d8468669-k7rgw   1/1     Running       0          15s     10.244.2.4   raspberrypi4-3   <none>           <none>
nginx-deployment-77d8468669-n9rkw   1/1     Running       0          5h52m   10.244.2.3   raspberrypi4-3   <none>           <none>
nginx-deployment-77d8468669-snxpm   1/1     Terminating   0          5h52m   10.244.1.3   raspberrypi4-2   <none>           <none>
nginx-deployment-77d8468669-zm2nv   1/1     Terminating   0          5h52m   10.244.1.2   raspberrypi4-2   <none>           <none>
```

raspberrypi4-2 に配置されていた pod は Terminating になっており、終了処理が行われていることがわかります。
（ただ、raspberrypi4-2 がシャットダウンされているため、終了処理が完了できずに pod が残ってしまっています）

raspberrypi4-2 の電源を入れると、ノードが再び Ready になります。

```sh
$ kubectl get nodes
NAME             STATUS   ROLES           AGE     VERSION
raspberrypi4-1   Ready    control-plane   6h45m   v1.30.3
raspberrypi4-2   Ready    <none>          6h35m   v1.30.3
raspberrypi4-3   Ready    <none>          6h30m   v1.30.3
```

raspberrypi4-2 に配置されていた pod は終了処理が完了し削除されました。

```sh
$ kubectl get pods -o wide
NAME                                READY   STATUS    RESTARTS   AGE    IP           NODE             NOMINATED NODE   READINESS GATES
nginx-deployment-77d8468669-2b9nx   1/1     Running   0          14m    10.244.2.5   raspberrypi4-3   <none>           <none>
nginx-deployment-77d8468669-k7rgw   1/1     Running   0          14m    10.244.2.4   raspberrypi4-3   <none>           <none>
nginx-deployment-77d8468669-n9rkw   1/1     Running   0          6h6m   10.244.2.3   raspberrypi4-3   <none>           <none>
```

Kubernetesは、ノードが追加されたからといって、既存のPodを新しいノードに移動させることはしないようです。

なお、Podを新しいノードに強制的に再配置したい場合は、手動でデプロイメントをスケールダウンしてからスケールアップするか、特定のPodを削除することで、新しいノードに再スケジュールされるようにできます。

## まとめ

### 今回の成果

- Raspberry Pi 3 台を使用して Kubernetes クラスタを構築しました。
- Kubernetes のネットワーク周りについて理解を深めることができました。

### 所要時間

全てで36時間ほどかかりました。

| 項目       | 所要時間 |
| ---------- | -------- |
| 部品調達   | 2時間    |
| 技術調査   | 4時間    |
| 設定・構築 | 24時間   |
| 記事作成   | 6時間    |
| 計         | 36時間   |

順調に進めばあまり時間は掛からないと思いますが、最初にコンテナランタイムに containerd を選択してから CRI-O に変更を行ったりと順調に進まない部分も多く時間が掛かってしまいました。

### 感想

これまで漠然としていたKubernetesのネットワーク周りについて理解を深める良い機会となったと思います。

特に、普段の業務ではあまり意識しないCNI（Container Network Interface）やロードバランサーの動作について、今回の構築を通じて意識する良い機会になりました。