Skip to content

Commit c8109aa

Browse files
committed
redo storage
1 parent b405978 commit c8109aa

14 files changed

Lines changed: 287 additions & 651 deletions

File tree

docs/storage-architecture.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Storage Architecture & Disaster Recovery
2+
3+
This document outlines the storage architecture for the cluster, focusing on data persistence, backup strategies, and disaster recovery workflows.
4+
5+
## Overview
6+
7+
The cluster uses **Longhorn** as the distributed block storage system. It provides highly available persistent storage for Kubernetes workloads and integrates with S3 for off-site backups.
8+
9+
## 1. Normal Operation (Write Path)
10+
11+
When an application writes data, it flows through the Kubernetes storage stack to Longhorn, which replicates it across nodes.
12+
13+
```mermaid
14+
graph LR
15+
subgraph "Application Pod"
16+
App[Application] --> Mount["/data (Mount)"]
17+
end
18+
19+
subgraph "Kubernetes Storage"
20+
Mount --> PVC[PersistentVolumeClaim]
21+
PVC --> PV[PersistentVolume]
22+
end
23+
24+
subgraph "Longhorn Storage Engine"
25+
PV --> LH_Vol[Longhorn Volume]
26+
LH_Vol --> Replica1[Replica 1 (Node A)]
27+
LH_Vol --> Replica2[Replica 2 (Node B)]
28+
end
29+
30+
style App fill:#f9f,stroke:#333,stroke-width:2px
31+
style LH_Vol fill:#bbf,stroke:#333,stroke-width:2px
32+
```
33+
34+
## 2. Backup Strategy (Automatic)
35+
36+
Backups are handled automatically by Longhorn's **Recurring Jobs**.
37+
- **Snapshots**: Local, instant point-in-time copies (kept for hours/days).
38+
- **Backups**: Deduplicated, compressed chunks sent to S3 (kept for days/weeks).
39+
40+
We use `RecurringJob` groups to assign policies:
41+
- **default**: Daily snapshot, Weekly backup (Applied to ALL new volumes).
42+
- **critical**: Hourly snapshot, Daily backup (Applied via `data-tier: critical` label).
43+
44+
```mermaid
45+
graph TD
46+
subgraph "Cluster"
47+
Volume[Longhorn Volume]
48+
Job[Recurring Job] -- Triggers --> Snapshot[Local Snapshot]
49+
end
50+
51+
subgraph "Off-site Storage (S3)"
52+
BackupStore[S3 Bucket]
53+
end
54+
55+
Snapshot -- "Deduplicated Upload" --> BackupStore
56+
57+
style Job fill:#f96,stroke:#333,stroke-width:2px
58+
style BackupStore fill:#6f6,stroke:#333,stroke-width:2px
59+
```
60+
61+
## 3. Disaster Recovery (The "Magic" Restore)
62+
63+
When the cluster is destroyed and rebuilt, data is restored from S3.
64+
65+
### The "Magic" Explained
66+
The "magic" is now powered by the **Automated Restore Job** (`restore-job.yaml`). It runs automatically when the cluster starts.
67+
68+
1. **Nuke & Rebuild**: Cluster is wiped. Longhorn installs.
69+
2. **Connect S3**: Longhorn connects to S3 and syncs backup metadata.
70+
3. **Dynamic Discovery**: The Restore Job scans **ALL** backups.
71+
4. **Match & Restore**: It finds the **LATEST** backup for your critical apps (e.g., `karakeep/data-pvc`) and creates a PV for it.
72+
5. **Bind**: Your App starts, sees the PV, and binds instantly.
73+
74+
```mermaid
75+
sequenceDiagram
76+
participant Admin
77+
participant Longhorn
78+
participant S3
79+
participant Job as Restore Job
80+
participant App
81+
82+
Note over Admin, App: Cluster Rebuilt (Empty State)
83+
84+
Longhorn->>S3: Sync Backup Metadata
85+
86+
Job->>Longhorn: "Do we have backups for Karakeep?"
87+
Longhorn-->>Job: "Yes, latest is from 2AM"
88+
89+
Job->>Longhorn: Restore Volume from 2AM Backup
90+
Longhorn->>S3: Download Data
91+
Longhorn->>Longhorn: Reconstruct Volume
92+
93+
Job->>App: Create PV "pv-restore-karakeep"
94+
95+
App->>Longhorn: Mount Volume
96+
Note over App: Application Starts with FRESH Data!
97+
```
98+
99+
### Why `longhorn-restore-karakeep` was "Out of Norm"
100+
The `longhorn-restore-karakeep` StorageClass contained a hardcoded `fromBackup` parameter.
101+
- **Pros**: Instant restore without scripts.
102+
- **Cons**: It pins the volume to *that specific backup forever*. If you write new data and restart, it might revert to the old backup depending on reclaim policy. It's not meant for general use.
103+
104+
**Best Practice**: Use the standard `longhorn` class. The **Automated Restore Job** will handle the disaster recovery binding for you.
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
apiVersion: v1
2+
kind: ServiceAccount
3+
metadata:
4+
name: restore-controller
5+
namespace: longhorn-system
6+
---
7+
apiVersion: rbac.authorization.k8s.io/v1
8+
kind: ClusterRole
9+
metadata:
10+
name: restore-controller-role
11+
rules:
12+
- apiGroups: ["longhorn.io"]
13+
resources: ["backupvolumes", "backups"]
14+
verbs: ["get", "list", "watch"]
15+
- apiGroups: [""]
16+
resources: ["persistentvolumes", "persistentvolumeclaims"]
17+
verbs: ["get", "list", "create", "patch"]
18+
- apiGroups: [""]
19+
resources: ["events"]
20+
verbs: ["create", "patch"]
21+
---
22+
apiVersion: rbac.authorization.k8s.io/v1
23+
kind: ClusterRoleBinding
24+
metadata:
25+
name: restore-controller-binding
26+
subjects:
27+
- kind: ServiceAccount
28+
name: restore-controller
29+
namespace: longhorn-system
30+
roleRef:
31+
kind: ClusterRole
32+
name: restore-controller-role
33+
apiGroup: rbac.authorization.k8s.io
34+
---
35+
apiVersion: batch/v1
36+
kind: Job
37+
metadata:
38+
name: restore-critical-volumes
39+
namespace: longhorn-system
40+
annotations:
41+
"helm.sh/hook": post-install,post-upgrade
42+
"helm.sh/hook-weight": "5"
43+
"argocd.argoproj.io/sync-wave": "2" # Run after Longhorn (Wave 1) but before Apps
44+
spec:
45+
template:
46+
spec:
47+
serviceAccountName: restore-controller
48+
restartPolicy: OnFailure
49+
containers:
50+
- name: restore-controller
51+
image: bitnami/kubectl:latest
52+
command: ["/bin/bash", "-c"]
53+
args:
54+
- |
55+
set -e
56+
57+
echo "Starting Automated Restore Controller (Dynamic Mode)..."
58+
59+
# Configuration: List of Critical PVCs to Restore
60+
# Format: "Namespace/PVC-Name"
61+
# We only restore these if they don't exist yet.
62+
CRITICAL_PVCS=(
63+
"karakeep/data-pvc"
64+
"home-assistant/home-assistant-config"
65+
"immich/immich-library"
66+
"n8n/n8n-data"
67+
"paperless-ngx/paperless-data-pvc"
68+
"paperless-ngx/paperless-media-pvc"
69+
)
70+
71+
# Wait for Longhorn CRDs
72+
echo "Waiting for BackupVolumes CRD..."
73+
kubectl wait --for=condition=established --timeout=60s crd/backupvolumes.longhorn.io || echo "CRD not ready yet, proceeding anyway..."
74+
75+
# Wait for BackupVolumes to be populated from S3
76+
echo "Waiting for BackupVolumes to sync from S3..."
77+
for i in {1..30}; do
78+
COUNT=$(kubectl get backupvolumes -n longhorn-system --no-headers 2>/dev/null | wc -l)
79+
if [ "$COUNT" -gt "0" ]; then
80+
echo "Found $COUNT BackupVolumes. Proceeding."
81+
break
82+
fi
83+
echo "No BackupVolumes found yet. Waiting... ($i/30)"
84+
sleep 5
85+
done
86+
87+
# Get all BackupVolumes with their PVC info
88+
# Format: BackupVolumeName|PVCName|Namespace
89+
echo "Building Backup Index..."
90+
# Note: We use a temporary file because bash arrays and pipes can be tricky
91+
kubectl get backupvolumes -n longhorn-system -o jsonpath='{range .items[*]}{.metadata.name}|{.status.kubernetesStatus.pvcName}|{.status.kubernetesStatus.namespace}{"\n"}{end}' > /tmp/backup_index.txt
92+
93+
for target in "${CRITICAL_PVCS[@]}"; do
94+
IFS='/' read -r target_ns target_pvc <<< "$target"
95+
96+
echo "------------------------------------------------"
97+
echo "Processing Target: $target_ns/$target_pvc"
98+
99+
# Check if PV already exists for this PVC (by checking our managed label)
100+
# Or just check if a PV exists that claims this PVC?
101+
# We'll stick to our naming convention pv-restore-<app> for simplicity in this script,
102+
# but ideally we should check if the PVC is already bound.
103+
104+
PV_NAME="pv-restore-${target_pvc}"
105+
if kubectl get pv "$PV_NAME" >/dev/null 2>&1; then
106+
echo "PV $PV_NAME already exists. Skipping."
107+
continue
108+
fi
109+
110+
# Find matching BackupVolume
111+
# We look for a line in index that matches |$target_pvc|$target_ns
112+
MATCH=$(grep "|${target_pvc}|${target_ns}$" /tmp/backup_index.txt | head -n 1)
113+
114+
if [ -z "$MATCH" ]; then
115+
echo "WARNING: No backup found for $target_ns/$target_pvc. Skipping."
116+
continue
117+
fi
118+
119+
BACKUP_VOL_NAME=$(echo "$MATCH" | cut -d'|' -f1)
120+
echo "Found matching BackupVolume: $BACKUP_VOL_NAME"
121+
122+
# Get Latest Backup Name
123+
LATEST_BACKUP=$(kubectl get backupvolume "$BACKUP_VOL_NAME" -n longhorn-system -o jsonpath='{.status.lastBackupName}')
124+
125+
if [ -z "$LATEST_BACKUP" ]; then
126+
echo "BackupVolume found but no backups inside. Skipping."
127+
continue
128+
fi
129+
130+
echo "Latest Backup: $LATEST_BACKUP"
131+
132+
# Construct Backup URL
133+
BACKUP_TARGET=$(kubectl get backuptarget default -n longhorn-system -o jsonpath='{.spec.backupTargetURL}' 2>/dev/null || echo "")
134+
if [ -z "$BACKUP_TARGET" ]; then
135+
BACKUP_TARGET=$(kubectl get cm longhorn-backup-config -n longhorn-system -o jsonpath='{.data.backup-target}')
136+
fi
137+
[[ "${BACKUP_TARGET}" != */ ]] && BACKUP_TARGET="${BACKUP_TARGET}/"
138+
139+
FULL_BACKUP_URL="${BACKUP_TARGET}?backup=${LATEST_BACKUP}&volume=${BACKUP_VOL_NAME}"
140+
141+
# Create PV
142+
echo "Creating PV $PV_NAME..."
143+
cat <<EOF | kubectl apply -f -
144+
apiVersion: v1
145+
kind: PersistentVolume
146+
metadata:
147+
name: $PV_NAME
148+
labels:
149+
restored-from: "$LATEST_BACKUP"
150+
managed-by: restore-controller
151+
spec:
152+
capacity:
153+
storage: 10Gi # Defaulting to 10Gi, Longhorn will resize if needed/possible
154+
accessModes:
155+
- ReadWriteOnce
156+
persistentVolumeReclaimPolicy: Retain
157+
storageClassName: longhorn
158+
csi:
159+
driver: driver.longhorn.io
160+
fsType: ext4
161+
volumeAttributes:
162+
numberOfReplicas: "3"
163+
staleReplicaTimeout: "30"
164+
fromBackup: "$FULL_BACKUP_URL"
165+
claimRef:
166+
name: $target_pvc
167+
namespace: $target_ns
168+
EOF
169+
170+
# Ensure Namespace exists
171+
kubectl create ns "$target_ns" --dry-run=client -o yaml | kubectl apply -f -
172+
173+
echo "Restored $target_ns/$target_pvc from $LATEST_BACKUP"
174+
done
175+
176+
echo "Restore process completed."

