Skip to content

Latest commit

 

History

History
785 lines (595 loc) · 46.3 KB

talos.md

File metadata and controls

785 lines (595 loc) · 46.3 KB

Talos Linux — メディア作成と machine config

対象: HP ProLiant DL360 Gen9(UEFI、iLO は 10.0.0.3、Xeon E5-2696 v4、NIC は BCM5719 ×4、ディスクは SAS の SSD 447 GB)。 検証用に Hyper-V でも同じ ISO を使う。VM ブートドリルは 2026-09-06 に v1.14.0 でやった。 いま使う版は talos/versions.yaml が正(2026-10-13 時点で v1.15.0-alpha.1)。下の URL の版もそれに合わせる。

1. Image Factory で ISO を作る

Talos は「素の ISO」ではなく、必要な system extension を焼き込んだ ISO を https://factory.talos.dev で作る。 組み合わせは schematic という YAML で表し、その内容のハッシュが schematic ID になる(同じ YAML なら誰が作っても同じ ID)。

この箱に要る extension は intel-ucode と dvb-pt3(BCM5719 の tg3、bonding、AHCI は Talos のカーネルに入っている)。 dvb-pt3 は PT3 チューナーのドライバで、v1.15.0-alpha.1 からある(decisions.md「PT3 チューナー」)。 PT3 のために本番を alpha に乗せている。 カーネル引数もここに入れる。 v1.14 では machine.install が UnattendedInstallConfig と衝突して machine config 側に 書けないため、IOMMU の引数は schematic に持たせる。

# schematic.yaml (本番用)
customization:
  extraKernelArgs:
    - intel_iommu=on      # 元は PT3 を KubeVirt に渡すため。dvb-pt3 が入ったいまは要らないが、パススルーの道として残す
    - iommu=pt
  systemExtensions:
    officialExtensions:
      - siderolabs/dvb-pt3
      - siderolabs/intel-ucode
curl -X POST --data-binary @schematic.yaml https://factory.talos.dev/schematics
# => {"id":"2d61dd07b20062062ea671b4d01873506103b67c0f7a4c3fb6cf4ee85585dcb8"}

実際に発行した ID は 3 つある。

用途 schematic ID
本番(dvb-pt3 + intel-ucode + IOMMU の引数。2026-10-13 から) 96371c8f956f7f5f9d09925bb49fe096c0cad05c5cf5bdb464790982c0b067ca
旧本番(intel-ucode + IOMMU の引数。PT3 なし) 32820716ca2384dc3cefbb672e6be929c67636e93e556d7740c312efb6538302
検証用(intel-ucode のみ) 2d61dd07b20062062ea671b4d01873506103b67c0f7a4c3fb6cf4ee85585dcb8

Web UI(factory.talos.dev → Bare-metal Machine → amd64 → version → extensions)でも同じ ID が出る。

用途 URL
ISO(通常、Secure Boot オフ) https://factory.talos.dev/image/96371c8f956f7f5f9d09925bb49fe096c0cad05c5cf5bdb464790982c0b067ca/v1.15.0-alpha.1/metal-amd64.iso
ISO(Secure Boot 用、Sidero の鍵で署名。UEFI に鍵登録が要るので今回は使わない) 同じ URL で metal-amd64-secureboot.iso
--install-image と talosctl upgrade --image factory.talos.dev/installer/96371c8f956f7f5f9d09925bb49fe096c0cad05c5cf5bdb464790982c0b067ca:v1.15.0-alpha.1

プレリリース(alpha / beta)も同じ形の URL で取れる。 版のところを差し替えるだけ。Web UI では既定で隠れているので、 pre-release を表示するチェックを入れる。2026-10-13 に v1.15.0-alpha.1 が出た当日、 上の本番 schematic で ISO(約 409 MB)と installer が 200 を返すのを確かめた。 ノード 1 台の本番を alpha に乗せるのは荒いが、PT3 のドライバがこの版からしか無いので割り切っている。 1.15.0 の正式版が出たら Renovate の PR で乗り換える。

ISO は約 400 MB。extension を足したくなったら schematic を作り直して ID を差し替え、talosctl upgrade で当てる(ISO を焼き直す必要はない)。

2. メディアに載せる

本番は USB でやる(2026-09-09 の判断。iLO の Virtual Media は USB を作らなくて済むが、 挙動が怪しいことがあったため)。ISO はハイブリッドなので、そのまま書けばよい。

# Linux から(USB が /dev/sdX として見えている前提)
sudo dd if=metal-amd64.iso of=/dev/sdX bs=4M status=progress oflag=sync

Windows なら Rufus で ISO を選び、書き込みモードを聞かれたら DD イメージモードを選ぶ。balenaEtcher でもよい。 WSL からは USB が見えないので(usbipd で渡す必要がある)Windows 側でやる方が早い。

起動のしかた:

  1. Secure Boot を Disabled に(POST 中に F9 → RBSU → Server Security → Secure Boot Settings)。 通常 ISO は署名されていない。Boot Mode は UEFI のまま。
  2. USB を挿して電源を入れ、POST 中に **F11(Boot Menu)**でその USB を選ぶ。
  3. インストールが終わって再起動したら USB を抜く。 挿したままだとブート順によっては また maintenance mode で上がる。

コンソールは iLO の Remote Console(HTML5)でよい ── 実機の前に立たなくても ブートメニューも maintenance mode の IP も見える。USB だけ物理で挿す。

iLO の Virtual Media を使う場合: Remote Console → Virtual Drives → Image File CD-ROM/DVD で 手元の ISO を選び(上の factory の URL を直接貼ってもよい)、Power → Reset して F11 で "iLO Virtual USB 3 : iLO Virtual CD-ROM" を選ぶ。

