話題の記事

GWなのでRaspberry Pi 5複数台をクラスタリングしてGrafana on Kubernetesを構築する

2024.05.01

はじめに

世の中ではGrafana Weekということで、Raspberry Pi 5複数台をクラスタリングしてKubernetesを作成し、Grafanaを載せてみたいと思います。

というのは冗談ですが、最近趣味で安価に常駐プロセスをデプロイできるホスティング環境に悩んでいました。常駐しないなら最近はゼロコールドスタートなV8 Isolateを使ったCloudflare WorkersやDeno Deployが無料枠が大きくいい感じです。 一方常駐プロセスはHerokuの無料プランがなくなりました。AWS AppRunnerは起動時間を人間が稼働している時間のみに絞っても10$はかかります。fly.ioは、Legacy hobby planでCPU-1x 256mb VM 3つと3 GB 永続ボリュームストレージは無料で扱えます。fly.ioはCLIもよくできているので、軽い検証の場合こちらで良さそうです。

より永続化領域とメモリが潤沢な環境を求めて、自前でコンテナオーケストレーション基盤を作ることにしました。

Kubernetesクラスタ自体は4年前くらいにRPi3で作ったことがあります。エコシステム的な難しさもあり、早々に飽きてしまったので今回はそこそこ真面目に構築しました。実務的で使ったことはないので、経験者からすると拙いのとN番煎じですが、ご参考にして頂けたら幸いです。

以下の点ご理解お願いします。

  • クラウドでサーバレス開発をしているので、電気やハード的なところはあまり参考にならないと思います
  • 本構成は動作を保証しません。特に電気的な部分は、他の記事と合わせて確認するのが良いと思います

以後Raspberry Pi 5は、RPi5と表記します。

構成

ゴール

Mac(Kubernetesクラスタ外)からkubectl port-forwardなしで、Grafanaが閲覧できるようになることを目指します。こんな形です。

img

注意点

本項以降の各手順は、最小2台で進めることを推奨します。理由は、一気に4台でやるよりは、2台でgrafana起動まで確認し、その後2ノード追加する方が何か手戻りがあった際にやり直しがしやすいためです。

前提

  • PoEは利用しない(RPi5ではまだPoE HATがないため)
  • RPi5のストレージには NVMe M.2 SSDを利用
    • microSDで代用可能です、RPi5側で対応しているのと家に余っていたので活用しました
  • RPi5のイメージには、Ubuntu Server 24.04 LTS(64bit)を利用
  • 電源はスイッチング電源2台で、各スイッチング電源に2つのRPi5を接続
名称 個数 補足 リンク
1 Raspberry Pi 5 8GB 4 RPi5本体 リンク
2 Raspberry Pi 5用アクティブクーラー 4 RPi5のクーラー リンク
3 Geekworm X1001 V1.1 PCIe NVMe M.2 拡張ボード 4 RPi5とM.2.を接続するための拡張ボード リンク
4 KIOXIA EXCERIA G2 SSD-CK1.0N3G2/N 1TB 4 M.2. M.2 NVMe SSD。正確には家に余っている別メーカーのものもありますが大きな差はないので割愛。 リンク
5 ORICO M.2 SSD 外付けケース 1 NVMe SSDへイメージを書き込むための外付けケース リンク
6 スイッチング電源 DC5V10A 出力50Wタイプ RWS50B-5 2 RPi5は5V/5Aなので、1つあたりRPi52台接続想定。GPIO 5Vピン2つから電源供給 リンク
7 Qiコネクタ 1 スイッチング電源からGPIO経由で電源供給するため利用 リンク
8 コネクタ用ハウジング 4 上記のQiコネクトを半分に切って、ケーブル4本を1つのハウジングに納めて計4本作ります リンク
9 TP-Link スイッチングハブ 5ポート PoE+ 1 PoE+は利用しません リンク
10 Type C ケーブル 0.5M 2本組 2 2本組を2つ購入。計4本 リンク
11 LANケーブル 0.3m 5本 4 5本組を1つ購入。計5本 リンク
12 microSD 32GB 1 ブート設定を変更するためだけに利用します。恒常的に利用しません。 なし
13 microSD読めるカードリーダー 1 なし
14 キッチンラック 1 ラズパイを積む用のラック。専用のものは高かったので、キッチンラックで代用しました...RPi5の裏面の導通ある部分がメタルラックに接触するのがリスクなので紙の上にRPi5を載せています リンク
15 片プラグ付耐熱コード 2 丸端子圧着済みなので、スイッチング電源に接続しやすいです リンク

