From 691d00df6463be18eb9f2fc516642de3698831b6 Mon Sep 17 00:00:00 2001 From: Ruk Doe Date: Sat, 17 Oct 2026 02:08:40 +0900 Subject: [PATCH 1/3] =?UTF-8?q?Talos=20=E3=81=AE=20bond0=20=E3=81=AE=20MAC?= =?UTF-8?q?=20=E3=82=92=20e6:2c:1b:1a:a0:39=20=E3=81=AB=E5=9B=BA=E5=AE=9A?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ルーターの IPv6 ホスト公開のエントリがこの MAC を指している。v1alpha1 の machine.network.interfaces では bond の MAC を指定できず、eno1 の MAC を引き継ぐので、 Talos に替えた日と Z440 に移した日に外からの IPv6 が止まる。BondConfig / LinkConfig に移して hardwareAddr で固定する。あわせて Ubuntu 側のネットワーク管理が systemd-networkd である ことに合わせてコメントを直し、リンク切れ(talos-install-media.md)を直す。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JbmWyUB9jcBcYCnNyiGBSV --- talos/patches/cluster.yaml | 72 ++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 30 deletions(-) diff --git a/talos/patches/cluster.yaml b/talos/patches/cluster.yaml index ffd570d..0a29e37 100644 --- a/talos/patches/cluster.yaml +++ b/talos/patches/cluster.yaml @@ -1,11 +1,11 @@ # クラスタ共通。v1.14 では型付きドキュメントと v1alpha1 を併記できないので、 -# 項目ごとに書く場所が決まっている(../docs/talos-install-media.md 参照)。 +# 項目ごとに書く場所が決まっている(../docs/talos.md「machine config の作法」)。 machine: sysctls: # これが無いと IPv6 の既定経路が消える。Kubernetes が上がると # `net.ipv6.conf.{all,bond0}.forwarding` が 1 になり、既定の `accept_ra=1` は # 「forwarding が有効なら RA を無視する」なのでカーネルが RA 由来の default route を落とす。 - # 2 = forwarding でも RA を受ける。Ubuntu 期は NetworkManager が userspace で RA を + # 2 = forwarding でも RA を受ける。Ubuntu 期は systemd-networkd(2026-09-24 までは NetworkManager)が userspace で RA を # 処理していた(実機は `accept_ra=0` のまま `proto ra` の既定経路が載っている)ので # この問題が出なかった。QEMU で再現・修正を確認済み(../docs/talos.md「VM ブートドリル」)。 net.ipv6.conf.bond0.accept_ra: "2" @@ -15,7 +15,7 @@ machine: # KernelParamSpecController が数秒おきに失敗し続ける(実測)。 # 3 は再起動ごとに IID が変わる。ブート間で固定したくなったら `stable_secret` を足して 2 に戻す。 # 公開する AAAA は下の静的アドレス(::2)。SLAAC のぶんは並んで付くだけ。 - # 実機の NetworkManager も ipv6.token は使っておらず、静的アドレスを直接書いている + # Ubuntu 期も IPv6 のトークンは使っておらず、静的アドレスを直接書いている # (`ip token show` はどのインタフェースも `token ::`)。 net.ipv6.conf.bond0.addr_gen_mode: "3" # Forgejo の Runner(dind)で vite などがファイル監視を使う。既定の 128 は全コンテナの root で @@ -27,34 +27,46 @@ machine: net.ipv6.conf.eno4.disable_ipv6: "1" network: # 名前は main 固定。local-path の PV の nodeAffinity が main を指している。 + # インタフェースは下の BondConfig / LinkConfig(型付きドキュメント)に書く。v1alpha1 の + # machine.network.interfaces は bond の MAC を指定できないため(2026-10-17 に移した)。 hostname: main - interfaces: - - interface: bond0 - bond: - interfaces: [eno1, eno2] - mode: balance-alb - miimon: 100 - dhcp: false - addresses: - - 10.0.0.2/24 - # 実機と同じ静的アドレス。プレフィックスは ISP 由来なので変わりうるが、 - # それは NetworkManager で運用しているいまも同じリスク。変わったときは - # cloudflare-ddns(local.iface.stable:bond0)が実際のアドレスを公開する。 - - 240f:6d:842b:1::2/64 - routes: - # IPv6 の既定経路は RA 由来(`default via fe80::… proto ra`)なので書かない。 - # 上の `accept_ra: "2"` とセット。Kubernetes が上がったあとに - # `talosctl get routes -o yaml` で `protocol: ra` の `dst: ""` が残っていることを確かめること - # (クラスタ起動前だけ見ても意味が無い。forwarding が 1 になるのはそのあと)。 - - network: 0.0.0.0/0 - gateway: 10.0.0.1 - - interface: eno4 - dhcp: false - addresses: - - 10.10.0.4/24 - routes: - - network: 10.10.0.0/24 - gateway: 10.10.0.1 +--- +# 10.0 側。**MAC は e6:2c:1b:1a:a0:39 に固定する。** ルーターの IPv6 ホスト公開のエントリがこの MAC を +# 指している(docs/router.md)。指定しないと bond は links の先頭(eno1)の MAC を引き継ぎ、 +# Talos に替えた日と Z440 に移した日に変わって、外からの IPv6 が全部止まる。 +# Ubuntu 期も netplan に macaddress を書いて同じ値に固定する(2026-10-17。それまでは指定が無く、 +# 再起動で 3a:43:95:a9:3e:b1 に変わっていた)。 +apiVersion: v1alpha1 +kind: BondConfig +name: bond0 +links: + - eno1 + - eno2 +bondMode: balance-alb +miimon: 100 +hardwareAddr: e6:2c:1b:1a:a0:39 +addresses: + - address: 10.0.0.2/24 + # 実機と同じ静的アドレス。プレフィックスは ISP 由来なので変わりうる(Ubuntu 期も同じリスク)。 + # 変わったときは cloudflare-ddns(local.iface.stable:bond0)が実際のアドレスを公開する。 + - address: 240f:6d:842b:1::2/64 +routes: + # destination を書かないと gateway の側の既定経路になる。 + # IPv6 の既定経路は RA 由来(`default via fe80::… proto ra`)なので書かない。 + # 上の `accept_ra: "2"` とセット。Kubernetes が上がったあとに + # `talosctl get routes -o yaml` で `protocol: ra` の `dst: ""` が残っていることを確かめること + # (クラスタ起動前だけ見ても意味が無い。forwarding が 1 になるのはそのあと)。 + - gateway: 10.0.0.1 +--- +# 10.10 側。DHCP は DHCPv4Config / DHCPv6Config を書かない限り無効(明示の設定が 1 つでもあれば既定の DHCP は止まる) +apiVersion: v1alpha1 +kind: LinkConfig +name: eno4 +addresses: + - address: 10.10.0.4/24 +routes: + - destination: 10.10.0.0/24 + gateway: 10.10.0.1 --- apiVersion: v1alpha1 kind: KubeNetworkConfig From b3cea69a257b3ee61dbb5a6ee7ca65a5483c3cd2 Mon Sep 17 00:00:00 2001 From: Ruk Doe Date: Sat, 17 Oct 2026 02:21:21 +0900 Subject: [PATCH 2/3] =?UTF-8?q?Talos=20=E3=82=92=E5=85=88=E3=81=AB?= =?UTF-8?q?=E3=80=81Z440=20=E3=82=92=E5=BE=8C=E3=81=AB:=20=E6=96=87?= =?UTF-8?q?=E6=9B=B8=E3=81=A8=E8=A8=AD=E5=AE=9A=E3=81=AE=E7=9F=9B=E7=9B=BE?= =?UTF-8?q?=E3=82=92=E7=9B=B4=E3=81=97=E3=81=A6=E5=9C=A7=E7=B8=AE=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 順番を入れ替える: Phase 2 = DL360 の上で k3s → Talos(iLO がある)、Phase 2.5 = Talos のまま Z440 へ。 Z440 は GPU が無いと POST しないので Arc A310 を常設する(AV1 のエンコードにも使う) - z440-migration.md を Talos 前提で書き直す(LinkAliasConfig、staged config、schematic の console=、AMT) - bond0 の SLAAC を止める(autoconf=0)。cloudflare-ddns は SLAAC のアドレスを AAAA に書いていた - 古くなった記述を直す: k8s の版、PT3、taint の外し方、SSD の種類、NetworkManager → systemd-networkd、 ghcr の資格情報、local-path-retain、etcd からの復元、リポジトリの旧名、存在しないリンクや見出しの参照 - migration-day に足す: get disks、bond0 の MAC と IPv6 の確かめ方、AAAA、PT3、Renovate、やめどき - 圧縮: talos.md のドリルの記録を表に(785 → 約 410 行)、decisions.md(877 → 約 700 行)、 ROADMAP・k8up・restore-drill・bootstrap の重複と経緯。理由はそれぞれ 1 か所に残す Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JbmWyUB9jcBcYCnNyiGBSV --- .gitignore | 2 +- README.md | 11 +- ROADMAP.md | 147 +++---- apps/cloudflare-ddns/application.yaml | 5 +- apps/k8up/README.md | 119 ++---- apps/netbird/deployment.yaml | 2 +- backup/README.md | 2 + bootstrap/README.md | 29 +- docs/decisions.md | 465 +++++++--------------- docs/migration-day.md | 60 ++- docs/restore-drill.md | 111 ++---- docs/router.md | 15 +- docs/talos.md | 536 ++++---------------------- docs/z440-migration.md | 245 ++++++------ pulumi/dns.ts | 2 +- pulumi/index.ts | 2 +- recovery/README.md | 8 +- talos/README.md | 34 +- talos/drift.sh | 12 +- talos/patches/cluster.yaml | 7 +- talos/patches/main.yaml | 3 +- talos/patches/volumes.yaml | 5 +- talos/render.sh | 22 +- talos/versions.yaml | 21 +- 24 files changed, 560 insertions(+), 1305 deletions(-) diff --git a/.gitignore b/.gitignore index 0b44172..5d86a4c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ talos/clusterconfig/ talos/secrets.plain.yaml # **平文のまま置かないこと。** talos/registries.yaml は SOPS 済みのものをコミットする -# (talos/README.md「ghcr.io の資格情報」)。書きかけの平文はこの名前に逃がす。 +# (talos/README.md「fj.doany.io の資格情報」)。書きかけの平文はこの名前に逃がす。 talos/registries.plain.yaml # pulumi/ の道具(bun.lock で固定してある) diff --git a/README.md b/README.md index 620d7cb..19e41b9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # gitops -k3s クラスタ上のセルフホストアプリを [Argo CD](https://argo-cd.readthedocs.io/) で管理するマニフェスト群。 +自宅の Kubernetes クラスタ(k3s から Talos へ移行中)上のセルフホストアプリを [Argo CD](https://argo-cd.readthedocs.io/) で管理するマニフェスト群。 ## 仕組み @@ -10,11 +10,8 @@ k3s クラスタ上のセルフホストアプリを [Argo CD](https://argo-cd.r [`deploy/argocd.yaml`](deploy/argocd.yaml) で決まる。**Argo CD が同期するのは `apps/` 以下と、 `bootstrap/argocd/repos.yaml` の 1 ファイルだけ**で、`bootstrap/` のほかは同期対象外(下記)。 -`repos.yaml` を同期しているのは、Argo CD の chart が自分で作る `apps` という Application -(`bootstrap/argocd/helmchart.yaml` の中。`directory.include: repos.yaml`、prune / selfHeal 付き)。 -ApplicationSet は Argo CD がいないと意味を持たないので、chart と一緒に入る ── **まっさらなクラスタでも -CI を待たずに走りはじめる**のはこのため。CI の `bootstrap-apply` も同じファイルを当てるが、 -中身が同じなので衝突しない。 +`repos.yaml` は Argo CD の chart が作る `apps` Application が拾うので、**まっさらなクラスタでも CI を待たずに走りはじめる** +(詳しくは [`bootstrap/README.md`](bootstrap/README.md))。 ## ディレクトリ @@ -22,7 +19,7 @@ CI を待たずに走りはじめる**のはこのため。CI の `bootstrap-app | --- | --- | | `apps/` | Argo CD が再帰的に同期するアプリのマニフェスト。**`_` で始まるディレクトリ (`apps/_glitchtip/` など) は同期しない** (動かしていないもの。下の表) | | [`bootstrap/`](bootstrap/) | クラスタそのものを組む層(Argo CD 本体・Infisical・cert-manager・Gateway・auth)。**Argo CD は触らない**(`apps/` の外にある。例外は `argocd/repos.yaml` だけ ── 上の「仕組み」)。**main へのマージで GitHub Actions が当てる**(SOPS 済みの 4 ファイルと `cilium/` だけ手で) | -| [`backup/`](backup/) | ホストのバックアップ(restic → Cloudflare R2)。毎日 04:00 JST | +| [`backup/`](backup/) | ホストのバックアップ(restic → Cloudflare R2)。毎日 04:00 JST。**k3s 期だけ**(後継は `apps/k8up/`) | | [`pulumi/`](pulumi/) | Cloudflare(R2・DNS・ゾーンの設定・通知)・Entra(リダイレクト URI・CI の信頼の設定)・NetBird の設定を Pulumi で持つ。**PR で差分がコメントされ、main に入ると当たる** | | [`mado/`](mado/) | **`main` が落ちたときの入口**。JCOM 側の PC(10.10.0.2)で [mado](https://github.com/danything/mado)(OIDC でログインしてブラウザから RDP する exe)を動かす(`md.doany.io`。クラスタの外) | | [`recovery/`](recovery/) | まっさらなホストから戻すための復元スクリプトと、暗号化した鍵 | diff --git a/ROADMAP.md b/ROADMAP.md index 8ff2220..46a4f31 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -7,11 +7,13 @@ Talos のインストールメディアは [`docs/talos.md`](docs/talos.md)、 ## いまここ -**Phase 2(k3s → Talos)の直前。** 当日やることは +**次は Phase 2(いまの DL360 の上で k3s → Talos)。** 当日やることは [docs/migration-day.md](docs/migration-day.md) を上から順に。 +**そのあと Phase 2.5(Talos のまま HP Z440 へ移す)。** 順番を逆にした理由は +[docs/decisions.md](docs/decisions.md)「移行の順番: Talos を先に、Z440 は後」(2026-10-17)。 下の「済んだこと」は記録で、読まなくても当日は進む。 -## Phase 2 — k3s → Talos(停止を伴う。**ネットワーク構成は変えない**) +## Phase 2 — k3s → Talos(DL360 のまま。停止を伴う。**ネットワーク構成は変えない**) **当日は [docs/migration-day.md](docs/migration-day.md) を上から順にやる。** ここは「何を決めたか」で、あちらが「どの順にやるか」。 @@ -22,15 +24,20 @@ Talos 側は **`KubeFlannelCNIConfig` を `$patch: delete` で消して `KubePro `k8sServicePort: 7445`(KubePrism)、`cgroup.autoMount.enabled: false` + `hostRoot: /sys/fs/cgroup` を足すだけ。 **v1alpha1 の `cni.name: none` はもう書けない**(型付きドキュメントと衝突する)。 +**実機の RAM は 2026-10-17 から RDIMM 6 枚中 5 枚(40GB)。** 1 枚は Z440 の BIOS 更新に貸している(Phase 2.5)。 + - [ ] 作業は LAN(10.0.0.2 / 10.10.0.4)か iLO(10.0.0.3)から。cloudflared 経由の ssh は使えない。 **作業中は何も見せない(2026-09-08 に決めた)。** Cloudflare 側でメンテナンス画面を出す案は、 止まっている数時間のために配線を 1 つ増やすことになるのでやめた。見に来るのは自分だけ。 - [ ] 最終バックアップを取り、`restic check` を通す。 - [ ] Talos を実機にインストール(`talos/README.md` の手順、schematic `96371c8f…`)。 +- [ ] **入れたら bond0 の MAC を見る。** `talosctl get links bond0` が `e6:2c:1b:1a:a0:39` であること + ([talos/patches/cluster.yaml](talos/patches/cluster.yaml) の `BondConfig` で固定。ルーターの IPv6 ホスト公開がこの MAC を指す)。 + 別の回線の IPv6 から 443 に届くことも確かめる([docs/router.md](docs/router.md)「IPv6 はノードだけを公開する」)。 - [ ] **service の IPv6 CIDR を `fd43::/108` に変える**(Talos は `/64` を受け付けない)。ClusterIP が振り直しになる。 設定は入っていて、VM で出ることも確かめた(2026-09-08。`kube-dns` が `["10.43.0.10","fd43::a"]`)。 当日は「振り直された ClusterIP で困るものが無いか」を見るだけ。 -- [ ] k8s オブジェクトは etcd 復元ではなく **git から ArgoCD で再構築**(k3s 固有の HelmChart 等が etcd に混ざっているため)。 +- [ ] k8s オブジェクトは etcd 復元ではなく **git から ArgoCD で再構築**(k3s は sqlite なので、etcd から持ち越すものは無い)。 **git に無いものが動いていないことは確認済み(2026-09-08)。** 追跡の印(ArgoCD の `tracking-id`・Helm・ k3s の `objectset`・owner)の無いオブジェクトは 11 個で、全部 CI が `kubectl apply` で当てる `bootstrap/` のもの。 例外の `infisical/data-postgresql-0` は StatefulSet の `volumeClaimTemplate` が作り直す。 @@ -38,56 +45,45 @@ Talos 側は **`KubeFlannelCNIConfig` を `$patch: delete` で消して `KubePro (中身は [apps/k8up/README.md](apps/k8up/README.md)「戻し方」。**順番と、`Succeeded` が 「戻った」の意味ではないこと**が要点。2026-09-08 に実際に流して確認済み)。 - [ ] Infisical → operator → 各アプリの順で疎通確認。DNS(cloudflare-ddns)、netbird、AdGuard の公開リゾルバを確認。 +- [ ] **Renovate が戻ったことを見る。** 2026-10-16 から k8s の CronJob([apps/renovate/](apps/renovate/))で動いているので、 + 再構築が済むまで依存更新の PR は止まる。ArgoCD が戻したあと、次の回で PR が来ること。 - [ ] **PT3**: 上流 PR は v1.15.0-alpha.1 に入った。schematic に `dvb-pt3` を入れ、`earth-pt3` を読ませてある。 実機で `/dev/dvb/adapter*` が出るのを確かめ、tuner-agent に `/dev/dvb` を hostPath で渡す ([docs/decisions.md](docs/decisions.md)「PT3 チューナー」)。 ### 用意が済んだもの -- **`talos/secrets.yaml`(クラスタの CA 一式)と `talos/registries.yaml`(2026-09-08)。** 無いと `render.sh` が - 動かず、private なイメージは全部 `ImagePullBackOff` になる。registries は 2026-09-15 に ghcr.io の PAT から - fj.doany.io の資格情報に作り直した([talos/README.md](talos/README.md))。k3s の registries.yaml は役目を終える。 -- **k3s の組み込みアドオンのうち Talos に無いもの(2026-09-08)。** `kubectl -n kube-system get addons.k3s.cattle.io` で洗い出した。 - - **local-path-provisioner** … 無いと **PVC が 1 つも bind しない**。[talos/manifests/local-path.yaml](talos/manifests/local-path.yaml) - (`local-path-storage` の PSA ラベルもここ)。置き場は **`/var/mnt/local-path`(専用パーティション。 - [talos/patches/volumes.yaml](talos/patches/volumes.yaml))**。**ディスクの割り方は入れ直さないと変えられない**ので、焼く前に確定させること - - **metrics-server** … 無いと `kubectl top` と各 UI の使用量表示が消える(HPA は 0 個)。 - [talos/metrics-server-values.yaml](talos/metrics-server-values.yaml)。**`--kubelet-insecure-tls` が要る**(Talos の kubelet は自己署名) - - coredns は Talos が入れる。ccm と rolebindings は k3s 固有。k3s の RuntimeClass 10 個はどの Pod も使っていない。 - クラスタスコープのもの(APIService・PriorityClass・IngressClass・webhook)も数え直し、残っていたのは下の Gateway API の CRD だけ -- **Gateway API の CRD を自分で入れる(2026-09-08)。** いま入っているのは消した Traefik の `traefik-crd` chart が - 置いていったもので、**Talos には無い。** 無いと `Gateway` も `HTTPRoute` も適用できず**公開経路が丸ごと消える** - (Cilium の chart は CRD を同梱しない)。[talos/render.sh](talos/render.sh) が `KubeExternalManifestConfig` で - URL を渡す。版は [talos/versions.yaml](talos/versions.yaml)。 -- **起動順序(2026-09-08)。** k3s の `HelmChart` CRD は Talos に無いので、[talos/render.sh](talos/render.sh) が - bootstrap 層を全部 `helm template` して inlineManifest にする(値はどこにも写さない。CI も同じ `render.sh` を使う)。 - **本物の値で焼いたドリルで `apply-config` 1 回で全部立ち上がった**([docs/talos.md](docs/talos.md)「ブートドリル 4 回目」)。 - 層の分け方と根拠は [docs/decisions.md](docs/decisions.md)「Talos の起動順序をどう組むか」、 - **`upgrade-k8s` の前には必ず描き直す**こと(古い machine config だと走っているものが巻き戻る)は - [talos/README.md](talos/README.md)「上げ方 / 当て直し方」。 -- **ファイルの PVC バックアップを k8up に寄せた(2026-09-08)。** ホストの `k3s-backup` は Talos にシェルが無いので - 持っていけない。全 namespace で成功を確認済み。切り分けは [apps/k8up/README.md](apps/k8up/README.md)。 - ホストのスクリプトを畳むのは Talos に移る時点。 -- **`local-path-retain` を消した(2026-10-05、k3s のうちに)。** 28 本の PVC を止めて作り直した。**データはコピーしていない** ── - PV の reclaim を `Retain` にして古い PVC を消し、PV の `claimRef` を外して `storageClassName` を `local-path` に書き換え、 - 新しい PVC を `volumeName` で同じ PV に結んだ(使い捨ての PVC で先に確かめた)。あわせて denpa の録画の PVC を - `denpa-recorded` → `denpa-raw`、`denpa-library` → `denpa-encoded` に改名。PV を使い回したので**ホスト上のディレクトリ名は - 改名前のまま**(Talos で引き直せば揃う)。PR は gitops と ashi#89 / blog#123 / lgtm#56 / xool#159 / yuzuriha#19 / - todoroku#342 / worklog-cloud#142 / denpa#430。 -- **改名前の録画のスナップショットを消した(2026-10-05)。** 新パスの 1 本目(`8a2c927b`)を取ってから旧パスの `539a08ac` を - forget → prune。**49.4 GiB 解放、88.6 → 37.3 GiB**、`restic check` はエラー無し。旧スナップショットにだけあった - 3 話分(ライブラリから削除済み)は戻せなくなった ── 1 世代の方針どおり。 -- **git と実機の helm 値が一致していることを確認(2026-09-08)、argocd と infisical の chart の版を固定** - (argo-cd 10.8.1 / infisical-standalone 1.10.0)。手順と注意は [bootstrap/README.md](bootstrap/README.md)。 -- **見直しで漏れを 3 つ塞いだ(2026-10-05)。** どれも 09-08 のドリルより後に足したものか、ドリルが通らない経路だった。 - - **CI の CA が k3s のままだった。** `bootstrap/apiserver/ca.crt` は k3s の CA で Talos の CA とは別物。当日の 6 で - `bootstrap-apply` が TLS で落ち、nodePort 80/443 の当て直しも走らず公開経路が戻らないところだった。両方を 1 ファイルに入れた - (移行後に k3s のぶんを消す。migration-day の 10) - - **Forgejo を戻すまで ApplicationSet が丸ごと止まる。** gitea の generator が失敗すると ApplicationSet は GitHub 側も含めて - Application を 1 本も作らない(argo-cd の `applicationset_controller.go`)。まっさらなクラスタでは抜けられない詰まり方だった。 - `repos` と `repos-forgejo` に分け、migration-day の 7 に「Forgejo を Infisical の次に戻す」を足した - - **cilium-drift が Talos で毎週偽の FAILED を出す。** `talos/cilium-values.yaml` を重ねないと `cgroup-root` が必ず - 食い違う。サーバの版で k3s / Talos を見分けて重ねるようにした +- **`talos/secrets.yaml` と `talos/registries.yaml`(2026-09-08、registries は 2026-09-15 に fj.doany.io の資格情報へ)** ── [talos/README.md](talos/README.md) +- **local-path-provisioner**(置き場は専用パーティション `/var/mnt/local-path`。**ディスクの割り方は入れ直さないと変えられない**)── [talos/manifests/local-path.yaml](talos/manifests/local-path.yaml)、[talos/patches/volumes.yaml](talos/patches/volumes.yaml) +- **metrics-server**(`--kubelet-insecure-tls` が要る)── [talos/metrics-server-values.yaml](talos/metrics-server-values.yaml)。k3s の他のアドオンは Talos が入れるか、要らない +- **Gateway API の CRD**(無いと公開経路が丸ごと消える)── [talos/render.sh](talos/render.sh) が URL で渡す。版は [talos/versions.yaml](talos/versions.yaml) +- **起動順序**(本物の値で焼いたドリルで `apply-config` 1 回で全部立ち上がった)── [docs/talos.md](docs/talos.md)「ブートドリル 4 回目」、[docs/decisions.md](docs/decisions.md)「Talos の起動順序をどう組むか」。**`upgrade-k8s` の前には必ず描き直す**([talos/README.md](talos/README.md)「上げ方 / 当て直し方」) +- **ファイルの PVC バックアップを k8up に寄せた(2026-09-08)** ── [apps/k8up/README.md](apps/k8up/README.md)。ホストのスクリプトを畳むのは Talos に移る時点 +- **`local-path-retain` を消した(2026-10-05)。** 28 本の PVC を PV の使い回しで `local-path` に付け替えた(データはコピーしていない)。denpa の録画の PVC を `denpa-raw` / `denpa-encoded` に改名、**ホスト上のディレクトリ名は改名前のまま**(Talos で揃う)── [docs/decisions.md](docs/decisions.md)「PVC を守る仕掛けをやめた」 +- **改名前の録画のスナップショットを消した(2026-10-05、88.6 → 37.3 GiB)** ── [docs/decisions.md](docs/decisions.md)「バックアップに何を含めるか」 +- **helm 値が git と実機で一致、argocd と infisical の chart の版を固定(2026-09-08)** ── [bootstrap/README.md](bootstrap/README.md) +- **見直しで塞いだ漏れ 3 つ(2026-10-05: CI の CA、ApplicationSet が Forgejo 待ちで止まる件、cilium-drift の偽 FAILED)** ── migration-day の 6・7・10 に反映済み + +## Phase 2.5 — HP Z440 へ移す(Talos のまま) + +**手順は [docs/z440-migration.md](docs/z440-migration.md)。** Talos の machine config は宣言的なので、 +箱の違いは schematic と NIC の名前に閉じ込める。bond0 の MAC は設定で固定してあるので、ルーターは触らない。 + +- [ ] **Intel Arc A310 を買う(常設)。** Xeon E5 に内蔵 GPU は無く、**Z440 は単体の GPU が無いと POST しない。** + ロープロファイル・補助電源なし・AV1 のハードウェアエンコード可で、denpa の QSV もこれで賄う + (いまの DL360 の画面は iLO の Matrox G200 だけで、denpa の Pod に `renderD*` が無い)。 + **Arc は UEFI + CSM 無効が要る。** +- [ ] **BIOS を 02.62 に上げてから E5-2696 v4 を載せる。** 上げるまでは仮の CPU と、DL360 から借りた RDIMM 1 枚で起こす。 +- [ ] **AMT を設定する(9.x、10.0.0.5)。** 電源と SOL だけで KVM は無い。iLO のように画面は見えない。 +- [ ] **Z440 用の schematic。** Intel GPU の extension(**`siderolabs/i915` で足りるか要確認**。新しい世代の Arc B シリーズなどは + `siderolabs/xe`。**両方は入れない** ── 同じ GPU を取り合う)と、SOL に出すための `console=` のカーネル引数。 + 変えたら `talos/versions.yaml` の ID と depName を差し替える。 +- [ ] **NIC を MAC で名付ける(`LinkAliasConfig`)。** 箱が替わるとインタフェース名が変わるので、bond のメンバーは名前でなく MAC で指す。 +- [ ] Z440 用の machine config を先に描いて置いておき、移す。 +- [ ] 移したら: `talosctl ls /dev/dri` に `renderD128`、`talosctl dmesg` に GuC / HuC の読み込みが出ること。 + **denpa 側は何もしなくてよい**(イメージの ffmpeg は QSV / VA-API 入りで、chart は `/dev/dri` をマウント済み)。 + `talosctl get links bond0` の MAC と、外からの IPv6 も Phase 2 と同じく見る。 +- [ ] docs の「iLO(10.0.0.3)」を AMT(10.0.0.5)に直す(talos.md・router.md など、移設後も使う手順)。 ## Phase 3 — Talos 定常運用 @@ -97,15 +93,6 @@ Talos 側は **`KubeFlannelCNIConfig` を `$patch: delete` で消して `KubePro (`talos/patches/etcd-backup.yaml`。`os:admin` の talosconfig を Secret に置く案はやめた)、 日次で同じ restic リポジトリへ(`--host etcd`)。**戻すときの主経路は git からの再構築のまま**で、 こちらは近道(docs/talos.md「etcd スナップショットからの復旧」)。 -- [ ] **いずれ Intel の GPU を足して QSV でエンコードする(2026-10-05 時点では未購入)。** いまの実機には - QSV が使える GPU が無い(Xeon E5-2696 v4 は内蔵 GPU なし、画面は iLO の Matrox G200 だけ)ので、 - denpa の Pod から見えるのは `card0` だけで `renderD*` が無い。候補は **Intel Arc A310**(ロープロファイル、 - 補助電源なし、AV1 のハードウェアエンコード可)。買ったらやること: - - Talos なら schematic に **`siderolabs/i915`** を足す(新しい世代の Arc B シリーズなどは `siderolabs/xe`。 - **両方は入れない** ── 同じ GPU を取り合う)。schematic を変えたら `talos/versions.yaml` の ID と depName を差し替え、 - `talosctl upgrade` で当てる(ISO は焼き直さなくてよい) - - `talosctl ls /dev/dri` に `renderD128`、`talosctl dmesg` に GuC / HuC の読み込みが出ること - - **denpa 側は何もしなくてよい。** イメージの ffmpeg は QSV / VA-API 入りで、chart は `/dev/dri` をマウント済み - [ ] 四半期ごとに VM で復元リハーサル(PV + etcd の両方)。**初回は Forgejo を含めること** (非公開アプリのイメージとマニフェストが Forgejo から来る経路は、まだ一度も通していない)。 - [ ] **k3s 期の残りを畳む。** ホストのスナップショット(`host=main`)は保持が `--keep-monthly 2` なので @@ -122,45 +109,19 @@ Talos 側は **`KubeFlannelCNIConfig` を `$patch: delete` で消して `KubePro 済んだもの: -- **k8up の失敗通知(2026-09-08)。** [apps/k8up/notify.yaml](apps/k8up/notify.yaml) の CronJob が日次で Matrix の - `server` の部屋に投げる。考え方は [apps/k8up/README.md](apps/k8up/README.md)「失敗したときに気づけるようにする」。 - `restic check` も [schedules.yaml](apps/k8up/schedules.yaml) に 1 本ある。 -- **R2 のずれ検知(2026-10-05)。** 初回から「一致」。容量の推移と、録画をホストのスクリプトから外して - `restic rewrite` で抜いた件は [docs/decisions.md](docs/decisions.md)「バックアップに何を含めるか」。 -- **`talosctl upgrade` / `upgrade-k8s` の手順(2026-09-08)。** [talos/README.md](talos/README.md)「上げ方 / 当て直し方」。 - `upgrade-k8s` は inlineManifests の reconcile も兼ねるので、Talos 期の「bootstrap 層を当て直す」操作でもある。 -- **通信の可視化は Hubble で足りている(2026-09-07、`hl.doany.io`。認証は oauth2-proxy 前段)。** - 「何がインターネットに出ているか」の一枚は作らない(2026-09-09 判断)。`kubectl get httproute,grpcroute -A` が正確で早い。 +- **k8up の失敗通知(2026-09-08)** ── [apps/k8up/notify.yaml](apps/k8up/notify.yaml) が日次で Matrix の `server` へ。[apps/k8up/README.md](apps/k8up/README.md)「失敗したときに気づけるようにする」。`restic check` も [schedules.yaml](apps/k8up/schedules.yaml) に +- **R2 のずれ検知(2026-10-05、初回から一致)** ── [docs/decisions.md](docs/decisions.md)「バックアップに何を含めるか」 +- **`talosctl upgrade` / `upgrade-k8s` の手順(2026-09-08)** ── [talos/README.md](talos/README.md)「上げ方 / 当て直し方」。`upgrade-k8s` は bootstrap 層の当て直しも兼ねる +- **通信の可視化は Hubble で足りる(2026-09-07、`hl.doany.io`)。** 公開しているものの一覧は作らない ── [docs/decisions.md](docs/decisions.md)「公開経路(HTTPRoute)をどこに置くか」 ## 済んだこと(記録) 1 行ずつ。詳しいことはリンク先にある。 -- **Phase 0 — k3s のまま restic 化(2026-09-06)。** `backup/k3s-backup` + systemd timer(毎日 04:00 JST)で - R2 の `doany-restic` へ、復元は `recovery/restore.sh` + `recovery/env.age`(パスフレーズ 1 つで戻る)。 - repo 内の秘密は SOPS + age。リハーサル 2 回([docs/restore-drill.md](docs/restore-drill.md))。 - ghcr の pull 認証をノード単位にして `imagePullSecrets` を全廃(decisions.md「ghcr の pull 認証」)。 - 旧方式(tar + rclone の `backup.sh` / `init.sh` / `setup-network.sh`)は削除済み。 -- **Phase 0.5 — `k3s/` を `deploy/` に改名(2026-09-06)。** 8 repo(denpa#66、lgtm#17、blog#20、yuzuriha#8、 - tamasagashi#63、worklog-cloud#106、xool#128、k3s-gitops#2)、`k3s-gitops` は `gitops` に改名、 - ApplicationSet は `deploy/argocd.yaml` だけを見る。 -- **Phase 1 — Talos の検証(2026-09-06〜07、本番に触らない)。** QEMU で v1.14.0 の起動・inlineManifests・ - デュアルスタック・etcd スナップショットからの復旧まで([docs/talos.md](docs/talos.md))。talhelper をやめて - `talosctl gen config` + パッチに。実機固有の確認(ドライバ、bond、静的 IPv6、`accept_ra: "2"`、wireguard)は - docs/talos.md「VM ブートドリル」。PSA のラベル(baseline は hostPort も弾く)は docs/talos.md「PSA のラベル」。 - アプリ層の HelmChart CRD を ArgoCD の Application に移した(decisions.md「HelmChart CRD から ArgoCD の Application へ」)。 - k8up を入れて論理バックアップを寄せた([apps/k8up/README.md](apps/k8up/README.md))。 -- **Phase 1.5 — k3s のまま Cilium + Gateway API に寄せた(2026-09-06〜07)。** CNI 交換 → ServiceLB をやめて hostPort → - cert-manager と Gateway → 切り替え本番 → Traefik 撤去 → Gateway を hostNetwork に。**Talos で後から替えるのが - 高いのは CNI だけ**なので先に済ませた。経緯・詰まった点・障害(AdGuard の DoH)は - decisions.md「ルーティングの選定」以下。Traefik を消した時点で切り戻しの道は閉じている。 -- **Phase 1.7 — 運用まわり(2026-09-07)。** Cilium の Helm 値を git に入れた(ConfigMap の直接編集が Helm に残っておらず、 - 素の `helm upgrade` で公開 Web が全部落ちる状態だった。[bootstrap/cilium/README.md](bootstrap/cilium/README.md))。 - Entra の認可をアプリロールにして redis を廃止([docs/entra.md](docs/entra.md))。依存更新の自動マージの範囲 - (decisions.md「依存の更新をどこまで自動で入れるか」)。Talos の更新は**検知は自動、適用は手動** - ([talos/versions.yaml](talos/versions.yaml))。R2 の無料枠超過の原因を除外(decisions.md「バックアップに何を含めるか」)。 -- **復元リハーサルで出た宿題(2026-09-07〜08、全部片付いた)。** Cilium 構成での再実施は 3 問とも Yes - ([docs/restore-drill.md](docs/restore-drill.md))。`Schedule` に `SkipDryRunOnMissingResource=true` - (apps/k8up/README.md「CRD がまだ無いクラスタで apps を止めないこと」)。`k8up-global` は `restore.sh` が作り直し、 - Talos 用には Infisical の `/k8up/k8up-global` に移した([apps/k8up/k8up-secrets.yaml](apps/k8up/k8up-secrets.yaml))。 -- **PVC を守る仕掛けをやめた(2026-09-07、`local-path-retain` の撤去は 2026-10-05)。** decisions.md「PVC を守る仕掛けをやめた」。 +- **Phase 0 — k3s のまま restic 化(2026-09-06)。** R2 の `doany-restic` へ日次、復元は `recovery/` のパスフレーズ 1 つ、リハーサル 2 回([docs/restore-drill.md](docs/restore-drill.md))。pull 認証はノード単位(2026-09-15 から fj.doany.io だけ。decisions.md「ghcr の pull 認証」) +- **Phase 0.5 — `k3s/` を `deploy/` に改名(2026-09-06)。** 8 repo、`k3s-gitops` は `gitops` に、ApplicationSet は `deploy/argocd.yaml` だけを見る +- **Phase 1 — Talos の検証(2026-09-06〜07)。** QEMU で起動から etcd 復旧まで([docs/talos.md](docs/talos.md)「VM ブートドリル」「PSA のラベル」)、HelmChart CRD を Application に(decisions.md「HelmChart CRD から ArgoCD の Application へ」)、k8up を導入([apps/k8up/README.md](apps/k8up/README.md)) +- **Phase 1.5 — k3s のまま Cilium + Gateway API に(2026-09-06〜07)。** **Talos で後から替えるのが高いのは CNI だけ**なので先に。経緯と障害は decisions.md「ルーティングの選定」。Traefik を消した時点で切り戻しの道は閉じている +- **Phase 1.7 — 運用まわり(2026-09-07)。** Cilium の Helm 値を git に([bootstrap/cilium/README.md](bootstrap/cilium/README.md))、Entra をアプリロールに([docs/entra.md](docs/entra.md))、自動マージの範囲(decisions.md「依存の更新をどこまで自動で入れるか」)、Talos の更新は検知だけ自動([talos/versions.yaml](talos/versions.yaml)) +- **復元リハーサルの宿題(2026-09-07〜08、全部片付いた)** ── [docs/restore-drill.md](docs/restore-drill.md)、apps/k8up/README.md「CRD がまだ無いクラスタで apps を止めないこと」、[apps/k8up/k8up-secrets.yaml](apps/k8up/k8up-secrets.yaml) +- **PVC を守る仕掛けをやめた(2026-09-07、`local-path-retain` の撤去は 2026-10-05)** ── decisions.md「PVC を守る仕掛けをやめた」 diff --git a/apps/cloudflare-ddns/application.yaml b/apps/cloudflare-ddns/application.yaml index af7a2f4..95acc35 100644 --- a/apps/cloudflare-ddns/application.yaml +++ b/apps/cloudflare-ddns/application.yaml @@ -3,7 +3,7 @@ # # CR を消すと helm がアンインストールされる。helm-controller は # `wrangler.cattle.io/on-helm-chart-remove` finalizer で削除ジョブを走らせるので、 -# 移すときは先に finalizer を外してから CR を消す(手順は docs/decisions.md)。 +# 移すときは finalizer ではなく helm のリリース Secret を先に消す(finalizer は外してもすぐ付け直される。手順は docs/decisions.md「HelmChart CRD から ArgoCD の Application へ」)。 apiVersion: argoproj.io/v1alpha1 kind: Application metadata: @@ -34,7 +34,8 @@ spec: # CNAME なので勝手に付いてくる。 domains: doany.io proxied: "true" - # AAAA は bond0 の static な ::2(k3s のノード IP でもある)を使う。 + # AAAA は bond0 の static な ::2(k3s のノード IP でもある)を使うつもり。ただし SLAAC のアドレスがあるとそちらを選ぶ + # (Ubuntu 期はそうなっていた。2026-10-17。Talos では bond0 の autoconf=0 で SLAAC を付けない。docs/router.md)。 # 外向きの probe が拾う一時アドレスではない。この provider は # インタフェースを読んで temporary / deprecated を飛ばす。 ip6Provider: local.iface.stable:bond0 diff --git a/apps/k8up/README.md b/apps/k8up/README.md index c59ed9e..6af111d 100644 --- a/apps/k8up/README.md +++ b/apps/k8up/README.md @@ -13,39 +13,36 @@ k8up は Pod として同じ restic リポジトリ(Cloudflare R2 の `doany-res git に載る。代わりに **operator の環境変数(グローバル設定)**に寄せて、`backend` を持たない `Backup` はそこへ落ちるようにしてある。 -Secret は手で作る(値が手元を通らないよう、ホストの env ファイルから割って入れる): +**Secret `k8up-global` の正本は Infisical**(`/k8up/k8up-global`)。[k8up-secrets.yaml](k8up-secrets.yaml) の +`InfisicalSecret` が作る。キーは `endpoint` / `bucket` / `accessKeyId` / `secretAccessKey` / `repoPassword` / +`mattermostWebhook` の 6 つで、**足りないとバックアップが全部落ちる**(operator は Infisical にあるものだけを書く)。 +2026-09-08 に入れて、手で作っていた Secret と 6 つとも突き合わせた。**手で作るのは災害復旧のときだけ**(下)。 + +**値を変えたら `kubectl -n k8up rollout restart deploy/k8up`。** 他のアプリのような +`auto-reload` の注釈は使えない ── あれは Deployment 自身の注釈に要るが、 +**k8up の chart は `podAnnotations` しか公開していない**。理由と例外 +(`mattermostWebhook` は再起動不要)は [k8up-secrets.yaml](k8up-secrets.yaml) に書いた。 + +**災害復旧のときだけ順番が逆になる。** この Secret の中身は R2 の資格情報そのもので、 +Infisical を戻すための `infisical-postgresql.sql` もその R2 にある。まっさらから戻すときは +`recovery/env.age` を手で復号して `kubectl create secret` する ── **平常時だけ Infisical が正本**、 +という二段構え。`recovery/` はその一度きりのために残す。 ```shell -sudo sh -c '. /etc/k3s-backup/env +# env.age の中身は KEY=値 の env ファイル。値を画面にもディスクにも出さずに割って入れる +( set -a; eval "$(age -d recovery/env.age)"; set +a raw="${RESTIC_REPOSITORY#s3:}" - k3s kubectl -n k8up create secret generic k8up-global \ + kubectl -n k8up create secret generic k8up-global \ --from-literal=endpoint="${raw%/*}" \ --from-literal=bucket="${raw##*/}" \ --from-literal=accessKeyId="$AWS_ACCESS_KEY_ID" \ --from-literal=secretAccessKey="$AWS_SECRET_ACCESS_KEY" \ --from-literal=repoPassword="$RESTIC_PASSWORD" \ --from-literal=mattermostWebhook="$MATTERMOST_WEBHOOK" \ - --dry-run=client -o yaml | k3s kubectl apply -f -' + --dry-run=client -o yaml | kubectl apply -f - ) ``` -`/etc/k3s-backup/env` は `recovery/restore.sh` が復元する。**復元時はスクリプトが -この Secret も作り直すので手作業は要らない**(2026-09-07 に追加。それまでは作られず、 -operator が `CreateContainerConfigError` で上がらなかった)。 -**Talos に移ったら Infisical に移す**(ホストに env ファイルが無くなるため)。 -受け皿は [k8up-secrets.yaml](k8up-secrets.yaml)。**Infisical の `/k8up/k8up-global` に -6 つのキーを入れてから**でないと当ててはいけない(operator は Infisical にあるものだけを -書くので、足りないと**バックアップが全部落ちる**)。**2026-09-08 に入れて、 -別名の Secret に一度出してから本物と 6 つとも突き合わせて確認した。** - -**値を変えたら `kubectl -n k8up rollout restart deploy/k8up`。** 他のアプリのような -`auto-reload` の注釈は使えない ── あれは Deployment 自身の注釈に要るが、 -**k8up の chart は `podAnnotations` しか公開していない**。理由と例外 -(`mattermostWebhook` は再起動不要)は [k8up-secrets.yaml](k8up-secrets.yaml) に書いた。 - -**災害復旧のときだけ順番が逆になる。** この Secret の中身は R2 の資格情報そのもので、 -Infisical を戻すための `infisical-postgresql.sql` もその R2 にある。まっさらから戻すときは -`recovery/env.age` を手で復号して `kubectl create secret` する ── **平常時だけ Infisical が正本**、 -という二段構え。`recovery/` はその一度きりのために残す。 +k3s 期は `recovery/restore.sh` がこれをやる(2026-09-07 から。それまでは作られず operator が `CreateContainerConfigError` だった)。 `mattermostWebhook` は [notify.yaml](notify.yaml) が使う。**新しい秘密を増やさないために `k8up-global` に相乗りさせている** ── 値は同じ env ファイルの `MATTERMOST_WEBHOOK` で、 @@ -94,15 +91,7 @@ FAILED を出している**ので、そこで気づけなかった、という 成功時も 1 行投げる。**通知の仕組み自体が生きていることの確認**になる(ホストのスクリプトと同じ考え方)。 -本番のリポジトリで実際に流して確かめた(2026-09-08): - -``` -追跡 32 系統 / 上位の階層が新しいので無視 2 件 - 無視 29.1h main/etc/rancher/k3s/config.yaml - 無視 29.1h main/etc/rancher/k3s/registries.yaml - -[k8up] OK ✅ (32 系統すべて 25 時間以内) -``` +本番のリポジトリで流して確かめた(2026-09-08。32 系統すべて OK、上の階層で取れている 2 件を無視)。 ## やめたアプリの残り(2026-09-19) @@ -127,7 +116,7 @@ repo をロックする。gitops には置かない(Argo CD が毎回流して `kubectl create -f -` に食わせる。1 時間で自分で消える(`ttlSecondsAfterFinished`)。 ```sh -sudo k3s kubectl -n k8up create -f - <<'JOB' +kubectl -n k8up create -f - <<'JOB' apiVersion: batch/v1 kind: Job metadata: @@ -164,7 +153,7 @@ spec: - name: RESTIC_PASSWORD valueFrom: {secretKeyRef: {name: k8up-global, key: repoPassword}} JOB -sudo k3s kubectl -n k8up logs -f job/k8up-retire +kubectl -n k8up logs -f job/k8up-retire ``` **2026-10-06 に mattermost・erpnext・zulip も `retired` にした**(チャットは Mattermost → Zulip → Matrix と移して畳み、 @@ -192,8 +181,7 @@ PVC のファイルは `k8up.io/backup: "true"` を付けたものだけ取る `"false"` と**注釈が無いのは同じ意味**(どちらも取らない)。同じファイルに `"true"` が 並んでいるときだけ、取らない側にも `"false"` を書いて意図を見えるようにしてある。 -いまホストの `backup/k3s-backup` と二重に取っているが、restic は内容で重複を除くので -実際に増えるのはメタデータだけ。**Talos に移る時点でホスト側を畳む**。 +ホストの `backup/k3s-backup` と二重に取っていても、restic は内容で重複を除くので増えるのはメタデータだけ。 Infisical の Postgres は**クラスタで一番失えないデータ**(全アプリの秘密)。DB そのものは `ENCRYPTION_KEY` で暗号化されているのでダンプだけ手に入っても読めない ── **復元にはその鍵も要る**。 @@ -207,16 +195,7 @@ Infisical 本体は `bootstrap/` に居るが、Schedule は `apps/` に置い 実際に `Fatal: Please specify repository location` で落ちた。 グローバル設定に寄せる(`backend` を書かない)のが正解。 - `backend` を持たない `Backup` が R2 へ書けることを、使い捨ての PVC で確認済み(確認後に削除)。 -- **ホストのスクリプトとは衝突しない。** 実測したスナップショットの内訳(当時。erpnext と mattermost はもう無い): - - ``` - 1 host=erpnext tags=['k8up'] path=/erpnext-gunicorn.sql - 1 host=infisical tags=['k8up'] path=/infisical-postgresql.sql - 1 host=mattermost tags=['k8up'] path=/mattermost-postgres.sql - 4 host=main tags=['k3s-host'] path=/etc/... - ``` - - **k8up の `hostname` は namespace**、ホストのスクリプトは `main`。 +- **ホストのスクリプトとは衝突しない。** **k8up の `hostname` は namespace**(タグ `k8up`)、ホストのスクリプトは `main`(タグ `k3s-host`)。 `restic/cli/prune.go` は `--host=<自分の namespace>` を必ず渡すので、 **prune は他の namespace にもホストのぶんにも届かない**。`tags: [k8up]` は二重の歯止め (`--tag` は `retention.tags` があるときだけ渡る)。 @@ -275,21 +254,14 @@ k8up.io/file-extension: .db ### netbird はサイドカーで取る 上流イメージ(`netbirdio/netbird-server`)には `sqlite3` が無く、あるのは perl だけ。 -こちらでは変えられないので、**`oven/bun:1.4.2-slim` のサイドカーを足して +こちらでは変えられないので、**bun の slim イメージ(版は [../netbird/deployment.yaml](../netbird/deployment.yaml)。[notify.yaml](notify.yaml) と同じ)のサイドカーを足して `k8up.io/backupcommand-container: backup` でそちらを指している**。 PVC は `readOnly: true` でマウントする。 `store.db` / `idp.db` / `events.db` の 3 つを `serialize()` して tar 1 本にまとめる。 同じ PVC の `GeoLite2-City`(65 MB)と `geonames`(7 MB)は**入れない** ── 起動時に落とし直せる。 -実際の PVC を読み取り専用でマウントして確認済み: - -``` -exit=0 bytes=911360 --rw-r--r-- root/root 745472 ./store.db --rw-r--r-- root/root 36864 ./events.db --rw-r--r-- root/root 122880 ./idp.db -``` +実際の PVC を読み取り専用でマウントして確認済み(3 ファイル入りの tar 約 0.9 MB が出た)。 **ハングした書き込みが残っていると失敗する。** 3 つとも rollback journal モードなので、 放置された `-journal` があると読み取り専用の接続はロールバックできず `SQLITE_READONLY_ROLLBACK` @@ -329,15 +301,7 @@ INFO backup finished {"new files": 0, "changed files": 0, "errors": 7} ### 直し方 Schedule 全部に `podSecurityContext: {runAsUser: 0, runAsGroup: 0}` を付けた。 -実測で `errors: 0` / `new files: 7` になることを確認済み: - -``` -$ kubectl -n adguardhome logs backup-perm-fix-probe-0-… -starting backup for folder {"foldername": "adguardhome-conf"} -backup finished {"new files": 7, "changed files": 0, "errors": 0} -starting backup for folder {"foldername": "adguardhome-work"} -backup finished {"new files": 6, "changed files": 0, "errors": 0} -``` +実測で `errors: 0`(adguardhome-conf が `new files: 7`)になることを確認済み。 **Talos の PSA `baseline` でも通る** ── root を禁じるのは `restricted` だけ。 @@ -387,21 +351,7 @@ spec: - スナップショット ID は `restic snapshots` で見る(`notify.yaml` の initContainer と 同じ形の使い捨て Job が手っ取り早い) -実測: - -``` -$ kubectl -n k8up-drill get restore -drill-lgtm-db Succeeded (18s) - -restoring snapshot 999784d1 of [/worklog-worklog.db] … to /restore -Summary: Restored 1 files/dirs (164.000 KiB) in 0:00 - -# 戻したものを開いてみる -file: worklog-worklog.db 167936 -integrity=ok -tables=13 users,identities,sessions,paywall_hits,… -rows=108 -``` +実測: worklog の DB(スナップショット `999784d1`)を捨ての namespace に戻し、18 秒で `Succeeded`。開いて `integrity=ok`、13 テーブル・108 行。 ### SQL のダンプは**先にロールを作る**(2026-09-08 に実測) @@ -409,17 +359,7 @@ rows=108 ロールが無いクラスタに流すと**そこだけ全部落ちる** ── データは入るが、所有者が postgres のままになる。 -当時の mattermost のダンプ(10.7 MB)を捨てクラスタに流して数えた: - -``` -== ロールを作らずに流したとき -130 role "mattermost" does not exist ← エラーはこの 1 種類だけ -118 テーブル / users 5 / posts 11922 ← データ自体は入る - -== 先に CREATE ROLE mattermost LOGIN してから -0 エラー -118 テーブル / users 5 / posts 11922 -``` +当時の mattermost のダンプで確かめた: ロール無しだと `role "mattermost" does not exist` が 130 件(データは入る)、先にロールを作ると 0 件。 なので順番は **ロール → データベース(`OWNER` 付き) → ダンプ**: @@ -441,7 +381,6 @@ infisical なら `pg_dump -U infisical -d infisicalDB` なので、ロール `in ## ファイルの PVC の扱い ホストの `backup/k3s-backup` が見ていたぶんを k8up に寄せた(2026-09-08 に全部済んだ)。 -残っているのはホストのスクリプトを畳むことだけで、それは Talos に移る時点。 | PVC | 中身 | どうするか | | --- | --- | --- | diff --git a/apps/netbird/deployment.yaml b/apps/netbird/deployment.yaml index 34fed8a..d344e8a 100644 --- a/apps/netbird/deployment.yaml +++ b/apps/netbird/deployment.yaml @@ -1,5 +1,5 @@ # NetBird の combined サーバ + ダッシュボード。VPN はこれ 1 本(wg-easy は 2026-09-07 に撤去)。 -# 選定の理由と比較は docs/decisions.md「VPN を何にするか」。 +# 選定の理由と比較は docs/decisions.md「VPN を wg-easy から NetBird にした」。 apiVersion: v1 kind: PersistentVolumeClaim metadata: diff --git a/backup/README.md b/backup/README.md index bddbea2..0bae15d 100644 --- a/backup/README.md +++ b/backup/README.md @@ -1,5 +1,7 @@ # backup +**k3s 期だけ。Talos 移行で廃止**(ホストにシェルが無い)。後継は [`../apps/k8up/`](../apps/k8up/)。 + ホストを丸ごと restic で Cloudflare R2 に取る。**毎日 04:00 JST**、systemd timer(`k3s-backup.timer`)。 戻し方は [`../recovery/`](../recovery/)、設計の理由は [`../docs/decisions.md`](../docs/decisions.md)。 diff --git a/bootstrap/README.md b/bootstrap/README.md index 64a0cdc..71b86d3 100644 --- a/bootstrap/README.md +++ b/bootstrap/README.md @@ -9,7 +9,7 @@ `infisical/secrets.yaml`、`infisical/helmchart.yaml`、`argocd/helmchart.yaml`、 `cert-manager/cloudflare-secret.yaml`。鍵は [`recovery/sops-age.key.age`](../recovery/)。 -いま動いているのは Ubuntu 26.04(NetworkManager、netplan バックエンド、TZ は UTC)の暫定構成で、 +いま動いているのは Ubuntu 26.04(netplan → systemd-networkd。2026-09-24 までは NetworkManager。TZ は UTC)の暫定構成で、 最終形は Talos Linux。進捗は [ROADMAP.md](../ROADMAP.md)、なぜそうしたかは [docs/decisions.md](../docs/decisions.md)。 Talos では**この層のほぼ全部が machine config の `inlineManifests` に載る**(平文の残りは引き続き GitHub Actions。 [docs/decisions.md](../docs/decisions.md)「層の分け方」)。 @@ -31,13 +31,10 @@ Talos では**この層のほぼ全部が machine config の `inlineManifests` **argocd と infisical の版は 2026-09-08 に固定した**(argo-cd 10.8.1 / infisical-standalone 1.10.0)。それまでは `HelmChart` CR に `version:` が無く、 コントローラが**そのときの最新**を入れていた ── 作り直すと別の版になるし、Renovate も追えない。 -**Renovate はこの 2 ファイルを見ない**(`renovate.json` の `ignorePaths`)。 -2026-09-08 に argo-cd 10.8.2 の PR が**暗号文の中の `version: 10.8.1` を平文置換して -自動マージされ、MAC が壊れて `sops -d` が通らなくなった。** 暗号化したファイルの -1 行を書き換える以上、どんな正規表現でも安全にはならない。**版の追跡はあきらめて、 -再現性(固定されていること)を取っている** ── 新しい版は人が見る。 +**Renovate はこの 2 ファイルを見ない**(`renovate.json` の `ignorePaths`)。2026-09-08 に暗号文の中の版を平文置換した PR が +自動マージされ、`sops -d` が通らなくなったため。**版は固定して人が上げる**([docs/decisions.md](../docs/decisions.md)「依存の更新をどこまで自動で入れるか」)。 -**中を触るときは平文で書き足さないこと**(上の MAC の話)。位置を選びたいので `sops set` ではなくこの順でやった: +中を触るときは、位置を選びたいので `sops set` ではなくこの順でやった: ```shell f=bootstrap/argocd/helmchart.yaml @@ -49,8 +46,7 @@ sops -d "$f" | diff /tmp/before.yaml - # 足した 1 行だけが出るこ rm -f /tmp/before.yaml ``` -**`git diff` では代用できない** ── `mac` と `lastmodified` は中身が変わらなくても -毎回書き換わるので、暗号文の差分を見ても何も分からない。**復号した中身どうしを比べる。** +**`git diff` では代用できない**(`mac` と `lastmodified` が毎回変わる)。**復号した中身どうしを比べる。** ## git と live がずれていないか @@ -162,17 +158,16 @@ ArgoCD の tracking-id までコピーされると、ArgoCD がその `Secret` 見なして sync のたびに prune する(復元リハーサル 2026-09-06 で発覚)。 `Prune=false` も一緒にコピーさせて、prune の対象から外す。 -**`SkipDryRunOnMissingResource=true`** ── **まっさらなクラスタでは CRD がまだ無い。** -ArgoCD は同期の前に dry-run するので、`secrets.infisical.com/v1alpha1` が引けないと -**`apps/` の同期が丸ごと落ちる。** CRD を入れる当の Application -(`apps/infisical-operator/`)が同じ同期の中にいるので抜けられない -(2026-09-08 のドリル 5 回目。[../docs/talos.md](../docs/talos.md))。 -**k8up の `Schedule` と同じ形の穴**で、あちらは 2026-09-07 に対処済みだった。 +**`SkipDryRunOnMissingResource=true`** ── **まっさらなクラスタでは CRD がまだ無く、`apps/` の同期が丸ごと落ちる** +(CRD を入れる `apps/infisical-operator/` が同じ同期の中にいる。2026-09-08 のドリル 5 回目)。 +仕組みは k8up の `Schedule` と同じで、説明は [apps/k8up/README.md](../apps/k8up/README.md)「CRD がまだ無いクラスタで apps を止めないこと」。 **`bootstrap/` の `InfisicalSecret` には要らない**(`argocd/oidc-secret.yaml`・ `auth/secrets.yaml`・`infisical/smtp-secret.yaml`)。**ArgoCD が見ていない**ためで、 -これらは GitHub Actions が `kubectl apply` で当てる ── app-of-apps は -`bootstrap/argocd` の `repos.yaml` **だけ**を `directory.include` で拾っている。 +これらは GitHub Actions が `kubectl apply` で当てる ── app-of-apps(Argo CD の chart が作る `apps` Application。 +`argocd/helmchart.yaml` の中、prune / selfHeal 付き)は `bootstrap/argocd` の `repos.yaml` **だけ**を `directory.include` で拾っている。 +chart と一緒に入るので、**まっさらなクラスタでも CI を待たずに ApplicationSet が走りはじめる**。CI の `bootstrap-apply` も +同じファイルを当てるが、中身が同じなので衝突しない。 ### アプリ側の書き方 diff --git a/docs/decisions.md b/docs/decisions.md index 7a44e0c..381cef8 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -10,15 +10,36 @@ | --- | --- | | restic リポジトリ形式・パスフレーズ・バケット | ホスト側 backup スクリプト + systemd timer | | local-path provisioner の PV ディレクトリ構造(`pvc-__`。**親のパスは変わる** ── k3s は `/var/lib/rancher/k3s/storage`、Talos は user volume の `/var/mnt/local-path`) | state.db の sqlite `.backup` スナップショット | -| `bootstrap/` の中身(Argo CD・Infisical・Cilium・cert-manager・auth の定義)。Talos では machine config の `cluster.inlineManifests` に載せる(下記) | | -| 公開の復元手順(秘密を含まない、`curl \| sh` できる)という**性質** | setup-network.sh / k3s-server-config.yaml / init.sh / **restore.sh**(k3s 期専用。Talos 期は machine config + etcd + k8up の Job に置き換わる) | +| `bootstrap/` の中身(Argo CD・Infisical・Cilium・cert-manager・auth の定義)。Talos では、入れるもの(Helm と SOPS の Secret)は machine config の `cluster.inlineManifests`、平文で当てるものは CI(下記「層の分け方」) | | +| 公開の復元手順(秘密を含まない、`curl \| sh` できる)という**性質** | setup-network.sh / k3s-server-config.yaml / init.sh / **restore.sh**(k3s 期専用。Talos 期は machine config + git からの再構築 + k8up の Job に置き換わる) | | Infisical のデータ(Postgres PVC)と `ENCRYPTION_KEY` | HelmChart CRD(helm.cattle.io)、k3s 同梱の Traefik / ServiceLB | | ArgoCD + ApplicationSet(repos.yaml)+ 各 repo の `deploy/argocd.yaml` | firewalld 無効化などホストの手作業 | | | **Traefik**(k3s 同梱だから使っていただけ。IngressRoute / Middleware(forward-auth)/ mydnschallenge ACME は Talos では持ち込まない) | -Talos ではホストに触れないので、「k8s オブジェクト」は etcd スナップショット、「ホスト設定」は machine config(git 管理)、 -「PV データ」だけが従来どおりバックアップツールの仕事になる。**ツール選定は PV データの部分にしか効かない。** +Talos ではホストに触れないので、「k8s オブジェクト」は **git から ArgoCD で再構築**(etcd スナップショットは近道)、 +「ホスト設定」は machine config(git 管理)、「PV データ」だけが従来どおりバックアップツールの仕事になる。 +**ツール選定は PV データの部分にしか効かない。** +## 移行の順番: Talos を先に、Z440 は後(2026-10-17) + +当初は「HP Z440 へ Ubuntu のまま移してから Talos」の順だった。**逆にした:先にいまの DL360 の上で k3s → Talos +(Phase 2、[migration-day.md](migration-day.md))、そのあと Talos のまま Z440 へ(Phase 2.5、[z440-migration.md](z440-migration.md))。** + +- **Z440 は単体の GPU が無いと POST しない**(Xeon E5 に内蔵 GPU は無い)ので、ヘッドレスで組めない。 + Intel Arc A310 を常設する(UEFI + CSM 無効が要る)。これが denpa の QSV / AV1 エンコードも兼ねる +- Z440 の遠隔管理は **AMT 9.x(電源と SOL だけ、KVM なし)**。一番危ない OS の入れ替えは、 + **iLO(10.0.0.3)で画面まで見える DL360** でやる +- Talos に移ってしまえば、箱の違いは schematic(GPU の extension、SOL 用の `console=`)と NIC の名前に閉じる + +## bond0 の MAC を固定する(2026-10-17) + +ルーターの IPv6 ホスト公開はノードを **bond0 の MAC(`e6:2c:1b:1a:a0:39`)で指している** +([router.md](router.md)「IPv6 はノードだけを公開する」)。Ubuntu では bond0 の MAC を固定しておらず、 +**2026-10-17 に再起動のあと `3a:43:95:a9:3e:b1` に変わっているのを見つけた**(このままだとエントリから外れる)。 + +- Ubuntu: netplan で固定した(ネットワークは 2026-09-24 から netplan → systemd-networkd。それまでは NetworkManager) +- Talos: [talos/patches/cluster.yaml](../talos/patches/cluster.yaml) を型付きの `BondConfig` / `LinkConfig` に移し、 + `hardwareAddr: e6:2c:1b:1a:a0:39` を書いた。**bond に固定値を与えるので、Talos への移行でも Z440 への移設でも変わらない** ## ツール選定(比較の結論) @@ -29,7 +50,7 @@ Talos ではホストに触れないので、「k8s オブジェクト」は etc | --- | --- | --- | --- | | **restic** | systemd timer で直接 | **k8up**(restic の operator)か restic イメージの CronJob | ◎ 両期で同じ repo 形式。手動復元も `restic restore` 1 本 | | kopia | 同上 | Velero の node-agent が kopia だが Velero 固有のレイアウトで、kopia CLI からの手動復元が素直でない | ○ 単体性能は上だが後継が繋がらない | -| Velero | 使えない(ホスト対象外) | k8s オブジェクトも一緒に取れる | △ Talos では etcd スナップショットと役割が被り、復元に Velero 自体の導入が要る | +| Velero | 使えない(ホスト対象外) | k8s オブジェクトも一緒に取れる | △ Talos では git からの再構築と役割が被り、復元に Velero 自体の導入が要る | | k8up 単体 | 使えない | ○ | k3s 期のホスト側(state.db、`/etc`)を別途どうにかする必要がある | | tar + rclone crypt | ○ | 使えない | △ 変更最小だが Talos に持っていけない、整合性 `check` が無い | | borg / duplicacy | ○ | 使えない or ライセンス | × | @@ -168,10 +189,8 @@ CI からは当てられない。一方 machine config は**もともと SOPS **Helm を実行する場所がクラスタの外(手元の生成時)だけ**になり、クラスタ内に Helm コントローラを 持たなくて済む。版の追従は `talos/versions.yaml` と同じく Renovate に見せる。 -**当初の見立てとのずれ。** 最初は「machine config が inlineManifests を持てば手で apply する層は消える」 -と見ていたが、外れた。実際は上の二段になり、平文のぶんは GitHub Actions が当てる。 -「inlineManifests は create-once で reconcile されない」と書いたのも半分誤りで、 -`talosctl upgrade-k8s` を通せば更新も削除もされる(2026-09-08 に VM で実測。[talos.md](talos.md))。 +**当初の見立てとのずれ**: 「手で apply する層は消える」「inlineManifests は reconcile されない」はどちらも外れ +(上の二段になり、`talosctl upgrade-k8s` を通せば更新も削除もされる。[talos.md](talos.md))。 ## machine config と Cilium の chart をどう連動させるか(2026-09-08 決定) @@ -224,7 +243,7 @@ LoadBalancer は k3s 組み込みの ServiceLB(Klipper)。 **Talos で後から替えるのが高いのは CNI だけ**なので、そこを先に決める。Ingress コントローラ・LB・証明書は Gateway API に寄せてあれば後から差し替えられる。単一ノードのうちは Cilium は過剰に見えるが、 -ハイブリッド化の見込みがあるならここで払うのが一番安い。**VM で実際に組んで動くことを確認済み**(下記)。 +ハイブリッド化の見込みがあるならここで払うのが一番安い。**VM で実際に組んで動くことを確認済み**(2026-09-06)。 | | 選択 | 置き換わるもの | | --- | --- | --- | @@ -239,47 +258,18 @@ Gateway API に寄せてあれば後から差し替えられる。単一ノー Cookie に載せる方式は 4096 バイトの壁に当たる。oauth2-proxy なら Cookie はセッション ID だけなのでトークン長に依存しない。 これは実装選択と独立に効く判断。 -### 検証結果(2026-09-06、QEMU/KVM の k3s で実機同等の CIDR) - -`flannel-backend: none` + `disable-kube-proxy` + `disable: [servicelb, traefik]` で k3s を入れ、Cilium 1.20.1 を Helm で導入。 +**Cilium 1.20 は Gateway API v1.6.1 の CRD を要求する**(backendtlspolicies / tlsroutes を含む)。古いと +GatewayClass が `Waiting for controller` のまま止まる。上書きは `--server-side --force-conflicts` と cilium-operator の再起動。 -- `KubeProxyReplacement: True`、デュアルスタックの IPAM(IPv4 `10.42.0.0/24` / IPv6 `fd42::/64`) -- **GatewayClass `cilium` が Accepted、Gateway が Programmed**、`CiliumLoadBalancerIPPool` から - LoadBalancer Service に IP が払い出される(ServiceLB の代替になる) -- **HTTPRoute 経由で実際に HTTP 200**(Host 不一致は 404)。L2 announcement も動作 - -**詰まった点**: Cilium 1.20 は **Gateway API v1.6.1** の CRD を要求する。v1.4.0 を入れていると -operator が `Required GatewayAPI resources are not found` を出して GatewayClass が `Waiting for controller` のまま止まる。 -必要なのは gatewayclasses / gateways / httproutes / referencegrants / grpcroutes / **backendtlspolicies** / **tlsroutes** -(v1.6.1 では TLSRoute が standard チャネルに入っている)。本番では k3s の Traefik が v1.5.1 を入れていたので -`--server-side --force-conflicts` で上書きし、**cilium-operator の再起動**も要った。 +### 本番での実施結果(段階 1、2026-09-06) -**メモの訂正**: 引き継ぎメモには「Cilium は TLSRoute のみ。TCPRoute / UDPRoute は非対応」とあったが、 -**1.20 のドキュメントは TCPRoute / UDPRoute を optional but supported として挙げていて、CRD も適用できた**。 -(ただし 3proxy は TCPRoute では移せなかった。下の「3proxy(TLS 終端 + 素の TCP)」) +k3s のまま flannel と kube-proxy を外して Cilium 1.20.1 を入れた(30 分ほど)。詰まりは 2 つ: -### 本番での実施結果(段階 1、2026-09-06) +- **flannel の残骸**(`flannel.1` / `flannel-v6.1` / `cni0`)が Cilium の VXLAN と衝突する。`ip link delete` で消す(conflist の退避だけでは足りない) +- **`terminationGracePeriodSeconds` が長い Pod**(denpa と tuner-agent、6 時間)は Recreate と組むと戻らない。`--grace-period=0 --force` で消す -k3s の config に `flannel-backend: none` / `disable-network-policy` / `disable-kube-proxy` を足して再起動し、 -Cilium 1.20.1 を Helm で導入。**Traefik と Ingress はこの段階では触っていない。** - -- ノードが Ready に戻り、`KubeProxyReplacement: True`、IPAM はデュアルスタックで 45 アドレス払い出し、 - 全 50 Pod が Running、外形も全サイト 200/302。**所要は 30 分ほど**(うち大半は下の 2 つの詰まり) -- **詰まり 1: flannel の残骸が Cilium の VXLAN と衝突する。** `flannel.1` / `flannel-v6.1` / `cni0` が - インターフェースとして残っていると、Cilium が `cilium_vxlan` を作れず `address already in use` で - datapath 初期化が延々とリトライする。Pod は `FailedCreatePodSandBox` で止まったまま。 - `ip link delete` で 3 つ消したら即座に復旧した。**CNI 設定ファイル(`10-flannel.conflist`)を退避するだけでは足りない。** -- **詰まり 2: `terminationGracePeriodSeconds` が長い Pod は Recreate 戦略と組み合わさると復帰しない。** - denpa と tuner-agent(21900 秒 = 6 時間)が Terminating のまま残り、通信は既に死んでいるのに - 新しい Pod が作られない。**強制削除(`--grace-period=0 --force`)が要る。** 猶予は「録画を最後まで流す」ためだが、 - CNI を抜いた時点でネットワークは死んでいるので待つ意味は無い。 -- **残った差異: ノード自身と Pod から `10.10.0.4`(eno4 の LAN IP)に届かない。** LAN の他のマシンからは届く。 - AdGuard の split-horizon でこの IP に解決される名前だけ、ノード内から引けなくなった。 - ServiceLB(klipper)の hostPort と Cilium の socket-LB の組み合わせが原因と見られる。 - **段階 2 でほぼ解消した**(2026-09-07 実測)。hostPort と Gateway の hostNetwork に寄せた結果、 - Pod からは `10.10.0.4` のどのポートにも届く。ノード自身から届かないのは **80/443 だけ**で、 - これは Gateway の nodePort が L7LB リダイレクトとして載っているため(同上の `nodePort`)。 - そのリダイレクトは**外から来る通信の経路そのもの**なので外せない。 +**ノード自身からは 80/443 に届かない。** Gateway の nodePort が L7LB リダイレクトとして載っているためで、 +それが外から来る通信の経路そのものなので外せない(他のポートと、Pod からは届く)。 ### LoadBalancer をどう置き換えるか(2026-09-06 決定・実施済み) @@ -307,87 +297,47 @@ ServiceLB は**ノード自身の IP**(`10.0.0.2` / `10.10.0.4` / `240f:6d:842b: 3. **hostPort と RollingUpdate は両立しない。** 新旧の Pod が同じホストポートを奪い合い、新しい方が Pending で止まる。 AdGuard と 3proxy は `strategy: Recreate` にして解決した(当時の Traefik は `deployment.kind: DaemonSet`)。 -### Gateway API の土台で分かったこと(2026-09-06) - -- CRD の版は上の「検証結果」の詰まった点を参照(v1.6.1 が要る)。 -- **Gateway はアドレスが付くまで `Programmed=False` のまま**で、Envoy にリスナーが載らない。 - 当初は LB-IPAM + L2 アナウンスでアドレスを払い出していたが、**`gatewayAPI.hostNetwork` に - したらノードの IP がそのまま `status.addresses` に入った**ので、どちらも外した(2026-09-07)。 -- cert-manager は `config.enableGatewayAPI=true` で Gateway の `cert-manager.io/cluster-issuer` 注釈を見る。 - Cloudflare のトークンは **cert-manager の namespace にも** Secret が要る(ClusterIssuer は自分の namespace しか読まない)。 +**Gateway はアドレスが付くまで `Programmed=False`** で、Envoy にリスナーが載らない。`gatewayAPI.hostNetwork` に +したらノードの IP がそのまま `status.addresses` に入ったので、LB-IPAM と L2 アナウンスは外した(2026-09-07)。 +cert-manager の Cloudflare トークンは **cert-manager の namespace にも** Secret が要る。 ### 3proxy(TLS 終端 + 素の TCP)は Cilium の Gateway では素直に移せない -Traefik では `IngressRouteTCP` が `HostSNI(px.doany.io)` で受けて **TLS をここで終端し、中身を素の TCP として -3proxy:3128 に流して**いた。Gateway API で同じことをしようとすると詰まる。 - -- `protocol: TLS` + `mode: Terminate` のリスナーに **TCPRoute は付けられない** - (`No matching listener protocol; route requires one of: [TCP]`) -- Cilium はそのリスナーに **TLSRoute しか許さず**、TLSRoute は本来 passthrough 用 - (`Listener not valid. None of the Allowed Route Kinds are supported.`) - -**なぜ Traefik では 443 のまま両立できていたのか。** Traefik は 443 のエントリポイントが 1 つあり、 -接続ごとに TLS の ClientHello を覗いて **SNI で HTTP ルーターと TCP ルーターに振り分けて**いた。 -Gateway API では TLS の扱いが**リスナーの属性**なので、同じポートに Terminate と Passthrough を同居させると -どちらとして扱うか決まらず **ProtocolConflict** になる(実際に試して 443 の `https` リスナーごと落ちた)。 - -**ただし仕様上は 443 のままでも書ける。** `protocol: TLS` + `mode: Terminate` のリスナーに TCPRoute を付ける形は -Traefik と同じ意味で、終端するリスナーが 1 つあるだけなので他の HTTPS リスナーとも衝突しない。 -**通らないのは Cilium がその組み合わせを実装していないから**で、ポートの制約ではない。 -Envoy Gateway はこれを実装しているので、443 のままにしたいなら選択肢になる。 +Traefik では `IngressRouteTCP` が `HostSNI(px.doany.io)` で TLS を終端し、中身を素の TCP として 3proxy:3128 に流していた。 +Gateway API では `protocol: TLS` + `mode: Terminate` のリスナーに TCPRoute を付ける形が同じ意味になるが、 +**Cilium はそのリスナーに TLSRoute しか許さない**(仕様上は書けるが実装が無い。Envoy Gateway は実装している)。 +同じ 443 に Terminate と Passthrough を同居させると **ProtocolConflict** になり、443 の `https` リスナーごと落ちた。 **採った形(2026-09-06): Gateway を使わず、Pod のサイドカーがホストのポートを直接受ける。** - -- 3proxy の Pod に nginx(stream)のサイドカーを足し、`3129` で TLS を終端して `127.0.0.1:3128` に渡す -- 証明書は cert-manager が `px.doany.io` で発行し、サイドカーがマウントする(Traefik 内蔵 ACME の置き換え) -- サイドカーは **hostPort 3129** で公開する。443 は Gateway が使うため。当初は 8444 だったが - (8443 は Mattermost calls が先に取っている)分かりにくいので、3proxy の平文 3128 の隣に移した(2026-09-07) -- **クライアント側の設定変更が要る**(`px.doany.io:443` → `px.doany.io:3129`)。 - SNI で振り分けるより**ポートで分ける方が構成として素直**なので、これを本採用とした(2026-09-06 判断)。 - 443 のままにしたい場合の代案は、px 専用の IP を LB-IPAM で払い出してルータ側で振り分けるか、 - Envoy Gateway に替えて TLS 終端リスナー + TCPRoute を使うか +3proxy の Pod に nginx(stream)のサイドカーを足し、**hostPort 3129** で TLS を終端して `127.0.0.1:3128` に渡す。 +証明書は cert-manager が `px.doany.io` で発行してマウントする。クライアントは `px.doany.io:3129` に向け直した ── +SNI で振り分けるより**ポートで分ける方が構成として素直**なので本採用。 ### クライアント IP(2026-09-07 確認、`Cluster` のままでよい) -Gateway の Service は `externalTrafficPolicy: Cluster`(Cilium の `gateway-api-service-externaltrafficpolicy`)。 -一般には `Cluster` は別ノードへ回すときに SNAT するが、**単一ノードでは backend が必ず同じノードに居るので -SNAT されない**。実際にクライアント IP は端まで届いている。 - -確かめ方(どちらも実測): - -- **denpa** は `TRUSTED_NETWORKS=10.10.0.0/16` と `ADDRESS_HEADER=x-forwarded-for` で動いている。 - 公開側から `dp.doany.io` を叩くと **401**、LAN から叩くと **200**。SNAT されていれば - X-Forwarded-For がノードの `10.10.0.4` になり、公開側からでも通ってしまう。**通らない**ので届いている。 -- **AdGuard の DoH** のクエリログに、Gateway 経由でも端末のグローバル IPv6 がそのまま残っている。 - サイドカーは `$http_x_forwarded_for` をそのまま渡し、AdGuard は `trusted_proxies` に - `127.0.0.0/8` を持っているので、loopback からの接続でもヘッダ側を採る。 +Gateway の Service は `externalTrafficPolicy: Cluster`。**単一ノードでは backend が必ず同じノードに居るので +SNAT されない**。実測: denpa は公開側からだと 401・LAN からだと 200(SNAT されていれば公開側も通る)、 +AdGuard の DoH のクエリログに端末のグローバル IPv6 が残る。 **ノードを足すときにやること**: `externalTrafficPolicy: Local` に変える (Cilium の Helm 値 `gatewayAPI.externalTrafficPolicy`)。そうしないと Envoy が居ないノードに 届いたぶんが SNAT されて、AdGuard のクライアント別統計と denpa の住所判定が壊れる。 -`Local` にすると Envoy の居るノードだけが応答するので、外側の振り分けもそれに合わせる -(いまは Envoy が hostNetwork でノードの 80/443 を直接掴んでいるので、宛先はノードの IP そのもの)。 +`Local` にすると Envoy の居るノードだけが応答するので、外側の振り分けもそれに合わせる。 ### AdGuard の DoH は backend が HTTPS でないと出ない(2026-09-07 障害) **切り替えの翌朝、ブラウザの名前解決が全部死んだ。** 原因は d.doany.io の backend を平文の 80 にしたこと。 AdGuard は **DoH(`/dns-query`)を HTTPS の口(443)でしか出さない**。80 は 404 を返す。 -Edge の「セキュア DNS」に `https://d.doany.io/dns-query` を入れていたので、DoH が死んだ時点で -そのブラウザからは何も引けなくなった。クエリログでは 06:34 を最後に DoH の問い合わせが止まっている -(DoT と plain は生きていたので、OS の名前解決は動いたまま**ブラウザだけ**死ぬ、という分かりにくい形になった)。 - -Traefik では `serversscheme: https` + `insecureSkipVerify` の `ServersTransport` で backend も HTTPS に繋いでいた。 -その置き換えを「クラスタ内なので平文でよい」と判断したのが誤り。**DoH は平文の口には出ない。** - -試して駄目だったもの: +Edge の「セキュア DNS」に DoH を入れていたので、DoT と plain は生きたまま**ブラウザだけ**死ぬ、という分かりにくい形になった。 +Traefik では backend にも HTTPS で繋いでいた。その置き換えを「クラスタ内なので平文でよい」と判断したのが誤り。 +**DoH は平文の口には出ない。** -| 案 | 結果 | +| 試して駄目だった案 | 結果 | | --- | --- | -| `BackendTLSPolicy`(Gateway API 標準) | **Cilium 1.20.1 は Accepted にするだけで実装していない。** Envoy の cluster に `transport_socket` が付かず、backend へ平文のまま繋ぐ。`sectionName` をポート名にしても番号にしても同じ。Web UI まで 400 になった | -| AdGuard の `tls.allow_unencrypted_doh: true` | **AdGuard は停止時に設定ファイルを書き直す**ので、Pod を止めずに足した値は消える。入れるには Pod を止めてから編集する必要がある | +| `BackendTLSPolicy`(Gateway API 標準) | **Cilium 1.20.1 は Accepted にするだけで実装していない。** backend へ平文のまま繋ぎ、Web UI まで 400 になった | +| AdGuard の `tls.allow_unencrypted_doh: true` | **AdGuard は停止時に設定ファイルを書き直す**ので、Pod を止めずに足した値は消える | -**採った形: nginx のサイドカーを足して `/dns-query` だけをそこへ回す。** サイドカーは同じ Pod の -`https://127.0.0.1:443` へ渡す(loopback なので証明書の検証はしない)。Web UI は 80 のまま。 +**採った形: nginx のサイドカーを足して `/dns-query` だけをそこへ回す**(同じ Pod の `https://127.0.0.1:443` へ)。 3proxy と同じ形なので、Cilium が `BackendTLSPolicy` を実装したら両方まとめて消せる。 **教訓**: 前段の置き換えでは「同じ URL が同じものを返すか」だけでなく、**その backend が @@ -395,83 +345,41 @@ Traefik では `serversscheme: https` + `insecureSkipVerify` の `ServersTranspo ### 認証は Cilium の Gateway では賄えない(2026-09-06 調査) -「Entra 側でトークンを小さくして Cilium の Envoy 機能で OIDC を賄う」案を検討したが、**Cilium には -Gateway API の OIDC が無い**。トークンの大きさ以前に機能が無い。 - -- `CiliumEnvoyConfig` で Envoy の `oauth2` フィルタを直接書く道は、公式に未サポートで - 「フィルタの型を解決できない」という報告がある(cilium/cilium#24848)。HTTPRoute と併用するのも難しい(#30587) -- HTTPRoute の `ExternalAuth`(GEP-1494)は**まだ未実装**で issue が開いたまま(cilium/cilium#45704)。 - 引き継ぎメモの「1.20 pre-release で入った」は誤り -- Keycloak で OAuth2 を組もうとした事例も未解決のまま(cilium/cilium#38889) - -したがって forward-auth が要るルートは、**oauth2-proxy を「前段のプロキシ」として置く** -(HTTPRoute → oauth2-proxy → アプリ)か、そのルートだけ Traefik か Envoy Gateway に残す。 -幸い forward-auth を使っているのは Traefik ダッシュボードと `sub` の 2 本だけで、 +**Cilium には Gateway API の OIDC が無い。** `CiliumEnvoyConfig` で Envoy の `oauth2` フィルタを書く道は未サポート +(cilium/cilium#24848、#30587)、HTTPRoute の `ExternalAuth`(GEP-1494)も未実装(cilium/cilium#45704)。 +forward-auth が要るルートは **oauth2-proxy を前段のプロキシとして置く**(HTTPRoute → oauth2-proxy → アプリ)。 アプリ側(ArgoCD、denpa、NetBird など)はそれぞれ自前で OIDC を持っている。 - -**Entra 側でトークンを小さくすること自体は独立して価値がある。** グループクレームを全部載せるのをやめて -**アプリロール**に切り替えると `roles: ["admin"]` の数十バイトで済む。将来 Envoy Gateway の内蔵 OIDC を -使う場合の前提にもなる。 +**Entra のトークンを小さくする(グループクレーム → アプリロール)こと自体は独立して価値がある**([entra.md](entra.md))。 ### 切り替え本番と Traefik の撤去(2026-09-06 / 2026-09-07) -Traefik の hostPort 80/443 を外すのと Gateway をそこへ出すのは**同時にしかできない**(片方が握っている間は -もう片方が Pending になる)。Gateway 側は `CiliumGatewayClassConfig` ではなく Service の 80/443 を -**nodePort として開く**形にした。これでノードのどのアドレスでも受けられる。 +Traefik の hostPort 80/443 を外すのと Gateway をそこへ出すのは**同時にしかできない**。Gateway 側は Service の 80/443 を +**nodePort として開く**形にした。詰まった点: -**詰まった点 2 つ:** +1. **Cilium は nodePort の範囲に入っている hostPort を張らない。** 範囲を `80-32767` に広げたら 853・8443・3proxy が + 一斉に落ちた。**`nodePort.range` を `80,443` の 2 つだけに絞る**ことで両立する。 +2. **Traefik を消したあとも Service に残った `externalIPs: [10.10.0.4]` が 80/443 を黒穴にする。** + `service.kubernetes.io/load-balancer-cleanup` finalizer を null にして消した。 +3. **自分で配っている chart の `IngressRoute` を取りこぼした。** denpa は chart で公開していたので `dp.doany.io` が 404 に。 + chart に `httpRoute.enabled` を足した(denpa#70)。gitops の `Ingress` を数えるだけでは足りない。 -1. **Cilium は nodePort の範囲に入っている hostPort を張らない。** nodePort の範囲を `80-32767` に広げたら、 - AdGuard の 853 と当時の Mattermost calls の 8443、3proxy の TLS ポートが一斉に落ちた。範囲を `80,443` の - 2 つだけに絞る(`nodePort.range`)ことで両立する。 -2. **Traefik を消したあとも Service に残った `externalIPs: [10.10.0.4]` が 10.10.0.4:80/443 を黒穴にする。** - DaemonSet を消しても Service は残り、Cilium はそのまま宛先無しの転送先を作り続ける。 - 削除は `service.kubernetes.io/load-balancer-cleanup` finalizer で止まるので、finalizer を null にして消した。 +2026-09-07 に Traefik のマニフェストと PVC `traefik-acme` を全部消した。ACME は cert-manager の ClusterIssuer `letsencrypt` が引き継いだ。 -**自分で配っている Helm chart の分を取りこぼした(2026-09-07)。** denpa は gitops 側の `Ingress` ではなく -**chart(`charts/denpa`)の `IngressRoute`** で公開していたので、Traefik の CRD を消した時点で -`dp.doany.io` が 404 になっていた。gitops の `Ingress` を数えるだけでは足りない。 -chart に `httpRoute.enabled` を足して移した(denpa#70)。`ingress.enabled` と `traefik.enabled` は -外にも配っている chart なので残してある。yosegaki の chart も同じ形にした。 -宅内向けの `dp.l.doany.io` は 2 段のワイルドカードなので、`*.l.doany.io` のリスナーと証明書を別に足した。 +### Traefik の sticky cookie に代替が無い(2026-09-07 調査) -翌 2026-09-07 に `Ingress` 15 本・`IngressRoute` 4 本・`Middleware` 4 つ・`IngressRouteTCP` 1 本と -PVC `traefik-acme` を削除し、gitops とアプリ 6 repo からマニフェストも消した。 -Traefik が持っていた ACME(`mydnschallenge`)は cert-manager の ClusterIssuer `letsencrypt` が引き継いでいる。 -`bootstrap/traefik/` にあった `sub-backend` の Service と EndpointSlice は `bootstrap/auth/sub-backend.yaml` へ、 -Cloudflare のトークンは `bootstrap/cert-manager/cloudflare-secret.yaml` へ移した。 +**Service の sticky cookie だけは Cilium 側に受け皿が無い。** 使っていたのは tamasagashi(`ts.doany.io`)1 本で、 +ローリング更新中に「HTML を新 Pod から受けたブラウザが、ハッシュ付きの CSS/JS を旧 Pod に取りに行って 404」を防ぐためのもの。 -### Traefik の sticky cookie に代替が無い(2026-09-07 調査) +- **Gateway API の session persistence(GEP-1619)は experimental チャネル限定**で、入っている v1.6.1 standard には無い +- **Cilium 1.20.1 が未実装。** cilium/cilium#48029 で main に入ったので **1.21 から** +- **`Service.spec.sessionAffinity: ClientIP` は Gateway 経路には効かない。** Envoy が EndpointSlice を直接見て振り分け、 + 送信元も全クライアント同じ(Gateway の Pod)になる -撤去した Traefik の Middleware / IngressRoute のうち、**Service の sticky cookie -(`traefik.ingress.kubernetes.io/service.sticky.cookie.*`)だけは Cilium 側に受け皿が無い。** -使っていたのは tamasagashi(`ts.doany.io`)1 本で、ローリング更新中に新旧 2 Pod が同時に配信される窓で -「HTML を新 Pod から受けたブラウザが、ハッシュ付きの CSS/JS を旧 Pod に取りに行って 404」を防ぐためのものだった。 - -- **Gateway API の session persistence(GEP-1619、`HTTPRoute.spec.rules[].sessionPersistence`)は使えない。** - 入っている Gateway API CRD は **v1.6.1 の standard チャネル**で、`httproutes` の v1 の rule には - `backendRefs / filters / matches / name / timeouts` しか無い。`BackendLBPolicy` の CRD も入っていない - (どちらも experimental チャネル限定) -- **Cilium 1.20.1 が未実装。** v1.20.1 タグのソースに `SessionPersistence` の文字が無く、ドキュメントのページも無い。 - 実装は cilium/cilium#48029 で 2026-09-01 に main へマージ(v1.20.1 のリリースは 2026-08-18)なので **1.21 から**。 - GatewayClass `cilium` の `status.supportedFeatures` にも該当 feature は載っていない -- **`Service.spec.sessionAffinity: ClientIP` は Gateway 経路には効かない。** 生成される - `CiliumEnvoyConfig/cilium-gateway-doany` の cluster は **type EDS**(lbPolicy 既定 = ROUND_ROBIN)で、 - Envoy が EndpointSlice の Pod IP を直接見て振り分ける。sessionAffinity を実装している ClusterIP のデータパスを通らない。 - 仮に通っても Envoy から見た送信元 IP は全クライアント同じ(Gateway の Pod)なので per-client の固定にならない。 - `service.cilium.io/affinity`(clustermesh の local/remote)や `service.cilium.io/lb-algorithm`(eBPF の random/maglev)も同じ理由で無関係 - -**いまは穴が開いたまま受け入れている。** tamasagashi は replicas 1・状態なしで、影響は更新中の 15 秒ほどの窓に限られ、 -アプリ側の SvelteKit の版チェックと Cloudflare の Browser Cache TTL 設定で緩和してある(danything/tamasagashi の `deploy/README.md`)。 -**Cilium 1.21 に上げるときに、Gateway API CRD を experimental チャネルへ入れ替えたうえで -tamasagashi の HTTPRoute(`danything/tamasagashi` の `deploy/httproute.yaml`)に -`sessionPersistence` を足す**のが本筋。 -CRD 入れ替えの際は、いまの Gateway API CRD が Traefik 撤去後も `traefik-crd` の Helm リリース所有のまま -(`helm.sh/resource-policy: keep` で残存)であることに注意。 - -**Cookie を使う他のアプリ(auth の oauth2-proxy など)は影響を受けない。** どれも replicas 1 で、 -セッションは Cookie 自体か外部ストアに入っていて Pod のメモリに無い。**将来どれかを複数 replica にするなら、 -Cilium 1.21 未満のあいだは Gateway 越しのセッション固定が無いことを先に確認すること。** +**いまは穴が開いたまま受け入れている**(replicas 1・状態なし。影響は更新中の 15 秒ほど。緩和は danything/tamasagashi の `deploy/README.md`)。 +**Cilium 1.21 に上げるときに、Gateway API CRD を experimental チャネルへ入れ替えて +tamasagashi の HTTPRoute に `sessionPersistence` を足す**のが本筋。いまの CRD は `traefik-crd` の Helm リリース所有のまま +(`helm.sh/resource-policy: keep` で残存)なので、入れ替えの際は注意。 +**将来どれかのアプリを複数 replica にするなら、1.21 未満のあいだは Gateway 越しのセッション固定が無いことを先に確認すること。** ### 却下した案 @@ -482,10 +390,7 @@ Cilium 1.21 未満のあいだは Gateway 越しのセッション固定が無 | ingress-nginx | EOL | | HAProxy / Kong / Istio | 単一ノードに過剰、または Gateway API 対応で劣る | -### 参考: Envoy Gateway を選ぶ場合に確認が要ること - -いまは採らないが、将来 Cilium で足りなくなったときのために残す。Envoy Gateway の OIDC は -トークンをブラウザの Cookie に保存するので、既知の問題がある。 +将来 Envoy Gateway を選ぶなら、OIDC はトークンを Cookie に保存するので次の既知の問題を先に見る: | Issue | 内容 | | --- | --- | @@ -493,12 +398,6 @@ Cilium 1.21 未満のあいだは Gateway 越しのセッション固定が無 | envoyproxy/gateway#8441 | OIDC Discovery が失敗すると毎リクエスト token introspection にフォールバック。IdP 停止時に白画面 | | envoyproxy/gateway#8649 | Gateway レベルとルートレベルで SecurityPolicy を二重掛けすると CSRF 検証で落ちる | -`cookieDomain` はサブドメイン間で共有するなら root domain(`.doany.io`)。後から変えると -ブラウザに残った古い Cookie が優先されて認証が壊れるので、変更時は Cookie クリアが要る。 - -Envoy Gateway 自体はマイナーが 2 週間強に 1 回出てサポート窓も短い。 -**Talos / Kubernetes / Gateway API CRD / Envoy Gateway の 4 つのバージョンマトリクス**を回す前提でコストを見る。 - ## chart を配るときと、chart で入れるとき(2026-09-07) **「HelmChart」は 2 つの別物を指す。** ここを混ぜると話が噛み合わない。 @@ -513,8 +412,7 @@ Envoy Gateway 自体はマイナーが 2 週間強に 1 回出てサポート窓 chart が悪いのではなく、**入れ方**が合わなくなった。 - **k3s 固有で Talos に無い。** OS を替える時点で全部書き直しになる -- **CR を消すと helm がアンインストールされる。** finalizer 駆動なので外しても付け直される。 - CRD を `templates/` に置く chart だと CRD ごと消えて CR が巻き添えになる +- **CR を消すと helm がアンインストールされる**(下の「HelmChart CRD から ArgoCD の Application へ」) - **ArgoCD と二重管理になる。** 同じリソースを 2 つのリコンサイラが見ることになり、 差分も同期状態も prune の制御も ArgoCD 側から見えない - **Job で走るので chart 側の事情が漏れる。** erpnext(2026-10-06 に撤去)は Job 名に描き出した時刻を入れるので、 @@ -545,11 +443,10 @@ Flux の `HelmRelease` でも同じものが入るのが正しい。**このク ## HelmChart CRD から ArgoCD の Application へ(2026-09-07) `HelmChart`(`helm.cattle.io`)は k3s の helm-controller が提供するもので **Talos には無い**。 -OS 交換時の変数を減らすため、k3s のうちに ArgoCD の `Application` へ移す。 +OS 交換時の変数を減らすため、k3s のうちに ArgoCD の `Application` へ移した。 **移す前に必ず `helm template` の出力と live を突き合わせる。** リリース名まで揃えれば -描き出されるものは一致し、引き取っても Pod は入れ替わらない(実測: cloudflare-ddns・ -infisical-operator・infisical-push-bridge の 3 つとも `kubectl diff` が空、Pod の AGE も変わらなかった)。 +描き出されるものは一致し、引き取っても Pod は入れ替わらない。 `secrets-operator` のように名前を切り詰める chart があるので、**リリース名を変えると別物として作り直しになる**。 ### **CR を消すと helm がアンインストールされる** @@ -560,32 +457,9 @@ CRD を `crds/` ではなく `templates/` に置いているため、**アンイ `InfisicalSecret` 15 本が巻き添えになる**(Secret は `creationPolicy: Orphan` なので残るが、 値の同期は止まる)。 -**手順(この順番でやる)**: - -```shell -# 1. Application を先に作って Synced になるのを確かめる(diff が空なら Pod は入れ替わらない) -kubectl apply -f apps//application.yaml -# 2. helm のリリース Secret を消す。これでアンインストールが「対象なし」になる -kubectl -n delete secret -l owner=helm,name= -# 3. HelmChart CR を消す。削除ジョブは走るが何も消さない -kubectl -n kube-system delete helmchart -``` - -`apps/` に置いたものは ArgoCD が prune するので、git から消すだけで CR も消える。 -その場合はアンインストールが走るので、**状態を持つものでは必ず先に 2 をやる** -(cloudflare-ddns では手を抜いて走らせたら Deployment が一度消えて、ArgoCD が数秒で作り直した)。 - -### 残り - -| chart | 状態 | 扱い | -| --- | --- | --- | -| cloudflare-ddns | 移行済み | `apps/cloudflare-ddns/application.yaml` | -| infisical-secrets-operator | 移行済み | `apps/infisical-operator/` | -| infisical-push-bridge | 移行済み | `apps/infisical-push-bridge/` | -| yosegaki | 移行済み | blog リポジトリの `deploy/yosegaki-application.yaml`。PVC 持ちなので上の手順で移した | -| erpnext | 移行済み(2026-10-06 に撤去) | Job 名に時刻が入る chart だったので、`jobs.createSite` と `jobs.configure` を止めて移した(描き出しの差はその 2 つの Job だけで、Pod は入れ替わらなかった) | -| infisical | **移さない**(2026-09-08 に確定) | chart が DB と Redis のパスワードを **Deployment の平文 env に焼き込む**ので、値を git に置けない。理由は下記 | -| argocd | 移さない | 自分自身。Talos では machine config の `inlineManifests` に載せる | +**移し方は 3 段**: Application を先に作って Synced を確かめる → helm のリリース Secret +(`owner=helm,name=`)を消して、アンインストールを「対象なし」にする → HelmChart CR を消す。 +cloudflare-ddns・infisical-operator・infisical-push-bridge・yosegaki・erpnext を移し終えた。argocd と infisical は移さない(下記)。 ### infisical だけは ArgoCD に移せない(2026-09-08、chart を読んで確定) @@ -623,60 +497,44 @@ REDIS_URL redis://default:<パスワード>@redis-master:6379 ## ghcr の pull 認証 -repo が private なのでパッケージも private。もとは名前空間ごとに `ghcr-pull`(PAT の dockerconfigjson)を -Infisical から作って `imagePullSecrets` で参照していた(tamasagashi、worklog)。アプリを足すたびに同じものが増える。 - -| 案 | 中身 | 評価 | -| --- | --- | --- | -| **node 単位の資格情報**(推奨) | k3s なら `/etc/rancher/k3s/registries.yaml`、Talos なら machine config の `machine.registries.config."ghcr.io".auth`。全 namespace に効く | `imagePullSecrets` と `ghcr-pull` の CR が全部消える。Talos では SOPS 暗号化した machine config に載るので、秘密の置き場も統一される。**Talos 移行と同時にやるのが自然** | -| パッケージだけ public にする | GHCR のパッケージ可視性は repo の可視性と独立。public にすれば資格情報ゼロ | いちばん簡単。ただしイメージの中身(ビルド済みのアプリ)が誰でも pull できる | -| GitHub App の短命トークン | CronJob で 1 時間ごとにトークンを発行して Secret を書き換える | いちばん安全だが、動く部品が増える。単一ノードの自宅クラスタには過剰 | -| そのまま(PAT を Infisical に) | 当時の形 | 動いてはいる。ローテーションは Infisical 側 1 回で済む | - -**結論: node 単位へ寄せた(2026-09-06、Talos を待たず k3s 側で実施)。** アプリを足すたびに Secret を -用意する必要が無くなった。Talos では同じ値が `talos/registries.yaml`(SOPS 済み)に入る -([talos/README.md](../talos/README.md))。 - -**その後(2026-09-15): ghcr の資格情報そのものが要らなくなった。** 非公開のリポジトリを GitHub から Forgejo に移し -(apps/forgejo/README.md)、ghcr.io に残るイメージは全部公開。ノードの `registries.yaml` から ghcr.io を外し、 -代わりに fj.doany.io を同じ node 単位の形で持つ。ghcr.io の PAT はどこでも使っていない(Infisical の `/worklog/ghcr-pull` も既に無い)。 - +もとは名前空間ごとに `ghcr-pull` を `imagePullSecrets` で参照していた。2026-09-06 にノード単位の資格情報へ寄せ、 +**2026-09-15 からは ghcr.io の資格情報そのものが要らない** ── 非公開のリポジトリを Forgejo に移し(apps/forgejo/README.md)、 +ghcr.io に残るイメージは全部公開。**ノード単位で持つのは fj.doany.io の資格情報だけ**(`talos/registries.yaml`、[talos/README.md](../talos/README.md))。 ## PT3 チューナー(Talos で動かすための算段) 実機には Earthsoft PT3(`earth_pt3`、Altera 1172:4c15)が刺さっていて、Talos のカーネルは `CONFIG_DVB_PT3` を -有効にしておらず、公式 extension にも無い。denpa の tuner-agent はこの箱の `/dev/dvb` と B-CAS リーダーに依存している。 -**余っている箱は無い**(2026-09-06 本人談)ので、次の 2 本立てで進めていた。 - -**結論(2026-10-13): 1 が通った。** pkgs#1682 は v1.15.0-alpha.1 に入り、`siderolabs/dvb-pt3` が Image Factory の -公式 extension に出た。本番の schematic を `96371c8f…`(dvb-pt3 + intel-ucode + IOMMU の引数)に作り直し、 -Talos を v1.15.0-alpha.1 に上げた(`talos/versions.yaml`)。extension は `earth_pt3` を blacklist して配るので、 -`talos/patches/main.yaml` の `machine.kernel.modules` で読ませる。2 の vfio は PT3 をホストのドライバと取り合うので外した。 -alpha をノード 1 台の本番に乗せることになるが、1.15.0 の正式版を待つより PT3 を先に取った。 +有効にしておらず、公式 extension にも無かった。denpa の tuner-agent はこの箱の `/dev/dvb` と B-CAS リーダーに依存している。 -1. **上流に PR 済み(本命)**: [siderolabs/pkgs#1682](https://github.com/siderolabs/pkgs/pull/1682) - (`CONFIG_DVB_PT3=m` の 1 行。依存する tc90522 / qm1d1c0042 / mxl301rf は既に `m`)と - [siderolabs/extensions#1238](https://github.com/siderolabs/extensions/pull/1238)(`dvb/pt3` extension、cx23885 と同じ形)。 - 後者は前者が入って Talos のリリースに乗るまでビルドできないので順番待ち。取り込まれれば Talos 側は素のままで済む。 - DCO の Signed-off-by は git config の `Ruk Doe `。conform が GPG 署名も求めるので、必要なら本人の鍵で作り直す。 - extension の `compatibility.talos.version` は `>= v1.15.0` と当て推量。 -2. **入らなかった場合の保険: KubeVirt の VM にパススルー。** 実機で確認済み — VT-d 有効(IOMMU グループ 66 個)、 - PT3(05:00.0)は**グループ 36 に単独**、B-CAS は USB(Gemalto GemPC Twin 08e6:3437)。Talos のカーネルは - `vfio-pci` / `vfio_iommu_type1` / KVM を持っている(`intel_iommu=on` は schematic に残してある。vfio は 2026-10-13 に `talos/patches/main.yaml` から外したので、この道に戻るなら足し直す)。 - tuner-agent だけ VM(Ubuntu)で動かせば Talos 本体は素のまま、アップグレードも Image Factory のままでいける。 +**結論(2026-10-13): 上流に入った。** [siderolabs/pkgs#1682](https://github.com/siderolabs/pkgs/pull/1682) +(`CONFIG_DVB_PT3=m`)が v1.15.0-alpha.1 に入り、`siderolabs/dvb-pt3` が Image Factory の公式 extension に出た。 +本番の schematic を `96371c8f…`(dvb-pt3 + intel-ucode + IOMMU の引数)に作り直し、Talos を v1.15.0-alpha.1 に上げた +(`talos/versions.yaml`)。extension は `earth_pt3` を blacklist して配るので、`talos/patches/main.yaml` の +`machine.kernel.modules` で読ませる。alpha をノード 1 台の本番に乗せることになるが、正式版を待つより PT3 を先に取った。 -自前 extension を `imager` で焼く案は、Image Factory から外れてアップグレードのたびに自前ビルドになるので採らない。 +**駄目だったときの保険: KubeVirt の VM にパススルー。** 実機で確認済み — VT-d 有効、PT3(05:00.0)は +**IOMMU グループ 36 に単独**、B-CAS は USB(Gemalto GemPC Twin 08e6:3437)。tuner-agent だけ VM(Ubuntu)で動かせば +Talos 本体は素のまま。`intel_iommu=on` は schematic に残してあるが、vfio は 2026-10-13 に +`talos/patches/main.yaml` から外した(PT3 をホストのドライバと取り合う)ので、この道に戻るなら足し直す。 +**自前 extension を `imager` で焼く案は採らない。** Image Factory から外れて、アップグレードのたびに自前ビルドになる。 ## 依存の更新をどこまで自動で入れるか(2026-09-07) Renovate は共有プリセット(`5ym/repo-config`。2026-10-07 に `5ym/renovate` から改名)を使っていて、**全部を 1 つの PR にまとめて自動マージ**する 設定だった。`separateMajorMinor: false` も付いていたので、**postgres 17 → 18 が nginx のパッチと -同じ PR に入り、自動マージの対象になっていた**(danything/gitops#7)。そのまま入っていたら当時の Mattermost が落ちていた。 +同じ PR に入り、自動マージの対象になっていた**(danything/gitops#7)。 -**直したこと**: プリセット側でメジャーを別の PR に分け、`automerge: false` にした(5ym/renovate#2)。 +**直したこと**: プリセット側でメジャーを別の PR に分け、`automerge: false` にした(5ym/repo-config#2)。 パッチとマイナーはこれまでどおり 1 つにまとめて自動マージする。小さくて頻繁で、タグを戻せば済むため。 +**SOPS で暗号化したファイルは Renovate に触らせない**(2026-09-08)。argo-cd 10.8.2 の PR が暗号文の中の `version: 10.8.1` を +平文置換して自動マージされ、MAC が壊れて `sops -d` が通らなくなった(コミットから戻した)。`renovate.json` の `ignorePaths` で外し、 +その 2 ファイルの版は人が見る([../bootstrap/README.md](../bootstrap/README.md))。 + +Renovate 自体は **2026-10-16 から k8s の CronJob([apps/renovate/](../apps/renovate/))で動かしている** +(GitHub Actions の workflow と Forgejo Actions の `doa/renovate` は 2026-10-17 に削除)。 + いまの自動マージの範囲(プリセット `5ym/repo-config`): | まとまり | 自動マージ | @@ -685,25 +543,21 @@ Renovate は共有プリセット(`5ym/repo-config`。2026-10-07 に `5ym/renova | `major dependencies` | しない | | `helm charts`(更新の種類を問わない) | しない | -**PostgreSQL は 17 の線に固定した**(`renovate.json` の `allowedVersions: "<18"`)。きっかけは Mattermost -(公表しているのが下限の 14.0+ だけだった。2026-10-06 に撤去)だが、規則は `postgres` のイメージ全部に効いている。 -外さない理由はもう 1 つのほう ── **PostgreSQL はメジャーが変わるとデータディレクトリの互換が切れる。** -イメージのタグを差し替えると `database files are incompatible with server` で起動を拒否する。上げるには +**PostgreSQL は 17 の線に固定した**(`renovate.json` の `allowedVersions: "<18"`)。 +**PostgreSQL はメジャーが変わるとデータディレクトリの互換が切れる。** +イメージのタグを差し替えると `database files are incompatible with server` で起動を拒否し、上げるには dump と restore が要る。17 のサポートは 2029-11 まであるので急がない。 ### メジャーを分けるだけでは足りない(2026-09-07 実例) -プリセットを直した直後に Renovate が #7 を作り直し、**自分でマージした**。残ったのは -postgres 17.10 → 17.11(パッチ、無害)と **erpnext の chart 8.0.15 → 8.0.78**。後者は版の付け方が -パッチなので、メジャーを分ける設定では止まらない。中身はキャッシュとキューの Dragonfly → Valkey の -入れ替え(Service 名も `erpnext-dragonfly-*` → `erpnext-valkey-*` に変わる)で、values に書いてあった -Dragonfly のチューニングが丸ごと無効になり、valkey は `resources: {}` で上限も無かった。 +プリセットを直した直後、Renovate は erpnext の chart 8.0.15 → 8.0.78 を自動マージした。版の付け方はパッチだが、 +中身は Dragonfly → Valkey の入れ替えで、values に書いてあったチューニングが丸ごと無効になった。 **版の番号は中身の大きさを表さない。** chart の場合はとくにそうで、自動マージに任せる範囲を 決めるときは「メジャーかどうか」だけでは足りない。 **そこでプリセット側で `helm` の datasource を丸ごと自動マージから外した** -(5ym/renovate#3)。chart は更新の種類を問わず人が見る。`helm charts` という別の +(5ym/repo-config#3)。chart は更新の種類を問わず人が見る。`helm charts` という別の グループにしてあるのは、chart を止めることで `all dependencies` の PR まで自動マージ されなくなるのを避けるため。 @@ -724,10 +578,10 @@ Dragonfly のチューニングが丸ごと無効になり、valkey は `resourc マニフェストを消すのは git のレビューを通るが、Application の削除は UI のボタン 1 つで済む。 そこの重みは違う、という点は残る。 -**`storageClassName` はバインド済み PVC では変更も削除もできない**(API が拒否する)。 -既存ぶんは `local-path-retain` のままで、PV の `persistentVolumeReclaimPolicy` を `Delete` に -パッチして挙動だけ揃えた。マニフェストから消せるのは **Talos の再構築時**で、 -そのとき既定の `local-path`(reclaim は `Delete`)になる。 +**`storageClassName` はバインド済み PVC では変更も削除もできない**(API が拒否する)ので、 +2026-10-05 に k3s のうちに付け替えた: 28 本の PVC を止め、PV を `Retain` にして古い PVC を消し、 +PV の `claimRef` を外して `storageClassName` を `local-path` に書き換え、新しい PVC を `volumeName` で同じ PV に結んだ +(**データはコピーしていない**)。`local-path-retain` はもう使っていない([../ROADMAP.md](../ROADMAP.md))。 ## バックアップに何を含めるか @@ -736,14 +590,7 @@ R2 の無料枠は 10 GB。**もう超えている。** | 日付 | リポジトリの実サイズ | できごと | | --- | --- | --- | | 2026-09-06 | 3.3 GiB | | -| 2026-09-07 | 13.92 GiB | `denpa-recorded` が 14 GB まで育っていた → 除外した | -| **2026-09-08** | **23.2 GiB** | **`denpa-library` が 972 MB → 7.9 GB に育った**(圧縮後 10.1 GiB) | -| 2026-09-10 | 33.5 GiB | R2 の表示は 36.05 GB(GiB との差)。うち 18.15 GiB が `denpa-library` の 3 世代 | -| 2026-10-05 | 88.6 GiB | ライブラリが 39.66 GiB に育ち、ホストのスクリプトの 10 世代がそれを掴んでいた → ホスト側から外した(下の表の「録画データ」) | -| 2026-10-05 | **37.3 GiB** | ホストの 10 本から録画を抜き(`restic rewrite`)、改名前のパスの 1 本を消して prune。録画はエンコード済みの 1 世代(約 35 GiB)だけ | - -**除外では解決しない。** `denpa-recorded` を外した効果は保持世代が回れば出るが、 -今度は「取ると決めた」`denpa-library` そのものが伸びている。 +| 2026-10-05 | **37.3 GiB** | 途中 88.6 GiB まで育った(下)。録画はエンコード済みの 1 世代(約 35 GiB)だけにした | **決定(2026-09-08): 有料でも取る。** 無料枠に収めることを目的にすると録画を捨てることになり、 本末転倒。R2 は下り(egress)が無料なので、復元のたびに費用が跳ねる心配も無い。 @@ -751,24 +598,17 @@ R2 の無料枠は 10 GB。**もう超えている。** **ホストのスクリプトが全部を二重に持っている(2026-09-10 に気づいた)。** `backup/k3s-backup` は `/var/lib/rancher/k3s/storage` を丸ごと取るので、**k8up が PVC ごとに -取っているものと同じ中身**が `host=main` のスナップショットにも入っている(除外しているのは -`denpa-recorded` だけ)。restic は重複を除くので容量は二重にならないが、**片方で世代を減らしても -もう片方が同じ blob を掴んでいるので解放されない。** +取っているものと同じ中身**が `host=main` のスナップショットにも入っている。restic は重複を除くので容量は二重にならないが、 +**片方で世代を減らしてももう片方が同じ blob を掴んでいるので解放されない。** 実際、`denpa-library` を 3 世代 → 1 世代にしても**解放されたのは 36 MiB だけ**だった (狙っていたのは 10 GiB)。`restic stats --mode raw-data ` が返すのは 「そのスナップショットが参照する量」で「そのスナップショットだけが持つ量」ではない、 という読み違い。**per-PVC の保持設計はホストのスクリプトがある間は効かない。** +これは**移行で消える**(Talos にはシェルが無い)。`host=main` が保持から落ちきったところで解放される。 -これは**移行で消える** ── Talos にはシェルが無いのでホストのスクリプトは持っていけない。 -`host=main` のスナップショットが保持(`keep-daily 7 / keep-weekly 4 / keep-monthly 2`)から -落ちきったところで、溜まっていたぶんが解放される。 - -**料金の見かた(2026-09-10 に確かめた)。** R2 のストレージは「いまのサイズ」ではなく -**期間中の日次ピークの平均(GB-month)**で、無料枠は 10 GB-month。リポジトリは 9/6 生まれで -請求期間(8/23〜9/23)の前半は空だったので、この期間の平均はまだ 4 GB-month ほど。 -だから 33.5 GiB あっても請求は **$0.00** のまま。丸ごと入る次の期間で **月 $0.4 前後**になる。 -操作のほうは桁違いに余っている(Class A 5.09k / 100万、Class B 14.34k / 1000万)。 +**料金**: R2 のストレージは**期間中の日次ピークの平均(GB-month)**で課金され、無料枠は 10 GB-month。 +いまの規模で月 $0.4 前後(2026-09-10 の見積もり)。操作の回数は桁違いに余っている。 容量を削るために外した / 外さなかったもの: @@ -786,24 +626,15 @@ wg-easy を使う理由は **プライバシー(全部を自宅経由にする) どれも「自宅の LAN に入れれば済む」もの。UI に不満はあったが、**スマホを足すときの QR は実際よく使う**ので UI を捨てる方向は取らなかった。 -比較したもの: - | | 結論 | | --- | --- | | **wg-easy 続投** | 動いてはいる。ただし自前の認証を切れず(要望 [wg-easy#1923](https://github.com/wg-easy/wg-easy/issues/1923) が 2025-06 から open)、SSO の後ろに置いても**二重ログイン**が残る | -| **wg-portal** | star は少ないが機能は足りる。ただし乗り換える動機が wg-easy 比で薄い | -| **Headscale** | 完成度は高い。**が、FAQ が「headscale を動かすマシンをサブネットルータにするな」と明記している。** ノードが 1 台のこの構成では回避できない。コンテナは「サポート対象外」だが放置ではなく(コンテナ/プロキシ関連の PR は 48 本マージ済み、[#3292](https://github.com/juanfont/headscale/pull/3292) は reverse-proxy のドキュメントを書き直している)、境界の引き方の問題 | +| **wg-portal** | 機能は足りる。ただし乗り換える動機が wg-easy 比で薄い | +| **Headscale** | 完成度は高い。**が、FAQ が「headscale を動かすマシンをサブネットルータにするな」と明記している。** ノードが 1 台のこの構成では回避できない | | **NetBird** | **採用。** routing peer が独立した概念で、コンテナで動かす前提のドキュメントがある。1 台構成でも上の問題が起きない | -star は headscale が 43.6k、netbird が 29.0k で headscale のほうが多い。**が、動いている量は netbird が上** -(2026-09-07 時点、直近 30 日のコミット 119 対 45、作者 21 人対 10 人、直近 90 日のマージ済み PR 399 対 51、 -1 年のリリース 109 対 16)。netbird の open issue 1,583 は放置ではなく分母の差で、 -GUI クライアント・IdP 連携・ポリシーまで同じリポジトリに入っている。 - **公式 Helm chart は使わない。** [netbirdio/helms](https://github.com/netbirdio/helms) の `charts/netbird` は -2026-04-24 から更新が止まっていて appVersion `0.46.0`(本体は `0.78.1`)。 -[#39](https://github.com/netbirdio/helms/issues/39) で指摘され、中の人が「combined に寄せるか両対応か」と -返したきり半年動いていない。archived ではないので廃止ではなく**宙に浮いている**。 +更新が止まっていて appVersion が本体から大きく遅れている([#39](https://github.com/netbirdio/helms/issues/39))。 上流は v0.65.0 で management/signal/relay を 1 つにまとめた **combined コンテナ**に移っており、 素の Deployment で足りる(`apps/netbird/`)。 @@ -823,13 +654,12 @@ GUI クライアント・IdP 連携・ポリシーまで同じリポジトリに ## 公開経路(HTTPRoute)をどこに置くか(2026-09-07) `apps/gateway-routes/` に全部まとめていたのをやめ、**それぞれが公開しているものと同じ場所**に置いた。 - -ArgoCD は `sourcePath` 配下を `recurse` で拾い、追跡は `argocd.argoproj.io/tracking-id` 注釈 -(`::/`)で行うので、**ファイルの場所は挙動に影響しない**。純粋に読み手の都合。 +ArgoCD は `sourcePath` 配下を `recurse` で拾い、追跡は `argocd.argoproj.io/tracking-id` 注釈で行うので、 +**ファイルの場所は挙動に影響しない**。純粋に読み手の都合。 | 行き先 | 対象 | | --- | --- | -| このリポジトリの `apps//httproute.yaml` | adguardhome / forgejo / headlamp / infisical / matrix / mta-sts / netbird / rybbit など(当時は erpnext と mattermost も) | +| このリポジトリの `apps//httproute.yaml` | adguardhome / forgejo / headlamp / infisical / matrix / mta-sts / netbird / rybbit など | | 各アプリのリポジトリの `deploy/httproute.yaml` | blog / yosegaki / lgtm / tamasagashi / worklog / xool / yuzuriha | | `bootstrap/` | argocd / auth ×3 / redirect-https | @@ -838,20 +668,12 @@ ArgoCD は `sourcePath` 配下を `recurse` で拾い、追跡は `argocd.argopr ArgoCD が壊れているときにその経路を ArgoCD 経由でしか直せない、という順序の逆転があった。 **まとめて 1 か所に置く利点(公開しているものの一覧になる)は失う。** 代わりに -`apps/gateway-routes/README.md` …ではなく、この表と `bootstrap/README.md` が索引になる。 -外部露出を一覧したいときは `kubectl get httproute,grpcroute -A` が正確で、git を読むより早い。 +この表と `bootstrap/README.md` が索引。外部露出を一覧したいときは `kubectl get httproute,grpcroute -A` が +正確で、git を読むより早い(一覧の一枚は作らない、2026-09-09)。 -### 落とさずに動かす手順 - -ArgoCD は「git から消えた + 自分が追跡している」ものを prune する。単に移すと**移動の瞬間に -公開経路が落ちる**。使った回避は 2 通り: - -- **別リポジトリへ移すもの** — 移送先を**先に**マージする。そのアプリの Application が同じ - ルートを apply した時点で tracking-id が書き換わり、元の Application の管理から外れる。 - そのあと元から消せば prune は起きない。7 本とも移動後に tracking-id を確認してから消した -- **`bootstrap/` へ移すもの** — 引き取り手が居ないのでこの手が使えない。先に - `argocd.argoproj.io/sync-options: Prune=false` を live に効かせておき、そのあと移す。 - 移し終えたら live の tracking-id 注釈を剥がして、`Prune=false` も外す +**落とさずに動かす**: ArgoCD は「git から消えた + 自分が追跡している」ものを prune する。別リポジトリへは +移送先を**先に**マージして tracking-id が書き換わってから元を消す。`bootstrap/` へは引き取り手が居ないので、 +先に live に `Prune=false` を効かせてから移し、tracking-id 注釈を剥がして `Prune=false` も外す。 ## DNS:直接つなぐアプリと Cloudflare を通すアプリを分ける(2026-10-06) @@ -866,12 +688,9 @@ ArgoCD は「git から消えた + 自分が追跡している」ものを prune アプリのリポジトリの 2 か所を直すことになる。ExternalDNS は HTTPRoute の注釈だけで決まり、自分が作ったレコード (所有の印の TXT 付き)にしか触れないので、メールや DDNS の apex と同居できる。 -比べたもの(2026-10-06 時点の GitHub のスター):ExternalDNS 9,104(kubernetes-sigs)/ DNSControl 3,961 / OctoDNS 3,772 / -Cloudflare の Terraform provider 1,342 / cloudflare-operator 695(トンネルが主) / k8s_gateway 356(自前の DNS サーバーで、 -1 年以上止まっている) / Crossplane の Cloudflare provider 10。疎結合にできるのは ExternalDNS・cloudflare-operator・Crossplane だけで、 -その中で DNS が主役で利用者が多いのは ExternalDNS。 +比べたもの:ExternalDNS / DNSControl / OctoDNS / Cloudflare の Terraform provider / cloudflare-operator / k8s_gateway / +Crossplane。疎結合にできるのは ExternalDNS・cloudflare-operator・Crossplane だけで、その中で DNS が主役で利用者が多いのは ExternalDNS。 **Entra のリダイレクト URI は中央(`pulumi/entra.ts`)のまま。** 共用のアプリ登録 1 つに全アプリの URI が入るので、 そもそも中央に集まる性質のもの。アプリごとに分けるにはアプリ登録を分ける必要があり、それを Kubernetes から作る 手軽な道具がまだ無い。 - diff --git a/docs/migration-day.md b/docs/migration-day.md index ab03b3d..07f6f2e 100644 --- a/docs/migration-day.md +++ b/docs/migration-day.md @@ -5,6 +5,7 @@ **なぜこの順番か**が要るなら [ROADMAP.md](../ROADMAP.md) の Phase 2 と [decisions.md](decisions.md)「Talos の起動順序をどう組むか」。 +**移行は今の DL360 でやる。** Z440 へ移すのはそのあと([talos.md](talos.md)「Z440 に移すときに変わるもの」)。 --- @@ -14,27 +15,28 @@ (`valid for metal mode` まで出ること) - [ ] 生成物に **inline 9 つ + CRD の URL 5 つ**が入っている ([talos/README.md](../talos/README.md)「上げ方 / 当て直し方」) -- [ ] `talos/versions.yaml` の版が意図どおり(**上げるなら今日ではない日に**) +- [ ] `talos/versions.yaml` の版が意図どおり(**上げるなら今日ではない日に**)。 + Kubernetes は k3s の v1.36.2 ではなく **`versions.yaml` の `kubernetes:` の版**で建つ。 + git から建て直す(k3s は sqlite で etcd を持ち越さない)ので、版が上がっても in-place の飛び級ではない - [ ] **作業中は何も見せない。** 決定済み(ROADMAP の Phase 2)。メンテナンス画面は用意しない +- [ ] **やめどきの時刻を決めておく**(下の「やめどき・戻せないこと」) - [ ] **手元の PC に道具がそろっている。** 移行後は**ノードにシェルが無い**ので `ssh main 'kubectl …'` が使えなくなる。5 以降は全部手元から叩く | | 用途 | | --- | --- | | `talosctl` | 3〜5。版はクラスタに合わせなくてよい | - | `kubectl` | 5〜9。**クラスタと同じ v1.36.2** | + | `kubectl` | 5〜9。**`versions.yaml` の `kubernetes:` と同じ版**(tools の Dockerfile が同じ depName で揃えている) | | `restic` | 8 でスナップショット ID を選ぶ | | `helm` / `sops` / `age` | 3(`render.sh`)と調べもの | | `jq` | 調べもの(手元に winget で入れてある。tools のコンテナには無い) | - 2026-09-10 に全部そろえた(`~/.local/bin`)。 + jq 以外は [tools/t.ps1](../tools/t.ps1) のコンテナに入っている([tools/README.md](../tools/README.md))。 + **コンテナの `/tmp` は実行ごとに消える**ので、t.ps1 で `render.sh` を回すなら出力先は + `talos/clusterconfig/`(`.gitignore` 済み)にする。 - **6 は GitHub Actions がやる**ので手元は要らない。5・7・9 の確認も - ワークフローに寄せられなくはないが(API サーバは `ks.doany.io:6443` で - 外から届く)、**3〜5 の `talosctl` は寄せられない** ── Talos の API は - ポート 50000 で **LAN からしか届かない**うえ、USB とコンソールは物理。 - **8 も寄せない** ── PVC に書き戻す操作を GitHub から実行できる状態は - 作らない(CI に `delete` を渡していないのと同じ理由)。 + **6 は GitHub Actions がやる。** 3〜5 は Talos の API(50000)が LAN からしか届かず USB とコンソールが物理なので、 + 8 は PVC に書き戻す操作を GitHub から実行できる状態を作らないため、どちらもワークフローに寄せない。 ## 1. 経路の確保 @@ -66,8 +68,10 @@ sudo systemctl start k3s-backup.service # ホストのぶん ## 4. 焼く -**前もって USB を作っておく**([talos.md](talos.md)「メディアに載せる」)。ハイブリッド ISO なので -そのまま書けばよい。Windows なら Rufus の **DD イメージモード**。 +**前もって USB を作っておく。** ISO はハイブリッドなのでそのまま書けばよい。 +Windows なら Rufus で ISO を選び、**DD イメージモード**(balenaEtcher でもよい)。 +WSL からは USB が見えない(usbipd で渡す必要がある)ので Windows 側でやる方が早い。 +URL の版と schematic は [talos/versions.yaml](../talos/versions.yaml) と揃える(下は 2026-10-13 の値)。 ```shell curl -LO https://factory.talos.dev/image/96371c8f956f7f5f9d09925bb49fe096c0cad05c5cf5bdb464790982c0b067ca/v1.15.0-alpha.1/metal-amd64.iso @@ -82,18 +86,21 @@ sudo dd if=metal-amd64.iso of=/dev/sdX bs=4M status=progress oflag=sync - [ ] **Secure Boot が Disabled** であること(RBSU は POST 中に F9 → Server Security → Secure Boot Settings)。通常 ISO は署名されていない。 Boot Mode は UEFI のまま -- [ ] USB を挿して電源を入れ、POST 中に **F11(Boot Menu)** で USB を選ぶ - (iLO の Virtual Media を使うなら "iLO Virtual USB 3 : iLO Virtual CD-ROM") +- [ ] USB を挿して電源を入れ、POST 中に **F11(Boot Menu)** で 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" - [ ] maintenance mode の IP をコンソールで確認 - [ ] `talosctl get links --insecure -n -e ` で**インタフェース名が `eno1` / `eno2` / `eno4` であること**を確かめる。違ったら `talos/patches/cluster.yaml` を直して 3 に戻る +- [ ] `talosctl get disks --insecure -n -e ` で**インストール先が `/dev/sda`(447 GB の SSD)であること**を確かめる。 + 違ったら `talos/patches/main.yaml` の `diskSelector` を直して 3 に戻る(**書いたら消える**) ```shell talosctl apply-config --insecure -n -f /tmp/talos-config/controlplane.yaml ``` -**`UnattendedInstallConfig` があるのでディスクに書かれる。** +**`UnattendedInstallConfig` があるのでディスクに書かれる。** ここから先は k3s に戻せない(下の「やめどき・戻せないこと」)。 - [ ] **書き終わって再起動したら USB を抜く。** 挿したままだと、ブート順によっては また maintenance mode で上がってくる @@ -108,10 +115,10 @@ talosctl --talosconfig $TC -n 10.0.0.2 -e 10.0.0.2 kubeconfig - [ ] `talosctl get volumestatus` に **EPHEMERAL(64GiB 上限)と u-local-path** が出る。 **ここが違ったら入れ直すしかない**([talos/patches/volumes.yaml](../talos/patches/volumes.yaml)) -- [ ] `kubectl get nodes` が **Ready / control-plane / v1.36.2** +- [ ] `kubectl get nodes` が **Ready / control-plane / `versions.yaml` の `kubernetes:` の版** - [ ] `kubectl get pods -A` で **cilium・coredns・metrics-server・local-path・ argocd・cert-manager・infisical** が Running - (ドリル 4 回目と同じ絵。[talos.md](talos.md)) + ([talos.md](talos.md)「ブートドリル 4 回目」と同じ絵) ## 6. 残りの bootstrap/ を当てる @@ -205,9 +212,19 @@ kubectl get httproute,grpcroute -A -o jsonpath='{range .items[*]}{range .spec.ho - [ ] **上の表と同じ形になる** - [ ] Infisical → operator → 各アプリの `Secret` が埋まる (`kubectl get infisicalsecret -A` が全部 `Synced`) -- [ ] DNS(cloudflare-ddns が A/AAAA を書く) +- [ ] `talosctl get links bond0` の MAC が **`e6:2c:1b:1a:a0:39`**(ルーターの IPv6 ホスト公開がこの MAC を指している。 + [talos/patches/cluster.yaml](../talos/patches/cluster.yaml)) +- [ ] **別の回線の IPv6 だけで 443 に届く**(A レコードもあるので、ブラウザで名前を開くだけでは IPv4 に落ちて確かめにならない。モバイル回線で `https://[240f:6d:842b:1::2]/` を開いて証明書の警告まで出れば届いている。[router.md](router.md)「IPv6 はノードだけを公開する」) +- [ ] `talosctl get routes -o yaml | grep -B10 'protocol: ra'` に `dst: ""` が残っている(IPv6 の既定経路。クラスタが上がってから見る) +- [ ] DNS(cloudflare-ddns が A/AAAA を書く)。**AAAA が `::2`** で、SLAAC のアドレスではないこと(`bond0` の `autoconf=0` が効いていれば SLAAC は付かない。Ubuntu 期は SLAAC を書いていた) - [ ] netbird(外から VPN に入れる)。**ここだけブラウザでは確かめられない** - [ ] AdGuard の公開リゾルバ(`dig @10.10.0.4 example.com`) +- [ ] **`/dev/dvb/adapter*` と B-CAS リーダーが見える**(`talosctl ls /dev/dvb`、`talosctl ls /dev/bus/usb`)。 + denpa の tuner-agent が上がって録画できる。**PT3 のために alpha に乗せている**ので、ここが通らなければ意味が無い + ([decisions.md](decisions.md)「PT3 チューナー」) +- [ ] bond0 の片方を抜いて通信が続き、挿し直して `/proc/net/bonding/bond0` の両方が `MII Status: up` に戻る + (`talosctl read /proc/net/bonding/bond0`。復旧方向は VM で確かめられていない) +- [ ] Renovate の CronJob が次の回で動く(`kubectl -n renovate get jobs`。[apps/renovate/](../apps/renovate/README.md)) ## 10. 後始末 @@ -234,6 +251,15 @@ kubectl get httproute,grpcroute -A -o jsonpath='{range .items[*]}{range .spec.ho --- +## やめどき・戻せないこと + +- **4 の `apply-config` で SSD が上書きされる。** k3s をその場で戻す手は無い。それより前なら USB を抜いて再起動すれば Ubuntu に戻る +- **やめどきの時刻を前日に決めておく**(例: 開始から何時間)。その時刻に 7 が終わっていなければ、どちらかを選ぶ: + - **Talos のまま直して進む**(既定)。git と restic は手つかずなので、詰まったところだけ直せばよい + - **Ubuntu を入れ直して restic から戻す**([recovery/README.md](../recovery/README.md) の `restore.sh`。k3s 期のホストを丸ごと戻す)。 + 2 の最終バックアップが戻り先になる +- 迷ったら前者。後者は Ubuntu の導入からやり直しで、当日中に終わる保証が無い + ## 詰まったときに見るところ | | | diff --git a/docs/restore-drill.md b/docs/restore-drill.md index e0db1d8..76b5b89 100644 --- a/docs/restore-drill.md +++ b/docs/restore-drill.md @@ -1,6 +1,8 @@ # 復元リハーサル(サーバ上の QEMU/KVM で) -Hyper-V は WSL から管理者権限で触れないので、本番サーバ(46 GB RAM、VT-x)の上に QEMU/KVM で Ubuntu の VM を立てて、 +**k3s 期だけのやり方。** Talos のノードにはシェルも QEMU も無いので、この形では流せない(Talos 期の戻し方は [recovery/README.md](../recovery/README.md)「Talos に移ったあと」)。 + +Hyper-V は WSL から管理者権限で触れないので、本番サーバ(当時 46 GB の RAM。2026-10-17 から 40 GB、VT-x)の上に QEMU/KVM で Ubuntu の VM を立てて、 そこに `recovery/` の `restore.sh` を `RESTORE_DRILL=1` で流す。初回 2026-09-06、Cilium 構成での再実施が 2026-09-07。 ## 副作用を出さないための遮断(VM の中で最初にやる) @@ -142,102 +144,35 @@ LAN(Pod から 10.10.0.4:80)→ 000 ss: 0.0.0.0:80 は cilium-envoy が LISTEN したまま ``` -**それ以外は健全。** 44 Running / 12 Completed、Gateway は `Accepted=True Programmed=True` で -LB-IPAM の `10.10.0.53` も 15 本のルートも戻った。全 Application が `Synced`。 - -起動しなかったものは、**どれもスナップショットが古いことの表れ**で欠陥ではない -(このリハーサルは 9/6 のスナップショットに対して行った): - -| | | -| --- | --- | -| `denpa/tuner-agent` | PT3 が無い。VM では毎回こうなる | -| `netbird/*` | **9/6 の時点で NetBird は存在しない。** git だけが先に進んでいるので Pod は作られるが、Infisical 側に Secret が無く `CreateContainerConfigError` | -| `erpnext`(いまは撤去)の socketio と worker | `ENOTFOUND erpnext-dragonfly-queue`。chart の版と、スナップショットの Service 名が食い違っていた | -| `k8up` | リハーサルの手当てで `k8up-global` を消しているため。意図どおり | - -**「git がバックアップより進んでいる」状態は復元では普通に起きる**、を改めて確認した形になった。 +**それ以外は健全**(44 Running / 12 Completed、Gateway・LB-IPAM・15 本のルートが戻り、全 Application が `Synced`)。 +起動しなかったのは PT3 の無い `denpa/tuner-agent` と、9/6 のスナップショットより git が先に進んでいたぶん(NetBird など)だけ。 ## 結果(2026-09-07、Cilium 1.20.1 + Gateway API) -3 回目。CNI を Cilium(kubeProxyReplacement / LB-IPAM / L2 アナウンス)に、入口を Gateway API に替えたあと、 -初めて通した。**確かめたかった 3 つはすべて Yes。手当ては何も要らなかった。** - -戻したのはスナップショット `df3ca7eb`(2026-09-06 19:11 UTC、15.954 GiB)。Cilium と Gateway への移行後のもの。 -`restore.sh` 開始 05:56:28 → k3s 起動 06:01:48 → 全 Pod が落ち着くまで 06:12 ごろ。イメージの pull がほとんどの時間。 - -| 確かめたこと | 結果 | 根拠 | -| --- | --- | --- | -| **Cilium が自力で上がるか** | **上がる。鶏卵にならない** | k3s 起動時点(06:01:44)で `/etc/cni/net.d/` は空。cilium エージェントが 06:02:28 に `05-cilium.conflist` を書き(`Wrote CNI configuration file`)、kubelet の `cni plugin not initialized` がそこで解消。k3s 起動の 40 秒後。`cilium-dbg status` は `KubeProxyReplacement: True` / `Cilium: Ok 1.20.1` / `Cluster health: 1/1 reachable` | -| **Gateway が戻るか** | **戻る。エージェントの再起動は要らない** | `kube-system/doany` が `PROGRAMMED=True`・`10.10.0.53`。リスナー 3 本(`https` 15 route / `https-apex` 1 / `http` 1)すべて Accepted + ResolvedRefs + Programmed。LB-IPAM は 4 本の LoadBalancer に `10.10.0.50`〜`.53` を払い出し、`cilium-l2announce-*` の Lease 4 本を `main` が保持 | -| **AdGuard の hostPort が張られるか** | **張られる** | `cilium-dbg service list` に HostPort のフロントエンドが 14 本(v4/v6 両方)。AdGuard は `0.0.0.0:53/UDP`・`:53/TCP`・`:853/TCP` → `10.42.0.19`。`dig @<ノード IP> example.com` が UDP でも TCP でも答えた | - -移行のときに要った「ConfigMap を書いてから cilium エージェントを再起動する」「hostPort を有効にして Pod を作り直す」は、 -**機能を有効にするための一度きりの手当て**だった。復元では DaemonSet と Helm のリリース Secret ごと戻るので初回で決まる。 - -**hostPort は `ss` に出ない。** Cilium は eBPF で張るのでソケットが存在しない。見るなら -`kubectl -n kube-system exec ds/cilium -c cilium-agent -- cilium-dbg status` と `cilium-dbg service list`。 - -一般の健全性(復元しただけで、まだ何も手を入れていない状態): - -- **47 Running / 15 Completed。** 起動しなかったのは前回と同じ 2 つだけで、どちらも VM の都合。 - `denpa/tuner-agent`(hostPath の `/dev/dvb`・`/dev/bus` が無い = PT3 が無い)と - `wireguard/wg-easy`(PostStartHook がカーネルの wireguard モジュールを見つけられない)。 -- PVC 23 件すべて Bound。PV 25 件の `nodeAffinity` はすべて `main`。 -- `InfisicalSecret` 13 件すべて OK(本番も同時点で 13 件)。Infisical 本体も上がった。 -- データも戻っている(当時の Mattermost の `posts` が 9832 行。本番は 10593 行で、スナップショットが 11 時間前なので妥当)。 -- 遮断は全部効いた(上の「Cilium にしても〜」)。 - -### 新しく見つかったこと: **Argo CD の app-of-apps が CRD 待ちで丸ごと止まる**(対処済み) - -``` -Failed | one or more synchronization tasks are not valid: - failed to discover server resources for group version k8up.io/v1: - the server could not find the requested resource (retried 5 times) -``` - -git には `k8up.io/v1` の `Schedule` があるのに、復元したクラスタに k8up の CRD が無い。CRD を入れる当の -Application(`apps/k8up/`)が同じ同期に含まれているので抜けられず、子 Application が 5 つ作られないまま -**11 個**(本番は 16 個)で止まった。戻したスナップショット(09-06 19:11)が k8up の導入(09-07 02:30 ごろ)より -古かったせいでもあるが、**「git がバックアップより進んでいる」状態は復元では普通に起きる。** -対処(`SkipDryRunOnMissingResource=true`)と理由は [../apps/k8up/README.md](../apps/k8up/README.md) -「CRD がまだ無いクラスタで apps を止めないこと」。 - -- 手で CRD を入れて同期をかけ直すと `successfully synced (all tasks run)` になった。**原因はこれで確定** -- **CRD を入れただけでは自動同期は再開しない。** Argo CD は API リソース一覧をキャッシュしていて、 - application-controller の再起動でも戻らなかった。`Application` に `operation` を書いて同期を起こす必要があった -- 同期を通したあと、operator が `Error: secret "k8up-global" not found` で止まった(git に無いため)。 - いまは `restore.sh` が作り直す(リハーサルモードでは作らない) - -このリハーサルは **`Schedule` を消し、R2 を iptables で塞ぎ、operator が起動できない**の 3 段構えにした。 -本番の restic リポジトリは**開始前と完全に同一**(7 スナップショット、ID もサイズも一致)で終わった。 - -### 細かい差分(どれも問題ではない) - -- スナップショットに移行途中の `traefik` DaemonSet と Pod、`traefik` の LoadBalancer Service が残っていて、 - 復元したクラスタでは Traefik が hostPort 80/443 を掴んだ。Gateway は LoadBalancer(`10.10.0.53`)なので衝突しない。 - 本番ではその後に消してある。 -- `3proxy` の TLS サイドカーはスナップショット時点で hostPort 8443、いまの git は 3129。同期を通せば追いつく。 +3 回目。CNI を Cilium に、入口を Gateway API に替えて初めて通した(スナップショット `df3ca7eb`、16 GiB。k3s 起動まで約 5 分、 +落ち着くまで約 16 分でほとんどがイメージの pull)。**確かめたかった 3 つはすべて Yes、手当ては要らなかった**: +Cilium は自力で上がる(CNI の設定を k3s 起動の 40 秒後に書く。鶏卵にならない)、Gateway は再起動なしで `Programmed=True`、 +AdGuard の hostPort も張られる(**`ss` には出ない**。eBPF なので `cilium-dbg service list` で見る)。 +47 Running / 15 Completed、PVC・`InfisicalSecret`・データも戻った。本番の restic リポジトリは開始前と同一のまま。 +新しく見つかったのは **Argo CD の app-of-apps が CRD 待ちで丸ごと止まる**こと(下の表)。 ## 結果(2026-09-06、flannel + kube-proxy + ServiceLB + Traefik の頃) -**過去の記録。** CNI が flannel、入口が Traefik の頃に 2 回実施し、**2 回目で全項目クリア。** -復元は 7 GiB を 2 分弱、k3s は同じバージョンで起動し、PV も Node 名もそのまま戻った。 - -1 回目で見つかった 4 件と、その対処: +**過去の記録。** 2 回実施し、1 回目で下の表の 4 件が見つかり、**2 回目で全項目クリア**(7 GiB を 2 分弱で復元、45 Running / 17 Completed)。 -| 見つかったこと | 原因 | 対処 | -| --- | --- | --- | -| Deployment が全部 `replicas: 0` で戻る | state.db のスナップショットを scale down の**後**に取っていた | 取る順序を入れ替えた(`backup/k3s-backup`)。git 管理外の Infisical が上がらず気付いた | -| 復元直後に名前解決が死ぬ | AdGuard の LoadBalancer(:53)に endpoint が無い間、kube-proxy がローカル宛 53 を REJECT し、systemd-resolved のスタブが巻き込まれる | `restore.sh` が `/etc/resolv.conf` をスタブから外す | -| Secret が 4 つ消える | **Argo CD が prune していた**。Infisical operator が CR の annotation(tracking-id 含む)を Secret にコピーするため、Argo CD が「git に無い管理対象」と誤認する | Argo CD の `resource.exclusions` で Secret を管理対象から外した | -| Infisical が起動しない | 起動時の SMTP 接続検証が通るまで HTTP を listen しない | 遮断するなら DROP ではなく **REJECT** で即座に落とす | +## 見つかったことと対処(3 回ぶん) -2 回目ではいずれも再発せず、消えていた Secret も全部残っていた(Argo CD が犯人だったことがこれで確定)。 -結果は 45 Running / 17 Completed、`InfisicalSecret` 15 件すべて OK。起動しなかった 2 つはどちらも VM の都合で、 -`denpa/tuner-agent`(PT3 が無い)と `wireguard/wg-easy`(カーネルに wireguard モジュールが無い)。 +| いつ | 見つかったこと | 原因 | 対処 | +| --- | --- | --- | --- | +| 09-06 | Deployment が全部 `replicas: 0` で戻る | state.db を scale down の**後**に取っていた | 取る順序を入れ替えた(`backup/k3s-backup`) | +| 09-06 | 復元直後に名前解決が死ぬ | AdGuard の :53 に endpoint が無い間、ローカル宛 53 が REJECT され systemd-resolved のスタブが巻き込まれる | `restore.sh` が `/etc/resolv.conf` をスタブから外す(Cilium でも要る) | +| 09-06 | Secret が 4 つ消える | Infisical operator が tracking-id を Secret にコピーし、**Argo CD が prune していた** | Secret を Argo CD の管理対象から外した(今は `InfisicalSecret` に `Prune=false`。[bootstrap/README.md](../bootstrap/README.md)) | +| 09-06 | Infisical が起動しない | 起動時の SMTP 接続検証が通るまで HTTP を listen しない | 遮断は DROP ではなく **REJECT**(上の「遮断」) | +| 09-07 | **Argo CD の app-of-apps が CRD 待ちで丸ごと止まる**(子 Application が 11/16 で止まる) | k8up の CRD を入れる Application が同じ同期の中にいる。CRD を入れても自動同期は再開しない(API リソース一覧のキャッシュ) | `SkipDryRunOnMissingResource=true`([../apps/k8up/README.md](../apps/k8up/README.md)「CRD がまだ無いクラスタで apps を止めないこと」) | +| 09-07 | operator が `secret "k8up-global" not found` | git に無い Secret | `restore.sh` が作り直す(リハーサルモードでは作らない) | +| 09-07 その 2 | Gateway の nodePort が 80/443 で戻らない | 上の「結果(2026-09-07 その 2)」 | `restore.sh` が最後に当て直す | -この 4 件は 2026-09-07 の Cilium 構成でも**再発していない**。`/etc/resolv.conf` の件は -kube-proxy が Cilium の kubeProxyReplacement に替わっても同じことが起きるので、`restore.sh` の手当てはそのまま要る。 +**「git がバックアップより進んでいる」状態は復元では普通に起きる。** どの回も、起動しなかったものの多くはスナップショットが古いことの表れだった。 ## 片付け diff --git a/docs/router.md b/docs/router.md index 3fb4d0f..f49df51 100644 --- a/docs/router.md +++ b/docs/router.md @@ -21,7 +21,7 @@ docs/talos.md に散らばっていたものを 1 つにまとめた(2026-10-07) | 項目 | 値 | | --- | --- | | LAN | `10.0.0.0/24`、ルーターは `10.0.0.1` | -| ノード `main` | `10.0.0.2`(bond0)、`240f:6d:842b:1::2`。**ノード側の静的設定**(`/etc/netplan/00-main.yaml`。DHCP は使わない。既定経路は `10.0.0.1`、IPv6 は RA を受ける) | +| ノード `main` | `10.0.0.2`(bond0)、`240f:6d:842b:1::2`。**ノード側の静的設定**(Ubuntu 期: `/etc/netplan/00-main.yaml` / Talos 後: `talos/patches/cluster.yaml`。DHCP は使わない。既定経路は `10.0.0.1`、IPv6 は RA を受ける) | | 別の LAN | `10.10.0.0/24`(ゲートウェイ `10.10.0.1`)。ノードの eno4 が `10.10.0.4`(静的。既定経路には使わない) | | DHCP | ルーターが配る。`10.0.0.10` から 128 個、リース 24 時間。固定割り当て・除外は 0 件。DHCPv6 のアドレス割り当ては自動 | | DNS | **ルーターの DHCP には DNS の欄が無い**(配るのはルーター自身の `10.0.0.1`)。AdGuard Home を使うのは、端末側で指定したものと、NetBird の端末(NetBird の DNS 設定で AdGuard を引く。pulumi/netbird.ts) | @@ -56,12 +56,17 @@ AdGuard Home は問い合わせ元を絞っておらず(apps/adguardhome/config. 「IPv6ホスト公開設定」の既定を「遮断」にし、ノード `main` だけをエントリで公開している(2026-10-07。それまでは既定が「公開」で、 LAN の全端末に外から IPv6 で直接届いていた)。 -- **エントリは MAC アドレスで指す**(bond0 の `e6:2c:1b:1a:a0:39`。`ssh main ip -br link show bond0` で見る。ルーターの「接続端末 情報画面」にも同じ値が出る)。IPv6 アドレスで指すと、RA のプレフィックスが変わったときに外れる。 - ノードの SLAAC のアドレス(同じ MAC から作る `…:e42c:1bff:fe1a:a039`)も一緒に公開される +- **エントリは MAC アドレスで指す**(bond0 の `e6:2c:1b:1a:a0:39`。Ubuntu 期は `ssh main ip -br link show bond0`、Talos 後は `talosctl -n 10.0.0.2 get links bond0` で見る。ルーターの「接続端末 情報画面」にも同じ値が出る)。IPv6 アドレスで指すと、RA のプレフィックスが変わったときに外れる +- **この MAC はノードの設定で固定している**: Ubuntu 期: `/etc/netplan/00-main.yaml` の bond0 の `macaddress` / Talos 後: `talos/patches/cluster.yaml` の `BondConfig` の `hardwareAddr`。 + **Ubuntu 期は固定していなかった**(netplan → systemd-networkd。2026-09-24 までは NetworkManager)ので、2026-10-16 の再起動で + `3a:43:95:a9:3e:b1` に変わってエントリが外れた。それで netplan に書いた。Talos に替えても Z440 に移しても、この値は変わらない +- **公開している AAAA は、いまは SLAAC のアドレス**(2026-10-17 に気づいた)。cloudflare-ddns の `local.iface.stable:bond0` は一時アドレス以外から選ぶので、 + 静的な `::2` ではなく EUI-64 の SLAAC(MAC から作る。MAC を固定したので再起動後は `…:e42c:1bff:fe1a:a039`)を書いている。 + Talos では `bond0` の `autoconf=0`(talos/patches/cluster.yaml)で SLAAC を付けないので、AAAA は `::2` になる - 公開の中身は DMZ と同じ(IPv6 すべて・ポート any)。ノードで受けているものは IPv6 でも全部外に出る -- `fj` / `nb` / `m` / `ks` / `lk` / `ssh` などの AAAA はノード(`240f:6d:842b:1::2`)を指すので、**このエントリを消すと IPv6 の利用者から直接つなぐアプリが見えなくなる** +- `fj` / `nb` / `m` / `ks` / `lk` / `ssh` などの AAAA はノード(上のとおり Ubuntu 期は SLAAC、Talos 後は `240f:6d:842b:1::2`)を指すので、**このエントリを消すと IPv6 の利用者から直接つなぐアプリが見えなくなる** - 変えたあと、別の回線の IPv6 から 443・6443・19198 に届くこと、`https://fj.doany.io` が 200 を返すことを確かめた -- ノードの bond0 の MAC を変えたら(NIC の交換・Talos への移行)、エントリも直す +- ノードの設定で bond0 の MAC を変えたら、エントリも直す(NIC の交換・Talos への移行・Z440 への移設では、上の固定があるので変わらない) ## ヘアピン NAT(NAT ループバック) diff --git a/docs/talos.md b/docs/talos.md index ffeccb2..1ac80b3 100644 --- a/docs/talos.md +++ b/docs/talos.md @@ -1,7 +1,8 @@ # 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 でやった。 +対象: HP ProLiant DL360 Gen9(UEFI、iLO は 10.0.0.3、Xeon E5-2696 v4、RDIMM は 6 枚中 5 枚で 40 GB、NIC は BCM5719 ×4)。 +ディスクは **SATA の SSD(Radeon R7、447 GB)を Smart Array(hpsa)越しに**見ている。Linux からは `/dev/sda`(`TRAN=sas` と出るが SAS ではない)。 +**Talos への移行はこの DL360 で先にやり、Z440 へ移すのはそのあと**(下の「Z440 に移すときに変わるもの」)。 いま使う版は [talos/versions.yaml](../talos/versions.yaml) が正(2026-10-13 時点で v1.15.0-alpha.1)。下の URL の版もそれに合わせる。 ## 1. Image Factory で ISO を作る @@ -29,17 +30,11 @@ customization: ```shell curl -X POST --data-binary @schematic.yaml https://factory.talos.dev/schematics -# => {"id":"2d61dd07b20062062ea671b4d01873506103b67c0f7a4c3fb6cf4ee85585dcb8"} +# => {"id":"96371c8f956f7f5f9d09925bb49fe096c0cad05c5cf5bdb464790982c0b067ca"} ``` -実際に発行した ID は 3 つある。 - -| 用途 | schematic ID | -| --- | --- | -| **本番**(dvb-pt3 + intel-ucode + IOMMU の引数。2026-10-13 から) | `96371c8f956f7f5f9d09925bb49fe096c0cad05c5cf5bdb464790982c0b067ca` | -| 旧本番(intel-ucode + IOMMU の引数。PT3 なし) | `32820716ca2384dc3cefbb672e6be929c67636e93e556d7740c312efb6538302` | -| 検証用(intel-ucode のみ) | `2d61dd07b20062062ea671b4d01873506103b67c0f7a4c3fb6cf4ee85585dcb8` | - +**本番の schematic ID は `96371c8f956f7f5f9d09925bb49fe096c0cad05c5cf5bdb464790982c0b067ca`**(上の YAML。2026-10-13 から)。 +それ以前は `32820716…`(intel-ucode + IOMMU の引数、PT3 なし)、ドリルは `2d61dd07…`(intel-ucode のみ)を使っていた。 Web UI(factory.talos.dev → Bare-metal Machine → amd64 → version → extensions)でも同じ ID が出る。 | 用途 | URL | @@ -48,44 +43,18 @@ Web UI(factory.talos.dev → Bare-metal Machine → amd64 → version → extens | 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 で乗り換える。 +**プレリリース(alpha / beta)も同じ形の URL で取れる**(版を差し替えるだけ。Web UI では pre-release を表示するチェックを入れる)。 +2026-10-13 に本番 schematic の ISO(約 409 MB)と installer が 200 を返すのを確かめた。1.15.0 の正式版が出たら Renovate の PR で乗り換える。 -ISO は約 400 MB。extension を足したくなったら schematic を作り直して ID を差し替え、`talosctl upgrade` で当てる(ISO を焼き直す必要はない)。 +extension を足したくなったら schematic を作り直して ID を差し替え、`talosctl upgrade` で当てる(ISO を焼き直す必要はない)。 ## 2. メディアに載せる -**本番は USB でやる**(2026-09-09 の判断。iLO の Virtual Media は USB を作らなくて済むが、 -挙動が怪しいことがあったため)。ISO はハイブリッドなので、そのまま書けばよい。 - -```shell -# 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 だけ物理で挿す。 +**本番は USB でやる**(2026-09-09 の判断。iLO の Virtual Media は USB を作らなくて済むが、挙動が怪しいことがあったため)。 +ISO はハイブリッドなのでそのまま書けばよい。書き方・起動のしかた(Secure Boot、F11、USB を抜く時機)・Virtual Media を使う場合は +[migration-day.md](migration-day.md) の 4 にまとめてある。 -**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 にならないので実機と条件が変わる。 +**コンソールは iLO の Remote Console(HTML5)でよい** ── 実機の前に立たなくてもブートメニューも maintenance mode の IP も見える。USB だけ物理で挿す。 ## 3. 起動したあと @@ -97,10 +66,11 @@ ISO から起動すると Talos は **maintenance mode** で立ち上がり、 ## 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](decisions.md)「PT3 チューナー」。 -- Secure Boot を切る(上記)。 +- **PT3 は dvb-pt3 extension(v1.15.0-alpha.1 から)で動く見込み。** extension は `earth_pt3` を blacklist して配るので + [patches/main.yaml](../talos/patches/main.yaml) で読ませている。denpa の tuner-agent は `/dev/dvb` と `/dev/bus`(B-CAS リーダー)を + hostDevices で掴む。**実機で `/dev/dvb/adapter*` と B-CAS リーダーが見えるかは当日確かめる**([migration-day.md](migration-day.md) の 9)。 + 経緯は [decisions.md](decisions.md)「PT3 チューナー」 +- Secure Boot を切る([migration-day.md](migration-day.md) の 4)。 - iLO の IP(10.0.0.3)と管理者パスワードを手元に。ISO 起動も再インストールも全部ここからできる。 ## machine config の作法(v1.14 で確認) @@ -119,8 +89,9 @@ ISO から起動すると Talos は **maintenance mode** で立ち上がり、 | インストール先ディスク | `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-` | +| bond とそのメンバー | `BondConfig` / `LinkConfig`。v1alpha1 の `machine.network.interfaces` は bond の MAC を指定できない([patches/cluster.yaml](../talos/patches/cluster.yaml)) | +| PV の置き場 | `UserVolumeConfig`。マウント先(`/var/mnt/`)は kubelet に自動で通るので `extraMounts` は要らない([patches/volumes.yaml](../talos/patches/volumes.yaml))。名前はトップレベルの `name`(1〜34 文字の英数字とハイフン)、ディスク上のラベルは `u-` | +| control-plane への scheduling 許可 | v1alpha1 の `allowSchedulingOnControlPlanes` は弾かれる。**`KubeNodeConfig` を `$patch: delete` して `labels` だけ書き戻す**(`HostnameConfig` も同じ手。理由は [patches/cluster.yaml](../talos/patches/cluster.yaml)) | ### service の IPv6 CIDR は `/108` 以下 @@ -180,14 +151,6 @@ configuration: namespaces: [kube-system] ``` -### 動いたこと - -- ISO(Image Factory の schematic `2d61dd07…`)から UEFI で起動、maintenance mode の API はポート 50000 -- `apply-config` → 自動でディスクへインストール → 再起動 → `bootstrap` → kubeconfig 取得 -- **`cluster.inlineManifests` は期待どおり適用された**(仕込んだ Namespace がラベルごと存在した)。 - `bootstrap/` をここに載せる案([decisions.md](decisions.md))は成立する -- flannel でデュアルスタック、CoreDNS / kube-proxy / scheduler すべて Running - ## etcd スナップショットからの復旧(検証済み) Talos 期のバックアップは **etcd(k8s オブジェクト)と PV データで別経路**になる。両方を試した。 @@ -231,8 +194,7 @@ restic dump /work/etcd.snapshot > etcd.snapshot ## VM の組み方(QEMU/KVM) -本番サーバの上で回す。**ホストのネットワークには触らない。** ドリル 4 回ぶんの -つまずきをここにまとめてある(各回で何が分かったかは下の記録)。 +k3s 期の本番サーバ(Ubuntu)の上で回していた。**ホストのネットワークには触らない。** ドリルのつまずきをここにまとめてある。 ### tap/bridge は要らない。QEMU の内部ハブで足りる @@ -260,7 +222,7 @@ sudo qemu-system-x86_64 -machine q35,accel=kvm -cpu host -smp 4 -m 12288 \ **KVM を使うので sudo で起動する。** `patches/*.yaml` からの読み替えは **インタフェース名とディスクだけ**(`eno1`/`eno2`/`eno4` → `enp0s2`/`enp0s3`/`enp0s4`、 `/dev/sda` → `/dev/vda`)。`balance-alb`・`miimon: 100`・静的 IPv6・ -**v6 のデフォルトルートを書かないこと**はそのまま。ボリュームの大きさだけディスクに合わせて縮める。 +**v6 のデフォルトルートを書かないこと**はそのまま。 ### つまずいた点 @@ -273,8 +235,7 @@ sudo qemu-system-x86_64 -machine q35,accel=kvm -cpu host -smp 4 -m 12288 \ - **`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 本にする」と - 書いていたが、原因はハブの挿し間違いのほうだった + ハブが正しければ **NIC 3 本でも問題ない** - **インタフェース名は起動ごとに変わる。** maintenance mode で `talosctl get links --insecure` を見てから patch を読み替えること - **コンソールが要るときは monitor の `screendump`。** `-display none` でも @@ -296,48 +257,11 @@ sudo qemu-system-x86_64 -machine q35,accel=kvm -cpu host -smp 4 -m 12288 \ `--endpoint https://127.0.0.1:` を渡す。`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](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` で落とす。** +app-of-apps まで同期させると cloudflare-ddns(本番の DNS を書き換える)・netbird(本番に繋ぐ)・ +k8up(本番の restic リポジトリに書く)・ACME・SMTP が外に出る。**Talos の VM の中には入れないし、ホスト側で宛先だけで落とすと本番の通信まで +巻き込む**ので、QEMU を専用ユーザーで動かして `-m owner --uid-owner` で落とす(イメージの取得は通るので同期は進む): ```shell sudo useradd -r -M -s /usr/sbin/nologin -G kvm qemudrill @@ -350,64 +274,47 @@ 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 --