ハイブリッド k3s · 第2回

ハイブリッド k3s #2: 眠っていた iMac を仲間に迎える(Lima VM で4ノード追加)

この記事の原文は Zenn で公開しています。


ホームラボ全体構成図

0. このシリーズについて

このシリーズは、上の構成図のように、今この瞬間も動いているホームラボをどう作ったのかを一つずつ記録していく記事です。

「これ、できるんだろうか?」という素朴な疑問から始めたトイプロジェクトは、満足のいく性能を手にし、壊しては組み直す過程を重ねるうちに、仕事で溜まったストレスを解いてくれる本物のおもちゃになってしまいました。リソースが潤沢なクラスタではありませんが、Kubernetes を存分に味わうには十分で、次に試したいことを次々と投げかけてくれます。

  • ノード 6台 — クラウド(AWS 東京)の Lightsail server 2台(コントロールプレーン + etcd) + 自宅(札幌)iMac 上の Lima VM agent 4台
  • 合計 19 vCPU / 61 GiB、ネームスペース 49個、Pod 248個(稼働 150)
  • デプロイは ArgoCD、認証は Keycloak OIDC、その上に CloudNativePG・Vault・CrowdSec・Prometheus/Grafana などが稼働中

この記事は、前回築いた クラウド2ノードのクラスタに、自宅で眠っていた iMac を Lima VM 4台に分けて agent として迎え入れる 話です。

1. 背景 — 眠っている iMac を仲間に迎えよう

前回作ったのはクラウド側の土台でした。

AWS Lightsail インスタンス2台(8GB・16GB)に k3s を載せ、コントロールプレーン + 埋め込み etcd の2ノードを構成し、両ノードを Tailscale で束ねて 100.x アドレスで互いを呼べるようにしました。王道の3ノードではなく2ノードにした代わりに、etcd の自動スナップショットで保険をかけた、いわば「頭」だけのクラスタでした。(もちろんそのノードにも、ケチって色々とアプリを載せて運用していますが…)

今回は「手足」を付けます。

自宅には、だいぶ古い 64GB RAM の iMac が遊んでいます。HDD なので足は遅いものの、メモリだけは潤沢で、macOS も仮想化(vz)が動くバージョンなので — ホストとしては十分です。これをクラスタのワーカーとして迎え入れるのが今回の目標です。

ただ、始めてすぐに決めることが一つありました。

iMac 1台をノード1つとして丸ごと入れるか、それとも複数に分けて入れるか?

楽なのは丸ごとです。

iMac に Ubuntu を入れて k3s agent を1つ立てれば終わりです。しかしこのホームラボの目的は「Kubernetes を本物のように扱ってみること」です。その観点で、二つの選択肢を公式資料に照らして検討しました。

丸ごと(ノード1台)の限界。

Kubernetes 公式ドキュメントは、耐障害性のために障害ゾーン(failure zone)ごとに最低1インスタンスを置くよう推奨しています。自宅側のノードが1台だけだと、それ自体が単一障害点になり、何より 複数ノードを前提とした練習が一切できません。

ノードを cordon/drain してワークロードを移すこと、Pod を複数ノードに分散(anti-affinity)させること、ローリングでノードを空けて戻すこと — ノードが1台ならすべてが無意味になってしまいます。

分割(複数ノード)の値打ち。

ノードを複数置けば、障害範囲(blast radius)が小さくなり、分散が可能になります。learnkube のワーカーノードサイズ分析はこれを数字で示しています — ノードが5台ならレプリカ5個を別々のノードに散らせるので、1ノードが落ちても失うレプリカは最大1個 です。

ノードが2台だけだと、レプリカをいくら増やしても実効分散は2に頭打ちになります。

もちろん分割にタダはありません。

同じ記事が指摘するように、ノードごとに kubelet と OS がリソースを予約します — たとえば 1 vCPU/4GB ノードは約 1.1GB を、4 vCPU/32GB ノードは約 3.66GB を取られます — つまり 細かく刻むほどシステムオーバーヘッドの比率が大きくなります。

ノードあたりの Pod 数も既定で110個に縛られています。要するに「無限に細かく」が答えではなく、意味のあるサイズのノードを、適度な数だけ 置くバランス点が必要です。

結論は分割でした。

学習が目的のホームラボで「本物のクラスタのように」スケジューリングと障害を扱ってみるなら、自宅側も複数ノードであるべきです。64GB というメモリが、この贅沢を可能にしてくれます。(何台に分けるかは §3 で決めます。)

ここでもう一つ問いが枝分かれします。物理マシン1台を、どうやって複数ノードに分けるのか?

2. Lima VM

物理 iMac 1台を複数の k3s ノードにする方法はいくつもあります。候補を並べ、このホームラボの条件(「ヘッドレスのサーバーとして常時稼働、再現可能に同じものを何台も量産、macOS は残す」)でふるいにかけました。

