diff --git a/.github/workflows/talos-boot.yml b/.github/workflows/talos-boot.yml index e7d01af5..61b8512b 100644 --- a/.github/workflows/talos-boot.yml +++ b/.github/workflows/talos-boot.yml @@ -37,8 +37,8 @@ env: jobs: boot: runs-on: ubuntu-latest - # 実測 8 分 - timeout-minutes: 30 + # 実測 8 分(PVC と再起動を足す前) + timeout-minutes: 40 steps: - uses: actions/checkout@v7 @@ -93,7 +93,7 @@ jobs: curl -fsSL --retry 3 \ "https://factory.talos.dev/image/${{ steps.v.outputs.schematic }}/${{ steps.v.outputs.version }}/metal-amd64.raw.zst" \ | zstd -d -o /tmp/vm/disk.raw - # EPHEMERAL の maxSize 64GiB と u-local-path の minSize 50GiB が両方取れる大きさ + # EPHEMERAL の maxSize 64GiB と r-lvm(LVM の PV)の minSize 50GiB が両方取れる大きさ truncate -s 140G /tmp/vm/disk.raw cp /usr/share/OVMF/OVMF_VARS_4M.fd /tmp/vm/OVMF_VARS.fd @@ -206,14 +206,18 @@ jobs: until kubectl -n kube-system get secret hubble-server-certs hubble-relay-client-certs >/dev/null 2>&1; do sleep 5; done ro kube-system deploy/hubble-relay ro kube-system deploy/hubble-ui - ro local-path-storage deploy/local-path-provisioner + # PVC の置き場(bootstrap/storage.yaml)。thin プールが出来てラベルが付くまで OpenEBS のノードの Pod は出てこない + ro kube-system ds/lvm-thinpool + ro kube-system ds/lvm-localpv-node + ro kube-system deploy/lvm-localpv-controller + ro kube-system deploy/snapshot-controller ro cert-manager deploy/cert-manager ro cert-manager deploy/cert-manager-webhook ro cert-manager deploy/cert-manager-cainjector ro argocd deploy/argocd-server ro argocd deploy/argocd-repo-server ro argocd sts/argocd-application-controller - # postgres の PVC は local-path なので、ここで PV の経路も見ている + # postgres の PVC は既定の StorageClass(OpenEBS LVM の thin)なので、ここで PV の経路も見ている ro infisical deploy/infisical kubectl -n infisical wait --for=jsonpath='{.status.phase}'=Bound pvc --all --timeout=1m # 上の一覧に無い Pod の取りこぼし止め @@ -263,7 +267,7 @@ jobs: set -euo pipefail fail() { echo "::error::$*"; exit 1; } talosctl get volumestatus -o json > /tmp/vol.json - for v in EPHEMERAL u-local-path; do + for v in EPHEMERAL r-lvm; do P=$(jq -rs --arg v "$v" '.[] | select(.metadata.id == $v) | .spec.phase' /tmp/vol.json) [ "$P" = ready ] || fail "volume $v が ready でない($P)" echo " volume $v: $P" @@ -294,6 +298,94 @@ jobs: done fail "bond0 に RA 由来の IPv6 既定経路が無い" + - name: PVC・スナップショット・スナップショットからの戻し + timeout-minutes: 8 + run: | + set -euo pipefail + fail() { echo "::error::$*"; exit 1; } + # thin プールは VG の 90%(bootstrap/storage.yaml)。OpenEBS が最初の PVC の大きさで作ったものではないこと + kubectl -n kube-system exec ds/lvm-thinpool -c watch -- lvs -a -o lv_name,lv_size,data_percent,metadata_percent,lv_attr data + VG=$(kubectl -n kube-system exec ds/lvm-thinpool -c watch -- vgs --noheadings --units b --nosuffix -o vg_size data | tr -d ' ') + POOL=$(kubectl -n kube-system exec ds/lvm-thinpool -c watch -- lvs --noheadings --units b --nosuffix -o lv_size data/data_thinpool | tr -d ' ') + [ $((POOL * 100 / VG)) -ge 85 ] || fail "thin プールが VG の $((POOL * 100 / VG))%(90% のはず)" + + # 既定の StorageClass で書き、スナップショットを取り、そこから別の PVC を作って同じ中身が読めるか(VolSync の populator の前提) + cat > /tmp/pvc.yaml <<'YAML' + apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + name: t-src + spec: + accessModes: [ReadWriteOnce] + resources: {requests: {storage: 1Gi}} + --- + apiVersion: v1 + kind: Pod + metadata: + name: t-src + spec: + securityContext: {runAsNonRoot: true, runAsUser: 1000, fsGroup: 1000, seccompProfile: {type: RuntimeDefault}} + containers: + - name: c + image: mirror.gcr.io/library/busybox:1.38 + # 再起動で起き直しても書き直さない(下の「再起動しても PV が戻る」が中身の残りを見る) + command: [sh, -c, '[ -f /data/marker ] || echo hello > /data/marker; sleep 86400'] + securityContext: {allowPrivilegeEscalation: false, capabilities: {drop: [ALL]}} + volumeMounts: [{name: v, mountPath: /data}] + volumes: [{name: v, persistentVolumeClaim: {claimName: t-src}}] + YAML + kubectl apply -f /tmp/pvc.yaml + kubectl wait --for=condition=Ready pod/t-src --timeout=3m + kubectl wait --for=jsonpath='{.status.phase}'=Bound pvc/t-src --timeout=1m + [ "$(kubectl exec t-src -- cat /data/marker)" = hello ] || fail "PVC に書けていない" + FS=$(kubectl exec t-src -- sh -c "grep ' /data ' /proc/mounts | cut -d' ' -f3") + [ "$FS" = xfs ] || fail "PVC が xfs でない($FS)" + echo " PVC t-src: Bound, xfs, 書けた" + + cat > /tmp/snap.yaml <<'YAML' + apiVersion: snapshot.storage.k8s.io/v1 + kind: VolumeSnapshot + metadata: + name: t-snap + spec: + source: {persistentVolumeClaimName: t-src} + YAML + kubectl apply -f /tmp/snap.yaml + kubectl wait --for=jsonpath='{.status.readyToUse}'=true volumesnapshot/t-snap --timeout=3m + echo " VolumeSnapshot t-snap: readyToUse(既定の VolumeSnapshotClass $(kubectl get volumesnapshot t-snap -o jsonpath='{.spec.volumeSnapshotClassName}'))" + + # t-dst はスナップショットの中身を読むだけ(書かない) + sed -e 's/name: t-src$/name: t-dst/' -e 's/claimName: t-src/claimName: t-dst/' \ + -e 's/\[ -f \/data\/marker \] || echo hello > \/data\/marker; //' /tmp/pvc.yaml \ + | yq '(select(.kind == "PersistentVolumeClaim") | .spec.dataSource) = {"apiGroup": "snapshot.storage.k8s.io", "kind": "VolumeSnapshot", "name": "t-snap"}' \ + > /tmp/dst.yaml + ! grep -q 'echo hello' /tmp/dst.yaml || fail "t-dst が書いてしまう形になっている" + grep -q 'kind: VolumeSnapshot' /tmp/dst.yaml || fail "t-dst に dataSource が付いていない" + kubectl apply -f /tmp/dst.yaml + kubectl wait --for=condition=Ready pod/t-dst --timeout=3m + [ "$(kubectl exec t-dst -- cat /data/marker)" = hello ] || fail "スナップショットから戻した PVC に中身が無い" + echo " PVC t-dst(t-snap から): 中身が戻った" + kubectl -n kube-system exec ds/lvm-thinpool -c watch -- lvs -a -o lv_name,lv_size,pool_lv,origin,data_percent data + + - name: 再起動しても PV が戻る + timeout-minutes: 10 + run: | + set -euo pipefail + fail() { echo "::error::$*"; exit 1; } + # thin プールと LV は Talos が起動時に有効にする(thin_check が無いのは飛ばされる)。 + # lvm-thinpool の pool も起動のたびに走るので、壊さないことも見ている + talosctl reboot --wait --timeout 8m + until kubectl get --raw /readyz >/dev/null 2>&1; do sleep 5; done + kubectl wait --for=condition=Ready node/main --timeout=5m + for p in t-src t-dst; do + until kubectl wait --for=condition=Ready "pod/$p" --timeout=10s >/dev/null 2>&1; do sleep 5; done + [ "$(kubectl exec "$p" -- cat /data/marker)" = hello ] || fail "再起動のあと $p の中身が無い" + done + kubectl -n infisical rollout status statefulset/postgresql --timeout=5m + kubectl -n kube-system rollout status ds/lvm-thinpool --timeout=3m + kubectl -n kube-system logs ds/lvm-thinpool -c pool + echo " 再起動のあとも t-src・t-dst・infisical の Postgres が上がった" + - name: 失敗したときの様子 if: failure() run: | @@ -309,6 +401,18 @@ jobs: echo "::group::GatewayClass / Gateway / Certificate" kubectl get gatewayclass,gateway -A -o yaml; kubectl get certificate -A echo "::endgroup::" + echo "::group::PVC の置き場(LVM)" + kubectl get sc,volumesnapshotclass,pvc,pv,volumesnapshot,volumesnapshotcontent -A + kubectl -n kube-system get lvmnode,lvmvolume,lvmsnapshot -o yaml | tail -120 + kubectl -n kube-system logs ds/lvm-thinpool -c pool --tail=50 + kubectl -n kube-system logs ds/lvm-localpv-node -c openebs-lvm-plugin --tail=80 + kubectl -n kube-system logs deploy/lvm-localpv-controller -c openebs-lvm-plugin --tail=50 + kubectl -n kube-system logs deploy/lvm-localpv-controller -c csi-provisioner --tail=50 + kubectl -n kube-system logs deploy/lvm-localpv-controller -c csi-snapshotter --tail=50 + kubectl -n kube-system logs deploy/snapshot-controller --tail=50 + talosctl get volumestatus + talosctl get rd | awk 'tolower($4) ~ /lvm/ {print $4}' | while read -r r; do talosctl get "$r" -o yaml 2>/dev/null | head -60; done + echo "::endgroup::" echo "::group::talosctl services"; talosctl services; echo "::endgroup::" echo "::group::talosctl get links / addresses / routes" talosctl get links; talosctl get addresses; talosctl get routes diff --git a/ROADMAP.md b/ROADMAP.md index 791ecc02..b4d27166 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -18,8 +18,11 @@ bond0 の MAC は設定で固定してあるのでルーターは触らない。 - [ ] AMT(10.0.0.5)、Z440 用の schematic(GPU の extension と SOL の `console=`)、NIC の別名(`LinkAliasConfig`) - [ ] 移して確かめる。docs の「iLO(10.0.0.3)」を AMT に直す - [ ] **ストレージとバックアップを入れ替える**(入れ直しのついでに。理由は [docs/decisions.md](docs/decisions.md)「Z440 で、ストレージを OpenEBS LVM LocalPV に…」) - - [ ] ディスクの切り方: `u-local-path` をやめ、`RawVolumeConfig` + `LVMVolumeGroupConfig` でボリュームグループ(thin プール)にする - - [ ] OpenEBS LVM LocalPV と snapshot-controller(CRD 込み)を入れる。Talos で動くかを先に QEMU(talos-boot)で確かめる + - [x] ディスクの切り方: `u-local-path` をやめ、`RawVolumeConfig` + `LVMVolumeGroupConfig` で VG `data` にする。 + thin プールは Talos で作れないので `bootstrap/storage.yaml` の DaemonSet が作る(書いてあるだけ。効くのは Z440 で入れ直したとき) + - [x] OpenEBS LVM LocalPV と snapshot-controller(CRD 込み)を入れる。StorageClass は名前を `local-path` のまま OpenEBS に。 + talos-boot で PVC・VolumeSnapshot・スナップショットからの PVC・再起動のあとまでを確かめている + - [ ] Z440 で入れ直し、`recovery/pv.sh - restore-all` で戻す([docs/z440-migration.md](docs/z440-migration.md) の 4〜7) - [ ] VolSync(restic mover、copyMethod: Snapshot)を入れ、PVC ごとに ReplicationSource。R2 は PVC ごとに別の経路 - [ ] 戻しは populator(PVC の `dataSourceRef`)。移設のときは `recovery/pv.sh restore-all`(k8up の restic から)で戻し、そのあと VolSync に切り替える - [ ] k8up・`backupcommand`・notify・restore-check を VolSync に合わせて畳む。k8up のリポジトリは保持期間(月次 6 か月)が切れるまで残す diff --git a/apps/adguardhome/deployment.yaml b/apps/adguardhome/deployment.yaml index c6b099bc..c2ee814a 100644 --- a/apps/adguardhome/deployment.yaml +++ b/apps/adguardhome/deployment.yaml @@ -7,7 +7,9 @@ metadata: # AdGuard 自身の認証は切ってある(`users: []`)。守っているのは前段の oauth2-proxy # (apps/auth/adguard-proxy.yaml、Entra のアプリロール `admin`)。 # Web の口はクラスタ内からは無認証になる。外からは Gateway 経由だけで、その手前に SSO がある。 - # クエリログの保持(querylog.interval)は 90d だと querylog.json が 2 GB に育ちバックアップに乗るので 7d。 + # クエリログの保持(querylog.interval)は 1d。2026-10 末に 1 日 10 GB 近く書くようになり、7d では querylog.json が + # 68 GB に育った(バックアップにも乗る)。PVC は LVM で大きさどおりの上限があるので、ファイル 2 本(今と 1 つ前)が + # adguardhome-work(pvc.yaml)に収まる長さにする。量が戻ったら延ばしてよい。 spec: replicas: 1 # hostPort を持つので同じポートを 2 つの Pod が同時に掴めない。RollingUpdate だと @@ -139,7 +141,7 @@ spec: querylog: dir_path: "" ignored: [] - interval: 7d + interval: 1d size_memory: 1000 enabled: true ignored_enabled: false diff --git a/apps/adguardhome/pvc.yaml b/apps/adguardhome/pvc.yaml index bee88a83..99b49095 100644 --- a/apps/adguardhome/pvc.yaml +++ b/apps/adguardhome/pvc.yaml @@ -28,4 +28,5 @@ spec: storageClassName: local-path resources: requests: - storage: 5Gi + # クエリログ(deployment.yaml の querylog.interval)の 2 本ぶん。LVM なのでこれが本当の上限になる + storage: 30Gi diff --git a/bootstrap/README.md b/bootstrap/README.md index f6f0e243..ad37e240 100644 --- a/bootstrap/README.md +++ b/bootstrap/README.md @@ -6,7 +6,7 @@ ArgoCD はここを見ない(例外は [argocd-repos.yaml](argocd-repos.yaml) | | 何を | いつ | | --- | --- | --- | -| **machine config**(このディレクトリ) | Talos の設定、Helm で入れるもの(cilium・cert-manager・argocd・infisical)、`apiserver-rbac.yaml`、`local-path.yaml`、SOPS 済みの Secret 2 つ | `render.sh` → `apply-config` → `upgrade-k8s`([talos.md](talos.md)「上げ方 / 当て直し方」) | +| **machine config**(このディレクトリ) | Talos の設定、Helm で入れるもの(cilium・cert-manager・argocd・infisical・lvm-localpv)、`apiserver-rbac.yaml`、`storage.yaml`、SOPS 済みの Secret 2 つ | `render.sh` → `apply-config` → `upgrade-k8s`([talos.md](talos.md)「上げ方 / 当て直し方」) | | **ArgoCD**(`apps/`) | 平文のマニフェスト(`ClusterIssuer`・Gateway・auth・`InfisicalSecret` など)。2026-10 まではここにあって、Application `bootstrap` が当てていた | main へのマージ(prune / selfHeal 付き) | ## ファイル @@ -24,7 +24,8 @@ ArgoCD はここを見ない(例外は [argocd-repos.yaml](argocd-repos.yaml) | [infisical-values.sops.yaml](infisical-values.sops.yaml) | chart の値と版(DB と Redis のパスワードだけ SOPS)。2 つめのドキュメントが infisical の起動に要る Secret(`ENCRYPTION_KEY` など) | | [cert-manager-values.sops.yaml](cert-manager-values.sops.yaml) | chart の値と版。2 つめのドキュメントが DNS-01 の Cloudflare のトークンの Secret(トークンだけ SOPS) | | [apiserver-rbac.yaml](apiserver-rbac.yaml) / [apiserver-ca.crt](apiserver-ca.crt) | API サーバの入口の RBAC と CA(下の「API サーバの入口」) | -| [local-path.yaml](local-path.yaml) | local-path-provisioner | +| [lvm-localpv-values.yaml](lvm-localpv-values.yaml) | chart の値と版(OpenEBS LVM LocalPV。PVC の置き場。下の「PVC の置き場」) | +| [storage.yaml](storage.yaml) | StorageClass `local-path`・VolumeSnapshotClass・thin プールを作る DaemonSet(chart が描かないもの) | | [argocd-repos.yaml](argocd-repos.yaml) | ArgoCD が当てる唯一のファイル(下の「ArgoCD が当てるもの」) | **SOPS(age)で暗号化したファイルは名前を `*.sops.yaml` で終える**(`.sops.yaml` の規則もその名前にしか一致しないので、 @@ -69,7 +70,7 @@ Talos・Kubernetes・Gateway API の版も同じ形で、[talos.sops.yaml](talos `render.sh` が復号してから helm に渡す。**2 つめのドキュメントがあれば、それは chart とは別に machine config に載せる Secret** (infisical の `infisical-secrets`、cert-manager の Cloudflare のトークン。ArgoCD は復号できないのでここに置く)で、 `render.sh` が `---` で分けて別の inlineManifest にする。 -argocd・infisical の版は自動マージしない(`renovate.json` の `bootstrap charts`)。 +argocd・infisical の版は自動マージしない(`renovate.json` の `bootstrap charts`)。lvm-localpv も(`openebs lvm`。`storage.yaml` の lvm-driver のイメージと同じ PR)。 Namespace は chart が描かないので、`render.sh` が kube-system 以外のものに付ける。 ## git と live がずれていないか @@ -106,6 +107,25 @@ kubectl -n kube-system exec ds/cilium -c cilium-agent -- cilium-dbg service list kubectl -n kube-system get gateway doany ``` +## PVC の置き場 + +**OpenEBS LVM LocalPV**(thin)。PVC ごとに VG `data` の thin プールから LV を切り、xfs で渡す。 +infisical の Postgres が要るので bootstrap 層に居る。選んだ理由と組んだ形は [../docs/decisions.md](../docs/decisions.md)「Z440 で、ストレージを OpenEBS LVM LocalPV に…」。 + +| 何を | 誰が | +| --- | --- | +| パーティション `r-lvm` と VG `data` | Talos([talos.sops.yaml](talos.sops.yaml) の `RawVolumeConfig` / `LVMVolumeGroupConfig`)。**入れ直したときにしか作られない** | +| thin プール `data_thinpool`(VG の 90%) | [storage.yaml](storage.yaml) の DaemonSet `lvm-thinpool`。出来たらノードに `doany.io/lvm-thinpool=ready` を付ける | +| CSI ドライバ(`local.csi.openebs.io`) | chart([lvm-localpv-values.yaml](lvm-localpv-values.yaml))。ノードの Pod は上のラベルが付いてから動く | +| StorageClass `local-path`(既定)・VolumeSnapshotClass `lvm`(既定) | [storage.yaml](storage.yaml) | +| snapshot-controller と VolumeSnapshot の CRD | `render.sh` が external-snapshotter のタグの URL で(版は [talos.sops.yaml](talos.sops.yaml) の先頭) | + +- **PVC の requests が本当の上限。** 足りなくなったら PVC の requests を上げる(`allowVolumeExpansion: true`。スナップショットのある PVC は広げられない) +- **thin プールが溢れると全部の PVC の書き込みが止まる。** `kubectl -n kube-system get pods -l app=lvm-thinpool` が 90% で NotReady になる。 + 埋まり具合は `kubectl -n kube-system exec ds/lvm-thinpool -c watch -- lvs data`。足すときは VG の残り(1 割)から + `kubectl -n kube-system exec ds/lvm-thinpool -c watch -- lvextend -L +10G data/data_thinpool` +- **新しい PVC の直下は root の 0755。** root 以外で書くアプリは Pod に `fsGroup` を付ける + ## Secret 平文の Secret はアプリのリポジトリにはコミットしない。値は **Infisical**(https://il.doany.io、このクラスタでセルフホスト)が持ち、 @@ -113,7 +133,7 @@ kubectl -n kube-system get gateway doany - **本体**(chart + Postgres/Redis)だけがここ(machine config)。`infisical-values.sops.yaml` の 2 つめのドキュメントの `ENCRYPTION_KEY` が DB の暗号鍵で、これを失うと Infisical の中身が全部読めなくなる(クラスタで一番失ってはいけない値)。 - Postgres の PVC は `local-path` で、`pg_dump` を k8up が日次で取っている。 + Postgres の PVC は既定の `local-path`(OpenEBS LVM)で、`pg_dump` を k8up が日次で取っている。 - **公開経路・operator・push-bridge は [`apps/infisical/`](../apps/infisical/)**(ArgoCD 自身は operator に依存しないので、ここに置く理由がない)。 - 認証は Infisical の Kubernetes auth: operator が SA `infisical-auth` のトークンで Infisical にログインし、 Infisical がそのトークンで TokenReview を呼んで本物か確かめる(なので SA に `system:auth-delegator` を付けてある。 diff --git a/bootstrap/local-path.yaml b/bootstrap/local-path.yaml deleted file mode 100644 index 64caf5c4..00000000 --- a/bootstrap/local-path.yaml +++ /dev/null @@ -1,139 +0,0 @@ -# local-path-provisioner。Talos には無いので、入れないと PVC が 1 つも bind せず、 -# ストレージを持つアプリが全部 Pending で止まる。 -# -# - データの置き場は `/var/mnt/local-path`([`talos.sops.yaml`](talos.sops.yaml) の -# `UserVolumeConfig` が切る別パーティション)。ディレクトリは `pvc-__` -# - namespace は `local-path-storage`(PSA は privileged。ヘルパー Pod が hostPath を使う) -# - StorageClass は既定の `local-path` 1 つだけ。PVC を守るのは Retain ではなく日次の restic -# (docs/decisions.md「PVC を守る仕掛けをやめた」) -apiVersion: v1 -kind: Namespace -metadata: - name: local-path-storage - labels: - # ヘルパー Pod が hostPath を使うので baseline では通らない。 - pod-security.kubernetes.io/enforce: privileged ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: local-path-provisioner-service-account - namespace: local-path-storage ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: local-path-provisioner-role -rules: - - apiGroups: [""] - resources: [nodes, persistentvolumeclaims, configmaps, pods, pods/log] - verbs: [get, list, watch] - - apiGroups: [""] - resources: [persistentvolumes] - verbs: [get, list, watch, create, patch, update, delete] - - apiGroups: [""] - resources: [events] - verbs: [create, patch] - - apiGroups: [storage.k8s.io] - resources: [storageclasses] - verbs: [get, list, watch] - - apiGroups: [""] - resources: [pods] - verbs: [create, delete] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: local-path-provisioner-bind -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: local-path-provisioner-role -subjects: - - kind: ServiceAccount - name: local-path-provisioner-service-account - namespace: local-path-storage ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: local-path-provisioner - namespace: local-path-storage -spec: - replicas: 1 - selector: - matchLabels: - app: local-path-provisioner - template: - metadata: - labels: - app: local-path-provisioner - spec: - serviceAccountName: local-path-provisioner-service-account - containers: - - name: local-path-provisioner - # Docker Hub の匿名の取得は絞られるので mirror.gcr.io(Docker Hub の pull-through キャッシュ。中身は同じ)。 - # 下の busybox も同じ - image: mirror.gcr.io/rancher/local-path-provisioner:v0.0.37 - command: [local-path-provisioner, start, --config, /etc/config/config.json] - volumeMounts: - - name: config-volume - mountPath: /etc/config/ - env: - - name: POD_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - volumes: - - name: config-volume - configMap: - name: local-path-config ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: local-path-config - namespace: local-path-storage -data: - config.json: |- - { - "nodePathMap":[ - { - "node":"DEFAULT_PATH_FOR_NON_LISTED_NODES", - "paths":["/var/mnt/local-path"] - } - ] - } - # `setup` と `teardown` は必須。ヘルパー Pod はこの ConfigMap を - # `subPath` で 1 キーずつマウントするので、キーが無いとヘルパー Pod が - # `configmap references non-existent config key: setup` で ContainerCreating のまま止まり、 - # PVC が永遠に Pending になる(2026-09-08 の VM ドリルで踏んだ。上流の - # deploy/local-path-storage.yaml には入っている)。 - setup: |- - #!/bin/sh - set -eu - mkdir -m 0777 -p "$VOL_DIR" - teardown: |- - #!/bin/sh - set -eu - rm -rf "$VOL_DIR" - helperPod.yaml: |- - apiVersion: v1 - kind: Pod - metadata: - name: helper-pod - spec: - containers: - - name: helper-pod - image: mirror.gcr.io/library/busybox:1.38.0 - imagePullPolicy: IfNotPresent ---- -apiVersion: storage.k8s.io/v1 -kind: StorageClass -metadata: - name: local-path - annotations: - storageclass.kubernetes.io/is-default-class: "true" -provisioner: rancher.io/local-path -reclaimPolicy: Delete -volumeBindingMode: WaitForFirstConsumer diff --git a/bootstrap/lvm-localpv-values.yaml b/bootstrap/lvm-localpv-values.yaml new file mode 100644 index 00000000..6432a090 --- /dev/null +++ b/bootstrap/lvm-localpv-values.yaml @@ -0,0 +1,31 @@ +# chart と版。Renovate が上げ、render.sh が読む(README.md「Helm で入れるもの」)。 +# renovate: datasource=helm depName=lvm-localpv registryUrl=https://openebs.github.io/lvm-localpv +# version: 1.10.1 +# OpenEBS LVM LocalPV の chart の値。PVC を talos.sops.yaml の VG `data` の thin プールの LV にする。 +# StorageClass・VolumeSnapshotClass・thin プールを作る DaemonSet は storage.yaml、 +# snapshot-controller と VolumeSnapshot の CRD は render.sh が URL で入れる(版は talos.sops.yaml の先頭)。 +# +# 容量で Pod の置き場を絞らせない。thin なので PVC の大きさは上限でしかなく、合計がプールを超えてよい +# (denpa は 1Ti と 300Gi)。true だと kube-scheduler が「プールの空き < PVC」のものを置けずに Pending にする。 +storageCapacity: false +# 匿名の利用統計(Google Analytics)を送らない +analytics: + enabled: false +crds: + # LVMVolume などの CRD は小さいので inline に入れる。VolumeSnapshot の CRD は render.sh が external-snapshotter から入れる + csi: + volumeSnapshots: + enabled: false +lvmController: + # snapshot-controller はクラスタに 1 つ(VolSync も使う)なので、chart の同梱ではなく render.sh が入れる + snapshotController: + enabled: false +lvmNode: + # thin プールが出来てから動かす(storage.yaml の lvm-thinpool がこのラベルを付ける)。 + # 先に PVC を受けると、OpenEBS が最初の PVC と同じ大きさの小さい thin プールを作ってしまう。 + nodeSelector: + doany.io/lvm-thinpool: ready +lvmPlugin: + image: + # Docker Hub の匿名の取得は絞られるので mirror.gcr.io(Docker Hub の pull-through キャッシュ。中身は同じ) + registry: mirror.gcr.io/ diff --git a/bootstrap/render.sh b/bootstrap/render.sh index cd308029..bb3a327a 100755 --- a/bootstrap/render.sh +++ b/bootstrap/render.sh @@ -44,6 +44,7 @@ TALOS=$(tver siderolabs/talos) SCHEMATIC=$(sed -n '/^# schematic: /{s/^# schematic: *//p;q;}' bootstrap/talos.sops.yaml) K8S=$(tver kubernetes/kubernetes) GWAPI=$(tver kubernetes-sigs/gateway-api) +SNAP=$(tver kubernetes-csi/external-snapshotter) # chart の値は -values.yaml、秘密を含むものは -values.sops.yaml(秘密のキーだけ SOPS)。 vfile() { # $1=name if [ -f "bootstrap/$1-values.sops.yaml" ]; then echo "bootstrap/$1-values.sops.yaml"; else echo "bootstrap/$1-values.yaml"; fi @@ -54,11 +55,12 @@ CILIUM=$(bver cilium) CERTMGR=$(bver cert-manager) ARGOCD=$(bver argocd) INFISICAL=$(bver infisical) -for v in "$TALOS" "$SCHEMATIC" "$K8S" "$GWAPI" "$CILIUM" "$CERTMGR" "$ARGOCD" "$INFISICAL"; do +LVM=$(bver lvm-localpv) +for v in "$TALOS" "$SCHEMATIC" "$K8S" "$GWAPI" "$SNAP" "$CILIUM" "$CERTMGR" "$ARGOCD" "$INFISICAL" "$LVM"; do [ -n "$v" ] || { echo "ERROR: talos.sops.yaml / -values(.sops).yaml の # version: から版を読めなかった" >&2; exit 1; } done -echo "talos=$TALOS k8s=$K8S gateway-api=$GWAPI" -echo "cilium=$CILIUM cert-manager=$CERTMGR argo-cd=$ARGOCD infisical-standalone=$INFISICAL" +echo "talos=$TALOS k8s=$K8S gateway-api=$GWAPI external-snapshotter=$SNAP" +echo "cilium=$CILIUM cert-manager=$CERTMGR argo-cd=$ARGOCD infisical-standalone=$INFISICAL lvm-localpv=$LVM" inline() { # $1=name stdin=manifest printf 'apiVersion: v1alpha1\nkind: KubeInlineManifestConfig\nname: %s\nmanifest: |-\n' "$1" @@ -146,7 +148,10 @@ chart() { # $1=name $2=namespace 残り=helm template に足す引数 # `--api-versions` が無いと helm template は GatewayClass を黙って描かず、公開経路が全部死ぬ。 chart cilium kube-system --api-versions gateway.networking.k8s.io/v1/GatewayClass -inline local-path < bootstrap/local-path.yaml > "$WORK/inline-local-path.yaml" +# PVC の置き場。infisical の Postgres が要るので bootstrap 層に置く。chart(CSI ドライバ)と、 +# chart が描かないもの(StorageClass・VolumeSnapshotClass・thin プール)は storage.yaml。 +chart lvm-localpv kube-system +inline storage < bootstrap/storage.yaml > "$WORK/inline-storage.yaml" # CI と人の RBAC。締め出されても直せるよう ArgoCD に載せない(ArgoCD は bootstrap/ を argocd-repos.yaml しか見ない)。 inline apiserver-rbac < bootstrap/apiserver-rbac.yaml > "$WORK/inline-apiserver-rbac.yaml" @@ -194,6 +199,28 @@ name: gateway-api url: https://github.com/kubernetes-sigs/gateway-api/releases/download/${GWAPI}/standard-install.yaml PATCH +# VolumeSnapshot の CRD 3 つと snapshot-controller(kube-system)。上流の置き場のまま、タグで固定して渡す。 +# グループのスナップショット(groupsnapshot.storage.k8s.io)は機能を有効にしていないので入れない。 +SNAPURL=https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/$SNAP +for c in volumesnapshotclasses volumesnapshotcontents volumesnapshots; do + cat >> "$WORK/external-crds.yaml" <> "$WORK/external-crds.yaml" <&2; exit 1; } done # 無いことを見る側(flannel はドキュメントが在るだけで有効になる) -for n in KubeFlannelCNIConfig /var/local-path-provisioner; do +# (snapshot-controller と VolumeSnapshot の CRD は URL で入れるので、chart の同梱が混ざると 2 つになる) +for n in KubeFlannelCNIConfig rancher.io/local-path UserVolumeConfig \ + sig-storage/snapshot-controller volumesnapshots.snapshot.storage.k8s.io; do ! grep -qF -- "$n" "$OUT/controlplane.yaml" || { echo "ERROR: '$n' が生成物に残っている" >&2; exit 1; } done # CRD は inline に入れない(入ると MB 単位で増える)。包む前のファイルを見る。 diff --git a/bootstrap/storage.yaml b/bootstrap/storage.yaml new file mode 100644 index 00000000..80e0ed12 --- /dev/null +++ b/bootstrap/storage.yaml @@ -0,0 +1,154 @@ +# PVC の置き場(OpenEBS LVM LocalPV。chart は lvm-localpv-values.yaml)のうち、chart が描かないもの。render.sh が inline に載せる。 +# +# - VG `data` は Talos が作る(talos.sops.yaml の RawVolumeConfig `lvm` と LVMVolumeGroupConfig)。thin プール `data_thinpool` は +# Talos では作れない(LVMLogicalVolumeConfig は linear と raid だけ)ので、下の lvm-thinpool が作る +# - StorageClass は既定の `local-path` 1 つ。名前は local-path-provisioner の頃のまま(各リポジトリの PVC が名指ししている)。 +# PVC を守るのは Retain ではなく日次の restic(docs/decisions.md「PVC を守る仕掛けをやめた」) +# - kube-system に置く(Talos は kube-system を Pod Security の対象から外している。どれも特権が要る) +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: local-path + annotations: + storageclass.kubernetes.io/is-default-class: "true" +provisioner: local.csi.openebs.io +parameters: + storage: lvm + volgroup: data + # thin でないとスナップショットから戻せない(OpenEBS の制限)。PVC の大きさは上限で、書いた分だけプールを使う + thinProvision: "yes" + # ext4 は lost+found を作り、PGDATA がボリュームの直下の Postgres(forgejo・rybbit)が initdb で落ちる + csi.storage.k8s.io/fstype: xfs +reclaimPolicy: Delete +volumeBindingMode: WaitForFirstConsumer +allowVolumeExpansion: true +--- +apiVersion: snapshot.storage.k8s.io/v1 +kind: VolumeSnapshotClass +metadata: + name: lvm + annotations: + snapshot.storage.kubernetes.io/is-default-class: "true" +driver: local.csi.openebs.io +deletionPolicy: Delete +--- +# thin プールを作り、出来たらノードにラベルを付けて OpenEBS のノードの Pod を動かす(lvm-localpv-values.yaml の nodeSelector)。 +# OpenEBS は thin プールが無いと最初の PVC と同じ大きさで作り、あとから広げない(Talos には dmeventd が無く、自動の拡張も効かない)。 +# LVM のメタデータを書くのは OpenEBS と 1 か所にしたい(コンテナごとにロックが別なので、同時に書くと片方が消える)ので、 +# 書くのはプールが無いか小さいときだけにして、ふだんは見るだけ。 +apiVersion: v1 +kind: ServiceAccount +metadata: + name: lvm-thinpool + namespace: kube-system +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: lvm-thinpool +rules: + - apiGroups: [""] + resources: [nodes] + # ノードは main 1 台(talos.sops.yaml の hostname)。台を増やしたら足す + resourceNames: [main] + verbs: [get, patch] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: lvm-thinpool +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: lvm-thinpool +subjects: + - kind: ServiceAccount + name: lvm-thinpool + namespace: kube-system +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: lvm-thinpool + namespace: kube-system +spec: + selector: + matchLabels: + app: lvm-thinpool + template: + metadata: + labels: + app: lvm-thinpool + spec: + serviceAccountName: lvm-thinpool + priorityClassName: system-node-critical + initContainers: + # データは VG の 90%。残りは溢れかけたときに足す分(`lvextend -L +10G data/data_thinpool`)。 + # メタデータは 1GiB(作るときに足りるだけ取る。溢れるとデータより先に全部の書き込みが止まる)。 + # 版は chart と同じ lvm-driver(中身は Alpine の lvm2。renovate.json で chart と一緒に上がる) + - name: pool + image: mirror.gcr.io/openebs/lvm-driver:1.10.1 + command: [sh, -euc] + args: + - | + VG=data POOL=data_thinpool + # dmeventd は無い(Talos にもこのイメージにも)。見張らせようとすると失敗の終了コードで返る(再起動のあとに踏んだ) + C='activation { monitoring = 0 }' + # 入れ直した直後は Talos が VG を作るまで待つ + until vgs "$VG" >/dev/null 2>&1; do echo "VG $VG を待つ"; sleep 5; done + # 起動時は Talos も有効にする。先に動いたときのため + vgchange --config "$C" -ay "$VG" + if ! lvs "$VG/$POOL" >/dev/null 2>&1; then + # --wipesignatures は thin プールには付けられない(Talos が新しいパーティションの先頭を消してある) + lvcreate --config "$C" --type thin-pool -l 90%VG --poolmetadatasize 1g --yes -n "$POOL" "$VG" + fi + # OpenEBS が作った小さいプール(最後の PV を消すとプールごと消し、次の PVC の大きさで作り直す)を広げる + n() { tr -d ' '; } + want=$(( $(vgs --noheadings --units b --nosuffix -o vg_size "$VG" | n) * 90 / 100 )) + have=$(lvs --noheadings --units b --nosuffix -o lv_size "$VG/$POOL" | n) + if [ "$have" -lt $(( want - 1073741824 )) ]; then lvextend --config "$C" -l 90%VG "$VG/$POOL"; fi + meta=$(lvs --noheadings --units b --nosuffix -o lv_metadata_size "$VG/$POOL" | n) + if [ "$meta" -lt 1073741824 ]; then lvextend --config "$C" --poolmetadatasize 1g "$VG/$POOL"; fi + lvs -a -o lv_name,lv_size,data_percent,metadata_percent "$VG" + securityContext: + privileged: true + volumeMounts: + - name: dev + mountPath: /dev + - name: label + image: registry.k8s.io/kubectl:v1.37.2 + args: [label, node, $(NODE), doany.io/lvm-thinpool=ready, --overwrite] + env: + - name: NODE + valueFrom: + fieldRef: + fieldPath: spec.nodeName + # 埋まり具合を 1 時間ごとにログへ出す。90% を超えたら NotReady になる(`kubectl -n kube-system get pods` で見える) + containers: + - name: watch + image: mirror.gcr.io/openebs/lvm-driver:1.10.1 + command: [sh, -c] + args: + - while :; do lvs -o lv_name,lv_size,data_percent,metadata_percent data/data_thinpool; sleep 3600; done + readinessProbe: + exec: + command: + - sh + - -c + - set -- $(lvs --noheadings -o data_percent,metadata_percent data/data_thinpool); [ "${1%%.*}" -lt 90 ] && [ "${2%%.*}" -lt 90 ] + periodSeconds: 60 + timeoutSeconds: 30 + securityContext: + privileged: true + resources: + requests: + cpu: 1m + memory: 16Mi + volumeMounts: + - name: dev + mountPath: /dev + volumes: + - name: dev + hostPath: + path: /dev + type: Directory diff --git a/bootstrap/talos.md b/bootstrap/talos.md index bfb204a1..12b72349 100644 --- a/bootstrap/talos.md +++ b/bootstrap/talos.md @@ -8,8 +8,8 @@ 新しい Talos が出たら PR を作る。**適用は自動化しない**(下の「適用は CI にやらせない」)。talhelper は使わない。 **組み立ては [render.sh](render.sh) が全部やる。** 手で書く部分は [talos.sops.yaml](talos.sops.yaml) の 1 本だけで、 -他から描いてくる部分(Cilium・cert-manager・argocd・infisical は `-values(.sops).yaml` から、 -local-path-provisioner は [local-path.yaml](local-path.yaml) から)と 1 か所で組む。**値をどこにも二度書かない**のが目的。 +他から描いてくる部分(Cilium・cert-manager・argocd・infisical・lvm-localpv は `-values(.sops).yaml` から、 +StorageClass と thin プールは [storage.yaml](storage.yaml) から)と 1 か所で組む。**値をどこにも二度書かない**のが目的。 CI([talos-boot](../.github/workflows/talos-boot.yml))も同じ `render.sh` を使う (別の作り方をすると「CI は通るが実機では通らない」が起きる)。 @@ -63,11 +63,11 @@ needle は `render.sh` の末尾の 1 か所だけに置く。 | --- | --- | --- | | `talos.sops.yaml` の先頭の Talos の `# version:`(または `# schematic:`) | `talosctl upgrade --image factory.talos.dev/installer/:<版> --k8s-endpoint https://10.10.0.4:6443` | **再起動** | | `talos.sops.yaml` の先頭の Kubernetes の `# version:` | `render.sh` → `apply-config` → `talosctl upgrade-k8s --to <新しい版>` | 無し | -| `talos.sops.yaml` の本文と、`bootstrap/` の残りの全部(`*-values(.sops).yaml`(2 つめのドキュメントの Secret も)・`apiserver-rbac.yaml`・`local-path.yaml`) | `render.sh` → `apply-config` → `talosctl upgrade-k8s --to <今の版>`(下の「inlineManifests を直したとき」) | 場合による | +| `talos.sops.yaml` の本文と、`bootstrap/` の残りの全部(`*-values(.sops).yaml`(2 つめのドキュメントの Secret も)・`apiserver-rbac.yaml`・`storage.yaml`) | `render.sh` → `apply-config` → `talosctl upgrade-k8s --to <今の版>`(下の「inlineManifests を直したとき」) | 場合による | ### inlineManifests を直したとき -**Cilium・local-path・`apiserver-rbac.yaml`・ArgoCD・infisical・ +**Cilium・OpenEBS LVM(`storage.yaml` も)・`apiserver-rbac.yaml`・ArgoCD・infisical・ cert-manager と、SOPS 済みの Secret 2 つは machine config の中にいる。** だから**マニフェストを直しただけでは何も起きない。** 描き直して当てる: diff --git a/bootstrap/talos.sops.yaml b/bootstrap/talos.sops.yaml index cb7fd3e4..3b4f8cc1 100644 --- a/bootstrap/talos.sops.yaml +++ b/bootstrap/talos.sops.yaml @@ -1,5 +1,5 @@ # ノード main の machine config のうち、手で書く部分の全部。render.sh が `gen config --config-patch` に渡す。 -# chart から描く部分(Cilium・ArgoCD・infisical・cert-manager・local-path)は render.sh が足す。版は下のコメント。 +# chart から描く部分(Cilium・ArgoCD・infisical・cert-manager・OpenEBS LVM)は render.sh が足す。版は下のコメント。 # # **SOPS で password の値だけ暗号化してある**(../.sops.yaml)。平文の部分はそのまま直してよい。 # パスワードを変えるときは `sops edit bootstrap/talos.sops.yaml`。render.sh が `sops -d` してから渡す。 @@ -26,6 +26,10 @@ # Gateway API の CRD(standard チャンネルで足りる)。 # renovate: datasource=github-releases depName=kubernetes-sigs/gateway-api # version: v1.7.0 +# +# VolumeSnapshot の CRD と snapshot-controller(storage.yaml の VolumeSnapshotClass が使う)。 +# renovate: datasource=github-releases depName=kubernetes-csi/external-snapshotter +# version: v8.7.0 machine: sysctls: # 無いと IPv6 の既定経路が消える(Kubernetes が forwarding=1 にすると accept_ra=1 は RA を無視する) @@ -43,13 +47,15 @@ machine: # `talosctl get links` で見て足す(無い名前の行は効かないだけ) net.ipv6.conf.eno4.disable_ipv6: "1" network: - # local-path の PV の nodeAffinity が main を指している + # PV の nodeAffinity(OpenEBS の openebs.io/nodename)が main を指している hostname: main # PT3 のドライバ(../docs/decisions.md「PT3 チューナー」)。dvb-pt3 は earth_pt3 を blacklist しているので明示で読ませる。 # 復調とチューナー(tc90522・qm1d1c0042・mxl301rf)は、v1.15.0-alpha.2 から Talos がカーネルの要求で読む(siderolabs/talos#14724)。 kernel: modules: - name: earth-pt3 + # PVC の thin プール(storage.yaml)。Talos のカーネルではモジュールで、OpenEBS のコンテナからは読めない + - name: dm_thin_pool # etcd スナップショットの日次取得(../apps/k8up/etcd-snapshot.yaml)のため、Pod から Talos API を叩く。 # 役割は os:etcd:backup だけ、namespace も k8up に絞る(os:admin を Pod に置かずに済む唯一の仕組み)。 features: @@ -112,7 +118,7 @@ sops: # ディスクの割り方。既定だと EPHEMERAL(/var)が全部を取り、PVC の暴走で etcd ごと道連れになる。 # ボリュームの設定は未確保のときにしか効かない(あとから書き足しても黙って無視され、直すには入れ直し)。 # EPHEMERAL に maxSize が無いと grow が全部持っていく(siderolabs/talos#12713)。 -# 64GiB は実機の使用量(containerd 22G ほか)から。残りが local-path 用。 +# 64GiB は実機の使用量(containerd 22G ほか)から。残りが PV 用(下の RawVolumeConfig `lvm`)。 apiVersion: v1alpha1 kind: VolumeConfig name: EPHEMERAL @@ -138,21 +144,42 @@ sops: mac_only_encrypted: true version: 3.13.3 --- -# PV データの置き場。/var/mnt/ 固定で、local-path.yaml の paths がここを指す。 +# PV データの置き場。残りを全部 LVM の PV にする(パーティションのラベルは r-lvm。中身は Talos は触らない)。 +# 447GB の SSD なら EPHEMERAL の 64GiB を引いて 380GiB ほど。minSize は talos-boot の VM のディスクでも取れる大きさ。 apiVersion: v1alpha1 -kind: UserVolumeConfig -name: local-path +kind: RawVolumeConfig +name: lvm provisioning: diskSelector: match: system_disk minSize: 50GiB grow: true -filesystem: - type: xfs -# /var 系は既定で nosuid,nodev,noexec になり、PVC の中の実行ファイル(Forgejo のサーバ側フック)が動かない。 -# コンテナは PSA と securityContext で押さえている。 -mount: - secure: false +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBIQnpITHlrS0lxZkZXb3dy + UmNMYUhFamtsR2Rpb3BTMXozYjQwQkoxOEhFClFOeDRQV2tTYTBlL09RUS9oMUZG + ZHZwNXVnYzA4dkIyME0rbnc0Zm44SEUKLS0tIDhTNmNMU1ZVeGEwbzltTDdpVVRP + M0FiWEJ2MHIzRkU2WHRxWlo3N0FsNlEKgYVZr+vTicTztpwMQ7oj/yLEk12f/JZX + uMTe29EqIoB7gE/SCG9z+qoLXd4NHrA1lfrLjTDSCYNEhGiDyMhIMQ== + -----END AGE ENCRYPTED FILE----- + recipient: age1sgnnkn67wpt4wgjsff4c36w3cmjkk557ng504e4aczx2lzls9y6ssy0x9r + encrypted_regex: ^password$ + lastmodified: "2026-10-30T16:40:35Z" + mac: ENC[AES256_GCM,data:HeBfzNkyaDPxwfMepe0/4U5g0tAUyMf9AZMYly5La46tj8Ap5K4z9jp3tcOR9tADq1nCBhgtIKDiLmrNqOOWLShV1jDVM35hpBDHMpYuJjAGish8pBZUb3TetvSa1fnzOE24JrCZxpaK69q5V9akQzKIlhM/qgRhU7jFqPhyDT8=,iv:8QW9fMoDnME0qsE/TMcpi3zKgl+Oy+9mN8jzRY7XnN4=,tag:gOxsxLzsRPBx8iQjY5RW/A==,type:str] + mac_only_encrypted: true + version: 3.13.3 +--- +# 上の r-lvm 1 つで VG `data` を作る。thin プール(data_thinpool)は Talos では作れないので +# storage.yaml の lvm-thinpool が作り、その上に OpenEBS LVM LocalPV が PVC ごとの thin の LV を切る。 +# Talos は足すだけで、知らない LV(OpenEBS のもの)を消さない。起動時の有効化も Talos がする。 +apiVersion: v1alpha1 +kind: LVMVolumeGroupConfig +name: data +provisioning: + volumeSelector: + match: volume_id == "r-lvm" sops: age: - enc: | diff --git a/docs/decisions.md b/docs/decisions.md index 68ac10ff..e7e2cb57 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -8,8 +8,8 @@ 「ホスト設定」は machine config(git 管理)、「PV データ」だけがバックアップツールの仕事になる (手順は [../recovery/README.md](../recovery/README.md))。**ツール選定は PV データの部分にしか効かない。** -変えないと決めてあるもの: restic のリポジトリ形式・パスフレーズ・バケット、local-path の PV ディレクトリ構造 -(`pvc-__`)、Infisical のデータと `ENCRYPTION_KEY`、ArgoCD + ApplicationSet + 各 repo の `deploy/argocd.yaml`、 +変えないと決めてあるもの: restic のリポジトリ形式・パスフレーズ・バケット、StorageClass の名前 `local-path` +(中身は Z440 で OpenEBS LVM に替えた。名前は各リポジトリの PVC が名指ししている)、Infisical のデータと `ENCRYPTION_KEY`、ArgoCD + ApplicationSet + 各 repo の `deploy/argocd.yaml`、 **復元手順は公開で秘密を含まない**という性質。 ## Z440 へは Talos のまま移す @@ -98,7 +98,7 @@ Talos には helm-controller が無く、inlineManifests は Helm を実行で | 層 | 中身 | 資格情報 | | --- | --- | --- | -| **machine config** | **`bootstrap/` の全部**(`argocd-repos.yaml` を除く)。inline が 8 つ(cilium / local-path / `apiserver-rbac.yaml` / argocd / cert-manager / infisical / SOPS の Secret 2 つ)、CRD だけ URL で 5 つ(gateway-api / argocd ×3 / cert-manager) | 入力(`bootstrap/talos-secrets.sops.yaml` など)が SOPS 済みで、生成物は git に置かない | +| **machine config** | **`bootstrap/` の全部**(`argocd-repos.yaml` を除く)。inline が 9 つ(cilium / lvm-localpv / `storage.yaml` / `apiserver-rbac.yaml` / argocd / cert-manager / infisical / SOPS の Secret 2 つ)、URL で 10 個(CRD が gateway-api / argocd ×3 / cert-manager / VolumeSnapshot ×3、snapshot-controller ×2) | 入力(`bootstrap/talos-secrets.sops.yaml` など)が SOPS 済みで、生成物は git に置かない | | **ArgoCD** | `apps/`。`ClusterIssuer`・Gateway・auth・`InfisicalSecret` など、本体が machine config に居る部品の平文のマニフェストもここ(2026-10 までは `bootstrap/` に混ぜて Application `bootstrap` が当てていた)。**infisical の本体は来ない**(下の「infisical だけは ArgoCD に移せない」) | — | - **CRD だけ URL で渡すのは大きさの都合。** 埋めると machine config が 341 KB → 4 MB を超える。 @@ -300,9 +300,30 @@ LVM の設定は v1.14 のドキュメントにあり、siderolabs/talos#12309 StorageClass を変えた PVC を作り直すだけで中身が埋まるので、ストレージの乗り換えも同じ手で済む。 入口(Gateway・AdGuard・LiveKit)と API の宛先は 1 台の IP に固定されているので、そのときは VIP(Cilium の L2 Announcements、Talos の共有 VIP)も要る。 -確かめていないこと: OpenEBS LVM LocalPV が Talos で動くか(`dm-snapshot` モジュール、読み取り専用のルートで LVM のコマンドが通るか)、 -VolSync の restic mover と R2 の組み合わせ、restic が戻したファイルの持ち主、**thin プールが溢れたときの挙動と見張り** -(データとメタデータのどちらが溢れても書き込みが止まる。スナップショットを定期的に取るので、余裕と見張りが要る)。 +### 組んだ形(2026-10-31。効くのは Z440 で入れ直したとき) + +- **VG は Talos、thin プールは DaemonSet。** `RawVolumeConfig` `lvm`(EPHEMERAL の残り全部)を `LVMVolumeGroupConfig` で VG `data` にする。 + Talos の `LVMLogicalVolumeConfig` は linear と raid だけで thin を作れない。OpenEBS は thin プールが無いと**最初の PVC と同じ大きさ**で作り、 + あとから広げない(Talos に dmeventd が無く、自動の拡張も効かない)。なので [bootstrap/storage.yaml](../bootstrap/storage.yaml) の + `lvm-thinpool` が VG の 90% で作り、出来たらノードにラベルを付け、OpenEBS のノードの Pod はそのラベルで初めて動く。 + LVM のメタデータを書くのを OpenEBS と取り合わないため(コンテナごとにロックが別)、`lvm-thinpool` が書くのはプールが無いか小さいときだけ +- **Talos の側で要ったもの**: `dm_thin_pool` のモジュール(Talos のカーネルではモジュールで、コンテナからは読めない)。 + `dm-snapshot` は組み込み。起動時の VG の有効化は Talos がする(`thin_check` が無いのは lvm2 が飛ばす)。Talos は知らない LV を消さない +- **StorageClass の名前は `local-path` のまま**(既定)。各リポジトリの PVC は書き換えない。`thinProvision: "yes"`(スナップショットから戻せるのは thin だけ)、 + xfs(ext4 は `lost+found` を作り、PGDATA がボリュームの直下の Postgres の initdb が落ちる) +- **`storageCapacity: false`。** thin なので PVC の大きさの合計はプールを超えてよい(denpa は 1Ti と 300Gi)。true だと kube-scheduler が + プールの空きより大きい PVC を Pending にする +- **snapshot-controller と VolumeSnapshot の CRD は chart の同梱を使わず**、external-snapshotter のタグの URL で入れる(版は `talos.sops.yaml` の先頭)。 + クラスタに 1 つのもので、VolSync も使う +- **talos-boot が確かめていること**: VG と thin プール(VG の 9 割)、PVC が xfs で bind、VolumeSnapshot が readyToUse、 + スナップショットから作った PVC に中身が戻る、再起動のあとも PV と infisical の Postgres が戻る + +local-path から変わったこと: **PVC の requests が本当の上限になった**(local-path はディスクの残り全部が使えた。 +`recovery/pv.sh` の `fit` が戻す前に見る)、**新しい PVC の直下が root の 0755**(local-path は 0777。root 以外で書くアプリは `fsGroup` が要る)。 + +確かめていないこと: VolSync の restic mover と R2 の組み合わせ、**thin プールが溢れたときの挙動と見張り** +(データとメタデータのどちらが溢れても書き込みが止まる。いまは `lvm-thinpool` が 90% で NotReady になるだけで、通知は無い。 +スナップショットを定期的に取るようになったら、余裕と通知が要る)。 ## VPN を wg-easy から NetBird にした diff --git a/docs/talos.md b/docs/talos.md index d02952a0..66d57da9 100644 --- a/docs/talos.md +++ b/docs/talos.md @@ -52,21 +52,20 @@ curl -X POST --data-binary @schematic.yaml https://factory.talos.dev/schematics | カーネル引数 | `machine.install.extraKernelArgs` は使えない(同じく衝突)。**schematic の `customization.extraKernelArgs`** | | host DNS | `ResolverConfig`。**既定で有効**なので普通は書かなくてよい | | bond とそのメンバー | `BondConfig` / `LinkConfig`。v1alpha1 の `machine.network.interfaces` は bond の MAC を指定できない([talos.sops.yaml](../bootstrap/talos.sops.yaml)) | -| PV の置き場 | `UserVolumeConfig`。マウント先(`/var/mnt/`)は kubelet に自動で通るので `extraMounts` は要らない([talos.sops.yaml](../bootstrap/talos.sops.yaml))。ディスク上のラベルは `u-` | +| PV の置き場 | `RawVolumeConfig`(フォーマットしないパーティション。ラベルは `r-`)を `LVMVolumeGroupConfig` で VG にする。thin の LV は Talos では作れない([talos.sops.yaml](../bootstrap/talos.sops.yaml)、[bootstrap/README.md](../bootstrap/README.md)「PVC の置き場」)。ボリュームは**未割り当ての領域にしか作られない**ので、切り方を変えるのは入れ直すときだけ | | control-plane への scheduling 許可 | v1alpha1 の `allowSchedulingOnControlPlanes` は弾かれる。**`KubeNodeConfig` を `$patch: delete` して `labels` だけ書き戻す**([talos.sops.yaml](../bootstrap/talos.sops.yaml)) | | CNI を止める | `KubeFlannelCNIConfig` を `$patch: delete`、`KubeProxyConfig` を `enabled: false`([talos.sops.yaml](../bootstrap/talos.sops.yaml))。**v1alpha1 の `cni.name: none` はもう書けない** | -| カーネルモジュール | `machine.kernel.modules`。**並べた順ではなく名前の順に読まれる**。v1.15.0-alpha.2 からはカーネルの要求(`request_module`)でも読むので、書くのは自分からは読まれないものだけでよい(PT3 は `earth-pt3` だけ。[talos.sops.yaml](../bootstrap/talos.sops.yaml)、[decisions.md](decisions.md)「PT3 チューナー」) | +| カーネルモジュール | `machine.kernel.modules`。**並べた順ではなく名前の順に読まれる**。v1.15.0-alpha.2 からはカーネルの要求(`request_module`)でも読むので、書くのは自分からは読まれないものだけでよい(PT3 は `earth-pt3` だけ。OpenEBS の thin プールの `dm_thin_pool` も書いてある。[talos.sops.yaml](../bootstrap/talos.sops.yaml)、[decisions.md](decisions.md)「PT3 チューナー」) | ### PSA のラベル(Pod Security Admission が既定で `baseline`) `KubeAdmissionControlConfig` に `enforce: baseline`(例外は `kube-system` のみ)が入っている。 **`hostPath` / `privileged` / `hostNetwork` / `hostPort` を使う workload は namespace にラベルが要る**(baseline は hostPort も弾く)。 -ラベルが無いと Pod が作られず、local-path のヘルパー Pod なら PVC が Pending のまま止まる。 +ラベルが無いと Pod が作られない(ReplicaSet や DaemonSet の events に出るだけで、Pod の一覧には何も出ない)。 | namespace | baseline に通らない理由 | | --- | --- | -| `kube-system` | Cilium(privileged・hostNetwork・hostPath・SYS_ADMIN/NET_ADMIN・hostPort)。**Talos の側で例外にしてある** | -| `local-path-storage` | ヘルパー Pod の hostPath。[`bootstrap/local-path.yaml`](../bootstrap/local-path.yaml) がラベルを付ける | +| `kube-system` | Cilium(privileged・hostNetwork・hostPath・SYS_ADMIN/NET_ADMIN・hostPort)、OpenEBS LVM と `lvm-thinpool`(privileged・hostPath の `/dev`)。**Talos の側で例外にしてある** | | `netbird` | hostPort 51820(routing peer の WireGuard)、NET_ADMIN 等、hostPath | | `denpa` | privileged・hostPath(`/dev/dvb`・`/dev/bus`・`/dev/dri`)。**ラベルは denpa リポジトリの `deploy/namespace.yaml`** にあり、gitops だけを grep すると抜けて見える | | `adguardhome` | hostPort 53 / 853 | diff --git a/docs/z440-migration.md b/docs/z440-migration.md index 4d2b3076..3594c3cf 100644 --- a/docs/z440-migration.md +++ b/docs/z440-migration.md @@ -1,6 +1,9 @@ -# Talos に移したノード main を SSD ごと Z440 に移す +# Talos に移したノード main を Z440 に移す(入れ直して restic から戻す) -DL360 Gen9 で動いている `main`を、**SSD ごと** HP Z440 に移す計画(ROADMAP の Phase 2.5)。 +DL360 Gen9 で動いている `main` を HP Z440 に移す計画(ROADMAP の Phase 2.5)。 +**SSD は持っていくが、中身は持っていかない。** Z440 で Talos を入れ直し(SSD は上書きされる)、PV のデータは k8up の restic から戻す +([../recovery/README.md](../recovery/README.md) の建て直しと同じ道)。入れ直すのは、ディスクの切り方をこのときしか変えられないから +(PV の置き場を local-path から OpenEBS LVM LocalPV にする。「2. NIC と bond0」の下の「ディスク」)。 あとで小さいサーバ(Talos)に 10.0 と 10.10 のあいだのルーターを任せ、Z440 は 10.0 側だけにする予定(「終わったら」)。 ## 構成と、Z440 での扱い @@ -49,12 +52,31 @@ selector: # BondConfig の links: [lan0]、LinkConfig の name: jcom0 ``` -**ディスク**: `disk.dev_path == "/dev/sda"` は Z440 でも SSD 1 本なら同じになるはず(`talosctl get disks` で見る。`bootstrap/talos.sops.yaml` の注)。 -**入れ直しのついでに、local-path の `u-local-path` を LVM のボリュームグループに切り替える**(ROADMAP の Phase 2.5、 -[decisions.md](decisions.md)「Z440 で、ストレージを OpenEBS LVM LocalPV に…」)。ボリュームは未割り当ての領域にしか作れないので、このときしか切り直せない。 +**Z440 の MAC を `LinkAliasConfig` に `||` で足してから描く**(手順 4)。足さないと Z440 で別名が当たらず、10.0 側が上がらない。 -**この config は DL360 では効かない**(MAC が当たらず 10.0 側が上がらない)ので、普通に当てない。 -`talosctl apply-config --mode=staged` で**次の起動から**効くように置き、電源を切る直前にやる(手順 4)。 +### ディスク + +`disk.dev_path == "/dev/sda"` は Z440 でも SSD 1 本なら同じになるはず(maintenance mode で `talosctl get disks` を見る。`bootstrap/talos.sops.yaml` の注)。 +**入れ直しで、PV の置き場を local-path の `u-local-path`(xfs のパーティション 1 つ)から LVM に切り替える** +([decisions.md](decisions.md)「Z440 で、ストレージを OpenEBS LVM LocalPV に…」)。ボリュームは未割り当ての領域にしか作れないので、このときしか切り直せない。 + +| | 大きさ(447GB の SSD) | 作るもの | +| --- | --- | --- | +| EFI・BOOT・META・STATE | 数 GiB | Talos | +| `EPHEMERAL`(/var) | 64GiB で頭打ち | Talos(`VolumeConfig`) | +| `r-lvm` | 残り全部(380GiB ほど) | Talos(`RawVolumeConfig`)。VG `data` の唯一の PV(`LVMVolumeGroupConfig`) | +| thin プール `data_thinpool` | VG の 90%(340GiB ほど)。残りは溢れかけたときに足す分 | [lvm-thinpool](../bootstrap/storage.yaml)(Talos は thin を作れない) | +| PVC ごとの thin の LV(xfs) | PVC の requests が上限。書いた分だけプールを使う | OpenEBS LVM LocalPV(StorageClass は既定の `local-path`。名前は前のまま) | + +local-path との違いで気を付けること: + +- **PVC の requests が本当の上限になる。** local-path はディスクの残り全部が使えたので、`adguardhome-work`(5Gi)に 68 GB 入っていた。 + 戻す前に `recovery/pv.sh - plan-all` が PVC に入るかを見る(`fit`。入らなければ止まる) +- **thin なので合計はプールを超えてよい**(denpa は 1Ti と 300Gi)。代わりに**プールが溢れると全部の PVC の書き込みが止まる。** + `kubectl -n kube-system get pods -l app=lvm-thinpool` が 90% で NotReady になる(ログに 1 時間ごとの使用率)。 + denpa の残量の表示(statfs)は PVC の大きさで見るので、プールの空きより多く出る +- **新しい PVC の直下は root の 0755**(local-path は 0777 だった)。root 以外で書くアプリは `fsGroup` が要る(worklog に足した。ほかは付いているか root) +- etcd は `EPHEMERAL` に居るので、プールが溢れても巻き込まれない ## 3. リモート管理: Intel AMT @@ -110,26 +132,58 @@ AMT の SOL は、OS からは PCI のシリアルポート(「Intel … KT Cont **ノードが止まっていると外からは届かない**。小さいサーバに入口を置けば、止まっていても外から電源を入れ直せる - **ノード自身からは自分の AMT に届かない**(NIC を OS と分け合っていて、自分宛ては折り返さない)。ほかの機器か NetBird の端末から使う -## 4. 移す直前 +## 4. 移す直前(DL360 で) + +コマンドは全部 `tools/t.ps1 --admin` のシェルで打つ([../recovery/README.md](../recovery/README.md)「道具と経路」)。 +LVM への切り替えは main に入っていること(入れたあと DL360 の Argo CD は `adguardhome-work` を 30Gi に広げられずに同期のエラーを出すが、 +local-path が広げられないだけで、入れ直せば消える)。worklog の `fsGroup`(fj.doany.io の doa/worklog-cloud#179)も入っていること。 + +1. **AdGuard のクエリログを消す**(Web の「設定 → 一般設定 → クエリログを消去」)。`querylog.json` が 68 GB あり、 + 最後のバックアップに乗ると、戻すときに `adguardhome-work`(30Gi)に入らず `plan-all` が止まる。 + 消す前に、どの端末が 1 日 10 GB も引いているかを見ておく(2026-10 末から。前は 1 日 0.3 GB ほど) +2. **Z440 の MAC を `LinkAliasConfig` に足して main にマージ**(「2. NIC と bond0」)。talos-boot が通ること +3. machine config を描く: `./bootstrap/render.sh bootstrap/clusterconfig`(`valid for metal mode` まで出ること)。 + Talos の ISO を USB に書いておく([../recovery/README.md](../recovery/README.md) の 2) +4. 計画停止を知らせる(止まるもの: 全部の Web、DNS、NetBird、Matrix、録画)。録画の予約が入っていない時間にする +5. **最後のバックアップ**。ここから先に書かれたものは戻らない: -1. **k8up のバックアップが成功している**こと: 夜のぶん(03:00 JST の通知)が OK ✅。手で確かめるなら - `kubectl -n k8up create job --from=cronjob/k8up-notify notify-manual`([../apps/k8up/README.md](../apps/k8up/README.md)) -2. 計画停止を知らせる(止まるもの: 全部の Web、DNS、NetBird、Matrix、録画) -3. Z440 用の config を置く: `render.sh` → `talosctl -n 10.0.0.2 apply-config --mode=staged -f bootstrap/clusterconfig/controlplane.yaml` -4. `talosctl -n 10.0.0.2 shutdown` + ```shell + sh recovery/pv.sh - backup-all # Schedule のある namespace を全部、今すぐ取って待つ。全部 Succeeded になること + kubectl -n k8up create job --from=cronjob/etcd-snapshot etcd-snapshot-final + ``` + +6. `talosctl -n 10.0.0.2 shutdown` ## 5. 載せ替え CPU(グリスを塗り直す)・メモリ 5 枚(Z440 に先に 1 枚ある)・SSD・PT3・USB 機器を Z440 に移す。仮の v3 の CPU は外す。 LAN は「2. NIC と bond0」のとおりにつなぐ。最初の起動はモニタ(Arc)とキーボードをつないでおく。 -## 6. 起動して確かめる +## 6. Z440 で入れ直して戻す + +[../recovery/README.md](../recovery/README.md) の 2〜10 をそのまま流す。違うのは次のところ: + +- **2. ISO で起動する**: USB を挿して、起動中に **F9**(ブートメニュー)で USB を選ぶ。AMT の IDE-R で ISO をつないでもよい(「3. リモート管理」。 + UEFI のまま IDE-R から起動できるかは未確認)。maintenance mode の IP は Arc の画面に出る。 + `talosctl get links --insecure` で **Z440 の MAC が 2. で足したものと一致する**こと、`talosctl get disks --insecure` で `/dev/sda` が 447GB の SSD であること +- **3. 当てる**: SSD は上書きされる(古いパーティションは消え、新しいパーティションは Talos が作るときに先頭を消す)。 + `talosctl get volumestatus` で `EPHEMERAL`(64GiB)と **`r-lvm`**(残り全部)が `ready` +- **4. クラスタを起こす**: 「Running」に `lvm-thinpool`・`lvm-localpv-node`・`lvm-localpv-controller`・`snapshot-controller`(kube-system)も入る。 + `kubectl -n kube-system logs ds/lvm-thinpool -c pool` の最後に `data_thinpool` が VG の 90% で出ていること。 + `kubectl get sc` で `local-path`(default)の PROVISIONER が `local.csi.openebs.io` +- **8. PV データを戻す**: `sh recovery/pv.sh - plan-all` の最後の「PVC に入るか」が全部通ること(入らないものは PVC を大きくするか restic から消す) + +## 7. 起動して確かめる - [ ] BIOS で CPU(E5-2696 v4、22 コア)とメモリ 48GB が見える -- [ ] SSD から起動する(UEFI の起動の選択に出なければ、SSD の `EFI/BOOT/BOOTX64.EFI` を選ぶ) +- [ ] 入れたあと SSD から起動する(UEFI の起動の選択に出なければ、SSD の `EFI/BOOT/BOOTX64.EFI` を選ぶ) +- [ ] [../recovery/README.md](../recovery/README.md) の 9 の確かめ(公開している名前・IPv6・DNS・NetBird・PT3 など) - [ ] `talosctl -n 10.0.0.2 get addresses`: `bond0` に 10.0.0.2 と `240f:6d:842b:1::2`、`jcom0` に 10.10.0.4。 `talosctl get links bond0` の MAC が `e6:2c:1b:1a:a0:39` - [ ] `kubectl get nodes` が Ready、`kubectl get pods -A` で止まっているものが無い +- [ ] `kubectl get pvc -A` が全部 Bound、`kubectl -n kube-system get pods -l app=lvm-thinpool` が Ready。 + 埋まり具合は `kubectl -n kube-system exec ds/lvm-thinpool -c watch -- lvs data` +- [ ] **一度 `talosctl -n 10.0.0.2 reboot` して、全部の Pod が戻る**(thin プールと LV を Talos が起動時に有効にする。talos-boot も見ている) - [ ] Web(`https://fj.doany.io` など)が家の中と外から開く。外からの IPv6 でも届く([router.md](router.md) の確かめ方) - [ ] DNS: 10.0.0.2 と 10.10.0.4 の両方で引ける - [ ] NetBird がつながり、10.10.0.0/24 に届く @@ -139,13 +193,10 @@ LAN は「2. NIC と bond0」のとおりにつなぐ。最初の起動はモニ ## 戻し方 -SSD とほかの部品を DL360 に戻し、iLO(10.0.0.3)のコンソールで見ながら起動する。 -**staged で置いた Z440 用の config のままでは、DL360 で 10.0 側が上がらない。** Z440 で動いているうちに戻すなら、 -DL360 用の config を `apply-config --mode=staged` してから `shutdown` して移す。 - -**別案(要検証)**: 別名の `match` に**両方の機械の MAC を `||` で並べ**、DL360 でも Z440 でも効く config を 1 つにする。 -移す前に DL360 で普通に当てて効くことを確かめられ、戻すときも何も変えなくてよい。 -DL360 の `eno2`(bond の 2 本目)のように、片方の機械にしか無い口の別名がエラーにならないかを先に確かめる。 +**SSD は入れ直した時点で上書きされるので、DL360 に挿し戻しても元には戻らない。** 戻すときも同じ道 +(DL360 に SSD を戻し、iLO(10.0.0.3)のコンソールで ISO から起動して、同じ config で入れ直し、restic から戻す)。 +別名の `match` には両方の機械の MAC が `||` で並んでいるので、config は DL360 でもそのまま効く。 +最後のバックアップより後に Z440 で書かれたものは、戻す前にもう一度 `backup-all` で取る。 ## 終わったら diff --git a/recovery/README.md b/recovery/README.md index 51c44b9f..7c735ab7 100644 --- a/recovery/README.md +++ b/recovery/README.md @@ -93,7 +93,7 @@ talosctl apply-config --insecure -n $IP -e $IP -f bootstrap/clusterconfig/contro ``` `UnattendedInstallConfig` があるので、ディスクに書いて自分で再起動する。**ここで SSD が上書きされる。** -`STATE` / `EPHEMERAL` / `u-local-path` が `ready` になったら USB を抜いてよい(挿したままだとブート順によっては +`STATE` / `EPHEMERAL` / `r-lvm`(PV 用。VG `data` になる)が `ready` になったら USB を抜いてよい(挿したままだとブート順によっては また maintenance mode で上がる): ```shell @@ -105,7 +105,8 @@ talosctl get volumestatus # EPHEMERAL は 64GiB 上限。違ったら入れ ```shell talosctl bootstrap # 単一ノードなので 1 回だけ kubectl get nodes # Ready / control-plane / talos.sops.yaml の先頭の kubernetes の版 -kubectl get pods -A # cilium・coredns・local-path・argocd・cert-manager・infisical が Running +kubectl get pods -A # cilium・coredns・lvm-thinpool・lvm-localpv・snapshot-controller・argocd・cert-manager・infisical が Running +kubectl -n kube-system logs ds/lvm-thinpool -c pool # 最後に data_thinpool(VG の 90%)が出る。出なければ PVC が 1 つも bind しない kubectl get gatewayclass # cilium が Accepted(無いと Gateway が `Waiting for controller` のまま) ``` @@ -163,7 +164,7 @@ kubectl get infisicalsecret -A # 全部 Ready になる。k8up/k8up-global 残りの namespace を 1 つずつ「止める → 戻す → 起こす」: ```shell -sh recovery/pv.sh - plan-all # 読むだけ。namespace ごとに何をするかを出す。先にこれを見る +sh recovery/pv.sh - plan-all # 読むだけ。namespace ごとに何をするかと、PVC に入るか(fit)を出す。先にこれを見る sh recovery/pv.sh - restore-all ``` @@ -172,7 +173,8 @@ sh recovery/pv.sh - restore-all 1. `argo-pause`(selfHeal が replicas を戻して、戻している最中にアプリが起き上がるので) 2. `forgejo restore` ── **Forgejo を先に。** 非公開アプリのイメージ(fj.doany.io のレジストリ。中身は `forgejo-repos`)も、 Argo CD が読むマニフェストも、それを読むトークン(Forgejo の DB)も全部 Forgejo にあるので、戻すまで 1 本も上がらない -3. `argo-resume` して ApplicationSet のエラーが消え、**残りの namespace と PVC が全部出そろって全部の `plan` が通る**まで待ち(10 分。通らなければ forgejo だけ戻した状態で止まる)、また `argo-pause` +3. `argo-resume` して ApplicationSet のエラーが消え、**残りの namespace と PVC が全部出そろって全部の `plan` が通る**まで待ち(10 分。通らなければ forgejo だけ戻した状態で止まる)、 + **全部が PVC に入るか(`fit`)を見て**(入らなければ同じく止まる)、また `argo-pause` 4. restic の host(forgejo・infisical・etcd を除く)を 1 つずつ ` restore` 5. `argo-resume` @@ -192,7 +194,9 @@ sh recovery/pv.sh - restore-all - **`Succeeded` は「戻った」の意味ではない。** `pv.sh` は `Restored N files` と(SELinux 以外の)エラー数を出すので、それを見る。 SQLite は大きさを restic と突き合わせてから付け替える - `pv.sh` がやっていることと理由(root で走らせる・数字だけの ID を引用符で囲む・バイナリを標準入力で流さない)は [pv.sh](pv.sh) の冒頭 -- PVC が `Pending`(local-path は使う Pod が付くまで bind しない)でも、Restore の Pod が付けば bind する +- PVC が `Pending`(StorageClass は使う Pod が付くまで bind しない)でも、Restore の Pod が付けば bind する +- **PVC の requests が本当の上限**(OpenEBS LVM。local-path の頃はディスクの残り全部が使えた)。`fit` が `restic stats` の大きさと + requests を比べ、9 割を超えるものがあれば `plan-all` は 1 で終わり、`restore-all` は上書きする前に止まる。PVC を大きくするか、restic から要らないものを消す ## 9. 確かめる @@ -236,7 +240,7 @@ rm -f ~/.config/restic.env ~/infisical-load.log | --- | --- | | ノードが上がらない | iLO のコンソール。ダッシュボードに版と健全性が出る | | Pod が Pending | **PSA のラベル**([../docs/talos.md](../docs/talos.md)「PSA のラベル」)か、control-plane の taint | -| PVC が Bound しない | local-path の ConfigMap に `setup` / `teardown` があるか | +| PVC が Bound しない | `kubectl -n kube-system logs ds/lvm-thinpool -c pool`(VG `data` が無い = `talosctl get volumestatus` の `r-lvm`)、ノードに `doany.io/lvm-thinpool=ready` が付いて `lvm-localpv-node` が動いているか、`kubectl -n kube-system logs ds/lvm-localpv-node -c openebs-lvm-plugin` | | 非公開イメージが引けない | `bootstrap/talos.sops.yaml` の fj.doany.io の `password` が machine config に入っているか、Forgejo が戻っているか(8) | | cilium-drift が TLS で落ちる | `talos-secrets.sops.yaml` を作り直した。`bootstrap/apiserver-ca.crt` を新しい `talos-secrets.sops.yaml` の Kubernetes の CA(`certs.k8s.crt` を base64 で戻したもの)にする | | Application が `OutOfSync / Missing` のまま | `retry`(約 33 分)で自力で戻る。30 分以上動かなければ `kubectl -n argocd patch application <名前> --type=merge -p '{"operation":{"sync":{"revision":"HEAD"},"initiatedBy":{"username":"me"}}}'`。**chart のものは `HEAD` ではなく `spec.source.targetRevision` の値** | diff --git a/recovery/pv.sh b/recovery/pv.sh index 99caec95..2be8839c 100755 --- a/recovery/pv.sh +++ b/recovery/pv.sh @@ -6,6 +6,8 @@ # # - plan-all / restore-all 全部(namespace は `-`)。plan は読むだけ。restore-all が README の 8 そのもの # plan / restore その namespace の最新のスナップショットを経路の形で戻す(下の「まとめて戻す」) +# fit folder で戻すものが PVC(LVM なので requests が上限)に入るか。plan-all と restore-all も見る +# backup-all 全 namespace の Backup を今すぐ作って待つ(入れ直す前の最後のバックアップ。namespace は `-`) # # 以下は restore が使う部品。手で続けるときにも使う # argo-pause / argo-resume Argo CD の application-controller を 0 / 1 にする。止めないと selfHeal が @@ -166,6 +168,44 @@ plan() { sort -n "$_o" } +# PVC の大きさ(requests)をバイトに。使っている単位(Ki・Mi・Gi・Ti と単位なし)だけ読む +bytes() { # $1=量 + case $1 in + *.*) echo "ERROR: $NS: PVC の大きさ $1 は小数なので読めない(整数の Gi などに)" >&2; return 1 ;; + *Ki) echo $((${1%Ki} * 1024)) ;; + *Mi) echo $((${1%Mi} * 1048576)) ;; + *Gi) echo $((${1%Gi} * 1073741824)) ;; + *Ti) echo $((${1%Ti} * 1099511627776)) ;; + *[!0-9]*) echo "ERROR: $NS: PVC の大きさ $1 を読めない" >&2; return 1 ;; + *) echo "$1" ;; + esac +} + +# folder で戻すものが PVC に入るか。**PVC は LVM の LV で、requests の大きさが本当の上限**(local-path の頃は +# ディスクの残り全部が使えたので、小さく書いた PVC に大きなデータが入っていた。2026-10 の adguardhome-work は 5Gi に 68 GB)。 +# xfs の分を見込んで 9 割を超えたら止める。restic の stats は 1 つずつ R2 を読むので、plan(待つ間に何度も呼ぶ)とは分ける +fit() { + _f=$(mktemp) + plan > "$_f" + bad=0 + # plan の行は「順番 操作 引数…」で、folder の引数は「PVC スナップショット」(plan の folder の行と揃える) + while read -r _ op pvc id _; do + [ "$op" = folder ] || continue + need=$(restic stats --mode restore-size --json "$id" | sed -n 's/.*"total_size":\([0-9]*\).*/\1/p') + req=$($K -n "$NS" get pvc "$pvc" -o jsonpath='{.spec.resources.requests.storage}') + have=$(bytes "$req") || exit 1 + [ -n "$need" ] || { echo "ERROR: $NS: restic stats $id が読めない" >&2; exit 1; } + if [ "$need" -gt $((have / 10 * 9)) ]; then + echo "ERROR: $NS: $pvc は $req、戻すものは $((need / 1048576)) MiB(9 割を超える)。PVC を大きくするか、restic から要らないものを消す" >&2 + bad=1 + else + echo "$NS: $pvc $((need / 1048576)) MiB / $req" + fi + done < "$_f" + rm -f "$_f" + [ "$bad" = 0 ] +} + restore() { steps=$(plan) [ -n "$steps" ] || { echo "$NS: 戻すものが無い"; return 0; } @@ -232,14 +272,21 @@ case $OP in stop) stop "$@" ;; start) start ;; plan) restic_env; plan ;; + fit) restic_env; fit ;; restore) restic_env; restore ;; - plan-all) # 読むだけ。forgejo と各 namespace で何をするかを出す。1 つでも決められなければ 1 + plan-all) # 読むだけ。forgejo と各 namespace で何をするかと、PVC に入るかを出す。1 つでも決められない・入らなければ 1 restic_env - plan_all forgejo $(hosts) ;; + H=$(hosts) + rc=0 + plan_all forgejo $H || rc=1 + echo "== PVC に入るか(folder のもの)" + for h in forgejo $H; do sh "$0" "$h" fit || rc=1; done + exit $rc ;; restore-all) # README の 8 を通しで。Forgejo を先に戻し、非公開アプリが出てくるのを待ってから残り restic_env # Forgejo の plan が通らなければ何も触らない plan_all forgejo >/dev/null || { echo "forgejo の plan が通らない(plan-all で見る)" >&2; exit 1; } + sh "$0" forgejo fit || { echo "forgejo の PVC に入らない" >&2; exit 1; } # 待っても直らないもの(戻し方が決まっていない経路。plan の終了コード 2)も、触る前に止める。 # namespace がまだ無いものはここでは見えない(Forgejo の後でしか出てこない) bad= @@ -262,9 +309,38 @@ case $OP in sleep 20 done [ -n "$ok" ] || { plan_all $H >/dev/null || true; echo "10 分待っても上の plan が通らない。forgejo だけ戻した状態で止めた(Argo CD は動いている)" >&2; exit 1; } + # どこも上書きする前に、全部が PVC に入るかを見る + big= + for h in $H; do sh "$0" "$h" fit >/dev/null || big="$big $h"; done + [ -z "$big" ] || { echo "PVC に入らないものがある:$big(上の ERROR。forgejo だけ戻した状態で止めた。Argo CD は動いている)" >&2; exit 1; } sh "$0" - argo-pause for h in $H; do sh "$0" "$h" restore; done sh "$0" - argo-resume ;; + backup-all) # 入れ直す前の最後のバックアップ。Schedule に backup のある namespace ごとに Backup を作り、全部終わるまで待つ + t=final-$(date -u +%Y%m%d%H%M) + nss=$($K get schedule -A -o jsonpath='{range .items[?(@.spec.backup)]}{.metadata.namespace}{"\n"}{end}') + [ -n "$nss" ] || { echo "backup のある Schedule が 1 つも無い(kubectl get schedule -A で見る)" >&2; exit 1; } + for ns in $nss; do + # Schedule と同じ形(tags は prune の歯止め、root は 0600 のファイルを読むため。apps/k8up/schedules.yaml) + $K create -f - >/dev/null < 0)}' && break + sleep 10 + done + echo "$st" + ! echo "$st" | awk 'NF < 2 || $2 != "Succeeded" {f=1} END {exit !f}' \ + || { echo "終わっていないか、Succeeded でないものがある(kubectl get backup -A で見る)" >&2; exit 1; } ;; folder) folder "$1" "$2" ;; sqlite) # $1=pvc $2=ファイル名 $3=snapshot $4=中のパス restic_env diff --git a/renovate.json b/renovate.json index 99de1bc0..0d1f4948 100644 --- a/renovate.json +++ b/renovate.json @@ -129,7 +129,7 @@ "allowedVersions": "<18" }, { - "description": "Talos・Kubernetes・Gateway API は自動マージしない(ノード 1 台で全停止、CRD は全経路が乗り換わる)。適用は人。talosctl のイメージと手元の CLI もノードの版と一緒に上げる。版の比べ方は semver にする。既定の semver-coerced は v1.15.0-alpha.1 と alpha.2 を同じ 1.15.0 とみなし、alpha・beta の更新を出さない。talosctl のイメージには開発版のタグ(v1.15.0-alpha.2-9-g567501669)もあるので、正式な版・alpha・beta・rc だけを許す。", + "description": "Talos・Kubernetes・Gateway API は自動マージしない(ノード 1 台で全停止、CRD は全経路が乗り換わる)。適用は人。talosctl のイメージと手元の CLI、bootstrap/storage.yaml の kubectl のイメージもノードの版と一緒に上げる。版の比べ方は semver にする。既定の semver-coerced は v1.15.0-alpha.1 と alpha.2 を同じ 1.15.0 とみなし、alpha・beta の更新を出さない。talosctl のイメージには開発版のタグ(v1.15.0-alpha.2-9-g567501669)もあるので、正式な版・alpha・beta・rc だけを許す。", "versioning": "semver", "allowedVersions": "/^v\\d+\\.\\d+\\.\\d+(-(alpha|beta|rc)\\.\\d+)?$/", "groupName": "talos", @@ -142,7 +142,21 @@ "kubernetes/kubernetes", "kubernetes-sigs/gateway-api", "ghcr.io/siderolabs/talosctl", - "siderolabs/talos" + "siderolabs/talos", + "registry.k8s.io/kubectl" + ] + }, + { + "description": "OpenEBS LVM LocalPV の chart と、bootstrap/storage.yaml の lvm-thinpool が使う同じ lvm-driver のイメージは同じ PR で上げる(LVM のコマンドを揃える)。全部の PVC の土台なので自動マージしない。当てるのは render.sh → apply-config → upgrade-k8s。", + "matchDepNames": [ + "lvm-localpv", + "mirror.gcr.io/openebs/lvm-driver" + ], + "groupName": "openebs lvm", + "groupSlug": "openebs-lvm", + "automerge": false, + "addLabels": [ + "needs-review" ] }, {