ブート設定

はじめに

SDカードにRasberry Pi OSを書き込む

書き込みには、Rasberry Pi Imagerを利用します。ダウンロードはこちらから可能です。私はWindows版を利用しました。

一時的にしか使わないので、OSはRASBERY PI OS LITE(64-BIT)を利用します

設定の編集をします

ユーザーとWi-Fi設定を入れたら、サービスタブに移動します

SSHを有効化します。書き込みの段階で指定できるの便利ですね!

書き込み開始します

書き込み完了まで待ちます。

M.2 SSDにUbuntu Serverを書き込む

先に恒常的に利用するM.2にもOSを書き込みます。6の外付けケースを利用して、PCに画像のような形で接続します。

OSはUbuntu Server 24.04 LTSを利用します

初回はイメージダウンロードの時間がかかるため、通信環境によっては時間がかかります。一度内部でイメージをダウンロードすれば、2回目以降は数分もかからず終わります。

書き込みが終わったら、再度M.2をPCにマウントします。すると画像のようにブートパーティションを認識します。

ブートパーティションを開いたら、config.txtを見つけます。

config.txtに2行追記します

[all]
+dtparam=nvme
+dtparam=pciex1

RPi5にクーラーを取り付け

RPi5 M.2拡張

PCIe NVME M.2 拡張ボードを取り付けます

裏は家にあったネジで高さを出すようにしました。

電源の設定

電源はRPi5の2,4ピンに+5V、6,9ピンにGNDに繋ぎます。スイッチング電源1つに2つのRPi5を接続します。以下の図が分かりやすいです。下の直方体はコネクタのハウジングを示してます。


※ 弊社のこの領域に強い方に教えて頂きました。ありがとうございます;;

実際はこんな形です。

ブート設定(EEPROM書き込み)

microSDを入れて、RPi5の電源を入れます。ホスト名はraspberrypi.localです。

$ ssh shuntaka@raspberrypi.local
(中略)
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
(中略)
Linux raspberrypi 6.6.20+rpt-rpi-2712 #1 SMP PREEMPT Debian 1:6.6.20-1+rpt1 (2024-03-07) aarch64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
shuntaka@raspberrypi:~ $

ブートローダーを更新します。

shuntaka@raspberrypi:~ $ sudo rpi-eeprom-update
*** UPDATE AVAILABLE ***
BOOTLOADER: update available
   CURRENT: Wed Dec  6 18:29:25 UTC 2023 (1701887365)
    LATEST: Fri Feb 16 15:28:41 UTC 2024 (1708097321)
   RELEASE: default (/lib/firmware/raspberrypi/bootloader-2712/default)
            Use raspi-config to change the release.

利用可能な更新がありそうなので、更新を実行します。

shuntaka@raspberrypi:~ $ sudo rpi-eeprom-update -d -a
*** PREPARING EEPROM UPDATES ***

BOOTLOADER: update available
   CURRENT: Wed Dec  6 18:29:25 UTC 2023 (1701887365)
    LATEST: Fri Feb 16 15:28:41 UTC 2024 (1708097321)
   RELEASE: default (/lib/firmware/raspberrypi/bootloader-2712/default)
            Use raspi-config to change the release.
   CURRENT: Wed Dec  6 18:29:25 UTC 2023 (1701887365)
    UPDATE: Fri Feb 16 15:28:41 UTC 2024 (1708097321)
    BOOTFS: /boot/firmware
'/tmp/tmp.ov5cNFr05e' -> '/boot/firmware/pieeprom.upd'

UPDATING bootloader.

*** WARNING: Do not disconnect the power until the update is complete ***
If a problem occurs then the Raspberry Pi Imager may be used to create
a bootloader rescue SD card image which restores the default bootloader image.

flashrom -p linux_spi:dev=/dev/spidev10.0,spispeed=16000 -w /boot/firmware/pieeprom.upd
UPDATE SUCCESSFUL

変わっていないので、再起動する必要がありそう

shuntaka@raspberrypi:~ $ sudo rpi-eeprom-update
*** UPDATE AVAILABLE ***
BOOTLOADER: update available
   CURRENT: Wed Dec  6 18:29:25 UTC 2023 (1701887365)
    LATEST: Fri Feb 16 15:28:41 UTC 2024 (1708097321)
   RELEASE: default (/lib/firmware/raspberrypi/bootloader-2712/default)
            Use raspi-config to change the release.