いちばん大きな分かれ道は コンテナでノードを真似るか、VM で本物のノードを作るか です。

iMac 1台を複数ノードに:5つの方法の比較

方法性格今回の状況では
ベアメタル Ubuntu 再インストールiMac に Linux を直接インストール、ノード1台macOS を消す必要があり、結局 ノード1個 → 分割の目的と衝突、除外
Docker + k3dk3s をコンテナで立ててマルチノードを模倣ノードがコンテナなので ホストカーネルを共有(弱い隔離)、「本物のノード」感が薄い → 学習にはもの足りない
Multipass (Canonical)Ubuntu VM を一行で launchVM=本物のノードで良いが Ubuntu 専用 で、同じ VM を宣言的に N 台量産するより単発起動寄り(バックエンドは 1.12 から QEMU が既定)
VirtualBox / VMware Fusion伝統的な GUI ハイパーバイザIntel Mac で動くが重く GUI 中心、スクリプトで N 台量産は面倒
UTMQEMU の macOS GUI フロントエンドGUI で1〜2台作るには良いが、ヘッドレス・再現には向きにくい
Colima「Containers on Lima」、Docker/k8s の抽象化内部で Lima を使う。VM を直接定義するより、コンテナランタイム提供が目的
Lima宣言的 YAML でヘッドレス Linux VMYAML 1枚で 同じ VM を何台も再現、ヘッドレス、containerd 親和、vz バックエンドでネイティブ速度 ← 採用

上の図のとおり、k3d はノードが コンテナ なのでカーネルを1つ分け合います。

速くて軽いものの、ノード同士でカーネルが隔離されないため「本物のノードを運用する」感覚とは距離があります。残り(Multipass・VirtualBox・UTM・Lima)はノードが それぞれ自分のカーネルを持つ VM なので隔離が強いです。その中でさらに分かれるのは 管理方式とバックエンド です。

Lima(Linux Machines)は macOS でヘッドレスの Linux VM を立てるオープンソースツールです。選んだ理由は3つです。

  1. 宣言的で再現可能。 VM のスペック(CPU・メモリ・ディスク・ディストリ)を YAML に書いておけば、同じ定義で同一の VM 4台をそのまま量産できる。手で GUI を4回クリックする方式とは再現性が違う。
  2. ヘッドレスに合う。 サーバーとして常時動かすので GUI は不要。Lima は CLI(limactl)だけで回る。
  3. バックエンドが速い。 Lima は v1.0 から macOS(13.5+)で vz(Apple Virtualization.framework)を既定バックエンドに使う。Intel Mac で Intel VM を動かす今回はエミュレーションではなくネイティブ仮想化なので軽い。(vz が使えない古い macOS なら vmType: qemu に落とせばよい。)

3つ目の根拠は Lima vmType 公式ドキュメント です。

Lima はどう動くのか

見た目は「Mac で Linux VM を立てるツール」ですが、構造を知ると、なぜ再現性と速度を同時に取れるのかが見えてきます。

Lima アーキテクチャ — limactl が vz でゲスト VM を立てる

  • limactl(ホスト CLI) — Lima の本体だ。limactl start ./k3s-agent.yaml の一行で、YAML に書いたスペックどおりに VM を作って起動する。GUI が無いので、そのままスクリプトに入れて4回繰り返せば4台が同じように立つ。
  • lima.yaml(宣言的設定) — VM の CPU・メモリ・ディスク・ディストリ・プロビジョニングスクリプトを書いておく一枚。これが「VM の設計図」なので、同じファイルを共有すれば誰でも同じ VM を得られる。
  • vz(Apple Virtualization.framework) — 実際に VM を動かすハイパーバイザだ。macOS 13.5+ で Lima の既定値であり、Intel ホストで Intel ゲストを動かすのでエミュレーションなしのネイティブで回る。起動した VM の中で systemd-detect-virt を実行すると apple が出るのがその証拠だ。
  • ゲスト VM(Ubuntu) — ホストと分離した自前の Linux カーネルを持つ。この上に containerd(k3s 内蔵)と k3s agent が立ち、別途入れた Tailscale が tailscale0 インターフェースで 100.x アドレスを受け取る。クラウドノードとはこの 100.x で参加する(§5・§6)。
  • ホスト↔ゲストの接続 — Lima が virtiofs のファイル共有、ポートフォワード、SSH 接続を自動で用意してくれる。だから Mac のターミナルからそのまま VM に入り、ファイルをやり取りできる。

まとめると「設計図(YAML)1枚 → limactl が vz で起動 → 自前カーネルを持つ本物のノード」という流れです。「1台を同じように何度も、スクリプトで、軽く」という条件に Lima がいちばんよく当てはまりました。

