1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

会社に眠っていたサーバをClaude Codeに渡して、ルータ化→k8s→Tinkerbell PXE自動プロビジョニングまで作ってもらった話

1
Posted at

はじめに

社内の技術系サークル(有志の勉強会みたいなやつ)で「なんか実験用の環境作りたいよね」という話になり、倉庫を漁ったら使われていない物理サーバが1台出てきました。
Ubuntu 25.10、20コア、64GB、2TBのNVMe、SFP+ 10Gが2ポート、ついでにIPMIも生えています。ロースペ機ではなく、そこそこ良い個体が眠っていたパターン。

「これでk8sクラスタ作って、ワーカーもPXEで自動追加できるようにしたら遊べそう」

—— とは思ったものの、業務の合間に手を動かすことを考えると腰が重い。ルーティング、iptables/nftables、kubeadm、CNI、そしてTinkerbellのHelm chartとCRD群…。頭では設計できるけど、平日夜に何時間もシェルを叩く元気はない。

そこで Claude Code に丸ごと渡してみることにしました。
私はSSHの資格情報と「こうしたい」という要件を伝えるだけ。Claudeがサーバに入り、コマンドを打ち、結果を検証し、必要ならAPIドキュメントを検索し、ハマれば原因を切り分けて修正する—— そんな流れで進めます。

結論から言うと、2セッションの空き時間だけ(合計3時間くらい)でPXEブートによるワーカー自動追加まで到達しました

この記事では、Claudeとの協業でベアメタル1台に以下を構築した体験を、技術ポイントとハマりどころも交えて紹介します。

  • ルータ化(WAN=enp88s0 / LAN=br-prov 10.1.0.0/24、NAT)
  • 単一ノードKubernetes(コントロールプレーン兼ワーカー)
  • Tinkerbell(PXEプロビジョニング基盤)
  • PXEブート → Ubuntu自動書込 → cloud-init → kubeadm join の完全自動化

何を使ったか

カテゴリ ツール/バージョン
AIアシスタント Claude Code (Opus 4.7/4.8)
OS Ubuntu 25.10 (control-plane) / Ubuntu 24.04.4 LTS (worker)
Kubernetes kubeadm v1.34.9
CRI containerd 2.2.1 (SystemdCgroup)
CNI Flannel
LB kube-vip(Tinkerbell chart同梱)
プロビジョニング Tinkerbell v0.23.0 (smee/tink/rufio/hegel/hookos)
BMC Supermicro IPMI 2.0(ipmitool経由)

完成形の全体像

                            Internet
                                │
                     [enp88s0] 198.51.100.10/24  (WAN)
                                │
                        ┌───────┴────────┐
                        │   worker-03    │ ルータ + k8s control-plane
                        └───────┬────────┘
                    ブリッジ br-prov 10.1.0.1/24
                        ┌───────┴──────────────┐
                enp87s0 ┤                       ├ enp2s0f1np1
              (Worker OS)│                       │(IPMI/BMC)
                        └────┬────────────┬─────┘
                             │            │
                     k8s-worker1        Worker1 BMC
                     eno1 = 10.1.0.20   = 10.1.0.10

最終的にこうなります。

$ kubectl get nodes
NAME          STATUS   ROLES           AGE   VERSION
worker-03     Ready    control-plane   3d    v1.34.9
k8s-worker1   Ready    <none>          5m    v1.34.9   ← PXEで自動追加

Claudeとの進め方

やり取りは終始こんな感じでした。

  • 私: 「このサーバをルータ化したい。LAN側は 10.1.0.0/24 で」
  • Claude: 現状のNIC構成をSSH越しに確認 → 設計案(NM経由 or netplan、DHCPの有無、NATするか)を選択肢として提示
  • 私: ボタンで選択
  • Claude: nmcli で設定、nftables を書く、疎通確認までやってレポート

要件確認は AskUserQuestion(複数選択UI)が出るので、聞かれたら選ぶだけ。ハマった時も自分で tcpdumpjournalctlipmitool を叩いて原因追跡してくれます。

私が意識的にやったのは主に3つ。

  1. 目的とスコープを最初に伝える
  2. 選択肢を提示されたら決める
  3. 危険な操作(電源制御、破壊的変更、認証情報を扱う操作)は確認要求を待つ

これだけで、あとは"読むだけ"で進みました。


ステップ1: ルータ化(15分)