sudo reboot 後再度実行したところ更新完了していることを確認

shuntaka@raspberrypi:~ $ sudo rpi-eeprom-update
BOOTLOADER: up to date
   CURRENT: Fri Feb 16 15:28:41 UTC 2024 (1708097321)
    LATEST: Fri Feb 16 15:28:41 UTC 2024 (1708097321)
   RELEASE: default (/lib/firmware/raspberrypi/bootloader-2712/default)
            Use raspi-config to change the release.

ブート設定を変更します。極度のnanoアレルギーなのでviを設定します。

env EDITOR=vi sudo -E rpi-eeprom-config --edit

変更前

[all]
BOOT_UART=1
POWER_OFF_ON_HALT=0
BOOT_ORDER=0xf461

以下のように設定変更します

変更後(diff)

[all]
BOOT_UART=1
POWER_OFF_ON_HALT=0
-BOOT_ORDER=0xf461
+BOOT_ORDER=0xf416
+PCIE_PROBE=1

実行結果

shuntaka@raspberrypi:~ $ env EDITOR=vi sudo -E rpi-eeprom-config --edit
Updating bootloader EEPROM
 image: /lib/firmware/raspberrypi/bootloader-2712/default/pieeprom-2024-02-16.bin
config_src: blconfig device
config: /tmp/tmpp6gugkfa/boot.conf
################################################################################
[all]
BOOT_UART=1
POWER_OFF_ON_HALT=0
BOOT_ORDER=0xf416
PCIE_PROBE=1

################################################################################

*** To cancel this update run 'sudo rpi-eeprom-update -r' ***

*** CREATED UPDATE /tmp/tmpp6gugkfa/pieeprom.upd  ***

   CURRENT: Fri 16 Feb 15:28:41 UTC 2024 (1708097321)
    UPDATE: Fri 16 Feb 15:28:41 UTC 2024 (1708097321)
    BOOTFS: /boot/firmware
'/tmp/tmp.53DnGVNp9Z' -> '/boot/firmware/pieeprom.upd'

UPDATING bootloader.

*** WARNING: Do not disconnect the power until the update is complete ***
If a problem occurs then the Raspberry Pi Imager may be used to create
a bootloader rescue SD card image which restores the default bootloader image.

flashrom -p linux_spi:dev=/dev/spidev10.0,spispeed=16000 -w /boot/firmware/pieeprom.upd
UPDATE SUCCESSFUL

M.2からブート

一度RPi5の電源をきり、microSDを抜いて、M.2を挿します。Ubuntu 24.04 LTSの文字があるので、M.2から起動していることが確認できました。

$ ssh shuntaka@pi1.local
shuntaka@pi1.local's password:
Welcome to Ubuntu 24.04 LTS (GNU/Linux 6.8.0-1004-raspi aarch64)
shuntaka@pi4:~$ sudo df -h
Filesystem      Size  Used Avail Use% Mounted on
tmpfs           795M  3.2M  792M   1% /run
/dev/nvme0n1p2  917G  1.9G  878G   1% /
tmpfs           3.9G     0  3.9G   0% /dev/shm
tmpfs           5.0M     0  5.0M   0% /run/lock
/dev/nvme0n1p1  505M   85M  420M  17% /boot/firmware
tmpfs           795M   12K  795M   1% /run/user/1000

カーネルの更新

Ubuntu Server 24.04 LTS では更新はありません。

$ uname -r
6.8.0-1004-raspi
sudo apt update
sudo apt -y upgrade
$ uname -r
6.8.0-1004-raspi

Ubuntu Server 23.10 の場合、クーラーが回りっぱなしで音が大きいので確実にやるのが良いです。 参考

ネットワーク設定

はじめに

以下のような形でアドレスを固定します。

ホスト名 eth0 IP
pi1 192.168.1.1
192.168.86.10
pi2 192.168.1.2
pi3 192.168.1.3
pi4 192.168.1.4

p1のnetplanの設定

cat <<EOF | sudo tee /etc/netplan/01-netcfg.yaml > /dev/null
network:
  version: 2
  ethernets:
    eth0:
      dhcp4: false
      addresses:
        - 192.168.1.1/24
        - 192.168.86.10/24
      nameservers:
        addresses: [8.8.8.8]