3. 意味のある単位で分ける

分割と決めたら、残るは台数です。2台でも8台でもなく4台にしたのには理由があります。

基準は RAM でした。ノード数の上限を決めるのは CPU ではなくメモリです。CPU は複数 VM が時間を分け合えますが(オーバーサブスクリプション)、メモリは一度割り当てたら重ねて使えません。

64GB のうち macOS ホストと余裕分で半分ほど残すと、VM の取り分は 32GB 前後。これをいくつに分けるか。

なぜ4台か — 64GB の分割とノード数のバランス点

ノード1台の最小サイズ。

細かく刻みすぎると、ノードごとに kubelet と OS が取る固定オーバーヘッドが目立ってきます。先の learnkube の数字では、1 vCPU/4GB ノードは約 1.1GB(28%!)をシステムに持っていかれます。8GiB くらいになるとこの比率が我慢できる程度になり、その上に意味のあるワークロードを載せる余地が生まれます。だから VM 1台 = 8GiB を単位にしました。

台数のバランス。 32GB ÷ 8GiB = 4台です。数字が合っただけでなく、学習目的にも噛み合います。

  • 2台 はマルチノードと呼ぶには物足りない。1台落ちると半分が飛び、レプリカを散らしても実効分散は2だ。
  • 8台 は 8GiB が 4GiB に縮んでノードあたりのオーバーヘッド比率がまた大きくなり、ホストのコアを8 VM が奪い合って CPU 競合とホスト RAM の圧迫がきつくなる。ノードが増えるほどノードコントローラのヘルスチェック負荷も一緒に増える。
  • 4台 はノードを1つ drain しても3台が支え、anti-affinity で Pod を散らしてローリングで空けて戻す練習をきちんとできるぎりぎりの線でありながら、8GiB というワークロード単位を守れる。

CPU が足りなくても大丈夫な理由。

1台あたり3 vCPU を割り当てたので、4台で 12 vCPU です。ホストの物理コア数を超えます — 正直、コアはこの合計に届きません(オーバーサブスクリプション)。それでも回るのは、このホームラボのワークロードの多くが ウォームスタートアップ 方式だからです。使わないときは最小リソースで眠っていて、リクエストが来ると目覚めて起動します。すべての Pod が同時にフルロードで回るのではなく、時間差で起きたり眠ったりを繰り返すので、物理コアを複数 VM で分け合っても実使用では無理がありません。メモリと違い、CPU は時分割で共有できるからです。これが、仮想化でオーバーサブスクリプションが問題なく成り立つ理由です。

まとめると初期設計は 自宅ノード4台、各 3 vCPU / 8GiB / ディスク 300GiB / Ubuntu 24.04 LTS です。ここに前回のクラウドコントロールプレーン2台を足すと、目標の6ノードになります。

自宅ノード(初期4台共通)
vCPU3
メモリ8 GiB
ディスク300 GiB
OSUbuntu 24.04 LTS
役割k3s agent(ワークロード専用)

先に一つ、断っておきます。上は 初期設計 で、今も4台のうち3台はそのままです。ただし運用しながら、大きめのアプリを載せてみたり、別バージョンの OS との互換チェックをしたりするうちに、4番目のノード(agent-4)だけ 4 vCPU / 16GiB に増やし、OS も Ubuntu 25.10 に入れ直しました。なので §4 で limactl list を実行すると、1台だけスペックが違って見えます。

4. Lima のインストールと VM の定義

ここから手を動かします。順番は ① Lima のインストール → ② VM の設計図(YAML)作成 → ③ 4台起動 → ④ 確認 です。以下の出力はすべて、実際にこの iMac で取得したものです。

4-1. Lima のインストール(ホスト = iMac の macOS)

Homebrew で一行です。

brew install lima
limactl --version
limactl version 2.0.3

vz を既定バックエンドにするには macOS 13.5 以上が必要です(この iMac は要件を満たします)。それより低いバージョンなら、下の YAML で vmTypeqemu に変えればよいです — 遅いものの動作は同じです。

4-2. VM の設計図 — k3s-agent.yaml

4台が同じになるよう、定義を一枚に固めます。この YAML がそのままノード4台の青写真です。

# k3s-agent.yaml — 自宅ノード4台 共通の設計図 (Lima)
images:
  - location: "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img"
    arch: "x86_64"
cpus: 3
memory: "8GiB"
disk: "300GiB"
# vmType 未指定 → macOS 13.5+ では vz が自動 (古い macOS なら vmType: "qemu")
# mounts/containerd は既定値。ノード用途なのでホストディレクトリ共有は無くてよい。

