TrinityPi Compact Cluster ―
Raspberry Pi 5 × 3台で構築するミニマルかつ本格的なk8sエッジクラスタ環境

Raspberry Pi 5 上に Raspberry Pi OS(Bookwormベース)を用いて、Kubernetes 3ノードクラスタを構築しました。すべてのノードが Control Plane + Worker を兼任する HA構成を実現しています。

🖥 構成概要
項目 | 内容 |
---|---|
ハードウェア | Raspberry Pi 5 × 3台 (推奨: 16GB RAM) |
OS | Raspberry Pi OS Lite 64bit(Bookwormベース) |
Kubernetes | kubeadm(統治的な K8s) |
Container Runtime | containerd |
CNI | Flannel(軽量) |
分散ストレージ | Longhorn(すべてのノードに導入) |
構成 | 3 Node での Control Plane + Worker 兼任(高可用性) |
インストールしたOSバージョンは以下の通り。Raspberry Pi OSのLiteバージョン(サーバーバージョン)です。
$ lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description: Debian GNU/Linux 12 (bookworm)
Release: 12
Codename: bookworm
$ uname -a
Linux raspi-cp01 6.6.51+rpt-rpi-2712 #1 SMP PREEMPT Debian 1:6.6.51-1+rpt3 (2024-10-08) aarch64 GNU/Linux
手順概要
- Raspberry Pi OS Lite 64bit(Bookworm)を各ノードにインストール
- containerd をセットアップ
- Kubernetes コンポーネント(kubeadm/kubelet/kubectl)をインストール
- Swap 無効化
- 最初のノードで kubeadm init 実行(Control Plane 構築)
- 残り2台を Control Plane ノードとして参加
- Flannel 導入(Podネットワーク)
- Longhorn 導入(分散ストレージ)
- taint 解除で全ノードをWorker化
詳細手順
【 1. Raspberry Pi OS Lite Bookworm のインストール 】
- Raspberry Pi Imager で「Raspberry Pi OS Lite(64-bit)」を選択
- 各Piのホスト名を
raspi-cp01
,raspi-cp02
,raspi-cp03
に設定 - 固定IPを
/etc/dhcpcd.conf
(Boookwormより前)またはsystemd-networkd
で設定
BookwormでLANのIPを固定する方法(NetworkManagerを使う)
接続名を確認します:
nmcli connection show
NAME UUID TYPE DEVICE
Wired connection 1 xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx ethernet eth0
lo xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx loopback lo
Zshたとえば Wired connection 1
や eth0
などが表示されるはずです。
# 固定IPを設定:
sudo nmcli con mod "Wired connection 1" \
ipv4.addresses 192.168.0.101/24 \
ipv4.gateway 192.168.0.1 \
ipv4.dns "192.168.0.1 8.8.8.8" \
ipv4.method manual
ZshIPは環境に合わせて設定。
# 設定を反映(再起動または接続の再アクティベート):
sudo nmcli con down "Wired connection 1" && sudo nmcli con up "Wired connection 1"
ZshこれでIPが固定されました。
以降の手順は、Ansibleを使って一気にセットアップします。まずはAnsibleを使えるようにしましょう。
Ansibleが使える様になったら、
Ansibleを実行し、3 node clusterを構成します。
4番目のラスパイをAnsibleコントロールノードとして使用します。OSは同じくRaspberry pi OSです。
https://github.com/emboss369/edgeaiagent/tree/main/ansible-k8s-setup
よりAnsibleのPlaybookをダウンロードしておきます。(現在非公開)
pi-cluster.iniは、3ノードクラスターを構成するRaspberry Pi 5のIPアドレスを指定します。ここで、端末名とIPアドレスを設定しておきます。
env.ymlでは、ログインユーザを指定します。ラズパイの場合デフォルトはpiというユーザーが作成されます。
# Ansibleの実行
ansible-playbook -i inventory/pi-cluster.ini playbooks/install-3node-cluster.yml --extra-vars "@playbooks/vars/env.yml"
ZshAnsibleを実行し、エラーなしで実行できれば、インストールは完了です。
3-Node Clusterとは?
すべてのノードが「コントロールプレーン」かつ「ワーカーノード」としての役割を担う、最小構成のクラスタです。
アーキテクチャ概要(3 Node Cluster)
ノード名 | 役割 |
---|---|
node01 | コントロールプレーン + ワーカー |
node02 | コントロールプレーン + ワーカー |
node03 | コントロールプレーン + ワーカー |
すべてのノードが次のコンポーネントを動かします:
- etcd(分散DB)
- kube-apiserver、scheduler、controller-manager
- CRI-O or containerd
- OpenShift Core Services
- アプリケーションPod
📌 参考図(イメージ)