enp88s0 がWAN側で既にDHCPでIPを取得済み、enp87s0 はケーブルが刺さっているけど何も設定されていない。まずはここを 10.1.0.1/24 の静的IPにし、フォワーディング+NATを設定します。

Claudeがやったこと(要点):

# NetworkManagerでLAN側を静的IP化
nmcli con mod "Wired connection 2" \
  ipv4.method manual ipv4.addresses 10.1.0.1/24 \
  ipv4.never-default yes ipv6.method ignore

# IP転送を永続有効化
echo "net.ipv4.ip_forward = 1" > /etc/sysctl.d/99-router.conf

# nftablesでNAT
cat > /etc/nftables.conf <<EOF
flush ruleset
table inet router {
    chain forward {
        type filter hook forward priority filter; policy accept;
        ct state established,related accept
        iifname "enp87s0" oifname "enp88s0" accept
        iifname "enp88s0" oifname "enp87s0" ct state new drop
    }
}
table ip nat {
    chain postrouting {
        type nat hook postrouting priority srcnat; policy accept;
        oifname "enp88s0" ip saddr 10.1.0.0/24 masquerade
    }
}
EOF
systemctl enable --now nftables

疎通確認までClaudeが自動で終わらせて完了。

ステップ2: 単一ノードKubernetes(30分)

kubeadmでシングルノード構築。「コントロールプレーンだけど普通のPodも動かしたい」と伝えると、taint除去まで込みで組んでくれました。

要点だけ:

# 前提: swap無効化、カーネルモジュール、sysctl
swapoff -a && sed -i '/swap.img/s/^/#/' /etc/fstab
modprobe overlay br_netfilter

# containerd + kubeadm/kubelet/kubectl v1.34
apt-get install -y containerd
containerd config default > /etc/containerd/config.toml
sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml

# init(Flannel用にPod CIDR指定)
kubeadm init \
  --pod-network-cidr=10.244.0.0/16 \
  --apiserver-advertise-address=198.51.100.10

# 単一ノード運用のtaint除去
kubectl taint nodes --all node-role.kubernetes.io/control-plane-

# Flannel
kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml

ここまで含めて、私はほぼ確認ボタンを押しているだけ。

Claudeの選択肢UI(AskUserQuestion)で「CNIはFlannel/Calico/Ciliumのどれ?」「k8sは最新安定/一つ前?」を聞かれ、Flannel + v1.34系を選択。Tinkerbellと後々親和する軽量な選択にしました。

ステップ3: Tinkerbell導入(1時間)

ここからが本題。ベアメタル向けのPXEプロビジョニング基盤 Tinkerbell をk8s上に載せます。

Tinkerbellは以下のコンポーネント群から成ります:

コンポーネント 役割
smee DHCP + TFTP + iPXE HTTP スクリプト配信
tink-server Hardware/Template/Workflow のCRD管理
hegel メタデータAPI
rufio BMC/IPMI経由の電源制御
hook ターゲットがPXEブートするインメモリOS

v0.23.0ではこれらが単一Pod + kube-vip同梱で提供されます。よくできてる。

ネットワーク設計を組み直す

TinkerbellはDHCPを喋るので、既存のLAN(10.1.0.0/24)にsmeeを乗せる必要があります。それに、ワーカーのOS-NICとIPMIを両方このセグメントに入れたいので、enp87s0 + enp2s0f1np1 の2つをブリッジ br-prov に統合します。

nmcli con add type bridge con-name br-prov ifname br-prov \
   ipv4.method manual ipv4.addresses 10.1.0.1/24 ipv4.never-default yes
nmcli con add type ethernet con-name br-prov-p1 ifname enp87s0     master br-prov
nmcli con add type ethernet con-name br-prov-p2 ifname enp2s0f1np1 master br-prov

# nftablesのフォワードルールも enp87s0 → br-prov に置換
sed -i 's/enp87s0/br-prov/g' /etc/nftables.conf
nft -f /etc/nftables.conf

Helm install

Tinkerbellは公式リポジトリ(github.com/tinkerbell/tinkerbell)をタグcheckoutしてローカルchartとしてインストールする流儀。

