From 5c84176bb23d7b0d3932811496c3187ea23793bd Mon Sep 17 00:00:00 2001 From: Ruk Doe Date: Tue, 20 Oct 2026 13:11:41 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(recovery):=20pv.sh=20=E3=81=AB=20plan?= =?UTF-8?q?=20/=20restore=20/=20plan-all=20/=20restore-all=20=E3=82=92?= =?UTF-8?q?=E8=B6=B3=E3=81=97=E3=80=81PV=20=E3=81=AE=E6=88=BB=E3=81=97?= =?UTF-8?q?=E3=82=92=201=20=E3=82=B3=E3=83=9E=E3=83=B3=E3=83=89=E3=81=AB?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit スナップショットの経路の形(/data/・/-.db・/-postgres.sql・netbird / matrix の tar)で 操作を決め、namespace ごとに「止める → 戻す → 起こす」を通しでやる。restore-all は Forgejo を先に戻し、 ApplicationSet のエラーが消えるのを待ってから残りを回す(README の 8 そのもの)。表に無い経路は plan で止める。 plan-all は本番の restic と突き合わせ、全 namespace の判定が README の表どおりになるのを確かめた。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NN4TmSXHrMP8dkTbtkjfz1 --- recovery/README.md | 69 +++++++++-------------------- recovery/pv.sh | 107 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 126 insertions(+), 50 deletions(-) diff --git a/recovery/README.md b/recovery/README.md index 0be1ae3..032b757 100644 --- a/recovery/README.md +++ b/recovery/README.md @@ -11,7 +11,7 @@ | [pv.sh](pv.sh) | PV のデータを k8up の `Restore` で戻す道具(下の 8) | | [infisical.sh](infisical.sh) / [infisical-ca.mjs](infisical-ca.mjs) | Infisical を restic から直接戻す道具(下の 6・7) | -**2 つの道具は、2026-10-17 に使ったものから決め打ちを外した形で、この形ではまだ流していない。** 次の建て直しで最初に確かめる。 +**2 つの道具は、2026-10-17 に使ったものから決め打ちを外した形で、この形ではまだ流していない。** 次の建て直しで最初に確かめる(`pv.sh - plan-all` は 2026-10-20 に本番の restic と突き合わせて、全 namespace の判定が上の表どおりになるのを確かめた)。 **手動のリハーサルは回さない。** 中身は月次の [restore-check](../apps/k8up/restore-check.yaml)、machine config は [talos-boot](../.github/workflows/talos-boot.yml) が確かめる。残るのはこの手順書の通し(道具そのもの)だけ。 @@ -159,64 +159,35 @@ kubectl get infisicalsecret -A # 全部 Ready になる。k8up/k8up-global ## 8. PV データを戻す(k8up) -**Argo CD を止めてから。** selfHeal が replicas を git の値に戻すので、アプリを 0 にしても起き上がってくる。 -戻し終わるまで application-controller を 0 にしておく(ApplicationSet は止まらないので Application は作られ続ける): +**1 本で流す。** Argo CD を止め、Forgejo を先に戻し、非公開アプリの Application と PVC が出てくるのを待ってから、 +残りの namespace を 1 つずつ「止める → 戻す → 起こす」: ```shell -sh recovery/pv.sh - argo-pause +sh recovery/pv.sh - plan-all # 読むだけ。namespace ごとに何をするかを出す。先にこれを見る +sh recovery/pv.sh - restore-all ``` -**Forgejo を先に。** 非公開アプリのイメージ(fj.doany.io のレジストリ。中身は `forgejo-repos`)も、Argo CD が読む -マニフェストも、それを読むトークン(Forgejo の DB)も全部 Forgejo にあるので、戻すまで 1 本も上がらない: +`restore-all` がやっていること(途中で止まったら、その namespace から手で続けられる): -```shell -restic snapshots --host forgejo --latest 1 --compact -sh recovery/pv.sh forgejo stop postgres -sh recovery/pv.sh forgejo pgload postgres-data /forgejo-postgres.sql postgres forgejo forgejo -sh recovery/pv.sh forgejo folder forgejo-repos -sh recovery/pv.sh forgejo start -sh recovery/pv.sh - argo-resume # repos-forgejo の ERR が False になり、非公開アプリの Application と PVC が出てくるのを待つ -kubectl -n argocd get applicationset -o custom-columns='NAME:.metadata.name,ERR:.status.conditions[?(@.type=="ErrorOccurred")].status' -sh recovery/pv.sh - argo-pause -``` +1. `argo-pause`(selfHeal が replicas を戻して、戻している最中にアプリが起き上がるので) +2. `forgejo restore` ── **Forgejo を先に。** 非公開アプリのイメージ(fj.doany.io のレジストリ。中身は `forgejo-repos`)も、 + Argo CD が読むマニフェストも、それを読むトークン(Forgejo の DB)も全部 Forgejo にあるので、戻すまで 1 本も上がらない +3. `argo-resume` して ApplicationSet のエラーが消えるのを待ち、また `argo-pause` +4. restic の host のうちクラスタに namespace があるもの(forgejo・infisical・etcd を除く)を ` restore` +5. `argo-resume` -残りは namespace ごとに `restic snapshots --host --latest 1 --compact` で経路と ID を見て、 -**`stop` → 経路ごとに戻す → `start`**。経路の形で操作が決まる: +1 つだけ戻すなら `sh recovery/pv.sh plan` で見てから `sh recovery/pv.sh restore`(Argo CD は先に `argo-pause`)。 +`restore` は namespace の最新のスナップショットを経路ごとに引き、**経路の形で操作を決める**: | 経路 | 操作 | | --- | --- | -| `/data/` | `folder `(`/data/adguardhome-ssl` だけは戻さない。どの Pod も使っていない PVC で、消した) | -| `/-.db`(SQLite) | `sqlite <ファイル名> /-.db`。xool・worklog・lgtm・todoroku は `-db` / `.db`、blog は `yosegaki-db` / `yosegaki.db`、denpa は `denpa-data` / `denpa.db`(**`/data/denpa-data` の `folder` を先に**。あちらは `denpa.db*` を除いて取っている) | -| `/rybbit-postgres.sql` | `stop postgres` にしてから `pgload postgres-data /rybbit-postgres.sql postgres rybbit analytics`(ダンプは `--clean` 付き) | -| `/netbird-backup.tar` | `untar netbird-data /netbird-backup.tar "store.db* idp.db* events.db*"`。戻すまで netbird-server は `no other identity providers configured` で CrashLoop する | -| `/matrix-backup.tar` | 下 | - -```shell -# 例: xool -sh recovery/pv.sh xool stop -sh recovery/pv.sh xool folder xool-assets -sh recovery/pv.sh xool sqlite xool-db xool.db /xool-xool.db -sh recovery/pv.sh xool start -``` - -matrix(Tuwunel)は DB のファイルではなく、Tuwunel 自身のバックアップ(`backups/`)と `media/` を tar で取っている。 -新しく作られた `db/` を消して置き、**1 回だけ `--restore-backup` 付きで起こす**(引数は Argo CD を戻すと selfHeal が消す): +| `/data/` | `folder`。PVC がもう無ければ飛ばす(やめたアプリの残り) | +| `/-.db`(SQLite) | `sqlite`。PVC は `-db`、無ければ `-data`(denpa)。ファイル名は `.db`。**folder を先に**流すので、`denpa.db*` を除いて取った `/data/denpa-data` を置いてから DB を戻す順になる | +| `/-postgres.sql` | `pgload`。`deploy/postgres` だけ止めずに残し、ユーザーと DB はその Pod の env から取る(ダンプは `--clean` 付き) | +| `/netbird-backup.tar` | `untar`(`store.db*` `idp.db*` `events.db*` を消してから)。戻すまで netbird-server は `no other identity providers configured` で CrashLoop する | +| `/matrix-backup.tar` | Tuwunel 自身のバックアップ(`backups/`)と `media/` の tar。新しく作られた `db/` を消して展開し、**1 回だけ `--restore-backup` 付きで起こす**(引数は Argo CD を戻すと selfHeal が消す) | -```shell -sh recovery/pv.sh matrix stop -sh recovery/pv.sh matrix folder tuwunel-data -sh recovery/pv.sh matrix shell tuwunel-data 'rm -rf db backups media; tar -xf matrix-backup.tar; rm -f matrix-backup.tar; chown -R 65532:65532 .; ls backups' -kubectl -n matrix patch sts tuwunel -p '{"spec":{"template":{"spec":{"containers":[{"name":"tuwunel","args":["--restore-backup"]}]}}}}' -sh recovery/pv.sh matrix start -kubectl -n matrix logs tuwunel-0 -c tuwunel | grep -iE 'restor|backup|error' -``` - -終わったら Argo CD を戻し、戻したアプリを一度入れ直して開いているファイルを掴み直させる: - -```shell -sh recovery/pv.sh - argo-resume -kubectl -n rollout restart deploy # 戻した namespace ごと -``` +表に無い経路があると `plan` の時点で止まる(黙って飛ばさない)。新しい形の経路を足したら pv.sh の `plan` とこの表に足す。 - **`Succeeded` は「戻った」の意味ではない。** `pv.sh` は `Restored N files` と(SELinux 以外の)エラー数を出すので、それを見る。 SQLite は大きさを restic と突き合わせてから付け替える diff --git a/recovery/pv.sh b/recovery/pv.sh index 61fc882..8b0a07e 100755 --- a/recovery/pv.sh +++ b/recovery/pv.sh @@ -4,6 +4,10 @@ # # tools/t.ps1 --admin sh recovery/pv.sh <操作> [引数…] # +# - plan-all / restore-all 全部(namespace は `-`)。plan は読むだけ。restore-all が README の 8 そのもの +# plan / restore その namespace の最新のスナップショットを経路の形で戻す(下の「まとめて戻す」) +# +# 以下は restore が使う部品。手で続けるときにも使う # argo-pause / argo-resume Argo CD の application-controller を 0 / 1 にする。止めないと selfHeal が # replicas を戻して、戻している最中にアプリが起き上がる # stop [残すもの…] その namespace の Deployment / StatefulSet を 0 にする(元の数は ~/.cache に控える) @@ -31,7 +35,8 @@ shift 2 K=kubectl REP="$HOME/.cache/pv-replicas-$NS" -restic_env() { set -a; . "$HOME/.config/restic.env"; set +a; } +# env にもう入っていれば(RESTIC_REPOSITORY)読まない +restic_env() { [ -n "${RESTIC_REPOSITORY:-}" ] && return 0; set -a; . "$HOME/.config/restic.env"; set +a; } stop() { mkdir -p "$(dirname "$REP")" @@ -107,6 +112,92 @@ P } unhelper() { $K -n "$NS" delete pod "h-$1" --wait=false >/dev/null; } +# --- まとめて戻す(plan / restore / plan-all / restore-all) --- +# スナップショットの経路の形で操作が決まる(README の 8 の表)。namespace = restic の host。 +# /data/ folder(PVC が無ければ飛ばす。やめたアプリの残り) +# /-.db sqlite。PVC は -db、無ければ -data。ファイル名は .db +# /-postgres.sql pgload(deploy/postgres は止めずに残す。ユーザーと DB はその Pod の env から) +# /netbird-backup.tar untar +# /matrix-backup.tar Tuwunel の --restore-backup(README の 8 の下) +# 順番は folder → sqlite → tar → pgload(denpa は /data/denpa-data を先に置いてから denpa.db を戻す)。 +# infisical は recovery/infisical.sh、etcd は docs/talos.md で戻すので、ここでは扱わない。 + +latest() { # $1=host → 「ID 経路」を経路ごとに 1 行(いちばん新しいもの) + restic snapshots --host "$1" --latest 1 --json \ + | bun -e 'for (const s of JSON.parse(await Bun.stdin.text()) ?? []) for (const p of s.paths) console.log(s.id, p)' +} + +pvc_exists() { $K -n "$NS" get pvc "$1" >/dev/null 2>&1; } + +# 「順番 操作 引数…」を出す。決められない経路は止める(黙って飛ばさない) +# パイプの中の while で exit しても外に伝わらないので、一覧はファイルに取ってから読む +plan() { + _l=$(mktemp); _o=$(mktemp) + latest "$NS" > "$_l" + while read -r id p; do + case $p in + /data/*) + pvc=${p#/data/} + if pvc_exists "$pvc"; then echo "1 folder $pvc $id"; else echo "# 飛ばす: $p(PVC $pvc が無い)" >&2; fi ;; + /"$NS"-postgres.sql) echo "4 pgload postgres-data $id $p" ;; + /"$NS"-*.db) + app=${p#/"$NS"-}; app=${app%.db} + if pvc_exists "$app-db"; then echo "2 sqlite $app-db $app.db $id $p" + elif pvc_exists "$app-data"; then echo "2 sqlite $app-data $app.db $id $p" + else echo "ERROR: $p を戻す PVC($app-db / $app-data)が無い" >&2; exit 1; fi ;; + /netbird-backup.tar) echo "3 untar netbird-data $id $p store.db*,idp.db*,events.db*" ;; + /matrix-backup.tar) echo "3 matrix tuwunel-data $id $p" ;; + *) echo "ERROR: $p の戻し方が決まっていない(README の 8 の表に足す)" >&2; exit 1 ;; + esac + done < "$_l" > "$_o" + sort -n "$_o"; rm -f "$_l" "$_o" +} + +restore() { + steps=$(plan) + [ -n "$steps" ] || { echo "$NS: 戻すものが無い"; return 0; } + echo "== $NS"; echo "$steps" | sed 's/^/ /' + keep=; echo "$steps" | grep -q ' pgload ' && keep=postgres + stop $keep + echo "$steps" | while read -r _ op a b c d; do + case $op in + folder) folder "$a" "$b" ;; + sqlite) sh "$0" "$NS" sqlite "$a" "$b" "$c" "$d" ;; + untar) sh "$0" "$NS" untar "$a" "$b" "$c" "$(echo "$d" | tr , ' ')" ;; + pgload) + u=$($K -n "$NS" exec deploy/postgres -- printenv POSTGRES_USER) + db=$($K -n "$NS" exec deploy/postgres -- printenv POSTGRES_DB) + sh "$0" "$NS" pgload "$a" "$b" "$c" postgres "$u" "$db" ;; + matrix) + folder "$a" "$b" + helper "$a" + $K -n "$NS" exec "h-$a" -- sh -euc "cd /pvc; rm -rf db backups media; tar -xf ${c#/}; rm -f ${c#/}; chown -R 65532:65532 .; ls backups" + unhelper "$a" + # 1 回だけ。Argo CD を戻すと selfHeal が引数を消す + $K -n "$NS" patch sts tuwunel -p '{"spec":{"template":{"spec":{"containers":[{"name":"tuwunel","args":["--restore-backup"]}]}}}}' >/dev/null ;; + esac + done + start +} + +# 戻す namespace(restic の host のうち、クラスタに namespace があるもの。forgejo・infisical・etcd を除く) +hosts() { + restic snapshots --json | bun -e 'console.log([...new Set(JSON.parse(await Bun.stdin.text()).map(s => s.hostname))].sort().join("\n"))' \ + | while read -r h; do + case $h in forgejo|infisical|etcd) continue ;; esac + if $K get ns "$h" >/dev/null 2>&1; then echo "$h"; else echo "# 飛ばす: host $h(namespace が無い)" >&2; fi + done +} + +wait_appsets() { # Forgejo が戻って ApplicationSet のエラーが消えるまで(非公開アプリの Application と PVC が出てくる) + for _ in $(seq 1 60); do + e=$($K -n argocd get applicationset -o jsonpath='{range .items[*]}{.status.conditions[?(@.type=="ErrorOccurred")].status}{"\n"}{end}' | grep -c True || true) + [ "$e" -eq 0 ] && { echo "applicationset: エラーなし"; return 0; } + sleep 10 + done + echo "applicationset のエラーが 10 分で消えない(kubectl -n argocd get applicationset で見る)" >&2; exit 1 +} + case $OP in argo-pause) $K -n argocd scale sts/argocd-application-controller --replicas=0 >/dev/null @@ -117,6 +208,20 @@ case $OP in echo "argo resumed" ;; stop) stop "$@" ;; start) start ;; + plan) restic_env; plan ;; + restore) restic_env; restore ;; + plan-all) # 読むだけ。forgejo と各 namespace で何をするかを出す + restic_env + for h in forgejo $(hosts); do echo "== $h"; sh "$0" "$h" plan | sed 's/^/ /'; done ;; + restore-all) # README の 8 を通しで。Forgejo を先に戻し、非公開アプリが出てくるのを待ってから残り + restic_env + sh "$0" - argo-pause + sh "$0" forgejo restore + sh "$0" - argo-resume + wait_appsets + sh "$0" - argo-pause + for h in $(hosts); do sh "$0" "$h" restore; done + sh "$0" - argo-resume ;; folder) folder "$1" "$2" ;; sqlite) # $1=pvc $2=ファイル名 $3=snapshot $4=中のパス restic_env From 96437ed271ae69ba35eb37ef92ecb6731a7ac499 Mon Sep 17 00:00:00 2001 From: Ruk Doe Date: Tue, 20 Oct 2026 13:15:29 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix(recovery):=20=E3=83=AC=E3=83=93?= =?UTF-8?q?=E3=83=A5=E3=83=BC=E6=8C=87=E6=91=98=20=E2=80=94=20=E7=84=A1?= =?UTF-8?q?=E3=81=84=20namespace=20/=20PVC=20=E3=81=AF=E6=AD=A2=E3=82=81?= =?UTF-8?q?=E3=82=8B=E3=80=81restore-all=20=E3=81=AF=E5=85=A8=E9=83=A8?= =?UTF-8?q?=E3=81=AE=20plan=20=E3=81=8C=E9=80=9A=E3=81=A3=E3=81=A6?= =?UTF-8?q?=E3=81=8B=E3=82=89=E4=B8=8A=E6=9B=B8=E3=81=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - plan は namespace や PVC が無ければ飛ばさずに止める(未作成とやめたアプリの残りを区別できない) - restore-all は Forgejo の後、残り全部の plan が通るまで Argo CD を動かしたまま待ち(10 分)、 通らなければどこも上書きせずに止める。plan-all は 1 つでも決められなければ 1 で終わる - restore のループは fd 3 で読む(中で呼ぶコマンドが残りの行を食わない)。plan の一時ファイルは trap で消す Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NN4TmSXHrMP8dkTbtkjfz1 --- recovery/README.md | 8 +++--- recovery/pv.sh | 66 +++++++++++++++++++++++++++++++++------------- 2 files changed, 51 insertions(+), 23 deletions(-) diff --git a/recovery/README.md b/recovery/README.md index 032b757..a7edbe2 100644 --- a/recovery/README.md +++ b/recovery/README.md @@ -172,8 +172,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 のエラーが消えるのを待ち、また `argo-pause` -4. restic の host のうちクラスタに namespace があるもの(forgejo・infisical・etcd を除く)を ` restore` +3. `argo-resume` して ApplicationSet のエラーが消え、**残りの namespace と PVC が全部出そろって全部の `plan` が通る**まで待ち(10 分。通らなければ forgejo だけ戻した状態で止まる)、また `argo-pause` +4. restic の host(forgejo・infisical・etcd を除く)を 1 つずつ ` restore` 5. `argo-resume` 1 つだけ戻すなら `sh recovery/pv.sh plan` で見てから `sh recovery/pv.sh restore`(Argo CD は先に `argo-pause`)。 @@ -181,13 +181,13 @@ sh recovery/pv.sh - restore-all | 経路 | 操作 | | --- | --- | -| `/data/` | `folder`。PVC がもう無ければ飛ばす(やめたアプリの残り) | +| `/data/` | `folder` | | `/-.db`(SQLite) | `sqlite`。PVC は `-db`、無ければ `-data`(denpa)。ファイル名は `.db`。**folder を先に**流すので、`denpa.db*` を除いて取った `/data/denpa-data` を置いてから DB を戻す順になる | | `/-postgres.sql` | `pgload`。`deploy/postgres` だけ止めずに残し、ユーザーと DB はその Pod の env から取る(ダンプは `--clean` 付き) | | `/netbird-backup.tar` | `untar`(`store.db*` `idp.db*` `events.db*` を消してから)。戻すまで netbird-server は `no other identity providers configured` で CrashLoop する | | `/matrix-backup.tar` | Tuwunel 自身のバックアップ(`backups/`)と `media/` の tar。新しく作られた `db/` を消して展開し、**1 回だけ `--restore-backup` 付きで起こす**(引数は Argo CD を戻すと selfHeal が消す) | -表に無い経路があると `plan` の時点で止まる(黙って飛ばさない)。新しい形の経路を足したら pv.sh の `plan` とこの表に足す。 +表に無い経路、namespace や PVC が無いものは `plan` の時点で止まる(黙って飛ばさない。「まだ Argo CD が作っていない」と「やめたアプリの残り」を区別できないので)。やめたアプリの残りは restic から消しておく([../apps/k8up/README.md](../apps/k8up/README.md)「やめたアプリの残り」)。新しい形の経路を足したら pv.sh の `plan` とこの表に足す。 - **`Succeeded` は「戻った」の意味ではない。** `pv.sh` は `Restored N files` と(SELinux 以外の)エラー数を出すので、それを見る。 SQLite は大きさを restic と突き合わせてから付け替える diff --git a/recovery/pv.sh b/recovery/pv.sh index 8b0a07e..5be830d 100755 --- a/recovery/pv.sh +++ b/recovery/pv.sh @@ -35,7 +35,8 @@ shift 2 K=kubectl REP="$HOME/.cache/pv-replicas-$NS" -# env にもう入っていれば(RESTIC_REPOSITORY)読まない +# restic.env を読む。restore-all などが自分を呼び直すとき、子は親が読んだ env を継ぐので読み直さない +# (RESTIC_REPOSITORY があれば読み終わっているとみなす。資格情報を env で直に渡すときも同じ) restic_env() { [ -n "${RESTIC_REPOSITORY:-}" ] && return 0; set -a; . "$HOME/.config/restic.env"; set +a; } stop() { @@ -114,13 +115,16 @@ unhelper() { $K -n "$NS" delete pod "h-$1" --wait=false >/dev/null; } # --- まとめて戻す(plan / restore / plan-all / restore-all) --- # スナップショットの経路の形で操作が決まる(README の 8 の表)。namespace = restic の host。 -# /data/ folder(PVC が無ければ飛ばす。やめたアプリの残り) +# /data/ folder # /-.db sqlite。PVC は -db、無ければ -data。ファイル名は .db # /-postgres.sql pgload(deploy/postgres は止めずに残す。ユーザーと DB はその Pod の env から) # /netbird-backup.tar untar # /matrix-backup.tar Tuwunel の --restore-backup(README の 8 の下) # 順番は folder → sqlite → tar → pgload(denpa は /data/denpa-data を先に置いてから denpa.db を戻す)。 # infisical は recovery/infisical.sh、etcd は docs/talos.md で戻すので、ここでは扱わない。 +# +# **namespace や PVC が無いのは飛ばさずに止める。** 「まだ Argo CD が作っていない」と「やめたアプリの残り」を +# 区別できないので。やめたアプリの残りは restic から消す(../apps/k8up/README.md「やめたアプリの残り」)。 latest() { # $1=host → 「ID 経路」を経路ごとに 1 行(いちばん新しいもの) restic snapshots --host "$1" --latest 1 --json \ @@ -129,28 +133,31 @@ latest() { # $1=host → 「ID 経路」を経路ごとに 1 行(いちばん新 pvc_exists() { $K -n "$NS" get pvc "$1" >/dev/null 2>&1; } -# 「順番 操作 引数…」を出す。決められない経路は止める(黙って飛ばさない) +# 「順番 操作 引数…」を出す。決められないものは止める(黙って飛ばさない) # パイプの中の while で exit しても外に伝わらないので、一覧はファイルに取ってから読む plan() { + $K get ns "$NS" >/dev/null 2>&1 || { echo "ERROR: $NS: namespace が無い" >&2; exit 1; } _l=$(mktemp); _o=$(mktemp) + trap 'rm -f "$_l" "$_o"' EXIT latest "$NS" > "$_l" while read -r id p; do case $p in /data/*) pvc=${p#/data/} - if pvc_exists "$pvc"; then echo "1 folder $pvc $id"; else echo "# 飛ばす: $p(PVC $pvc が無い)" >&2; fi ;; + pvc_exists "$pvc" || { echo "ERROR: $NS: $p を戻す PVC $pvc が無い" >&2; exit 1; } + echo "1 folder $pvc $id" ;; /"$NS"-postgres.sql) echo "4 pgload postgres-data $id $p" ;; /"$NS"-*.db) app=${p#/"$NS"-}; app=${app%.db} if pvc_exists "$app-db"; then echo "2 sqlite $app-db $app.db $id $p" elif pvc_exists "$app-data"; then echo "2 sqlite $app-data $app.db $id $p" - else echo "ERROR: $p を戻す PVC($app-db / $app-data)が無い" >&2; exit 1; fi ;; + else echo "ERROR: $NS: $p を戻す PVC($app-db / $app-data)が無い" >&2; exit 1; fi ;; /netbird-backup.tar) echo "3 untar netbird-data $id $p store.db*,idp.db*,events.db*" ;; /matrix-backup.tar) echo "3 matrix tuwunel-data $id $p" ;; - *) echo "ERROR: $p の戻し方が決まっていない(README の 8 の表に足す)" >&2; exit 1 ;; + *) echo "ERROR: $NS: $p の戻し方が決まっていない(README の 8 の表に足す)" >&2; exit 1 ;; esac done < "$_l" > "$_o" - sort -n "$_o"; rm -f "$_l" "$_o" + sort -n "$_o" } restore() { @@ -159,7 +166,8 @@ restore() { echo "== $NS"; echo "$steps" | sed 's/^/ /' keep=; echo "$steps" | grep -q ' pgload ' && keep=postgres stop $keep - echo "$steps" | while read -r _ op a b c d; do + # 一覧は fd 3 で読む(標準入力に流すと、中で呼ぶコマンドが残りの行を食いうる) + while read -r _ op a b c d <&3; do case $op in folder) folder "$a" "$b" ;; sqlite) sh "$0" "$NS" sqlite "$a" "$b" "$c" "$d" ;; @@ -176,20 +184,30 @@ restore() { # 1 回だけ。Argo CD を戻すと selfHeal が引数を消す $K -n "$NS" patch sts tuwunel -p '{"spec":{"template":{"spec":{"containers":[{"name":"tuwunel","args":["--restore-backup"]}]}}}}' >/dev/null ;; esac - done + done 3< s.hostname))].sort().join("\n"))' \ - | while read -r h; do - case $h in forgejo|infisical|etcd) continue ;; esac - if $K get ns "$h" >/dev/null 2>&1; then echo "$h"; else echo "# 飛ばす: host $h(namespace が無い)" >&2; fi - done + restic snapshots --json \ + | bun -e 'console.log([...new Set(JSON.parse(await Bun.stdin.text()).map(s => s.hostname))].sort().join("\n"))' \ + | grep -vxE 'forgejo|infisical|etcd' } -wait_appsets() { # Forgejo が戻って ApplicationSet のエラーが消えるまで(非公開アプリの Application と PVC が出てくる) +# 全 host の plan が通るか。通らなければ 1(何も触らない) +plan_all() { + rc=0 + for h in "$@"; do + echo "== $h" + if out=$(sh "$0" "$h" plan); then echo "$out" | sed 's/^/ /'; else rc=1; fi + done + return $rc +} + +wait_appsets() { # Forgejo が戻って ApplicationSet のエラーが消えるまで(非公開アプリの Application が出てくる) for _ in $(seq 1 60); do e=$($K -n argocd get applicationset -o jsonpath='{range .items[*]}{.status.conditions[?(@.type=="ErrorOccurred")].status}{"\n"}{end}' | grep -c True || true) [ "$e" -eq 0 ] && { echo "applicationset: エラーなし"; return 0; } @@ -210,17 +228,27 @@ case $OP in start) start ;; plan) restic_env; plan ;; restore) restic_env; restore ;; - plan-all) # 読むだけ。forgejo と各 namespace で何をするかを出す + plan-all) # 読むだけ。forgejo と各 namespace で何をするかを出す。1 つでも決められなければ 1 restic_env - for h in forgejo $(hosts); do echo "== $h"; sh "$0" "$h" plan | sed 's/^/ /'; done ;; + plan_all forgejo $(hosts) ;; restore-all) # README の 8 を通しで。Forgejo を先に戻し、非公開アプリが出てくるのを待ってから残り restic_env + # Forgejo の plan が通らなければ何も触らない + plan_all forgejo >/dev/null || { echo "forgejo の plan が通らない(plan-all で見る)" >&2; exit 1; } sh "$0" - argo-pause sh "$0" forgejo restore sh "$0" - argo-resume wait_appsets + # 残りの namespace と PVC が全部出そろうまで待つ(Argo CD は動かしたまま)。 + # 1 つでも欠けていれば、どこも上書きする前に止まる + H=$(hosts); ok= + for _ in $(seq 1 30); do + plan_all $H >/dev/null 2>&1 && { ok=1; break; } + sleep 20 + done + [ -n "$ok" ] || { plan_all $H >/dev/null || true; echo "10 分待っても上の plan が通らない。forgejo だけ戻した状態で止めた(Argo CD は動いている)" >&2; exit 1; } sh "$0" - argo-pause - for h in $(hosts); do sh "$0" "$h" restore; done + for h in $H; do sh "$0" "$h" restore; done sh "$0" - argo-resume ;; folder) folder "$1" "$2" ;; sqlite) # $1=pvc $2=ファイル名 $3=snapshot $4=中のパス From 628ca753fd8ed4abe6815671baf46f8718cd9628 Mon Sep 17 00:00:00 2001 From: Ruk Doe Date: Tue, 20 Oct 2026 13:18:46 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix(recovery):=20=E3=83=AC=E3=83=93?= =?UTF-8?q?=E3=83=A5=E3=83=BC=E6=8C=87=E6=91=98=20=E2=80=94=20pgload=20?= =?UTF-8?q?=E3=81=AE=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC=E3=81=A8=20DB=20?= =?UTF-8?q?=E3=81=AF=20plan=20=E3=81=A7=E6=B1=BA=E3=82=81=E3=82=8B?= =?UTF-8?q?=E3=80=81=E6=88=BB=E3=81=97=E6=96=B9=E3=81=AE=E7=84=A1=E3=81=84?= =?UTF-8?q?=E7=B5=8C=E8=B7=AF=E3=81=AF=20Forgejo=20=E3=81=AE=E5=89=8D?= =?UTF-8?q?=E3=81=AB=E6=AD=A2=E3=82=81=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NN4TmSXHrMP8dkTbtkjfz1 --- recovery/README.md | 2 +- recovery/pv.sh | 27 ++++++++++++++++++++------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/recovery/README.md b/recovery/README.md index a7edbe2..d558852 100644 --- a/recovery/README.md +++ b/recovery/README.md @@ -183,7 +183,7 @@ sh recovery/pv.sh - restore-all | --- | --- | | `/data/` | `folder` | | `/-.db`(SQLite) | `sqlite`。PVC は `-db`、無ければ `-data`(denpa)。ファイル名は `.db`。**folder を先に**流すので、`denpa.db*` を除いて取った `/data/denpa-data` を置いてから DB を戻す順になる | -| `/-postgres.sql` | `pgload`。`deploy/postgres` だけ止めずに残し、ユーザーと DB はその Pod の env から取る(ダンプは `--clean` 付き) | +| `/-postgres.sql` | `pgload`。`deploy/postgres` だけ止めずに残す。ユーザーと DB は `plan` の時点で deploy/postgres の env から決める(forgejo は `forgejo` / `forgejo`、rybbit は `rybbit` / `analytics`。ダンプは `--clean` 付き) | | `/netbird-backup.tar` | `untar`(`store.db*` `idp.db*` `events.db*` を消してから)。戻すまで netbird-server は `no other identity providers configured` で CrashLoop する | | `/matrix-backup.tar` | Tuwunel 自身のバックアップ(`backups/`)と `media/` の tar。新しく作られた `db/` を消して展開し、**1 回だけ `--restore-backup` 付きで起こす**(引数は Argo CD を戻すと selfHeal が消す) | diff --git a/recovery/pv.sh b/recovery/pv.sh index 5be830d..fa7f392 100755 --- a/recovery/pv.sh +++ b/recovery/pv.sh @@ -117,7 +117,7 @@ unhelper() { $K -n "$NS" delete pod "h-$1" --wait=false >/dev/null; } # スナップショットの経路の形で操作が決まる(README の 8 の表)。namespace = restic の host。 # /data/ folder # /-.db sqlite。PVC は -db、無ければ -data。ファイル名は .db -# /-postgres.sql pgload(deploy/postgres は止めずに残す。ユーザーと DB はその Pod の env から) +# /-postgres.sql pgload(deploy/postgres は止めずに残す。ユーザーと DB は plan で deploy の env から) # /netbird-backup.tar untar # /matrix-backup.tar Tuwunel の --restore-backup(README の 8 の下) # 順番は folder → sqlite → tar → pgload(denpa は /data/denpa-data を先に置いてから denpa.db を戻す)。 @@ -146,7 +146,12 @@ plan() { pvc=${p#/data/} pvc_exists "$pvc" || { echo "ERROR: $NS: $p を戻す PVC $pvc が無い" >&2; exit 1; } echo "1 folder $pvc $id" ;; - /"$NS"-postgres.sql) echo "4 pgload postgres-data $id $p" ;; + /"$NS"-postgres.sql) + # ユーザーと DB は deploy/postgres の env(値で書いてあるもの)。触る前に決めておく + pe() { $K -n "$NS" get deploy postgres -o "jsonpath={.spec.template.spec.containers[0].env[?(@.name==\"$1\")].value}" 2>/dev/null; } + u=$(pe POSTGRES_USER); db=$(pe POSTGRES_DB) + [ -n "$u" ] && [ -n "$db" ] || { echo "ERROR: $NS: deploy/postgres の POSTGRES_USER / POSTGRES_DB が読めない" >&2; exit 1; } + echo "4 pgload postgres-data $id $p $u $db" ;; /"$NS"-*.db) app=${p#/"$NS"-}; app=${app%.db} if pvc_exists "$app-db"; then echo "2 sqlite $app-db $app.db $id $p" @@ -154,7 +159,8 @@ plan() { else echo "ERROR: $NS: $p を戻す PVC($app-db / $app-data)が無い" >&2; exit 1; fi ;; /netbird-backup.tar) echo "3 untar netbird-data $id $p store.db*,idp.db*,events.db*" ;; /matrix-backup.tar) echo "3 matrix tuwunel-data $id $p" ;; - *) echo "ERROR: $NS: $p の戻し方が決まっていない(README の 8 の表に足す)" >&2; exit 1 ;; + # 終了コード 2 = 待っても直らない(restore-all はこれを Forgejo を戻す前に見る) + *) echo "ERROR: $NS: $p の戻し方が決まっていない(README の 8 の表に足す)" >&2; exit 2 ;; esac done < "$_l" > "$_o" sort -n "$_o" @@ -173,9 +179,8 @@ restore() { sqlite) sh "$0" "$NS" sqlite "$a" "$b" "$c" "$d" ;; untar) sh "$0" "$NS" untar "$a" "$b" "$c" "$(echo "$d" | tr , ' ')" ;; pgload) - u=$($K -n "$NS" exec deploy/postgres -- printenv POSTGRES_USER) - db=$($K -n "$NS" exec deploy/postgres -- printenv POSTGRES_DB) - sh "$0" "$NS" pgload "$a" "$b" "$c" postgres "$u" "$db" ;; + set -- $d # plan が足したユーザーと DB + sh "$0" "$NS" pgload "$a" "$b" "$c" postgres "$1" "$2" ;; matrix) folder "$a" "$b" helper "$a" @@ -194,7 +199,7 @@ STEPS hosts() { restic snapshots --json \ | bun -e 'console.log([...new Set(JSON.parse(await Bun.stdin.text()).map(s => s.hostname))].sort().join("\n"))' \ - | grep -vxE 'forgejo|infisical|etcd' + | { grep -vxE 'forgejo|infisical|etcd' || true; } } # 全 host の plan が通るか。通らなければ 1(何も触らない) @@ -235,6 +240,14 @@ case $OP in restic_env # Forgejo の plan が通らなければ何も触らない plan_all forgejo >/dev/null || { echo "forgejo の plan が通らない(plan-all で見る)" >&2; exit 1; } + # 待っても直らないもの(戻し方が決まっていない経路。plan の終了コード 2)も、触る前に止める。 + # namespace がまだ無いものはここでは見えない(Forgejo の後でしか出てこない) + bad= + for h in $(hosts); do + rc=0; sh "$0" "$h" plan >/dev/null 2>&1 || rc=$? + [ "$rc" = 2 ] && bad="$bad $h" + done + [ -z "$bad" ] || { echo "戻し方が決まっていない経路がある:$bad(sh recovery/pv.sh plan で見る)" >&2; exit 1; } sh "$0" - argo-pause sh "$0" forgejo restore sh "$0" - argo-resume From 9cede28e92c634c98f450c526bdcb15516d84162 Mon Sep 17 00:00:00 2001 From: Ruk Doe Date: Tue, 20 Oct 2026 13:20:34 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix(recovery):=20restore-all=20=E3=81=8C?= =?UTF-8?q?=E9=80=94=E4=B8=AD=E3=81=A7=E6=AD=A2=E3=81=BE=E3=81=A3=E3=81=9F?= =?UTF-8?q?=E3=82=89=20Argo=20CD=20=E3=81=AE=E6=88=BB=E3=81=97=E6=96=B9?= =?UTF-8?q?=E3=81=A8=E7=B6=9A=E3=81=91=E6=96=B9=E3=82=92=E5=87=BA=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NN4TmSXHrMP8dkTbtkjfz1 --- recovery/README.md | 2 +- recovery/pv.sh | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/recovery/README.md b/recovery/README.md index d558852..dc24fa0 100644 --- a/recovery/README.md +++ b/recovery/README.md @@ -167,7 +167,7 @@ sh recovery/pv.sh - plan-all # 読むだけ。namespace ごとに何をす sh recovery/pv.sh - restore-all ``` -`restore-all` がやっていること(途中で止まったら、その namespace から手で続けられる): +`restore-all` がやっていること(途中で止まったら Argo CD が止まったままのことがあるので、止まったところの表示に従う。**流し直すと Forgejo からやり直す**ので、続きは ` restore` を 1 つずつ): 1. `argo-pause`(selfHeal が replicas を戻して、戻している最中にアプリが起き上がるので) 2. `forgejo restore` ── **Forgejo を先に。** 非公開アプリのイメージ(fj.doany.io のレジストリ。中身は `forgejo-repos`)も、 diff --git a/recovery/pv.sh b/recovery/pv.sh index fa7f392..99caec9 100755 --- a/recovery/pv.sh +++ b/recovery/pv.sh @@ -248,6 +248,8 @@ case $OP in [ "$rc" = 2 ] && bad="$bad $h" done [ -z "$bad" ] || { echo "戻し方が決まっていない経路がある:$bad(sh recovery/pv.sh plan で見る)" >&2; exit 1; } + # ここから先で落ちたら、Argo CD が止まったままかもしれないことと続け方を出す + trap 'rc=$?; [ $rc = 0 ] || echo "restore-all が途中で止まった($rc)。Argo CD は止まったままかもしれない(sh recovery/pv.sh - argo-resume で戻す)。止めた namespace は sh recovery/pv.sh start で起こせる。流し直すと Forgejo からやり直すので、続きは restore を 1 つずつ" >&2' EXIT sh "$0" - argo-pause sh "$0" forgejo restore sh "$0" - argo-resume