3 Node Clusterの論理構成図
Flannel CNIを導入
Flannelは、CoreOSが開発した軽量なオーバーレイネットワークです。Kubernetesクラスタ上の各ノードに仮想ネットワークを構築し、Pod同士がノードをまたいで通信できるようにします。
Kubernetesでは、Podが一意なIPアドレスを持ち、他のPodとIPレベルで通信できる必要があります(これはContainer Networking Model (CNM)の原則です)。Flannelはこれを満たす代表的なCNIの一つです。
Flannelの仕組み
1. Podネットワークの分割
Flannelは、クラスタ全体に仮想ネットワーク(例:10.244.0.0/16
)を割り当て、各ノードには /24
のサブネット(例:10.244.1.0/24
)を割り振ります。
Node A → 10.244.1.0/24
Node B → 10.244.2.0/24
Node C → 10.244.3.0/24
Zshこれにより、PodはノードをまたいでIPベースで通信できます。
2. 通信方式(バックエンド)
Flannelは以下のような複数のバックエンド方式を持ちます:
バックエンド | 説明 |
---|---|
vxlan | デフォルト。UDPベースでオーバーレイネットワークを構築。 ルーティング不要で導入しやすい |
host-gw | ノード間がL2で直結されている場合に高速(IPルーティング使用) |
ipsec | VXLANにIPsecで暗号化を追加 |
udp | 古い方法。今は非推奨 |
3. etcd or Kubernetesを使った設定管理
- Flannelはノード間のサブネット情報などを etcd または KubernetesのCRD(Custom Resource) を用いて保持します。
- Kubernetesの場合、Flannelは
kube-subnet-manager
モードで動作し、kube-apiserver
経由で設定を管理できます。
📌 導入方法(YAMLによる例)(Ansibleのタスク)
---
- name: Apply Flannel manifest using correct kubeconfig
shell: >
KUBECONFIG=/etc/kubernetes/admin.conf
kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml
Zsh注意:
Pod CIDR(例:10.244.0.0/16
)とKubernetesの設定(kubeadm init
の--pod-network-cidr
)が一致している必要があります。
Kubernetesをkubeadm init
で初期化する際に、**「クラスタ全体のPodにどんなIP帯を使うか」**を最初に決める必要があります。それが --pod-network-cidr
です。
Flannelは、デフォルトで 10.244.0.0/16
のIP帯を使う設計になっています。
設定は、、、kubeadm initで行います。Ansibleでは次のように設定しています。
(Ansibleプレイブックの抜粋)
- name: Initialize the cluster with kubeadm
command: >
kubeadm init
--control-plane-endpoint={{ inventory_hostname }}:6443
--pod-network-cidr=10.244.0.0/16
register: kubeadm_init_result
args:
creates: /etc/kubernetes/admin.conf # このファイルが存在する場合スキップ
Zshこれで、kube-apiserver や kube-controller-manager がFlannelが使うことを想定してネットワークを構成できるようになります。
もし一致していなかったら?
Flannelが使おうとしているIPレンジと、Kubernetesが把握しているPod CIDRがずれていると:
- ノードのステータスが
NotReady
のままになる - Podが起動しても通信できない(他ノードと通信不可)
kubectl get pods -A
などでCrashLoopBackOff
などのエラーが出る
といった不具合が起こります。
📦 Longhornとは?
Longhorn は、Rancher Labsが開発した クラウドネイティブな分散ブロックストレージ です。Kubernetesクラスタ上でPersistent Volume(永続ボリューム)を提供します。
つまり:KubernetesのPodが再起動しても、データを失わずに保つためのストレージを、クラスタ上で簡単に構築・管理できるソリューションです。
Longhorn導入の背景
Kubernetesの永続ボリューム(PV)は、以下のような用途で必要です:
- データベース(PostgreSQL, MySQLなど)
- AIモデルのキャッシュ
- Whisper / LLM / CVなどのモデルファイル
- ログやメディアファイルの永続化
通常、クラウドではEBS(AWS)やPD(GCP)などを使いますが、オンプレミスやRaspberry Piクラスタでは代替手段が必要です。Longhornはこのギャップを埋めてくれます。
Longhornの特徴
機能 | 説明 |
---|---|
✅ 分散ブロックストレージ | ノードにまたがるレプリケーション・冗長性を持つ |
✅ UI(Webベース) | ストレージ状態をGUIで管理できる |
✅ 自己修復機能 | ノード障害時に自動でレプリカを再構築 |
✅ バックアップ機能 | オンデマンド/スケジュールバックアップをS3互換先に保存 |
✅ CSI準拠 | Kubernetesの標準的なストレージとして利用可能 |
導入ステップ(Kubernetesクラスタに導入する手順)
🔹 1. 前提条件
- Kubernetes v1.21以降(v1.26でもOK)
- 各ノードにストレージ領域あり(デフォルトの場所:/var/lib/longhorn)
- シンプルなCNI構成(Flannel)
🔹 2. Longhorn 1.8.1 のインストール
Ansibleで導入します(Ansibleプレイブックの抜粋):
- name: Deploy Longhorn on main node
shell: kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/v1.8.1/deploy/longhorn.yaml
environment:
KUBECONFIG: /etc/kubernetes/admin.conf
when: "'main' in group_names" # mainグループにのみ適用
Zsh🔹 3. Longhorn UIへのアクセス
デフォルトでは、サービスはClusterIP
なのでNodePort に変更。(Ansibleプレイブックの抜粋):
# (永続アクセスしたい場合):Service タイプを NodePort に変更。これでクラスタ外からもアクセス可能になる
# 割り当てられたポートの確認は以下のコマンドで行う
# kubectl -n longhorn-system get svc longhorn-frontend
- name: Change Longhorn frontend service to NodePort
shell: |
kubectl -n longhorn-system patch service longhorn-frontend \
-p '{"spec": {"type": "NodePort"}}'
environment:
KUBECONFIG: /etc/kubernetes/admin.conf
when: "'main' in group_names" # mainグループにのみ適用
Zshkubectl -n longhorn-system get svc longhorn-frontendコマンドでポートを確認する。30160に設定されている場合、
$ kubectl -n longhorn-system get svc longhorn-frontend
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
longhorn-frontend NodePort 10.104.177.55 <none> 80:30160/TCP 106m
Zsh確認したポートを使い、クラスターのIPにアクセスする。http://<任意ノードのIP>:30780
にブラウザでアクセス可能です。
例)http://192.168.0.101:30160/