要点だけ押さえると、こうです。

  • images — Ubuntu 24.04 LTS のクラウドイメージ(Intel なので x86_64)。Lima 既定の ubuntu-24.04 テンプレートと同じイメージだ。(Apple Silicon なら arch: "aarch64" のイメージに変える。)
  • cpus/memory/disk — §3 で決めた 3 vCPU / 8GiB / 300GiB だ。
  • vmType を書かないのは意図的だ — macOS 13.5+ では自動で vz が選ばれる。だから VM の中で systemd-detect-virtapple になる。

注意 — ディスクはスパース(疎)割り当てだが、実使用分は本当に使う。 disk: 300GiB は上限なので最初はイメージ分しか占めないが、4台が埋めていくと合計でホストのディスクをかなり食います。HDD ならなおさら余裕を見て確保してください。

4-3. 4台起動

同じ設計図で 名前だけ変えて 4回立てます。

for n in k3s-agent k3s-agent-2 k3s-agent-3 k3s-agent-4; do
  limactl start --name="$n" ./k3s-agent.yaml --tty=false
done

Lima はインスタンス名の頭に lima- を付けてホスト名を作ります → lima-k3s-agentlima-k3s-agent-2 …。この名前が後で kubectl get nodes にそのままノード名として出ます。(--tty=false は自動化用 — エディタを開かずそのまま作るためのフラグです。)

4-4. 確認(実際の出力)

ホストで4台が立ったか見ます。

limactl list
NAME           STATUS     SSH                CPUS    MEMORY    DISK      DIR
k3s-agent      Running    127.0.0.1:61372    3       8GiB      300GiB    ~/.lima/k3s-agent
k3s-agent-2    Running    127.0.0.1:61392    3       8GiB      300GiB    ~/.lima/k3s-agent-2
k3s-agent-3    Running    127.0.0.1:60490    3       8GiB      300GiB    ~/.lima/k3s-agent-3
k3s-agent-4    Running    127.0.0.1:61460    4       16GiB     300GiB    ~/.lima/k3s-agent-4

4台とも Running です。

VM の中に入って、スペック・仮想化バックエンド・OS を確認します。

limactl shell k3s-agent -- nproc
limactl shell k3s-agent -- systemd-detect-virt
limactl shell k3s-agent -- free -h
limactl shell k3s-agent -- grep PRETTY_NAME /etc/os-release
3
apple
               total        used        free      shared  buff/cache   available
Mem:           7.8Gi       2.3Gi       744Mi       232Mi       5.3Gi       5.5Gi
Swap:             0B          0B          0B
PRETTY_NAME="Ubuntu 24.04.3 LTS"

vCPU 3個、メモリ約 7.8GiB、systemd-detect-virtapple(vz で動いている証拠)、Ubuntu 24.04.3 LTS です。(used/buff/cache がすでに乗っているのは、このノードが今まさに k3s のワークロードを動かしているからです — 作りたて直後ならほぼ空です。)

運用中に入れ替えた k3s-agent-4 だけ OS が違います。

limactl shell k3s-agent-4 -- grep PRETTY_NAME /etc/os-release
limactl shell k3s-agent-4 -- nproc
PRETTY_NAME="Ubuntu 25.10"
4

クラスタから見れば OS のバージョンが違っても問題ありません — k3s のバージョンとコンテナランタイムさえ揃っていればよいのです(§7 で確認します)。

ここまでで Ubuntu VM 4台が iMac 上に立っています(3台は初期スペック、1台はテスト用に増強した状態)。まだクラスタとは無関係の、ただの Linux マシン4台です。次は、この4台を東京のクラウドノードと一つのプライベートネットワークで束ねる Tailscale(§5)です。

5. Tailscale — 4台を東京のクラスタと一つのプライベートネットワークに

§4 までで作ったのは iMac 上に立つ空の Ubuntu VM 4台です。これらを東京のクラウドノードと束ねるには、先に解くべき問題があります。

自宅にはグローバル IP がありません。ルータの NAT の内側なので、外(クラウド)から自宅 VM へ先に接続を張れません。ポートフォワード + DDNS で穴を開けることもできますが、ルータを触って自宅 IP をインターネットに晒す道は避けたいところです。そしてこの問題の答えは、前回すでに決めてあります — Tailscale です。

Tailscale は WireGuard ベースのメッシュ VPN です。すべての機器が 外向き(outbound)に接続を張る ので、両側が NAT の内側でも互いに直結し(直結に失敗したら DERP 中継サーバー経由で迂回)、各機器に 100.64.0.0/10 帯の固定プライベートアドレスを与えます。前回クラウド2ノードをすでにこの方式で束ねたので、今回は同じ tailnet に自宅 VM 4台を足すだけです。