git clone --depth 1 --branch v0.23.0 https://github.com/tinkerbell/tinkerbell
helm install tinkerbell ./tinkerbell/helm/tinkerbell \
  --create-namespace --namespace tinkerbell \
  --set "trustedProxies={10.244.0.0/24}" \
  --set "publicIP=10.1.0.2" \
  --set "artifactsFileServer=http://10.1.0.3:7173" \
  --set "deployment.init.sourceInterface=br-prov" \
  --set "optional.kubevip.interface=br-prov" \
  --set "deployment.imageTag=v0.23.0" \
  --set "deployment.agentImageTag=v0.23.0"

kube-vip が br-prov 上で 10.1.0.2(tinkerbell)と 10.1.0.3(hookos)にVIPをARP広告してくれます。MetalLB不要

BMCの発見

IPMIは常時給電でリンクが上がっているので、br-prov上でDHCPをキャプチャすれば見つかります。

sudo tcpdump -i br-prov -n -e 'udp port 67' | grep -i request
# → 90:5a:08:xx:xx:xx が Request を投げてる(OUIから Supermicro)

MAC発見 → Hardware CRでDHCP予約 → BMCが 10.1.0.10 を取得 → ipmitool で認証情報を検証 → OK。

$ ipmitool -I lanplus -H 10.1.0.10 -U ADMIN -P <REDACTED> chassis power status
Chassis Power is off

ステップ4: PXEで自動プロビジョニング

いよいよ本番。ワーカーを電源投入 → PXEブート → Ubuntu書き込み → cloud-initで kubeadm join するところまで自動化します。

Rufio Machine登録

Rufio(BMCコントローラ)にワーカーのBMCを教えます。

apiVersion: v1
kind: Secret
metadata:
  name: worker1-bmc-secret
  namespace: tinkerbell
stringData:
  username: ADMIN
  password: <REDACTED>
---
apiVersion: bmc.tinkerbell.org/v1alpha1
kind: Machine
metadata:
  name: worker1
  namespace: tinkerbell
spec:
  connection:
    host: 10.1.0.10
    insecureTLS: true
    authSecretRef:
      name: worker1-bmc-secret
      namespace: tinkerbell
    providerOptions:
      preferredOrder: [ipmitool]   # ← 罠 (後述)

Ubuntu 24.04イメージの準備

Tinkerbellの image2disk アクションが読み込む形式に加工します。

curl -o noble.img https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img
qemu-img convert -O raw noble.img noble.raw
gzip -1 noble.raw

これを hostPath で nginx Podに載せ、kube-vip経由で http://10.1.0.4/noble.raw.gz として配信します。

Hardware + Template + Workflow

Hardwareでワーカーの情報を登録:

apiVersion: tinkerbell.org/v1alpha1
kind: Hardware
metadata:
  name: worker1
  namespace: tinkerbell
spec:
  bmcRef:
    apiGroup: bmc.tinkerbell.org
    kind: Machine
    name: worker1
  disks:
    - device: /dev/nvme0n1
  interfaces:
    - dhcp:
        mac: "90:5a:08:yy:yy:yy"
        hostname: "k8s-worker1"
        ip:
          address: "10.1.0.20"
          netmask: "255.255.255.0"
          gateway: "10.1.0.1"
        name_servers: ["8.8.8.8","1.1.1.1"]
      netboot:
        allowPXE: true
        allowWorkflow: true

Templateで作業手順を定義:

apiVersion: tinkerbell.org/v1alpha1
kind: Template
metadata:
  name: ubuntu-join
  namespace: tinkerbell
spec:
  data: |
    version: "0.1"
    name: ubuntu-join
    global_timeout: 3600
    tasks:
      - name: "os-install"
        worker: "{{.device_1}}"
        volumes: [/dev:/dev, /dev/console:/dev/console, /lib/firmware:/lib/firmware:ro]
        actions:
          - name: "stream-ubuntu"
            image: quay.io/tinkerbell/actions/image2disk:latest
            environment:
              IMG_URL: "http://10.1.0.4/noble.raw.gz"
              DEST_DISK: /dev/nvme0n1
              COMPRESSED: true
          - name: "grow-partition"
            image: quay.io/tinkerbell/actions/cexec:latest
            environment:
              BLOCK_DEVICE: /dev/nvme0n1p1
              FS_TYPE: ext4
              CHROOT: y
              DEFAULT_INTERPRETER: "/bin/sh -c"
              CMD_LINE: "growpart /dev/nvme0n1 1 && resize2fs /dev/nvme0n1p1"
          - name: "write-userdata"
            image: quay.io/tinkerbell/actions/writefile:latest
            environment:
              DEST_DISK: /dev/nvme0n1p1
              FS_TYPE: ext4
              DEST_PATH: /var/lib/cloud/seed/nocloud/user-data
              CONTENTS: |
                #cloud-config
                # containerd + kubeadm 導入 → kubeadm join まで自動実行
                runcmd:
                  - [ bash, -c, "/opt/provision.sh >> /var/log/provision.log 2>&1" ]
          # ... 他 write-metadata, write-provision-script など