EOF
sudo netplan apply

pi2以降のnetplanの設定

pi2~pi4のnetplanの設定をします

cat <<EOF | sudo tee /etc/netplan/01-netcfg.yaml > /dev/null
network:
    version: 2
    ethernets:
      eth0:
        dhcp4: false
        addresses:
          - 192.168.1.2/24
          # pi3の場合
          # - 192.168.1.3/24
          # pi4の場合
          # - 192.168.1.4/24
        nameservers:
          addresses:
            - 8.8.8.8
EOF
sudo netplan apply

192.168.1.2が割り当てられていることを確認

shuntaka@pi2:~$ ip a
(中略)
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
(中略)
    inet 192.168.1.2/24 brd 192.168.1.255 scope global eth0
       valid_lft forever preferred_lft forever
(中略)

Kubernetesクラスタの構築

kubelet kubeadm kubectlのインストール

モジュールのロード設定を実施します

全てのノード

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

読み込まれているか確認

全てのノード

lsmod | grep br_netfilter
lsmod | grep overlay

kubelet kubeadm kubectlをインストールします

全てのノード

sudo apt update
sudo apt install -y apt-transport-https ca-certificates curl # <--最初は apt-getでやっていたの確認

echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.28/deb/ /" | sudo tee /etc/apt/sources.list.d/kubernetes.list

curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.28/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg

sudo apt update
sudo apt install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl

cri-oのインストール

download.opensuse.orgから要件にあったものを選択

全てのノード

export OS=xUbuntu_22.04

sudo echo "deb https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS/ /" | sudo tee -a /etc/apt/sources.list.d/cri-o-stable.list >/dev/null
sudo echo "deb http://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/1.28/$OS/ /" | sudo tee -a /etc/apt/sources.list.d/cri-o.list >/dev/null
 
sudo curl -L https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable/$OS/Release.key | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/libcontainers-crio-archive-keyring.gpg
sudo curl -L http://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/stable:/cri-o:/1.28/$OS/Release.key | sudo gpg --dearmor -o /etc/apt/trusted.gpg.d/cri-o-apt-keyring.gpg
 
sudo apt update
sudo apt install cri-o cri-o-runc -y