なぜ VM ごとに Tailscale を入れるのか。k3s agent が自分を広告するアドレス(--node-ip)に Tailscale 100.x を使うからです(§6 で実際のフラグで確認します)。ノード1台につき固定の 100.x が1つ要るので、VM 4台それぞれに Tailscale を入れます。

一つの tailnet — 東京のクラウドと札幌の自宅を一つのプライベートネットワークに

5-1. 各 VM に Tailscale をインストール

4台とも同じ一行です。

curl -fsSL https://tailscale.com/install.sh | sh

limactl shell で各 VM に入って実行するか、§4-2 の lima.yaml のプロビジョニングスクリプトに入れて、VM 作成時に自動で入るようにしてもよいです。

5-2. 認証 — ヘッドレスなので auth key で

VM にはブラウザが無いので、対話ログインの代わりに auth key で非対話の認証をします。Tailscale 管理コンソールの Settings → KeysReusable キーを1つ発行すると(4台に同じキーを使うため)、tskey-auth-… という形式のキーが表示されます。一度しか表示されないので、その場でコピーしておきます。

各 VM で:

sudo tailscale up --auth-key=tskey-auth-XXXXXXXX...

このホームラボは タグ無しの個人アカウント で参加させました — シンプルに。アクセス制御をさらに締めたいなら、タグ(tag:server など)と ACL ポリシーを重ねられます。

5-3. 確認 — 6ノードが一つの tailnet に

各 VM が 100.x を受け取ったか見ます。

limactl shell k3s-agent -- tailscale ip -4
100.84.x.x

tailnet 全体を見ます。

limactl shell k3s-agent -- tailscale status
100.84.x.x    lima-k3s-agent     me@…   linux   -
100.98.x.x    lima-k3s-agent-2   me@…   linux   active; direct
100.117.x.x   lima-k3s-agent-3   me@…   linux   active; direct
100.90.x.x    lima-k3s-agent-4   me@…   linux   active; direct
100.71.x.x    ip-172-26-3-146    me@…   linux   active; direct   # クラウド server-A (前回)
100.99.x.x    ip-172-26-2-70    me@…   linux   active; direct   # クラウド server-B (前回)
100.88.x.x    seon-mbp-m4        me@…   macOS   -                # 作業用ノートPC

同じアカウントの機器(iMac ホスト・NAS など)も一緒に見えますが、上では省きました。active; direct は DERP 中継なしで 直結 された印です(NAT 越え成功)。これで自宅 VM 4台と東京のクラウド2台が、一つの tailnet の中で互いに 100.x で届きます。

5-4. ファイアウォール — Tailscale は新たな公開ポートを一切開かない

ここで一度立ち止まって「今、インターネットに何か余計に晒したか?」を確かめます。

ファイアウォール — Tailscale を入れても新たに開く公開ポートは0個

Tailscale は外向きの接続(UDP 41641、塞がれたら DERP を 443 の上に)しか使いません。なので VM に Tailscale を入れても、inbound のファイアウォールに新たに開く必要のあるポートは0個 です。露出面を増やさず、プライベートネットワークだけを広げた形です。王道は、apiserver(6443)のようなクラスタのポートを公開網に開かず、tailnet の中だけ で届くようにすることです(ノートPC の接続も §7 で tailnet 経由にします)。

注意 — 「公開ポート完全0」は後の記事で完成させるつもりです。 apiserver(6443)は tailnet 接続に切り替えれば、すぐ公開網側で閉じられます。ただし ノード間の Pod 通信(flannel VXLAN, UDP 8472) まで公開網で閉じるには、flannel を tailscale の上に載せる追加設定が要ります(でないとノード同士の Pod 通信が切れます — §6 の注意参照)。その作業と限界(二重カプセル化のオーバーヘッドなど)は後で扱います。この記事で確実なのは「Tailscale 自体は露出を一切増やさない」ところまで です。

運用してみて分かったのですが、ここでかなりの時間を取られます。この記事の範囲でも、Pod 同士を通信させるためにポートを開ける場面が出てきます。しかもポートを開けても、Tailscale 仮想網の速度とレイテンシの制約でかなり苦労します。その解決は次回に書きます。

ここまでで6台(クラウド2 + 自宅4)が一つのプライベートネットワークで固定 100.x を使って互いを呼びます。まだ一つのクラスタには束ねられていません — 次の §6 で、この 100.x をそのまま k3s agent の join に挿します。

6. agent の join — Tailscale 100.x をそのまま node-ip に挿す

いよいよこの記事のゴールです。一つの tailnet に入った VM 4台を、前回立てたクラウドのクラスタに agent として参加 させます。

やり方は前回の server インストールとほぼ同じです。k3s インストールスクリプトに環境変数2つ(K3S_URLK3S_TOKEN)と agent のフラグを渡すだけです。肝は、ノードが自分を広告するアドレス(--node-ip)に、§5 で受け取った Tailscale 100.x を挿すこと です。これでノードが tailnet 経由で apiserver に届き、クラスタに参加します。