Workflowを流すと、Rufioが電源制御→ワーカーがPXEブート→HookOSでtink-agentが動く→アクション実行、と勝手に進みます。

$ kubectl -n tinkerbell get workflow install-worker1
NAME              TEMPLATE      STATE     ACTION           AGENT
install-worker1   ubuntu-join   SUCCESS   write-userdata   90:5a:08:yy:yy:yy

そしてディスクブートに切り替えて電源サイクル。cloud-initが走り、しばらく待つと…

$ kubectl get nodes
NAME          STATUS   ROLES           AGE     VERSION
k8s-worker1   Ready    <none>          5m17s   v1.34.9   ← 🎉
worker-03     Ready    control-plane   3d      v1.34.9

ワーカーが自動でクラスタに参加しました。

ハマりどころ(Claudeが解いてくれた)

1. Rufioの電源ONが効かない

ipmitool chassis power on は手で打つと動くのに、Rufio経由だと600秒タイムアウトで諦める。

Claudeがログを深堀り → Rufioは複数プロバイダ(ipmitool, gofish/Redfish, supermicro, ...)を試すが、この基板ではRedfishのPOSTが受理されつつも実際には電源が入らない、という挙動を発見。

修正:

spec:
  connection:
    providerOptions:
      preferredOrder: [ipmitool]   # ipmitool固定

2. cloud-initのapt競合でcontainerdが入らない

初回起動時、cloud-init自身のバックグラウンドapt処理と runcmdapt install containerd が競合し、dpkgのconffileプロンプトで固まって失敗。結果、kubeletは入るがcontainerdが無いのでjoinできず。

Claudeが /var/log/cloud-init-output.log を読んで原因特定 → その場は手動修復で通しつつ、Templateを堅牢化:

# dpkgロック待機 + --force-confold + リトライ
for i in $(seq 1 120); do
  if fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1 ||
     pgrep -x unattended-upgr >/dev/null 2>&1; then sleep 5; else break; fi
done
APT="apt-get -y -o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef"
for t in 1 2 3 4 5; do
  $APT update && $APT install -y containerd && dpkg -s containerd >/dev/null 2>&1 && break
  sleep 10
done

3. kube-vipがworkerでCrashLoop

Tinkerbell同梱のkube-vipはDaemonSetで全ノードに立とうとする。ワーカーで動く必要はないので:

kubectl -n tinkerbell patch ds kube-vip --type=merge \
  -p '{"spec":{"template":{"spec":{"nodeSelector":{"node-role.kubernetes.io/control-plane":""}}}}}'

4. Workerの一時的なDNS失敗でImagePullBackOff

起動直後の数分、systemd-resolvedが安定するまで flannel の image pull が i/o timeout に。時間経過後にPod削除で再試行すればOK。

Claudeに任せてみて分かったこと

良かった点

  • ドキュメントを跨いだ設計判断が速い。Tinkerbellのvalues.yaml、CRDのJSONSchema、GitHubのアクションREADMEを横断して「今の状況ならこう設定するのが妥当」と判断してくれる。
  • 障害切り分けが早いjournalctlkubectl describetcpdumpipmitool を組み合わせて根本原因まで到達する。何度も助けられた。
  • 無停止での修正。SSH経由でNM設定を組み替える時「今のSSHセッションを切らないか」を気にする。ブリッジ化のとき安心して見ていられた。
  • メモリ機能で状態を持ち越せる。セッションを分けても「BMC MAC」「join token」「LB IPの割当」を覚えていた。

気をつけた点

  • 認証情報や電源制御のような "取り返しのつかない操作" は必ず確認する ように依頼。実際、電源投入前には必ず選択肢UIで聞いてきた。
  • 設計判断は自分でやる。CNIをFlannel/Calicoどちらにするか、DHCPを reservation/auto-proxy どちらにするか等、思想が絡む部分は選択肢を出してもらって自分で決める。
  • sshpass + パスワード認証は自宅環境限定。本番なら鍵認証と限定sudo権限で運用する。