ラズパイでトータル1.37テラバイトの容量を確保!
Raspberry Pi 5 × 3台という超小型・省電力構成で、以下のような本格的な分散ストレージ基盤(1.37TiB)を構築している点が特に注目ポイントです:
この構成がすごい!
ポイント | 解説 |
---|---|
🔧 Longhornで1.37TiBの分散ストレージ | 軽量なSBCでありながら、耐障害性のある本格的なiSCSIストレージ層を実現している |
🧠 ストレージスケジューラブルな容量 967GiB | 即時にボリューム割当可能。しかも3ノード分の冗長構成が可能 |
⚙️ スナップショット・バックアップ・レプリケーション対応 | 本番同等の機能をこの構成で実現可能 |
🔌 外部ストレージ(USB SSDやHDD)と組み合わせる構成 | ラズパイの小型性とストレージ拡張性が両立している |
「片手サイズのエッジノードで本格的なKubernetes + 分散ストレージクラスタを構築したすごい構成」
構成管理用ラズパイにもkubectlを導入
下記を参考にして、別のRaspberry Pi 5にkubectlを入れておきます。
Step 1: 最初のアプリをデプロイしてみる
なにはともあれエンジンエックスを動かしてみましょう
📦 1-1. NGINXアプリをデプロイ
pi@raspi-cp01:~ $ kubectl create deployment nginx --image=nginx
deployment.apps/nginx created
Zsh🔍 1-2. Podが動いているか確認
pi@raspi-cp01:~ $ kubectl get pods
NAME READY STATUS RESTARTS AGE
nginx-5869d7778c-7cv22 1/1 Running 0 34s
ZshSTATUS が Running
になっていればOK!
Step 2: 外部からアクセスできるようにする
📡 2-1. NodePortでサービス公開
pi@raspi-cp01:~ $ kubectl expose deployment nginx --port=80 --type=NodePort
service/nginx exposed
Zsh2-2. 割り当てられたポートを確認
pi@raspi-cp01:~ $ kubectl get svc nginx
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nginx NodePort 10.111.64.41 <none> 80:31051/TCP 60s
Zsh2-3. ブラウザでアクセス
http://<任意のノードIP>:31051
Zshたとえば raspi-cp01
が 192.168.0.101
なら
http://192.168.0.101:31051
で “Welcome to nginx!” のページが表示されるはずです!

