Sougen Blog

Raspberry Pi 1 五台で Nomad クラスタを組んでいる

nomadraspberry-pihomelabansible

前回の記事 で「自宅の Raspberry Pi 5 台を Nomad で束ねたクラスタ」と書きましたが、その中身の話です。

台数の内訳は Raspberry Pi Model B+ (2014 年発売、armv6 のシングルコア 700MHz、RAM 512MB) が五台です。
最新の Pi 5 ではなく、12 年前のモデルに Raspberry Pi OS Lite (trixie ベース) を入れ、その上で HashiCorp Nomad を動かしています。

なぜ Nomad にしたか

Pi 1 は 512MB RAM の単コアで、公式に「2 コア・2GB RAM 以上」を要求する k3s の最小要件を満たしません。
無理に載せると、コントロールプレーンとアプリで RAM とスワップを取り合うことになります。

Nomad は単一バイナリを server / client / 両方のどれかの役割で動かせるので、構成の弾力性があります。
今回は piclus1-3 を server + client 兼務、piclus4-5 を client 専用にして、3 台で Raft クラスタを組みました (quorum は 2 台)。
piclus3 が現在の leader で、Nomad agent の実測 RSS は約 88MB、CPU は 20% 前後です。
follower 側はもっと軽く、Pi 1 の 512MB RAM に無理なく収まります。

Consul や Vault が必須ではないため、初期構築の手間も省けます。

armv6 縛りで Docker は諦める

Docker と containerd の公式パッケージは、対応が arm/v7 以降で armv6 は対象外です。
自前ビルドや非公式パッケージを保守する重さを避けたかったので、raw_exec ドライバに寄せました。
raw_exec はホスト上でバイナリを直接実行し、コンテナや chroot によるファイルシステム隔離を行いません。
自分で管理するバイナリしか置かないクラスタなので、隔離との引き換えは許容しています。

Nomad 本体も armv6 の公式配布は現行版では途絶えているので、v1.8.2 のソースを dockcross の armv6 イメージで自前ビルドしました。
ビルド済みバイナリは Ansible が /usr/local/bin/nomad に配ります。

アプリ側は、Go の場合ならクロスコンパイル時に GOARM=6 を指定します。

GOOS=linux GOARCH=arm GOARM=6 go build -o app-armv6 ./cmd/app

このバイナリは GitHub Releases に置き、Nomad の template stanza でダウンロード用シェルスクリプトを生成させて、raw_exec で実行しています。
リポジトリを private にしている都合で、素の artifact stanza では取れず、PAT を渡す curl コマンドを template で組み立てる遠回りが要りました。

監視は Prometheus と Grafana Cloud

各ノードに node_exporter を Ansible で配布し、Prometheus でスクレイプしています。
Prometheus 本体は別ホストに置き、Grafana Cloud の無料枠に remote_write でメトリクスを送っています。
Pi 五台分の node_exporter が生成するシリーズ数は、無料枠に収まる規模です。

一つ嵌ったのは、Grafana Cloud の Adaptive Metrics です。
Adaptive Metrics は過去のクエリ・ダッシュボード・アラートの利用状況を見て、参照されていないラベルの組み合わせを集約するルールを提案する仕組みです (適用は明示操作)。
node_load1 や node_memory_MemAvailable_bytes に対して提案されたルールを適用したところ、集約後は instance ラベルが落ち、インスタンス別に描くつもりで書いたクエリがエラーになりました。
保持されている集約方式に合わせてクラスタ全体の平均・合計 (avg(node_load1) sum(node_memory_MemAvailable_bytes) など) に書き換えると通ります。
一台ずつ見たいメトリクスは、Adaptive Metrics 側で除外ルールを設定して未集約の系列を保持できます。

入口は Traefik を system job で

自宅内から HTTP でアクセスする入口は Traefik v3.7.8 です。
Nomad の system ジョブとして 5 台全部に配置し、DNS ラウンドロビンでどのノードの 80 番ポートにも到達できるようにしています。
Nomad の service タグ (traefik.enable=true と traefik.http.routers.foo.rule=Host(...)) を書くと、Nomad Catalog Provider を有効にした Traefik が Nomad API からサービス情報とタグを取得してルーターとバックエンドを構成します。

Traefik v3 は linux_armv6 の公式ビルドが配布されており、実測 RSS は 1 台あたり 17MB 程度です。
Nomad のサービス情報を直接参照できて設定ファイル生成の手間が要らないので Traefik を選びましたが、他候補との比較の勘所は次の通りです。

LB Pi 1 での armv6 ビルド Nomad 連携
Traefik v3 公式配布あり providers.nomad で Nomad API を直接参照
Caddy 公式配布あり nomad template で設定ファイルを生成して再読み込み
HAProxy ディストリビューション経由 nomad template 経由

メモリを削りたい時は Caddy や HAProxy が候補になります。

動いているもの

今このクラスタで動いているのは、前回書いた blog サーバー、Cloudflare Tunnel の cloudflared、自宅内向けの小さな Web ページ 2 つです。
外向けのサービスも含まれますが、raw_exec で動かすアプリケーションは自分 (AI に書かせたものを含む) の管理下のコードに絞っています。
直近 48 日間、5 台とも再起動なし、Nomad client の離脱もなく動いています。

後日談

この 48 日連続稼働に至る前に 1 台の microSD が抜ける不幸な事故がありましたが、差し直したところ問題なく復帰しました。

そもそもこの Pi 1 五台は、フリマアプリで Pi 3 だと思い込んで買ったら Pi 1 が届き、悲しみに暮れていたものです。
k3s も動かず持て余していましたが、Nomad に切り替えてからは blog サーバーや Cloudflare Tunnel の replica など用途が見つかって、一安心しました。

まとめ

12 年前の Pi 1 五台に、Nomad と、静的リンクした Go バイナリを載せる形で、監視・LB・オーケストレーションを一通り成立させています。
現行ソフトウェアの公式 armv6 配布は縮小傾向にあり、Nomad は自前ビルド、Docker は諦めて raw_exec、というのが armv6 縛りの現実的な落としどころでした。