費用対効果

トータル3時間ちょっと。自分でやったら少なくとも1日半は溶かしていたと思います。何よりドキュメントを読んで理解する時間がゼロで済むのが大きい。

今後の展望

① 完全ゼロタッチ運用(WorkflowRuleSet)

現状はノードごとに Hardware + Workflow を登録する半自動方式です。次は Tinkerbell の WorkflowRuleSet を使って、未登録マシンがPXEブートするだけで自動プロビジョニング & join されるゼロタッチ運用にする予定。

  • smeeを reservationauto-proxy に切替
  • ノード名/IP/ディスクをテンプレート変数化
  • WorkflowRuleSet でエージェント属性 (Quaminaパターン) にマッチしたら自動でWorkflow生成

物理サーバを追加して電源を入れるだけで、勝手にk8sクラスタが太っていく世界を目指します。

② 他のベアメタルプロビジョニングツールとの比較

今回はTinkerbellを選びましたが、ベアメタルのプロビジョニング領域には他にも有力なツールがあります。同じサーバを使って一度クラスタを壊し、他ツールで組み直してDXやハマりどころを比較してみたい、というのが次のテーマです。

比較候補と、それぞれ気になっているポイント:

ツール 特徴 気になる点
Metal3 Kubernetes-native、Ironic (OpenStack由来) をバックエンドに、Cluster APIのプロバイダとして統合 Tinkerbellよりも本格運用寄りの印象。CAPIとの統合でクラスタ自体の宣言的管理まで踏み込める。学習コストとリソース消費はどうか?
MAAS (Canonical) 老舗のベアメタルプロビジョニング。cloud-initとの親和性◎、独自のWeb UI k8s-nativeではなく単独プロダクト。UIが充実している一方、CRDで宣言的に扱えないのは今回の構成と相性が違いそう
Omni (Sidero Labs) Talos Linux + マネージド管理面。ベアメタル/クラウド/エッジ横断 ホストOSがTalosになるのでkubeadm不要、思想が根本的に異なる。SaaSかセルフホストか、ライセンス面
Cluster API + BYO/bare-metal providers CAPIのエコシステムに乗る Metal3と重なる部分あり。純粋なCAPIとの組み合わせで何がどこまで代替できるか
Cobbler / Foreman 老舗、伝統的なPXE/Kickstart系 k8s以前からある枯れたスタック。宣言的ではないが安定性・実績は最強クラス

観点として比較したいのは以下:

  1. DX(開発者体験): 「MAC + IP を渡すだけで、電源投入から join まで自動化」までの学習コストとステップ数
  2. k8s統合の深さ: CRDだけで完結するか、外部システムとの結合が必要か
  3. リソースフットプリント: 単一ノードにプロビジョニング基盤自体を同居させても現実的か
  4. BMC対応の柔軟性: 今回のようにRedfishが微妙な機体でも動くか
  5. 障害復旧のしやすさ: 中途半端に失敗したノードのリセット、状態のリコンサイル
  6. ドキュメントの充実度: これは意外と大きい。ハマった時に情報にたどり着けるか

特に Metal3 は、Tinkerbellと同じくk8s-nativeでCRDベースなので、直接的な比較対象として面白そう。Ironicのおかげで対応するBMCプロトコルの幅も広く、「エンタープライズ寄りの選択肢」としての比較ができそうです。

おわりに

「AIにインフラを触らせるのは怖い」という声はあります。実際、破壊的操作の可能性がある局面はあるし、認証情報の扱いは特に注意が必要です。本番システムでいきなり試すのはやめたほうがいい。

一方で、明確な要件と適切なガードレール(確認要求、選択肢UI)があれば、AIが実行者になり人間はレビュアーになるという分担がとても機能的でした。今回のように「社内に眠っているハードで実験する」ような、心理的リスクの低い環境で慣らしていくのは相性が良いと思います。

副産物として、サークルの実験用k8s基盤も手に入りました。今後はここで社内向けに色々なワークロードを試していく予定です。

Claude Codeで転がってるベアメタルにk8s+PXEを組む、けっこうおすすめです。倉庫に眠ってるサーバありませんか?

Happy provisioning!

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?