infrastructure/storage/longhorn/values.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ defaultSettings:
1010
storageOverProvisioningPercentage: "100"
1111
allowRecurringJobWhileVolumeDetached: "true"
1212
replicaAutoBalance: "best-effort"
13+
# Ensure all new volumes automatically get the "default" backup schedule
14+
# (Daily snapshots + Weekly backups)
15+
default-recurring-job-group: "default"
1316
# Ensure new consolidated settings that can be per-engine stay simple scalars for V1-only clusters
1417
# These avoid parse errors like strconv.ParseBool/Int seen in manager logs when JSON strings are provided.
1518
# Note: Use plain boolean for fastReplicaRebuildEnabled, not JSON format

my-apps/home/home-assistant/pvc.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ metadata:
77
app.kubernetes.io/name: home-assistant
88
app.kubernetes.io/component: home-automation
99
app.kubernetes.io/part-of: smart-home
10+
# Critical data tier ensures hourly snapshots and daily backups
11+
data-tier: critical
1012
annotations:
1113
# Longhorn backup settings - Important tier for smart home data
1214
longhorn.io/recurring-job-source: enabled
@@ -18,4 +20,4 @@ spec:
1820
resources:
1921
requests:
2022
storage: 10Gi
21-
storageClassName: longhorn-restore-home-assistant
23+
storageClassName: longhorn

