Add Ansible-driven DR bootstrap (#9) + PVC capture in backups
restore-k8s-apps.sh's own header explicitly scoped out fresh-cluster
provisioning ("assumes the target cluster/namespaces/PVCs already
exist"). This adds exactly that missing piece as an Ansible playbook
rather than another bash script — a better fit for "provision a target
into a working state" than for the actual data-restore logic, which stays
in restore-k8s-apps.sh unchanged and is just invoked as the final step.
dr-bootstrap.yml, given an app name + a backup archive:
1. Checks the target's environment (disk/mem/cpu) against k3s minimums
before touching anything
2. Installs k3s only if not already present (checked, not assumed)
3. Creates the namespace, PVC(s), Secret, and Deployments/Services/
Ingress from the backup's captured manifests — sanitized first
(sanitize_k8s_manifest.py strips resourceVersion/uid/status/
volumeName, the same class of bug as the earlier Secret-apply fix,
generalized) since a raw `kubectl get -o yaml` dump can't be
reapplied to a different cluster as-is
4. Hands off to the existing, already-validated restore-k8s-apps.sh for
the actual data population
backup-k8s-apps.sh gets a small but necessary addition: PVC specs were
never captured at all before (only Deployment/Service/ConfigMap/Ingress),
so there was nothing for a fresh-cluster bootstrap to provision the PVC
from. Captured with the same label selector as the existing manifests
capture, so it stays in lockstep automatically.
Verified for real, not just syntax-checked: ran the full playbook against
the standby VM (freshly installed k3s, empty namespace) using a live n8n
backup. Namespace/PVCs/Secret/Deployments provisioned from nothing, data
restored, pod healthy, and the workflow count in the restored Postgres DB
(11) matched the real source server exactly.
This commit is contained in:
60
ansible/sanitize_k8s_manifest.py
Executable file
60
ansible/sanitize_k8s_manifest.py
Executable file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
"""sanitize_k8s_manifest.py — strip source-cluster-specific fields from a
|
||||
captured `kubectl get ... -o yaml` dump so it can be applied fresh onto a
|
||||
different (DR/standby) cluster.
|
||||
|
||||
backup-k8s-apps.sh's manifests.yaml/pvc.yaml/secret.yaml are raw `kubectl
|
||||
get -o yaml` output: server-assigned fields like resourceVersion/uid are
|
||||
always present, and PVCs additionally carry spec.volumeName + a Bound
|
||||
status pointing at a specific PersistentVolume that only exists on the
|
||||
ORIGINAL cluster. Applying any of that verbatim onto a fresh cluster either
|
||||
fails outright (a stale resourceVersion trips optimistic-concurrency
|
||||
checks — the exact bug fixed in restore-k8s-apps.sh's Secret handling) or
|
||||
silently tries to bind a PVC to a PV that was never created there.
|
||||
|
||||
Usage: sanitize_k8s_manifest.py <in.yaml> <out.yaml>
|
||||
"""
|
||||
import sys
|
||||
import yaml
|
||||
|
||||
DROP_METADATA_KEYS = (
|
||||
'resourceVersion', 'uid', 'creationTimestamp', 'selfLink',
|
||||
'generation', 'finalizers', 'managedFields', 'annotations',
|
||||
)
|
||||
|
||||
|
||||
def sanitize_item(item):
|
||||
meta = item.get('metadata') or {}
|
||||
for key in DROP_METADATA_KEYS:
|
||||
meta.pop(key, None)
|
||||
|
||||
item.pop('status', None)
|
||||
|
||||
if item.get('kind') == 'PersistentVolumeClaim':
|
||||
spec = item.get('spec') or {}
|
||||
spec.pop('volumeName', None)
|
||||
|
||||
return item
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
print(f"usage: {sys.argv[0]} <in.yaml> <out.yaml>", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
with open(sys.argv[1]) as f:
|
||||
doc = yaml.safe_load(f)
|
||||
|
||||
if doc is None:
|
||||
items = []
|
||||
elif doc.get('kind') == 'List':
|
||||
items = [sanitize_item(i) for i in doc.get('items', [])]
|
||||
else:
|
||||
items = [sanitize_item(doc)]
|
||||
|
||||
with open(sys.argv[2], 'w') as f:
|
||||
yaml.safe_dump_all(items, f, default_flow_style=False)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user