agent の join — Tailscale 100.x を node-ip に挿す

6-1. server から join トークンを取り出す

agent が参加するときに使うトークンは、server(前回の cluster-init ノード)にあります。

sudo cat /var/lib/rancher/k3s/server/node-token
K10<ハッシュ>::server:<ランダム>

この値をコピーしておきます(下の K3S_TOKEN)。トークンの場所と意味は k3s 公式 token ドキュメント に整理されています。

6-2. 各 VM を agent として join

VM 4台それぞれで実行します。K3S_URLserver の Tailscale アドレス--node-ip/--node-external-ipその VM の Tailscale 100.x です。

curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=v1.34.3+k3s1 \
  K3S_URL=https://100.71.x.x:6443 \
  K3S_TOKEN=K10<ハッシ>::server:<ランダ> \
  sh -s - agent \
    --node-ip=<この VM 100.x> \
    --node-external-ip=<この VM 100.x>

フラグを分解すると、こうです。

  • K3S_URL=https://100.71.x.x:6443 — server(前回の cluster-init)の apiserver だ。Tailscale アドレスなので、6443 を公開網で閉じていても tailnet の中で届く。
  • K3S_TOKEN — 6-1 で取り出した値。URL と TOKEN が両方あれば、k3s インストールスクリプトは server ではなく agent としてインストールする。
  • --node-ip=100.x / --node-external-ip=100.x — このノードの InternalIP / 外部広告アドレスに Tailscale アドレスを使う。

実際に参加したノードを覗くと、同じものが書かれています(実測、トークンはマスク)。

# /etc/systemd/system/k3s-agent.service
ExecStart=/usr/local/bin/k3s agent --node-external-ip=100.x.x.x --node-ip=100.x.x.x

# /etc/systemd/system/k3s-agent.service.env
K3S_URL='https://100.71.x.x:6443'
K3S_TOKEN=********

フラグの意味は k3s agent 公式リファレンス にあります。CNI は前回の server で決めた flannel vxlan をそのまま引き継ぎ、6台とも k3s バージョンは v1.34.3+k3s1 に揃えています(§7 で確認)。

注意 — この記事は「ノード参加(Ready)」までです。ノード間の Pod 通信は後のネットワーク回の話です。 --node-ip=100.x だけで、ノードは tailnet 経由で apiserver に届き Ready になります(これが「iMac をノードとして参加」という今回の目標)。しかし、別ノードの Pod 同士 を通信させるには flannel VXLAN がノード間を渡る必要があり、ここに落とし穴があります。

  • flannel は既定で各ノードの デフォルトルートのインターフェース IP を VXLAN の宛先(public-ip)として広告する。クラウドノードはそれが VPC プライベート網(例: 172.26.x)、自宅 VM は自分のプライベート網なので — 互いに別の underlay で、直接は届かないことがある。
  • だから「VXLAN を tailnet(100.x)の上に載せる」設定(--flannel-iface=tailscale0 など)が要るが、agent 片側だけ変えてはいけない。 agent だけに付けると server 側の宛先(VPC)と食い違い、片方向だけ通る非対称で壊れる。 server・agent の両方を揃える必要がある(=前回の server 構成も触ることになる)。
  • この「flannel を tailnet にきちんと載せる + 二重カプセル化(WireGuard の上に VXLAN)のオーバーヘッドと限界 + 最適化」は次回で扱う。だから今回はノードが参加して Ready になるところまでにとどめる。

6-3.(任意)lima.yaml に自動化

4台に手で繰り返す代わりに、§4 の lima.yaml のプロビジョニングスクリプトに Tailscale のインストール(§5)と上の agent join を入れておけば、limactl start 一発でノードまで完成します。「設計図1枚で同一ノード4台」という §2 の狙いが、ここで完結します。

7. ノートPC で検証 — 6ノードが一つのクラスタに

王道どおり、apiserver(6443)は公開網に開かず tailnet の中だけ でアクセスします。なので確認するには、ノートPC も同じ tailnet に入れ、kubeconfig が server の tailnet アドレスを見るように合わせます。(ノートPC が Mac でも Windows でも Linux でも原理は同じです。)

7-1. ノートPC を tailnet に(Mac / Windows / Linux)

ノードに入れたのと同じ Tailscale を、同じアカウントでノートPC にも入れます。

  • macOS — GUI アプリは brew install --cask tailscale-app(または App Store)を入れてログイン。
  • Windowstailscale.com/download のインストーラを入れてログイン。
  • Linuxcurl -fsSL https://tailscale.com/install.sh | shsudo tailscale up