my-apps/home/home-assistant/storageclass-restore.yaml

Lines changed: 0 additions & 18 deletions
This file was deleted.

my-apps/home/paperless-ngx/pvc.yaml

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@ metadata:
77
app: paperless-ngx
88
type: data
99
annotations:
10-
# Longhorn backup settings - Critical tier for user documents
11-
longhorn.io/recurring-job-source: enabled
12-
longhorn.io/recurring-job-group: critical
1310
volume.beta.kubernetes.io/storage-provisioner: driver.longhorn.io
1411
spec:
1512
accessModes:
@@ -28,9 +25,6 @@ metadata:
2825
app: paperless-ngx
2926
type: media
3027
annotations:
31-
# Longhorn backup settings - Critical tier for user documents
32-
longhorn.io/recurring-job-source: enabled
33-
longhorn.io/recurring-job-group: critical
3428
volume.beta.kubernetes.io/storage-provisioner: driver.longhorn.io
3529
spec:
3630
accessModes:
@@ -49,9 +43,6 @@ metadata:
4943
app: paperless-ngx
5044
type: export
5145
annotations:
52-
# Longhorn backup settings - Critical tier for user documents
53-
longhorn.io/recurring-job-source: enabled
54-
longhorn.io/recurring-job-group: critical
5546
volume.beta.kubernetes.io/storage-provisioner: driver.longhorn.io
5647
spec:
5748
accessModes:
@@ -70,9 +61,6 @@ metadata:
7061
app: paperless-ngx
7162
type: consume
7263
annotations:
73-
# Longhorn backup settings - Critical tier for user documents
74-
longhorn.io/recurring-job-source: enabled
75-
longhorn.io/recurring-job-group: critical
7664
volume.beta.kubernetes.io/storage-provisioner: driver.longhorn.io
7765
spec:
7866
accessModes:

my-apps/media/immich/library-pvc.yaml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@ metadata:
77
app.kubernetes.io/name: immich
88
app.kubernetes.io/component: library
99
annotations:
10-
# Longhorn backup settings - Critical tier for photo library (daily backups)
11-
longhorn.io/recurring-job-source: enabled
12-
longhorn.io/recurring-job-group: critical
1310
spec:
1411
accessModes:
1512
- ReadWriteOnce

my-apps/media/jellyfin/pvc.yaml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@ metadata:
77
app: jellyfin
88
type: config
99
annotations:
10-
# Longhorn backup settings - Standard tier for media configuration
11-
longhorn.io/recurring-job-source: enabled
12-
longhorn.io/recurring-job-group: standard
1310
volume.beta.kubernetes.io/storage-provisioner: driver.longhorn.io
1411
spec:
1512
storageClassName: longhorn

0 commit comments

Comments
 (0)