Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 20 additions & 49 deletions recovery/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) が確かめる。残るのはこの手順書の通し(道具そのもの)だけ。
Expand Down Expand Up @@ -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` がやっていること(途中で止まったら Argo CD が止まったままのことがあるので、止まったところの表示に従う。**流し直すと Forgejo からやり直す**ので、続きは `<ns> restore` を 1 つずつ):

```shell
restic snapshots --host forgejo --latest 1 --compact
sh recovery/pv.sh forgejo stop postgres
sh recovery/pv.sh forgejo pgload postgres-data <ID> /forgejo-postgres.sql postgres forgejo forgejo
sh recovery/pv.sh forgejo folder forgejo-repos <ID>
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 のエラーが消え、**残りの namespace と PVC が全部出そろって全部の `plan` が通る**まで待ち(10 分。通らなければ forgejo だけ戻した状態で止まる)、また `argo-pause`
4. restic の host(forgejo・infisical・etcd を除く)を 1 つずつ `<ns> restore`
5. `argo-resume`

残りは namespace ごとに `restic snapshots --host <ns> --latest 1 --compact` で経路と ID を見て、
**`stop` → 経路ごとに戻す → `start`**。経路の形で操作が決まる:
1 つだけ戻すなら `sh recovery/pv.sh <ns> plan` で見てから `sh recovery/pv.sh <ns> restore`(Argo CD は先に `argo-pause`)。
`restore` は namespace の最新のスナップショットを経路ごとに引き、**経路の形で操作を決める**:

| 経路 | 操作 |
| --- | --- |
| `/data/<pvc>` | `folder <pvc> <ID>`(`/data/adguardhome-ssl` だけは戻さない。どの Pod も使っていない PVC で、消した) |
| `/<ns>-<app>.db`(SQLite) | `sqlite <pvc> <ファイル名> <ID> /<ns>-<app>.db`。xool・worklog・lgtm・todoroku は `<app>-db` / `<app>.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 <ID> /rybbit-postgres.sql postgres rybbit analytics`(ダンプは `--clean` 付き) |
| `/netbird-backup.tar` | `untar netbird-data <ID> /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 <ID>
sh recovery/pv.sh xool sqlite xool-db xool.db <ID> /xool-xool.db
sh recovery/pv.sh xool start
```

matrix(Tuwunel)は DB のファイルではなく、Tuwunel 自身のバックアップ(`backups/`)と `media/` を tar で取っている。
新しく作られた `db/` を消して置き、**1 回だけ `--restore-backup` 付きで起こす**(引数は Argo CD を戻すと selfHeal が消す):
| `/data/<pvc>` | `folder` |
| `/<ns>-<app>.db`(SQLite) | `sqlite`。PVC は `<app>-db`、無ければ `<app>-data`(denpa)。ファイル名は `<app>.db`。**folder を先に**流すので、`denpa.db*` を除いて取った `/data/denpa-data` を置いてから DB を戻す順になる |
| `/<ns>-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 が消す) |

```shell
sh recovery/pv.sh matrix stop
sh recovery/pv.sh matrix folder tuwunel-data <ID>
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 <ns> rollout restart deploy # 戻した namespace ごと
```
表に無い経路、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 と突き合わせてから付け替える
Expand Down
150 changes: 149 additions & 1 deletion recovery/pv.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
#
# tools/t.ps1 --admin sh recovery/pv.sh <namespace> <操作> [引数…]
#
# - 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 に控える)
Expand Down Expand Up @@ -31,7 +35,9 @@ shift 2
K=kubectl
REP="$HOME/.cache/pv-replicas-$NS"

restic_env() { set -a; . "$HOME/.config/restic.env"; set +a; }
# restic.env を読む。restore-all などが自分を呼び直すとき、子は親が読んだ env を継ぐので読み直さない
# (RESTIC_REPOSITORY があれば読み終わっているとみなす。資格情報を env で直に渡すときも同じ)
restic_env() { [ -n "${RESTIC_REPOSITORY:-}" ] && return 0; set -a; . "$HOME/.config/restic.env"; set +a; }

stop() {
mkdir -p "$(dirname "$REP")"
Expand Down Expand Up @@ -107,6 +113,114 @@ 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/<pvc> folder
# /<ns>-<app>.db sqlite。PVC は <app>-db、無ければ <app>-data。ファイル名は <app>.db
# /<ns>-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 を戻す)。
# 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 \
| 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() {
$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/}
pvc_exists "$pvc" || { echo "ERROR: $NS: $p を戻す PVC $pvc が無い" >&2; exit 1; }
echo "1 folder $pvc $id" ;;
/"$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"
elif pvc_exists "$app-data"; then echo "2 sqlite $app-data $app.db $id $p"
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" ;;
# 終了コード 2 = 待っても直らない(restore-all はこれを Forgejo を戻す前に見る)
*) echo "ERROR: $NS: $p の戻し方が決まっていない(README の 8 の表に足す)" >&2; exit 2 ;;
esac
done < "$_l" > "$_o"
sort -n "$_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
# 一覧は 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" ;;
untar) sh "$0" "$NS" untar "$a" "$b" "$c" "$(echo "$d" | tr , ' ')" ;;
pgload)
set -- $d # plan が足したユーザーと DB
sh "$0" "$NS" pgload "$a" "$b" "$c" postgres "$1" "$2" ;;
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 3<<STEPS
$steps
STEPS
start
}

# 戻す namespace = restic の host(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"))' \
| { grep -vxE 'forgejo|infisical|etcd' || true; }
}

# 全 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; }
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
Expand All @@ -117,6 +231,40 @@ case $OP in
echo "argo resumed" ;;
stop) stop "$@" ;;
start) start ;;
plan) restic_env; plan ;;
restore) restic_env; restore ;;
plan-all) # 読むだけ。forgejo と各 namespace で何をするかを出す。1 つでも決められなければ 1
restic_env
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; }
# 待っても直らないもの(戻し方が決まっていない経路。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 <ns> 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 <ns> start で起こせる。流し直すと Forgejo からやり直すので、続きは <ns> restore を 1 つずつ" >&2' EXIT
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 $H; do sh "$0" "$h" restore; done
sh "$0" - argo-resume ;;
folder) folder "$1" "$2" ;;
sqlite) # $1=pvc $2=ファイル名 $3=snapshot $4=中のパス
restic_env
Expand Down
Loading