ログインすると、ノートPC も 100.x アドレスを受け取り、他のノードと同じ tailnet に入ります。tailscale status にクラウド・自宅のノードが見えれば準備完了です。

注意 — Homebrew のパッケージ名が紛らわしい。 macOS の GUI アプリ(メニューバー)は cask の tailscale-app です。brew install tailscale(formula)は CLI(tailscaled)だけが入ります。ノートPC で GUI ログインするなら brew install --cask tailscale-app を使ってください。

7-2. kubeconfig を tailnet アドレスに

server(前回の cluster-init)の kubeconfig は /etc/rancher/k3s/k3s.yaml にあり、server フィールドが既定で https://127.0.0.1:6443 です。このファイルをノートPC に持ってきて(例: ~/.kube/config)、serverserver の Tailscale アドレス に変えます。

kubectl config set-cluster default --server=https://100.71.x.x:6443

注意 — 証明書 SAN が合っていないと TLS が通りません。 server を tailnet に変えても、apiserver 証明書の SAN にその 100.x が無いと x509: certificate is valid for ... not ... で弾かれます。k3s は server を --node-ip=100.x で立てると、そのアドレス(InternalIP)を証明書 SAN に自動で含めます — 実際に証明書を覗くと 100.x が入っています:

X509v3 Subject Alternative Name:
    DNS:kubernetes, DNS:kubernetes.default, ..., IP Address:10.43.0.1,
    IP Address:100.71.x.x, IP Address:100.99.x.x, IP Address:127.0.0.1, ...

もし 100.x が SAN に無ければ、server に --tls-san=100.71.x.x を足して証明書を再発行してください(k3s server ドキュメント)。

7-3. 確認 — kubectl get nodes

ノートPC からそのまま見ます。まず見るべきは、6台すべて Ready、そして agent の INTERNAL-IP が 100.x です。

kubectl get nodes -o wide
NAME               STATUS   ROLES                AGE    VERSION        INTERNAL-IP   OS-IMAGE             CONTAINER-RUNTIME
ip-172-26-2-70…    Ready    control-plane,etcd   140d   v1.34.3+k3s1   100.99.x.x    Amazon Linux 2023   containerd://2.1.5-k3s1
ip-172-26-3-146…   Ready    control-plane,etcd   140d   v1.34.3+k3s1   100.71.x.x    Amazon Linux 2023   containerd://2.1.5-k3s1
lima-k3s-agent     Ready    <none>               140d   v1.34.3+k3s1   100.84.x.x    Ubuntu 24.04.3 LTS  containerd://2.1.5-k3s1
lima-k3s-agent-2   Ready    <none>               140d   v1.34.3+k3s1   100.98.x.x    Ubuntu 24.04.3 LTS  containerd://2.1.5-k3s1
lima-k3s-agent-3   Ready    <none>               140d   v1.34.3+k3s1   100.117.x.x   Ubuntu 24.04.3 LTS  containerd://2.1.5-k3s1
lima-k3s-agent-4   Ready    <none>               140d   v1.34.3+k3s1   100.90.x.x    Ubuntu 25.10        containerd://2.1.5-k3s1

読み方はこうです。

  • クラウド2台は control-plane,etcd(前回)、自宅4台は ROLES <none> — ワークロード専用の agent だ。
  • すべてのノードの INTERNAL-IP が 100.x(Tailscale) だ。ノードが tailnet で参加した印だ。ここに 192.168.x のような LAN IP が入っていたらノード広告が誤っているので、join 直後に必ずこの欄を見る。
  • バージョン v1.34.3+k3s1・ランタイム containerd://2.1.5-k3s1 が6台とも同じだ。lima-k3s-agent-4 だけ OS が Ubuntu 25.10(§4 のあのテストノード)だが、k3s バージョンとランタイムが同じなので参加には問題ない。

自宅ノードにはラベルを付けてクラウドと区別しておきます(実測)。

kubectl get nodes -L node-type
... ip-172-26-2-70    ... lightsail
... ip-172-26-3-146   ... lightsail
... lima-k3s-agent    ... lima
... lima-k3s-agent-2  ... lima
... lima-k3s-agent-3  ... lima
... lima-k3s-agent-4  ... lima

後でワークロードを自宅/クラウドに振り分けるとき、この node-type ラベルを nodeSelector に使います(配置戦略は後の回で)。

ちなみに、ネットワークまで仕上げた現在の運用 では、各ノードに Pod がこう分かれて乗っています(完了済み Job も含む総数で、今回のステップ直後ではなく、改善後に運用している今の姿です)。

ip-172-26-2-70    68   (クラウド)
ip-172-26-3-146   14   (クラウド)
lima-k3s-agent    26   (自宅)
lima-k3s-agent-2  18   (自宅)
lima-k3s-agent-3  25   (自宅)
lima-k3s-agent-4  96   (自宅、大きめアプリのテストノード)