無事アクセスできました。
Step 3: ノード分散確認(Podの移動)
どのノードで動いているか確認。
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-5869d7778c-7cv22 1/1 Running 0 6m21s 10.244.2.11 raspi-cp02 <none> <none>
Zshraspi-cp02で動いていることがわかりました。kubectl drain
コマンドで移動してみましょう。
$ kubectl drain raspi-cp02 --ignore-daemonsets --delete-emptydir-data
Zshkubectl get pods -o wideで確認してみましょう。
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nginx-5869d7778c-5nw97 1/1 Running 0 13s 10.244.1.14 raspi-cp03 <none> <none>
Zshnginx
Pod が raspi-cp01
または raspi-cp03
に再スケジューリングされていれば成功です。
その後、raspi-cp02
をスケジューリング可能に戻すには以下を実行します:
$ kubectl uncordon raspi-cp02
node/raspi-cp02 uncordoned
ZshnginxのテストはOK。最後にクリーンアップしましょう。
$ kubectl delete deployment nginx
$ kubectl delete service nginx
# 残リソース確認
$ kubectl get all
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 8h
Zshservice/kubernetesは Kubernetes クラスタが最初から持っている デフォルトのAPIサーバー向けサービス であり、削除してはいけないリソースです。それ以外のリソースが表示されなければ削除は完了です。
これでクリーンアップ完了です。「ほぼ初期状態のクラスタ」に戻りました。
NFSを用意する
LonghornはKubernetes上で動作するCSI準拠の分散ブロックストレージとして優秀で、単一Podから読み書きされるデータボリュームに最適なソリューションです。ファイルを読み取り専用で共有したい、といった場合には向いていないのでNFSも用意しておきたいと思います。
こちらもAnsibleでセットアップしていきます。
---
# /playbooks/roles/nfs-server/tasks/main.yml'
- name: Update apt package index
apt:
update_cache: yes
- name: Install nfs-kernel-server
apt:
name: nfs-kernel-server
state: present
- name: Create NFS share directory
file:
path: /nfs-share
state: directory
owner: nobody
group: nogroup
mode: "0777" #実運用では適切な権限管理が必要ですが、まずは動作確認目的で 777
- name: Add NFS export for /nfs-share
blockinfile:
path: /etc/exports
block: |
/nfs-share 192.168.0.0/24(rw,sync,no_subtree_check,no_root_squash)
marker: "# {mark} ANSIBLE MANAGED BLOCK: NFS export for /nfs-share"
notify: Reload NFS exports
- name: Ensure NFS server is running
service:
name: nfs-kernel-server
state: started
enabled: yes
# GUIでのNFS管理
- name: Install dependencies for Webmin
apt:
name:
- wget
- perl
- libnet-ssleay-perl
- libauthen-pam-perl
- libio-pty-perl
- apt-show-versions
- libapt-pkg-perl
state: present
update_cache: yes
- name: Download Webmin .deb from SourceForge
get_url:
url: http://prdownloads.sourceforge.net/webadmin/webmin_2.303_all.deb
dest: /tmp/webmin.deb
mode: "0644"
- name: Install Webmin from .deb
apt:
deb: /tmp/webmin.deb
state: present
- name: Remove Webmin .deb after install
file:
path: /tmp/webmin.deb
state: absent
Zsh---
# playbooks/roles/nfs-server/handlers/main.yml
- name: Reload NFS exports
command: exportfs -ra
Zshこのrole(ロール)を実行すると、K8sの管理用に用意したRaspberry Pi 5(raspi-ctrl)に、NFSを導入し、かつWebminを導入してUIツールによる管理が可能になります。https://<raspi-ctrlのIPアドレス>:10000/ にアクセスし、Raspberry Pi 5のユーザ名とパスワードでログインします。
補足:Webmin とは何ですか?
Webminは、Unix系サーバーおよびサービス向けのWebベースのシステム管理ツールで、世界中で年間約100万件のインストール実績があります。Webminを使用すると、ユーザー、ディスククォータ、サービス、設定ファイルといったオペレーティングシステム内部の設定に加え、BIND DNSサーバー、Apache HTTPサーバー、PHP、MySQLなど、多数のオープンソースアプリケーションの変更や制御が可能です。
Webminを使えば /nfs-share
のパーミッションや使用量も簡単にブラウザから確認可能です
動的プロビジョニングに対応しよう