Hyper-V(検証用): 第 2 世代 VM、Secure Boot オフ、メモリ 4 GB 以上、CPU 2 以上、ディスク 40 GB 以上、 DVD ドライブに ISO を接続、ネットワークは外部スイッチ(LAN の DHCP で IP が付く)。第 1 世代でも動くが UEFI にならないので実機と条件が変わる。

3. 起動したあと

ISO から起動すると Talos は maintenance mode で立ち上がり、コンソールに IP が出る。まだ何もインストールされていない。 ここから先は手元の PC の talosctl で行う(ホストには一切ログインしない)。手順は ../talos/README.md(render.sh → apply-config → bootstrap → kubeconfig)、 当日の順番は migration-day.md。 バックアップからの復元なら bootstrap に --recover-from=etcd.snapshot を付ける(下の「etcd スナップショットからの復旧」)。

4. この箱で先に潰しておくこと

  • PT3(Earthsoft、earth_pt3)が Talos のカーネルに入っていない(CONFIG_DVB_PT3 is not set、extension も無い)。 denpa の tuner-agent は /dev/dvb と /dev/bus(B-CAS リーダー)を hostDevices で掴むので、今のままだと Talos 上では動かない。 算段は decisions.md「PT3 チューナー」。
  • Secure Boot を切る(上記)。
  • iLO の IP(10.0.0.3)と管理者パスワードを手元に。ISO 起動も再インストールも全部ここからできる。

machine config の作法(v1.14 で確認)

2026-09-06 に QEMU/KVM で v1.14.0 を実際に起動して確かめたこと。1.13 以前の書き方は通らない。

設定が複数ドキュメントに分かれた

talosctl gen config は v1alpha1 の 1 枚ではなく、KubeNetworkConfig / KubeletConfig / UnattendedInstallConfig / ResolverConfig / KubeNodeConfig … という型付きドキュメントの束を吐く。 同じ項目を v1alpha1 側にも書くと apply が弾かれる(... is already set in v1alpha1 config)。

やりたいこと 1.14 での書き場所
pod / service の CIDR KubeNetworkConfig の podSubnets / serviceSubnets
インストール先ディスク UnattendedInstallConfig の provisioning.diskSelector.match(CEL 式。例 disk.dev_path == "/dev/sda")
カーネル引数 machine.install.extraKernelArgs は使えない(同じく衝突)。Image Factory の schematic の customization.extraKernelArgs に入れる
host DNS ResolverConfig。既定で有効なので普通は書かなくてよい
kubelet の extraMounts KubeletConfig には無い。KubeletConfig を $patch: delete してから v1alpha1 の machine.kubelet に書く
control-plane への scheduling 許可 v1alpha1 の allowSchedulingOnControlPlanes は弾かれる。KubeNodeConfig の taints を消す必要があるが、strategic merge の空マップ(taints: {})では消えなかった。talhelper 経由か、起動後に kubectl taint nodes --all node-role.kubernetes.io/control-plane-

service の IPv6 CIDR は /108 以下

fd43::/64 は service subnets: invalid subnet: fd43::/64 is too large, it must be at least /108 で弾かれる。 k3s はこれを許していたので、移行時に fd43::/108 へ変更が要る(ClusterIP が振り直しになる)。 pod 側の fd42::/64 はそのままでよい。検証では 10.43.0.10 と fd43::a の両方が付いた。

PSA のラベル(Pod Security Admission が既定で baseline)

KubeAdmissionControlConfig に enforce: baseline(例外は kube-system のみ)が入っている。k3s には無かった制限で、 hostPath / privileged / hostNetwork を使う workload は namespace にラベルが要る。

kubectl label ns <ns> pod-security.kubernetes.io/enforce=privileged

実際に local-path-provisioner はこれで詰まった(ヘルパー Pod が hostPath を使うため PVC が Pending のまま)。 ラベルを付けたら PVC が Bound になり、Pod から書いた内容がディスクに残ることまで確認した。

baseline は hostPort も弾く。 ここを見落としやすい。走っている Pod を全部数えて(2026-09-07、09-08 に数え直し)、 ラベルが要る namespace は次のとおり。どれも namespace.yaml に enforce: privileged が入っていて、実機でも確認した:

namespace baseline に通らない理由
kube-system Cilium(privileged・hostNetwork・hostPath・SYS_ADMIN/NET_ADMIN・hostPort)。Talos の側で例外にしてある(下の設定)
local-path-storage ヘルパー Pod の hostPath。talos/manifests/local-path.yaml がラベルを付ける
netbird hostPort 3478(内蔵 STUN。UDP なのでゲートウェイを通せない)・51822、NET_ADMIN 等、hostPath
denpa privileged・hostPath(/dev/dvb・/dev/bus・/dev/dri)。ラベルは denpa リポジトリの deploy/namespace.yaml にあり、gitops だけを grep すると抜けて見える
adguardhome hostPort 53 / 853
matrix hostPort 8443(LiveKit。2026-10-06 に Mattermost Calls から同じ番号を引き継いだ)
3proxy hostPort 3129(TLS 終端サイドカー)
cloudflare-ddns hostNetwork
forgejo Runner の docker(dind)が privileged

(当時は erpnext の CAP_CHOWN(baseline が足せる capability は NET_BIND_SERVICE だけ)と mattermost の hostPort 8443 もあった。どちらも 2026-10-06 に撤去)

残りの namespace は baseline のままでよい。argocd / cert-manager / infisical はラベル無しで上がった(ドリル 4 回目)。 洗い出しは Pod の spec を直接数えて出す:

kubectl get pods -A -o json | jq -r '.items[] | . as $p | [$p.metadata.namespace] +
  (if $p.spec.hostNetwork then ["hostNetwork"] else [] end) +
  ($p.spec.containers[] | (.securityContext.privileged // false | if . then ["privileged"] else [] end) +
   ((.securityContext.capabilities.add // []) | map("cap:"+.)) +
   ((.ports // []) | map(select(.hostPort)) | map("hostPort:"+(.hostPort|tostring)))) +
  (($p.spec.volumes // []) | map(select(.hostPath)) | map("hostPath")) | @tsv' | sort -u

kube-system の例外は Talos の設定:

kind: KubeAdmissionControlConfig
name: PodSecurity
configuration:
  defaults: {enforce: baseline, audit: restricted, warn: restricted}
  exemptions:
    namespaces: [kube-system]

動いたこと

  • ISO(Image Factory の schematic 2d61dd07…)から UEFI で起動、maintenance mode の API はポート 50000
  • apply-config → 自動でディスクへインストール → 再起動 → bootstrap → kubeconfig 取得
  • cluster.inlineManifests は期待どおり適用された(仕込んだ Namespace がラベルごと存在した)。 bootstrap/ をここに載せる案(decisions.md)は成立する
  • flannel でデュアルスタック、CoreDNS / kube-proxy / scheduler すべて Running

etcd スナップショットからの復旧(検証済み)

Talos 期のバックアップは etcd(k8s オブジェクト)と PV データで別経路になる。両方を試した。

talosctl etcd snapshot ./etcd.snapshot          # 1.8 MB / 394 keys (検証時)

復旧は、同じ secrets.yaml で machine config を作り直すのが肝(PKI が一致しないと復旧できない)。

# まっさらなディスクに ISO から入れ直したあと
talosctl apply-config --insecure -n <IP> -f controlplane.yaml
# 再起動を待って
talosctl bootstrap --recover-from=./etcd.snapshot

2026-09-06 に空のディスクから実際に通した結果:

  • Node が同じ名前・同じ作成時刻で Ready に戻り、Namespace・Deployment・PVC/PV の紐付け・ PSA のラベルまでスナップショット時点の状態が復元された
  • PV の中身は戻らない。 /var/local-path-provisioner(当時のパス。いまは /var/mnt/local-path)は空のままだった。 etcd は「PVC がこの PV に紐づいている」という事実しか持っていない
  • したがって Talos 期の復元は etcd スナップショット → PV データを restic(k8up)から戻す の 2 段になる。 順序は etcd が先(PV オブジェクトが無いと戻す先が決まらない)

bootstrap --recover-from は etcd サービスが上がるまで bootstrap is not available yet を返すので、 数分待って再試行する。

どちらで戻すか(2026-10-05 に揃えた)。 主経路は git から建て直すほう (docs/migration-day.md の 3 以降。ドリルで通してあり、スナップショットの鮮度に左右されない)。 etcd スナップショットは近道で、git に無い状態(Application の同期状態、発行済みの証明書、 API サーバが割り当てた nodePort など)まで一度に戻したいときに使う。日次で取っているのは ../talos/after-migration/etcd-snapshot.yaml (移行後は apps/k8up/)で、restic の --host etcd。取り出しは:

restic snapshots --host etcd
restic dump <ID> /work/etcd.snapshot > etcd.snapshot

VM の組み方(QEMU/KVM)

本番サーバの上で回す。ホストのネットワークには触らない。 ドリル 4 回ぶんの つまずきをここにまとめてある(各回で何が分かったかは下の記録)。

tap/bridge は要らない。QEMU の内部ハブで足りる

「bond は user-mode ネットワークでは試せない」というのは誤りだった。-netdev hubport で QEMU の中だけに L2 セグメントを作れるので、ホストに tap も bridge も作らずに 2 本の NIC を 同じセグメントに挿せる。SLIRP(-netdev user)は ipv6=on で RA を送ってくるので、 RA 由来のデフォルトルートもここで試せる。

# hub 1 = bond のメンバー 2 本 + SLIRP(IPv6 あり)、hub 2 = eno4 相当の別 LAN
sudo qemu-system-x86_64 -machine q35,accel=kvm -cpu host -smp 4 -m 12288 \
  -drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd \
  -drive if=pflash,format=raw,file=OVMF_VARS.fd \
  -drive file=disk.raw,if=virtio,format=raw -boot order=c \
  -netdev 'user,id=n0,ipv4=on,net=10.0.0.0/24,host=10.0.0.1,dhcpstart=10.0.0.15,ipv6=on,ipv6-net=240f:6d:842b:1::/64,ipv6-host=240f:6d:842b:1::1,hostfwd=tcp:127.0.0.1:50201-10.0.0.15:50000,hostfwd=tcp:127.0.0.1:50211-10.0.0.2:50000,hostfwd=tcp:127.0.0.1:50221-10.0.0.2:6443' \
  -netdev hubport,id=hp0,hubid=1,netdev=n0 \
  -netdev hubport,id=hp1,hubid=1 -device virtio-net-pci,netdev=hp1,mac=52:54:00:ee:00:01 \
  -netdev hubport,id=hp2,hubid=1 -device virtio-net-pci,netdev=hp2,mac=52:54:00:ee:00:02 \
  -netdev 'user,id=n1,ipv4=on,ipv6=off,net=10.10.0.0/24,host=10.10.0.1' \
  -netdev hubport,id=hp3,hubid=2,netdev=n1 \
  -netdev hubport,id=hp4,hubid=2 -device virtio-net-pci,netdev=hp4,mac=52:54:00:ee:00:05 \
  -display none -serial file:serial.log -monitor unix:monitor.sock,server,nowait

KVM を使うので sudo で起動する。 patches/*.yaml からの読み替えは インタフェース名とディスクだけ(eno1/eno2/eno4 → enp0s2/enp0s3/enp0s4、 /dev/sda → /dev/vda)。balance-alb・miimon: 100・静的 IPv6・ v6 のデフォルトルートを書かないことはそのまま。ボリュームの大きさだけディスクに合わせて縮める。

つまずいた点

  • ディスクは 140 GB 取る(sparse なので実消費は 4 GB ちょい)。 小さくすると volumes.yaml の値を縮めることになり、maxSize: 64GiB が効くかを試したことにならない
  • ISO ではなく metal-amd64.raw.zst を焼く。 ISO 経由だと「ISO で起動 → apply-config → 再起動 → ISO 側がインストール → 再起動 → ディスク」という段取りに なり、外す時機を間違えると PXE ブートに落ちる。raw なら最初から maintenance mode で上がる。 実機は USB で ISO(上の「メディアに載せる」)── ここだけ実機と違う
  • NETDEV WATCHDOG: transmit queue 0 timed out は QEMU 側のミス。 bond の 2 本目を 何も繋がっていないハブに挿していた。QEMU 自身が warning: hub 3 is not connected to host network と言うので、qemu.log を必ず読む。 ハブが正しければ NIC 3 本でも問題ない ── 2 回目の記録に「NIC は 1 本にする」と 書いていたが、原因はハブの挿し間違いのほうだった
  • インタフェース名は起動ごとに変わる。 maintenance mode で talosctl get links --insecure を見てから patch を読み替えること
  • コンソールが要るときは monitor の screendump。 -display none でも screendump /path/foo.ppm で画面が撮れる。kubelet の版ズレはこれで見つけた (シリアルには Talos のダッシュボードが流れてこない)。socat/nc が無ければ python の socket.AF_UNIX で monitor.sock に直接書けばよい
  • 前の VM を殺し忘れると hostfwd のポートを握ったままで、新しい VM が黙って死ぬ。 pgrep -af qemu-system-x86_64 で確認する。pkill -f のパターンは qemu-system-[x]86_64 のように書く ── そうしないと自分の ssh セッションごと殺す(実際に踏んだ)
  • ipv6=on を付けると ipv4=on を明示しないと IPv4 disabled but netmask/host/dns provided で起動しない
  • v1.14 の talosctl は --nodes / --endpoints がグローバルフラグではない。 talosctl --insecure -n … get links は unknown command になる。 talosctl get links --insecure -n … -e … の順で書く
  • maintenance mode は -e 127.0.0.1:50201 -n 127.0.0.1:50201。設定投入後は apid 経由なので -n はノードの実アドレス(-n 10.0.0.2 -e 127.0.0.1:50211)。 -n 127.0.0.1:50211 は invalid target になる
  • talosctl apply-config -m reboot は無い(auto / no-reboot / staged / try)
  • talosctl upgrade-k8s は k8s の API に直接繋ぎに行くので、SLIRP のときは --endpoint https://127.0.0.1:<hostfwd> を渡す。kubeconfig の書き換えだけでは足りない
  • ハブの構成は monitor の info network で確認できる

ブートドリル 6 回目(2026-09-09、移行直前の確認)

5 回目のあとに 7 本マージしたので、machine config に効くぶんを確かめ直した (Entra を 2 つめの issuer にした bootstrap/apiserver/ と talos/patches/apiserver.yaml)。

今回はディスクを 140 GB にした。 3〜5 回目は VM のディスクが小さくて talos/patches/volumes.yaml の値を縮めて焼いていたので、本番と同じ maxSize: 64GiB / minSize: 50GiB のまま試したのはこれが初めて。

確かめたこと:

apiserver が Entra の issuer を載せて起動する --authentication-config=/system/config/kubernetes/kube-apiserver/authentication-config.yaml が渡り、ファイルに issuer が 2 本(github + login.microsoftonline.com)。anonymous の livez/readyz/healthz も残っている
groups の CEL entra:admin / entra:viewer が両方描かれている
RBAC が inlineManifest から入る entra-admin(→ cluster-admin)・entra-viewer(→ view)・entra-viewer-extra(→ readonly-extra)と ClusterRole readonly-extra
EPHEMERAL ちょうど 64.0 GiB。maxSize が効く
u-local-path 72.0 GiB(残りに grow)、ready
control-plane の taint 無し
Pod 25 個が Running / Completed。そのあと ArgoCD が Application 16 本を復元しはじめる

所要: apply-config → apid 復帰 75 秒 → bootstrap → ノード Ready 200 秒 → bootstrap 層が全部 Running まで 120 秒。

headlamp は secret "headlamp-oidc" not found で止まる。これは正常。 Infisical の DB が 空なので InfisicalSecret を使うアプリは Secret をもらえない。 migration-day.md の 7 が「先に Infisical の DB だけ戻す」と言っているとおりで、 当日は 8 の手順で 1 本だけ戻してから待つ。portainer の namespace が作られないことも確認した。

ブートドリル 5 回目(2026-09-08、app-of-apps まで通した)

4 回目は ArgoCD の extraObjects を空にして app-of-apps を外していた。5 回目は 外さずに、本物のリポジトリ群を同期させた。 見たかったのは 「まっさらなクラスタで ArgoCD が apps/ を再構築しきるか」の一点。

移行当日に何も上がらない不具合が出た。 詳細は下。

副作用の遮断: QEMU を専用ユーザーで走らせる

復元リハーサル(Ubuntu の VM)では中で iptables を叩いていたが、 Talos の VM には入れない。 ホスト側で落とすと本番の通信まで巻き込む。

QEMU を専用ユーザーで動かして -m owner --uid-owner で落とす。

sudo useradd -r -M -s /usr/sbin/nologin -G kvm qemudrill
for ip in <api.cloudflare.com 6 つ> <acme> <本番の公開 IP> <R2 2 つ>; do
  sudo iptables -I OUTPUT 1 -m owner --uid-owner qemudrill -d $ip -j REJECT
done
for p in 587 465 25; do   # SMTP は REJECT。DROP だと Infisical が起動しない
  sudo iptables -I OUTPUT 1 -m owner --uid-owner qemudrill -p tcp --dport $p -j REJECT --reject-with tcp-reset
done
sudo ip6tables -I OUTPUT 1 -m owner --uid-owner qemudrill -d 2000::/3 -j REJECT

止めたのは cloudflare-ddns(本番の DNS を書き換える)・netbird(本番に繋ぐ)・ k8up(本番の restic リポジトリに書く)・ACME・SMTP。 イメージの取得(ghcr.io / docker.io など)は通るので同期は進む。

  • sudo -u qemudrill は通らない(sudoers が root 宛てだけ NOPASSWD)。 sudo runuser -u qemudrill -- <script> を使う
  • 起動は script にする。 nohup … > log のリダイレクトは呼び出し側の権限で 開かれるので、qemudrill が所有するディレクトリには書けない
  • /var/tmp/drill を qemudrill 所有にすると kubeconfig を書き出せない。 出力先は別の場所にする

見つけた問題: apps/ の同期が丸ごと止まる

gitops   OutOfSync  Missing
  one or more synchronization tasks are not valid: failed to discover server
  resources for group version secrets.infisical.com/v1alpha1: the server could
  not find the requested resource (retried 5 times)

InfisicalSecret の CRD がまだ無い。 ArgoCD は同期の前に dry-run するので、 CRD が引けないとその Application が丸ごと落ちる ── そして CRD を入れる当の Application(apps/infisical-operator/)が同じ同期の中にいるので 抜けられない。

k8up の Schedule で 2026-09-07 に踏んだのと同じ形なのに、 InfisicalSecret には SkipDryRunOnMissingResource=true が付いていなかった。 復元リハーサルでは出ない ── あちらは etcd から戻すので CRD が最初からある。 まっさらから組む Talos の移行でだけ出る。

直したら通った(同じドリルの中で確認):

  CRD infisical 系            0 → 7
  Pod のある namespace        5 → 18
  erpnext 15 / netbird 5 / cloudflare-ddns 4 / mattermost 2 / adguardhome 2 /
  infisical-operator / infisical-push-bridge / k8up / headlamp / 3proxy …

5 回失敗した Application は自動では戻らない

CRD が入っても、先に 5 回失敗していた Application は (retried 5 times) のまま止まったままだった。refresh=hard の注釈では動かず、 明示的に sync を投げたら Synced / Healthy になった。

移行当日は、gitops の同期が通ったあとに、止まっているアプリを 1 回ずつ叩く。 migration-day.md の 7 に入れた。

ブートドリル 4 回目(2026-09-08、本物の値で bootstrap 層まで)

3 回目までは Cilium・local-path・metrics-server だけだった。4 回目は ArgoCD・cert-manager・infisical と SOPS 済みの Secret を入れた machine configを そのまま焼いて、apply-config 1 回でどこまで立ち上がるかを見た。

立ち上がった:

main   Ready   control-plane   v1.36.2

argocd        argocd-application-controller-0 / applicationset / notifications /
              redis / repo-server / server            すべて Running
cert-manager  cert-manager / cainjector / webhook     Running(startupapicheck は Completed)
infisical     infisical / postgresql-0 / redis-master-0  Running、/api/status が 200
kube-system   cilium / envoy / operator / coredns ×2 / hubble ×2 / metrics-server
local-path-storage  local-path-provisioner

EPHEMERAL      partition  ready  /dev/vda5  13 GB
u-local-path   partition  ready  /dev/vda6  11 GB

ドリル専用の細工は 1 つだけ ── ArgoCD の extraObjects を空にして app-of-apps を外した。本物のリポジトリを同期しに行くと cloudflare-ddns が DNS を書き換え、netbird が本番に繋ぎに行く(「副作用を出さないための遮断」と 同じ理由)。それ以外は本番と同じ値。

見つけた問題: chart が metadata.namespace を書かないものがある

infisical の Deployment と Service が default namespace に落ちた。

$ kubectl get deploy -A | grep infisical
default     deployment.apps/infisical   0/1     0   0   4m15s      ← ここ
infisical   (無い)

helm template -n infisical は .Release.Namespace を渡すが、 chart 側がそれを metadata.namespace に書かないことがある(bitnami の subchart は 書いていた。infisical 本体のテンプレートだけが書いていなかった)。helm や kubectl なら -n で決まるが、Talos のマニフェスト適用には既定の namespace が無い。

infisical-secrets は infisical namespace にあるので、default に落ちた Pod は 永久に起動しない。 移行当日は Infisical が上がらず、そこから全アプリの秘密が 配られないので、何も動かないところだった。

直し方は talos/add-namespace.awk ── helm の描き出しを 通して、metadata.namespace が無い名前空間付きリソースに補う。クラスタスコープのもの (Namespace / ClusterRole / CRD など)には足さない(足すと API が弾く)。 全部の chart の描き出しに通してある。

upgrade-k8s が本当に reconcile することを実地で確かめた

直した machine config を apply-config してから talosctl upgrade-k8s を流すと、 infisical namespace に Deployment が作られた(default の古いものは残る。 まっさらな導入なら存在しない)。../talos/README.md「上げ方 / 当て直し方」に書いた手順が、実際にそのとおり効くことの裏付け。

ついでに分かったこと

  • infisical はメール無しでも上がる。 起動時に Testing smtp connection で 止まって見えるが、infisical-smtp(Infisical operator が作る Secret)が 無くても最終的に 1/1 Running になり /api/status が 200 を返す。 鶏卵にはならないことの確認になった
  • 起動直後に CreateContainerConfigError: failed to sync secret cache が数回出るが、 optional な Secret の同期待ちで、放っておくと直る

ブートドリル 3 回目(2026-09-08、render.sh の出力をそのまま焼いた)

1・2 回目は talos/patches/ だけを起動した。3 回目は talos/render.sh が 描いた machine config を丸ごと焼いて、Cilium・local-path・metrics-server まで上がるところを見た。

移行当日に踏むはずだった穴が 3 つ出た。

1. kubelet だけ v1.37.0 で動いていた

コンソールに出ていたのはこれ:

KUBERNETES  v1.37.0        KUBELET  x Unhealthy

patches/cluster.yaml が KubeletConfig をドキュメントごと消していて、 --kubernetes-version が入れていた kubelet のイメージも一緒に消えていた。 生成物には kubelet のイメージが 1 行も無く、Talos の既定で上がる。

== --kubernetes-version v1.36.2 あり:  kubelet:v1.36.2 / kube-apiserver:v1.36.2
== 無し(既定):                        kubelet:v1.37.0 / kube-apiserver:v1.37.0

kubelet が API サーバより新しいのは Kubernetes のサポート外。 CI は kube-apiserver:<版> しか見ておらず、「kubelet が無い」ことに気づけなかったので、 needle を足した(#114)。その後 user volume を入れて KubeletConfig を消す必要が なくなったので、いまはこの穴自体が塞がっている。needle は再発の見張りとして残してある。

2. control-plane の taint で、アプリが 1 つも上がらない

KubeNodeConfig は既定で node-role.kubernetes.io/control-plane: NoSchedule を入れる。 ノードが 1 台のこのクラスタでは、toleration を持たない Pod が全部 Pending になる (k3s は単一ノードを control-plane 兼 worker として扱うので、この問題が無かった)。

質が悪いのは、ノードが Ready に見えること。 cilium と CoreDNS は toleration を 持っているので上がる:

kube-system          cilium-l4xvm                              1/1     Running
kube-system          coredns-6cb54fb45c-9k5sl                  1/1     Running
kube-system          metrics-server-5ffb6b8554-drtss           0/1     Pending
local-path-storage   local-path-provisioner-75f8fcb898-j9hqb   0/1     Pending

Warning  FailedScheduling  default-scheduler  no nodes available to schedule pods

taints はマップなので taints: {} では消えない。中に $patch: replace を書くと キーとして残ってしまい name "$patch" is invalid で validate が落ちる。 KubeletConfig / HostnameConfig と同じくドキュメントごと消して labels だけ 書き戻すのが唯一の手(#115)。

3. EPHEMERAL がディスク全部を取る

EPHEMERAL   partition   ready   /dev/vda5   24 GB     ← 26 GB のディスクほぼ全部
ETCD        directory   ready                          ← その中
CRI         directory   ready                          ← その中

etcd もコンテナイメージも PV データも同じ 1 枚。local-path は PVC の容量を強制しないので、 1 個の PVC が暴走すると etcd ごと道連れになる。ボリュームの設定は「まだ確保されていない ときにしか効かない」ので、直せるのは入れ直すときだけ。

VolumeConfig(EPHEMERAL に maxSize)と UserVolumeConfig を足して割り直した。 システムボリュームはユーザーボリュームより先に確保されるので、EPHEMERAL に上限を 書き忘れると取り分が残らない(siderolabs/talos#12713)。

ついでに分かったこと

  • user volume のマウント先は kubelet コンテナに自動で通る。 extraMounts のハックが 要らなくなり、上の 1 の原因ごと消えた
  • UserVolumeConfig は metadata.name ではなくトップレベルの name。名前は 1〜34 文字の英数字とハイフンで、ディスク上のラベルは u-<name> になる

通しで確かめたこと

Cilium のデータパスは k3s 期と同じ形で上がる。

KubeProxyReplacement:  True   [bond0  10.0.0.2 240f:6d:842b:1::2 (Direct Routing), enp0s4  10.10.0.4]
Cilium:                Ok     1.20.1
Routing:               Network: Tunnel [vxlan]   Host: BPF
Masquerading:          BPF    [bond0, enp0s4]   10.42.0.0/24 fd42::/64 [IPv4: Enabled, IPv6: Enabled]

cgroup.hostRoot と KubePrism(localhost:7445)の上書き(talos/cilium-values.yaml)が 効いていることの裏付けでもある。

service-node-port-range: 80-32767 は実際に効く。 生成物を見るだけでなく、 クラスタで nodePort 80 の Service を作れることを確かめた (Gateway の公開経路がここに乗っている。talos/patches/apiserver.yaml)。

$ kubectl create svc nodeport np --tcp=80:80 --node-port=80
np   NodePort   10.43.251.48   <none>   80:80/TCP

dual-stack も出る。 fd43::/108 に変えた影響を確かめた:

$ kubectl -n kube-system get svc kube-dns -o jsonpath='{.spec.clusterIPs}'
["10.43.0.10","fd43::a"]      ipFamilyPolicy: RequireDualStack

inlineManifests は「更新できない」ではない(2026-09-08、VM で実測)

talosctl upgrade-k8s を通せば更新も削除もされる。 Sidero のドキュメントには

Talos only creates missing resources from inline manifests — it never deletes or updates them.

とある一方で「更新するには machine config を直して talosctl upgrade-k8s」とも書いてあり、 どちらが効くのか読んでも分からなかったので、QEMU で確かめた。前者は Talos 自身の 起動時の適用の話で、upgrade-k8s は別の経路だった。

プローブ用の ConfigMap を inlineManifests に載せ、値を BEFORE → AFTER に書き換えて試した。

操作 値の更新 config から消したとき
talosctl apply-config されない(BEFORE のまま) 消えない
talosctl upgrade-k8s --to <いまと同じ版> される(AFTER になる) オブジェクトも消える

upgrade-k8s の出力は差分つきで、SSA とインベントリで完全な reconcile をしている。

 < configured ConfigMap/default/drill-probe
-  value: BEFORE
+  value: AFTER

 < deleted ConfigMap/default/drill-probe     # config から消したあと

フラグにも表れている ── --manifests-inventory-policy(既定 AdoptIfNoInventory)、 --manifests-no-prune、--manifests-force。prune は既定で有効。

何が言えるか

inlineManifests + upgrade-k8s は、machine config だけで回せる本物の経路。 「初回だけ入れて以後は触れない」ではない。この実測を根拠に、Talos 期は bootstrap/ をまるごと inlineManifests に載せることにした (decisions.md「machine config と Cilium の chart をどう連動させるか」)。

当時の懸念(値が machine config に埋まってレビューできない)は、 render.sh が bootstrap/ の値から描く形にして消えている ── 読むのは bootstrap/cilium/values.yaml のままで、machine config はその派生物。

Talos ブートドリル 2 回目(2026-09-07、apiserver.yaml を足して実際に bootstrap まで)

1 回目はネットワークだけを見た。2 回目は API サーバの設定(patches/apiserver.yaml)を足して bootstrap まで通し、コントロールプレーンが上がるところまで確かめた。

確かめられたこと:

  • KubeAuthenticationConfig を入れてもコントロールプレーンは上がる。 configuration は既定を置き換えるので anonymous を書き忘れるとヘルスチェックが 匿名で通らなくなる ── と書いていたが、明示して書けば正しく上がることを実地で確認した。 kube-apiserver / kube-controller-manager / kube-scheduler の 3 つとも CONTAINER_RUNNING
  • certExtraSANs は効く。 6443 に繋いで証明書を見ると DNS:ks.doany.io が入っている
  • ネットワークは 1 回目と同じ(bond0 に 10.0.0.2/24 と静的 IPv6 ::2、enp0s4 に 10.10.0.4/24)

見つかった問題: versions.yaml の Kubernetes 版が使われていなかった。

生成物:        kube-apiserver:v1.37.0     ← Talos v1.14.0 の既定
versions.yaml: kubernetes v1.36.2         ← 宣言しているだけ

talosctl gen config は versions.yaml を読まないので、--kubernetes-version で渡さないと Talos の既定になる。移行当日に意図せず 1.36 → 1.37 の飛び級をするところだった。 README の手順と talos-validate のワークフローに --kubernetes-version を足し、 生成物に宣言どおりの版が入っているかも CI で見るようにした。

VM 側の読み替え(NIC 名は起動ごとに変わる、qemu は sudo)は上の「VM の組み方」。

VM ブートドリル(2026-09-07、talos/patches/ をそのまま起動した)

talosctl validate が通るだけで一度も起動していなかった talos/patches/cluster.yaml / main.yaml を、本番サーバ上の QEMU で実際に起動して確かめた。ホストのネットワークには一切触っていない。

1. bond0 は上がる

talosctl read /proc/net/bonding/bond0 -n 10.0.0.2 -e 127.0.0.1:50011
Bonding Mode: adaptive load balancing      # = balance-alb
Currently Active Slave: enp0s3
MII Status: up
MII Polling Interval (ms): 100             # = miimon
Slave Interface: enp0s3 / MII Status: up / Link Failure Count: 0
Slave Interface: enp0s4 / MII Status: up / Link Failure Count: 0

monitor から片方の carrier を落とす(set_link virtio-net-pci.0 off)と 8 秒以内に MII Status: down / Link Failure Count: 1 になり、Currently Active Slave が enp0s4 に移って talosctl の応答は途切れなかった。balance-alb らしく メンバー間で MAC が入れ替わる (get links で enp0s3 が …aa:00:02、enp0s4 が …aa:00:01 になる)のも見えた。 なお QEMU の set_link … on ではゲスト側の carrier が戻らないので、復旧方向は確かめられていない。

2. 静的 IPv6 は載る。だが accept_ra: "2" が無いと既定経路が消える

静的アドレスと SLAAC は素直に並ぶ。

bond0/10.0.0.2/24
bond0/240f:6d:842b:1::2/64                      # patches に書いた静的アドレス
bond0/240f:6d:842b:1:9f48:ba23:fcbb:1cd0/64     # SLAAC
bond0/fe80::8628:e278:4977:732/64

問題は既定経路のほうで、Kubernetes が上がった瞬間に IPv6 の default route が消えた。

時点 net.ipv6.conf.bond0.forwarding accept_ra protocol: ra の default route
maintenance mode 0 1(既定) あり
クラスタ起動後 1 1(既定) 無い
クラスタ起動後 1 2 あり

accept_ra=1 は「forwarding が有効なら RA を無視する」という意味で、flannel / kube-proxy が net.ipv6.conf.all.forwarding=1 にした時点でカーネルが RA 由来の経路を落とす。 Ubuntu 期にこれが問題にならなかったのは NetworkManager が userspace で RA を処理していたからで、 実機は現在 bond0/accept_ra = 0 のまま default via fe80::… proto ra metric 300 が載っている。 Talos には RA を代行するものが無いので、カーネルに任せる = accept_ra: "2" が必須。

patches/cluster.yaml に足した:

net.ipv6.conf.bond0.accept_ra: "2"

入れたあとの確認(クラスタが Ready になってから見ること):

talosctl get routes -o yaml -n 10.0.0.2 -e … | grep -B10 'protocol: ra'
#     id: bond0/inet6/fe80::2//1024
#     dst: ""
#     gateway: fe80::2
#     outLinkName: bond0
#     protocol: ra

addr_gen_mode: "2" は書いても効かない(黙って失敗し続ける)

同時に見つかった。net.ipv6.conf.bond0.addr_gen_mode: "2"(stable-privacy)は stable_secret が未設定だとカーネルが EINVAL を返すので、一度も適用されない。 talosctl get addresses は EUI-64 のまま(240f:6d:842b:1:5054:ff:feaa:1)で、 KernelParamSpecController が数秒おきにこれを吐き続ける:

ERROR controller failed {"controller": "runtime.KernelParamSpecController",
  "error": "write /proc/sys/net/ipv6/conf/bond0/addr_gen_mode: invalid argument"}

"3"(random)に変えたら通った。 書き込みが成功し、link-local も SLAAC も MAC 由来でなくなる (fe80::8628:e278:4977:732 / 240f:6d:842b:1:9f48:ba23:fcbb:1cd0)。 実機の NetworkManager も同じ見た目(240f:6d:842b:1:a096:1754:8738:2ede)なので状態としては揃う。 違いは 3 は再起動ごとに IID が変わること。ブート間で固定したければ stable_secret を足して 2 に戻す (cloudflare-ddns の local.iface.stable:bond0 が ::2 ではなく SLAAC 側を拾っていた場合はそれが要る)。

3. wireguard はカーネル組み込み。AppArmor 相当の回避は要らない

/proc/modules にも /lib/modules/…/kernel/ にも wireguard は出てこない。=y で組み込まれているからで、

talosctl read /sys/module/wireguard/version -n 10.0.0.2 -e …   # => 1.0.0

実際に privileged + hostNetwork の Pod(namespace に pod-security.kubernetes.io/enforce=privileged)から wg-quick up wg0 を通した(ip link add dev wg0 type wireguard が通り、UDP 51820 を v4 / v6 で LISTEN)。 ホスト側の talosctl get links にも wg0 … KIND wireguard が現れる。 Ubuntu で必要だった AppArmor プロファイルの無効化は Talos には存在しない(そもそも LSM が SELinux)。

ついでに確認できたこと

  • UnattendedInstallConfig の CEL diskSelector で実際にインストールされた(EPHEMERAL が /dev/vda4)
  • schematic のカーネル引数が載る: /proc/cmdline に intel_iommu=on iommu=pt
  • patches/main.yaml の vfio_pci / vfio_iommu_type1 が /proc/modules に出る
  • net.ipv6.conf.eno4.disable_ipv6: "1" は効く(相当インタフェースに v6 アドレスが一切付かない)
  • bond0 は Talos が作ったあとに sysctl が適用される(存在しないパスなら ENOENT のはずが EINVAL だった)

VM では確かめられていないこと

読み替えたぶんと、SLIRP が本物でないぶんは未検証のまま:

  • インタフェース名は enp0s3/enp0s4/enp0s5。実機は eno1/eno2/eno4(tg3)。 名前が違えば machine.sysctls のキーも interfaces も当たらないので、実機では talosctl get links で名前を確認してから流すこと
  • ディスクは /dev/vda で試した。実機は /dev/sda(talosctl get disks で確定させる)
  • 相手が SLIRPなので、balance-alb の ARP ネゴシエーションを本物のスイッチ相手に試したことにはならない。 ハブなので送ったフレームが相方の NIC にも返ってくるが、それで bond が壊れることは無かった
  • RA も SLIRP のもの(fe80::2、プレフィックス 240f:6d:842b:1::/64 は手で合わせた)。 ISP のルータの RA(MTU オプション、RDNSS、valid/preferred lifetime)は別物
  • carrier の復旧方向、PT3 本体(当時は vfio パススルーの予定。いまは dvb-pt3 で /dev/dvb が出るか)

ネットワークまわりの前提

Talos 1.14.0 の既定は Flannel 0.28.9 + kube-proxy(1.13 から Flannel も NetworkPolicy に対応。L3/L4 まで)。 このクラスタは Cilium に替える(decisions.md「ルーティングの選定」。CNI の交換は構築時にやる ── 稼働中の差し替えは全 Pod 再起動が前提で、後からだとコストが一桁変わる)。Talos 側の止め方は ../talos/patches/cni.yaml(v1alpha1 の cni.name: none はもう書けない)、 Cilium 側の Talos 固有の値は ../talos/cilium-values.yaml。

  • LoadBalancer の実体は無い(k3s の ServiceLB 相当が無い)。hostPort と Gateway の hostNetwork で済ませた (decisions.md「LoadBalancer をどう置き換えるか」)
  • クライアント IP: 単一ノードなので externalTrafficPolicy: Cluster のままで保たれる(decisions.md「クライアント IP」)
  • NetworkDefaultActionConfig: block を使うなら、NetworkRuleConfig で要るポートを明示的に開ける

管理モデル(3 つのレイヤーを混同しない)

レイヤー 決めること 変更手段
イメージ カーネル引数、system extension(GPU ドライバ等) Image Factory で schematic を作って talosctl upgrade
machine config CNI、kube-proxy、ディスク、ネットワーク、ファイアウォール talosctl apply-config
Kubernetes ワークロード、Envoy Gateway、Cilium 本体 kubectl / Helm / GitOps

SSH もシェルもパッケージマネージャも無く gRPC API だけなので、障害時は「直さず作り直す」。 machine config は必ず git に置く(唯一の真実になる)。デバッグは talosctl dmesg / logs / read、1.13 以降は debug コンテナ。