クラウドと自宅のノード両方に Pod が乗って動いているのが、ハイブリッドクラスタの姿です。

これで自宅 iMac がクラスタの働き手になりました。空の Lima VM 4台 → Tailscale で一つのプライベートネットワーク → k3s agent として参加 → 東京2 + 札幌4 = 6ノードがすべて Ready。apiserver(6443)は、ノードもノートPC も tailnet 経由でだけ見ます。

8. コスト — 増えたお金はゼロ

今回新たにかかった費用は、実質ゼロです。ノード4台を足しましたが、使ったのはすべて無料か、もともと持っていたものだけです。

項目コスト (USD)
Lightsail server-A (8GB)$44 / 月 (前回のまま)
Lightsail server-B (16GB)$84 / 月 (前回のまま)
k3s / Lima$0 (オープンソース)
Tailscale Personal$0
自宅ノード (Lima VM ×4, iMac)$0 (もともと持っていた iMac)
今回の増加分+$0

一つずつ見ると、こうです。

  • クラウド — 前回の Lightsail 2台のまま。今回追加したインスタンスは0台 → クラウドの増加分 $0。
  • k3s・Lima — どちらもオープンソース。ノードを増やしてもライセンス費は無い。
  • Tailscale — 個人向け Personal プランは無料。ノード6台 + ノートPC 程度なら無料枠にはるかに届かない → $0。
  • 自宅ノード — 遊んでいた iMac を再利用した → 追加購入0。

注意 — 電気代はあえて数字で入れません。 iMac を24時間つけっぱなしにするので、電気は実際にかかります。ただしこの費用は地域(札幌・東京・読者それぞれの国)・契約プラン・季節や使用量によって大きく変わるので、ドルで一つの数字を出すと大半の読者にとって誤った値になります。だから数値化しません — ご自分の環境ではスマートプラグで実際の消費電力(W)を測るのがいちばん正確です。要点は クラウド・ソフトウェアの追加費用はゼロ で、現実の追加費用は「すでに持っている機械の電気」だけ、ということです。

9. 用語の整理 — 今回出てきたもの

ひととおり見渡せる用語の整理です。

  • Lima / limactl — macOS でヘッドレスの Linux VM を宣言的 YAML で立てるオープンソースツール。limactl がその CLI だ。
  • vz (Apple Virtualization.framework) — macOS 内蔵のハイパーバイザ。Lima が 13.5+ で既定に使う。ゲストで systemd-detect-virtapple なら vz で動いている。
  • ゲスト VM vs コンテナ — VM は自前のカーネルを持ち隔離が強く、コンテナはホストのカーネルを共有して隔離が弱い。この記事が「本物のノード」のために VM を選んだ理由だ。
  • k3s server / agent — server はコントロールプレーン(+etcd)、agent はワークロード専用ノード。インストール時に K3S_URL+K3S_TOKEN を渡すと agent として付く。
  • tailnet / 100.x — Tailscale が作るプライベートメッシュ網。各機器が 100.64.0.0/10(CGNAT)帯の固定アドレスを受け取る。
  • WireGuard / DERP — Tailscale の VPN エンジンが WireGuard、直結できないときは DERP 中継サーバーで迂回する。
  • auth key (Reusable) — ブラウザ無しで非対話に tailnet に付くキー。複数ノードに使い回せる。
  • flannel / VXLAN — k3s 既定の CNI が flannel、その既定バックエンドが VXLAN だ。ノード間の Pod パケットを UDP(8472)でカプセル化して運ぶ。
  • --node-ip / InternalIP — ノードがクラスタに広告するアドレス。ここに Tailscale 100.x を挿すと、ノードは tailnet 経由で参加する(Pod 同士の通信まで tailnet に載せるのは別設定 — 次回)。
  • node-token — agent がクラスタに参加するときに使うシークレット。server の /var/lib/rancher/k3s/server/node-token にある。
  • node-type ラベル / nodeSelector — ノードに付けたラベル(ここでは lima/lightsail)。後でワークロードを自宅/クラウドに振り分けるとき nodeSelector に使う。

10. 次は

6ノードが整ったので、いよいよこの上に何を載せるか、です。

最初は浮かれて色々と載せてみたものの、通信が通らず、結局は原則を無視して 8472(udp / flannel VXLAN)ポートを開けて通信を成り立たせ運用していました。ですが、Longhorn や CNPG を入れてみるうちに本当の問題が始まりました。ノード間通信のレイテンシでエラーが噴き出し、Pod が再起動を延々と繰り返す事態に直面して、数えきれない試行錯誤を重ねることになります。

次回はその部分を話したいと思います。

お忙しいなか、最後まで読んでくださってありがとうございます。