複数Podでストレージを共有したい場合、NFSが必要になります。raspi-ctrlにNSFサーバをインストールしましたので、次はKubernetesをセットアップしていきましょう。
動的プロビジョニングも使えるようにします。
動的プロビジョニングと複数Podからの共有を考慮する場合、NFS Subdir External Provisioner を導入するのがベストです。これは、NFSをバックエンドに使いつつ、StorageClass + PVCだけで簡単に共有ストレージを管理できるソリューションです。
Helmを使った「NFS Subdir External Provisioner」導入と運用の手順を、Raspberry Pi Kubernetesクラスター環境向けに、最終構成として整理していきましょう。(手動でインストールも可能だが今回はHelmを使う)
目的と前提
目的
- NFSサーバ(raspi-ctrl) を使って、複数Podから共有できるストレージをKubernetes上で動的にプロビジョニング
- Helmで導入し、再現性・保守性・アップグレード性を確保
構成前提
- raspi-ctrl (192.168.0.161): NFSサーバとして
/nfs-share
を提供 - Kubernetes v1.33.0 が動作する Raspberry Pi 5 × 3 ノード
- Helm クライアントが
raspi-ctrl
もしくは任意のマスター端末で使用可能
可能
ステップバイステップ:NFS Subdir External Provisioner (Helm導入)
Helm セットアップ(raspi-ctrl もしくはクライアントPC)
Helmのインストール(未インストールの場合)
インストールスクリプトを使う方法と、aptを使う方法があります。ラズパイではaptを使う方法が手軽です。
$ curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
$ sudo apt-get install apt-transport-https --yes
$ echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
$ sudo apt-get update
$ sudo apt-get install helm
# バージョン確認
$ helm version
version.BuildInfo{Version:"v3.17.3", GitCommit:"e4da49785aa6e6ee2b86efd5dd9e43400318262b", GitTreeState:"clean", GoVersion:"go1.23.7"}
Zsh詳しい手順については、https://helm.sh/ja/docs/intro/install を参照
NFS Provisioner のリポジトリ追加
$ helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
"nfs-subdir-external-provisioner" has been added to your repositories
$ helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "nfs-subdir-external-provisioner" chart repository
Update Complete. ⎈Happy Helming!⎈
ZshHelm で Provisioner をインストール
helm installコマンドでインストールすることができます。
$ helm install nfs-client nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
--namespace nfs-provisioner \
--create-namespace \
--set nfs.server=192.168.0.161 \
--set nfs.path=/nfs-share \
--set storageClass.name=nfs-client \
--set storageClass.accessModes={ReadWriteMany} \
--set storageClass.defaultClass=false
Zsh各オプションの意味
項目 | 説明 |
---|---|
–namespace nfs-provisioner | 名前空間を指定。なければ –create-namespace で作られる。 |
nfs.server=192.168.0.161 | NFS サーバーの IP アドレスを指定します。NFS クライアント(Pod)はこの IP に接続して共有フォルダにアクセスします。 |
nfs.path=/nfs-share | NFS サーバー上で共有されているディレクトリのパスを指定します。このパスが Persistent Volume のルートになります。 |
storageClass.name=nfs-client | 作成する StorageClass の名前を指定します。この名前を使って PVC(PersistentVolumeClaim)からアクセスできます。 |
storageClass.accessModes={ReadWriteMany} | アクセスモードを指定します。ReadWriteMany は「複数の Pod が同時に読み書き可能」を意味します(NFSなら可能)。 |
storageClass.defaultClass= false | これを true に設定すると、このStorageClassがデフォルト(default)として設定され、storageClassName を明示しない PVC に自動的に使われます。 |
Helm は内部的に kubectl
を使っています。つまりHelm
を実行したときに「どの Kubernetes クラスタにリソースがデプロイされるか」は、現在の kubectl
コンテキストに依存します。
🔍 どの名前空間にデプロイされるの?
Helm チャートによって異なりますが、指定しなければ default
namespace に入ります。
helm install my-release mychart/ --namespace longhorn-system
のように --namespace
をつけることで任意のネームスペースにいれることもできます。
$ kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
longhorn (default) driver.longhorn.io Delete Immediate true 30h
longhorn-static driver.longhorn.io Delete Immediate true 30h
nfs-client cluster.local/nfs-client-nfs-subdir-external-provisioner Delete Immediate true 12m
emboss@raspi-ctrl:~ $
Zshnfs-clientが追加されていることを確認します。
NFS Subdir External Provisionerとは?
- Kubernetesの動的プロビジョニングを可能にするプロビジョナーで、NFS上のサブディレクトリ単位でPVを自動作成します。
/nfs-share
の下にpvc-xxxx...
というサブディレクトリを作成し、それぞれのPVCに対応づけます。- ローカルストレージと違い、複数Podで共有するユースケースに最適です(ログ共有、モデル共有など)。
Ansibleでセットアップする場合
次のようなAnsibleのロールを用意しておきインストールを実行します。
---
- name: Add Helm repository for nfs-subdir-external-provisioner
ansible.builtin.command:
cmd: helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
register: helm_repo_add_result
- name: Update Helm repositories
ansible.builtin.command:
cmd: helm repo update
when: helm_repo_add_result is succeeded
- name: Install or upgrade NFS client provisioner using Helm
ansible.builtin.command:
cmd: >
helm upgrade --install nfs-client nfs-subdir-external-provisioner/nfs-subdir-external-provisioner
--namespace nfs-provisioner
--create-namespace
--set nfs.server={{ ansible_host }}
--set nfs.path={{ nfs_share_path }}
--set storageClass.name=nfs-client
--set storageClass.accessModes={ReadWriteMany}
--set storageClass.defaultClass=false
when: helm_repo_add_result is succeeded
YAML