sudo rm -rf /etc/cni/net.d/*

sudo systemctl daemon-reload
sudo systemctl enable crio
sudo systemctl start crio

cri-oの動作確認

全てのノード

shuntaka@pi1:~$ sudo crictl info
{
  "status": {
    "conditions": [
      {
        "type": "RuntimeReady",
        "status": true,
        "reason": "",
        "message": ""
      },
      {
        "type": "NetworkReady",
        "status": false,
        "reason": "NetworkPluginNotReady",
        "message": "Network plugin returns error: No CNI configuration file in /etc/cni/net.d/. Has your network provider started?"
      }
    ]
  },
  "config": {
    "sandboxImage": "registry.k8s.io/pause:3.9"
  }

Kubernetesクラスタの初期化を実行します。

マスターノード

$ sudo kubeadm init \
  --pod-network-cidr=10.244.0.0/16 \
  --control-plane-endpoint=192.168.1.1 \
  --apiserver-advertise-address=192.168.1.1 \
  --apiserver-cert-extra-sans=192.168.1.1

ワーカーノード側で実行する必要があるコマンドをメモします。

メモする出力結果

sudo kubeadm join 192.168.1.1:6443 --token rwmoll.7vfynzwt0wbparok \
        --discovery-token-ca-cert-hash sha256:bf28ffd7b92f505b93409da63dacc610d05e4d3566e50b629738a6a4cf4b259f

kubectl が利用できるように、設定を行います

マスターノード

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

Kubernetesクラスタに参加するコマンドを実行します

ワーカーノード

sudo kubeadm join 192.168.1.1:6443 --token rwmoll.7vfynzwt0wbparok \
        --discovery-token-ca-cert-hash sha256:bf28ffd7b92f505b93409da63dacc610d05e4d3566e50b629738a6a4cf4b259f

ノードを確認します。この段階ではNotReadyですが、CNIをインストールするとReady状態になります

マスターノード

$ kubectl get node
NAME   STATUS     ROLES           AGE     VERSION
pi1    NotReady   control-plane   9m21s   v1.28.9
pi2    NotReady   <none>          9s      v1.28.9

CNI(calico)のインストール

手順と異なり、Ubuntu Server 23.10の場合、以下の設定が必要です

全てのノード

# Ubuntu Server 23.10利用の場合のみ
sudo apt install linux-modules-extra-raspi -y

CNIをインストールします

マスターノード

kubectl create -f https://raw.githubusercontent.com/projectcalico/calico/v3.27.3/manifests/tigera-operator.yaml
curl https://raw.githubusercontent.com/projectcalico/calico/v3.27.3/manifests/custom-resources.yaml -O

# custom-resources.yamlのcidr: 10.244.0.0/16 に修正
kubectl create -f custom-resources.yaml
kubectl get pods -n calico-system

CNIがインストールされ、全てReadyになりました

$ kubectl get nodes
NAME   STATUS   ROLES           AGE     VERSION
pi1    Ready    control-plane   17m     v1.28.9
pi2    Ready    <none>          8m16s   v1.28.9

エイリアス設定

使いやすいように、エイリアスと補完設定を入れます。この設定で省略されたkコマンドとtabでの補完が効くようになります。

cat <<EOF >> ~/.bashrc
# k8s
source <(kubectl completion bash)
alias k=kubectl
complete -o default -F __start_kubectl k
EOF

Helmの設定

はじめに

今回KubernetesリソースはHelm経由でデプロイすることにします。

Helmのインストール

マスターノード

curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
sudo apt 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 update
sudo apt install helm

Helmfileのインストール

マスターノード

export VERSION="0.163.1"
wget -O "/tmp/helmfile_${VERSION}_linux_arm64.tar.gz" "https://github.com/helmfile/helmfile/releases/download/v${VERSION}/helmfile_${VERSION}_linux_arm64.tar.gz"
tar -xzvf "/tmp/helmfile_${VERSION}_linux_arm64.tar.gz" -C /tmp

sudo mv /tmp/helmfile /usr/bin/helmfile
sudo chmod 755 /usr/bin/helmfile

MetalLBの設定

はじめに

MetalLBは、ベアメタルKubernetes環境向けのロードバランサーでKubernetesクラスタ外からアクセス出来るようにIPを割り当てます。

IPアドレスプールの定義

割り当てるIPプールを定義します。IPレンジはKubernetesの内部IPではなく、ルータで払い出されているものを利用してください。

ip-address-pool.yaml

apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: default
  namespace: metallb
spec:
  addresses:
    - 192.168.86.200-192.168.86.250
  autoAssign: true
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: default
  namespace: metallb
spec:
  ipAddressPools:
  - default

適用します

kubectl apply -f ip-address-pool.yaml

MetalLBのホスティング

こちら を参考に、以下のコマンドで、strictARPをtrueにします

kubectl edit configmap -n kube-system kube-proxy

helmfile.yaml

repositories:
  - name: metallb
    url: https://metallb.github.io/metallb

releases:
  - name: metallb
    namespace: metallb
    chart: metallb/metallb
    version: 0.14.5
    values:
      - values.yaml

実行自体はこけますが、repositoriesは追加されます。

helmfile sync

以下のコマンドで変更可能箇所を出力します。出力しますが変更箇所はありません。

helm show values metallb/metallb>values.yaml

適用します。

helmfile sync

PromethuesとGrafanaの設定

はじめに

  • PromethuesとGrafana間で通信させるために同じnamespaceに所属
  • MetalLBを使ってGrafanaをクラスタ外へ公開

永続ストレージ設定

PersistentVolumeとPersistentVolumeClaimの設定をします。prometheus-alertmanagerは、ストレージは作っていますが、後述のprometheusの設定でOFFにするのでなくても大丈夫です。

ストレージタイプ name path
PersistentVolume prometheus-server /mnt/k8s/pv/prometheus-server
PersistentVolume prometheus-alertmanager /mnt/k8s/pv/prometheus-alertmanager
PersistentVolume grafana /mnt/k8s/pv/grafana
ストレージタイプ namespace name
PersistentVolumeClaim monitoring prometheus-server
PersistentVolumeClaim monitoring prometheus-alertmanager
PersistentVolumeClaim monitoring grafana

monitoringネームスペース作成

kubectl create namespace monitoring

pvとpvcはcontrol-planeにデフォルトでは設定できないため、pi2側で設定します

pi2で実行!!!

sudo mkdir -p /mnt/k8s/pv/grafana
sudo mkdir -p /mnt/k8s/pv/prometheus-server
sudo mkdir -p /mnt/k8s/pv/prometheus-alertmanager

pvとpvcを定義します

volume.yaml

# grafana
kind: PersistentVolume
apiVersion: v1
metadata:
  name: grafana
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: grafana
  local:
    path: /mnt/k8s/pv/grafana
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - pi2
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: grafana
  namespace: monitoring
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
  storageClassName: grafana
---

# prometheus-server
kind: PersistentVolume
apiVersion: v1
metadata:
  name: prometheus-server
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: prometheus-server
  local:
    path: /mnt/k8s/pv/prometheus-server
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - pi2
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: prometheus-server
  namespace: monitoring
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
  storageClassName: prometheus-server
---

# prometheus-alertmanager
kind: PersistentVolume
apiVersion: v1
metadata:
  name: prometheus-alertmanager
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: prometheus-alertmanager
  local:
    path: /mnt/k8s/pv/prometheus-alertmanager
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - pi2
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: prometheus-alertmanager
  namespace: monitoring
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
  storageClassName: prometheus-alertmanager

volumeを適用します

kubectl apply -f volume.yaml

表通り作成されていることを確認できたらOKです

kubectl get pv
kubectl get pvc -A

Promethuesの設定

helmfileを書きます

helmfile.yaml

repositories:
  - name: prometheus-community
    url: https://prometheus-community.github.io/helm-charts

releases:
  - name: prometheus
    namespace: monitoring
    chart: prometheus-community/prometheus
    version: 25.9.0
    values:
      - values.yaml

実行自体はこけますが、repositoriesは追加されます

helmfile sync

以下のコマンドで変更可能箇所を出力します。出力しますが変更箇所はありません。

helm show values prometheus-community/prometheus > values.yaml

以下のように修正します

values.yaml

server:
  ## Prometheus server container name
  ##
  (中略)
  persistentVolume:
  (中略)
    existingClaim: "prometheus-server" # <- 先ほど作成したPVCを指定
  (中略)
    size: 10Gi # <- 8Giを10Giに変更
  (中略)
  service:
    (中略)
    type: NodePort # <- ClusterIPをNodePortへ変更
(中略)
alertmanager:
  ## If false, alertmanager will not be installed
  ##
  enabled: false # <- 今回は利用しないためtrueからfalseへ変更

適用します

helmfile sync

Grafanaの設定

同様にhelmfileを書きます

helmfile.yaml

repositories:
  - name: grafana
    url: https://grafana.github.io/helm-charts

releases:
  - name: grafana
    namespace: monitoring
    chart: grafana/grafana
    version: 7.2.3
    values:
      - values.yaml

こちらも同様です。

helmfile sync

以下のコマンドで変更可能箇所を出力します。

helm show values grafana/grafana > values.yaml

以下のように修正します

values.yaml

service:
  enabled: true
  type: NodePort # <- ClusterIPをNodePortへ変更
(中略)
persistence:
  type: pvc
  enabled: true # <- ストレージ永続化ため、falseからtrueへ変更
  (中略)
  existingClaim: "grafana" # <- 先ほど作成したPVCを指定
(中略)
# データソースを追加
datasources:
  datasources.yaml:
    apiVersion: 1
    datasources:
    - name: Prometheus
      type: prometheus
      url: http://prometheus-server.monitoring.svc.cluster.local/
      isDefault: true

適用します

helmfile sync

以下のコマンド実行して、マスターノードのクラスタ外から見れるプライベートIP:3000で、PCからアクセスしてGrafanaのログイン画面が見れれば成功です。

マスターノード

kubectl port-forward --address 0.0.0.0 svc/grafana 3000:80 -n monitoring

ログインIDとパスワードは以下のコマンド出力できます

kubectl get secret grafana --namespace monitoring  -o jsonpath="{.data.admin-user}" | base64 --decode ; echo
kubectl get secret grafana --namespace monitoring -o jsonpath="{.data.admin-password}" | base64 --decode ; echo

Grafanaをクラスタ外から閲覧する

MetalLBを利用して外部アクセス可能なLoadBalancerタイプのサービスを定義します。宛先にgrafanaを指定します。

grafana-loadbalancer.yaml

apiVersion: v1
kind: Service
metadata:
  name: grafana-loadbalancer
  namespace: monitoring
spec:
  type: LoadBalancer
  ports:
    - port: 80
      targetPort: 3000
      protocol: TCP
  selector:
    app.kubernetes.io/instance: grafana
    app.kubernetes.io/name: grafana

適用します

kubectl apply -f grafana-loadbalancer.yaml

grafana-loadbalancer192.168.86.200が振られていることが確認できます。

$ k get svc -A
NAMESPACE          NAME                                  TYPE           CLUSTER-IP       EXTERNAL-IP      PORT(S)                  AGE
calico-apiserver   calico-api                            ClusterIP      10.109.235.205   <none>           443/TCP                  5h2m
calico-system      calico-kube-controllers-metrics       ClusterIP      None             <none>           9094/TCP                 5h2m
calico-system      calico-typha                          ClusterIP      10.96.11.42      <none>           5473/TCP                 5h3m
default            kubernetes                            ClusterIP      10.96.0.1        <none>           443/TCP                  5h8m
kube-system        kube-dns                              ClusterIP      10.96.0.10       <none>           53/UDP,53/TCP,9153/TCP   5h8m
metallb            metallb-webhook-service               ClusterIP      10.98.53.232     <none>           443/TCP                  4h31m
monitoring         grafana                               NodePort       10.100.215.182   <none>           80:31851/TCP             4h35m
monitoring         grafana-loadbalancer                  LoadBalancer   10.98.112.253    192.168.86.200   80:30699/TCP             4h17m
monitoring         prometheus-kube-state-metrics         ClusterIP      10.110.198.118   <none>           8080/TCP                 4h34m
monitoring         prometheus-prometheus-node-exporter   ClusterIP      10.108.144.213   <none>           9100/TCP                 4h34m
monitoring         prometheus-prometheus-pushgateway     ClusterIP      10.107.27.149    <none>           9091/TCP                 4h34m
monitoring         prometheus-server                     NodePort       10.111.109.182   <none>           80:31648/TCP             4h34m

http://192.168.86.200にアクセスすると、http://192.168.86.200/loginにリダイレクトされて、grafanaのダッシュボードが閲覧できました

Data sourcesを開きます

DatasourceはHelmのvalue.yamlで定義しているため、Prometheusがすでにある状態です。

今回は、こちらのダッシュボードを利用しますので、11074を入力してLoadします。

Loadされたら、importします

無事importできました。このときはまだ3つしかノードがないので、正常に動作しています!

最後に

先人が沢山いたのですが、かなり大変でした。環境を完全に消すのも大変だったので、都度M.2にイメージを焼き直してました。気づいたらUbuntu Server 24.04 LTSがリリースされていて手順を差し替えるか迷いました。23.10だと、クーラー周りの問題で音がかなり厳しいのでカーネルアップデートが必須でしたが、24.04は問題なかったので、結果待ち時間が少ない24.04の手順にしました。

大変だった点は主にネットーワーク周りになります。最初はp1をNAT化し、pi2以降はpi1をデフォルトゲートウェイとしてネットーワークに出ていたのですが、MetalLBを入れてから通信がループして設定を削除したりなど。。ここら辺もう少し再現確認してブログにできたらと思います。

各ワーカーノードのストレージとしてM.2を使いましたが、容量の大きいNFSサーバーを作ってそこに永続化リソースをまとめるの方が良いのかなとも思いました!

また弊社のハードに強い方にいくつかアドバイス頂き感謝です!

今後経過についてもブログ記事化できたらと思います!

付録

トラブルシューティング

MetalLBで割り当てられたIPに接続できない

curlやブラウザ経由でMetalLBで割り当てられたIPでgrafanaの画面が閲覧できな場合です。以下のコマンドでARPリクエスト送信し、tcpdumpでARP応答を返していることを確認できれば、MetalLBとしては動作しています。FW設定などに問題あるかどうかを切り分けできます。

# ARPリクエストを送信
arping -I eth0 192.168.86.100
# 応答していることを確認
sudo tcpdump -n -i eth0 arp src host 192.168.86.100

Grafanaダッシュボードからテストリクエストが通らない

corednsを再起動したら動くケースがありました。

kubectl rollout restart deployment coredns -n kube-system

PrometheusやGrafanaのホスティングをやり直したい

永続化ストレージの消し忘れに注意

helm uninstall prometheus -n monitoring
helm uninstall grafana -n monitoring

sudo rm -rf /mnt/k8s/pv/grafana
sudo rm -rf /mnt/k8s/pv/prometheus-server
sudo rm -rf /mnt/k8s/pv/prometheus-alertmanager

参考資料