Provision Hetzner Object Storage backup buckets (offsite, nas-1-independent) #23

Merged
k.renner merged 6 commits from worktree-sharded-growing-sedgewick into master 2026-06-25 12:10:59 +00:00
Owner

Why

First stage of moving cluster backups off nas-1's local Garage S3 to offsite Hetzner Object Storage (fsn1). Decouples cluster backups from nas-1 so it can be powered off nightly to save power; also a DR improvement (backups + bootstrap secrets no longer share a box with, or get wiped alongside, the cluster).

What's in this PR (provisioning only — verified live)

  • object_storage.tf — aminueza/minio provider (fsn1, virtual-hosted, SSL) + 3 buckets: renner-uno-k8s-cnpg, renner-uno-k8s-velero, renner-uno-k8s-secrets. Domain-prefixed: Hetzner uses a global bucket namespace (k8s-velero was already taken).
  • variables_hetzner.tf — endpoint/region/keypair vars + enable_hetzner_buckets gate.
  • providers.tf — add aminueza/minio ~> 3.33; pin unifi ~> 0.49.x.
  • .woodpecker/deploy-infrastructure.yml — wire TF_VAR_hetzner_s3_{access,secret}_key.

All three buckets are created and in tofu state — tofu plan reports No changes.

Incidental fix: unifi provider pin

deploy-infrastructure's plan was already broken for everyone: CI tofu init -upgrade + ~> 0.41 pulled unifi 0.50.0+, which changed dns_record.ttl to a duration string and rejected the existing ttl = 300/60. Bisected: 0.49.0 is the last int-compatible release; pinned ~> 0.49.0.

Next stages (not here; blocked on the keypair values for SOPS)

Cluster-side cutover, paperless-first: repoint CNPG ObjectStores + Velero BSL to Hetzner, SOPS-encrypt the keypair into the cluster secrets, validate with a scratch restore, then broaden to authentik/immich.

🤖 Generated with Claude Code

## Why First stage of moving cluster backups off nas-1's local Garage S3 to **offsite Hetzner Object Storage** (fsn1). Decouples cluster backups from nas-1 so it can be powered off nightly to save power; also a DR improvement (backups + bootstrap secrets no longer share a box with, or get wiped alongside, the cluster). ## What's in this PR (provisioning only — verified live) - `object_storage.tf` — `aminueza/minio` provider (fsn1, virtual-hosted, SSL) + 3 buckets: `renner-uno-k8s-cnpg`, `renner-uno-k8s-velero`, `renner-uno-k8s-secrets`. Domain-prefixed: Hetzner uses a global bucket namespace (`k8s-velero` was already taken). - `variables_hetzner.tf` — endpoint/region/keypair vars + `enable_hetzner_buckets` gate. - `providers.tf` — add `aminueza/minio ~> 3.33`; pin `unifi ~> 0.49.x`. - `.woodpecker/deploy-infrastructure.yml` — wire `TF_VAR_hetzner_s3_{access,secret}_key`. All three buckets are created and in tofu state — `tofu plan` reports `No changes`. ## Incidental fix: unifi provider pin `deploy-infrastructure`'s plan was already broken for everyone: CI `tofu init -upgrade` + `~> 0.41` pulled unifi `0.50.0+`, which changed `dns_record.ttl` to a duration string and rejected the existing `ttl = 300/60`. Bisected: `0.49.0` is the last int-compatible release; pinned `~> 0.49.0`. ## Next stages (not here; blocked on the keypair values for SOPS) Cluster-side cutover, paperless-first: repoint CNPG ObjectStores + Velero BSL to Hetzner, SOPS-encrypt the keypair into the cluster secrets, validate with a scratch restore, then broaden to authentik/immich. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
k.renner added 4 commits 2026-06-25 11:49:51 +00:00
opentofu: provision Hetzner Object Storage backup buckets
ci/woodpecker/manual/deploy-infrastructure Pipeline failed
b27c4c8afe
Add the offsite S3 backup target that decouples cluster backups from
nas-1's local Garage, so nas-1 can be powered off nightly to save power
(and as a genuine DR improvement: backups + bootstrap secrets no longer
share a box with, and survive a rebuild of, the cluster).

Hetzner has no API to generate S3 credentials (Console-only, confirmed
empirically against the live hcloud API), so provisioning is two-part:
the keypair is created by hand in the Console, then the aminueza/minio
provider creates the buckets (k8s-cnpg, k8s-velero, k8s-secrets,
k8s-migration) over the S3 API.

- object_storage.tf: minio provider (fsn1) + for_each bucket resources
- variables_hetzner.tf: endpoint/region/keypair vars + enable flag
- providers.tf: aminueza/minio ~> 3.33
- deploy-infrastructure.yml: wire TF_VAR_hetzner_s3_{access,secret}_key
  from Woodpecker secrets into plan + apply

Bucket names mirror the current Garage layout so the cluster-side change
is only an endpoint/region/addressing swap (handled in follow-up commits,
staged paperless-first).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
opentofu: pin unifi provider to 0.49.x (unblock plan)
ci/woodpecker/push/deploy-infrastructure Pipeline was successful
ci/woodpecker/manual/deploy-infrastructure Pipeline failed
d3f94d359b
deploy-infrastructure's `tofu plan` was failing for everyone (not just
this branch): CI runs `tofu init -upgrade`, and the loose `~> 0.41`
constraint resolved to unifi 0.50.0+, which changed the dns_record `ttl`
schema from an integer to a Go duration string. Every plan then died on
the existing integer TTLs:

  Error: Invalid Time Duration String Value
    unifi_dns_record.renner_uno   (ttl = 300)
    unifi_dns_record.authentik_db (ttl = 60)

Bisected the provider: 0.49.0 is the last version that accepts integer
TTLs; 0.50.0 introduced the break. Pin to ~> 0.49.0 so the existing
config keeps validating. (A future bump past 0.49 needs those TTLs
rewritten to duration strings first.)

Also regenerates the lock: adds aminueza/minio, drops the stale unused
hashicorp/aws entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
opentofu: prefix Hetzner bucket names (global-namespace collision)
ci/woodpecker/push/deploy-infrastructure Pipeline was successful
ci/woodpecker/manual/deploy-infrastructure Pipeline failed
cd5a59f871
First apply created k8s-cnpg/k8s-secrets/k8s-migration but k8s-velero
failed: "The requested bucket name is not available." Hetzner Object
Storage uses a global bucket namespace (virtual-hosted style → buckets
are endpoint subdomains), so generic k8s-* names collide with other
customers. Prefix all buckets with the domain (renner-uno-k8s-*) to stay
unique and consistent. The three already-created plain buckets are empty
and get recreated under the new names by the targeted apply.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
opentofu: drop unused migration bucket
ci/woodpecker/push/deploy-infrastructure Pipeline was successful
ci/woodpecker/manual/deploy-infrastructure Pipeline was successful
625f068014
The migrate-* PVC-tarball workflow is retired, so renner-uno-k8s-migration
is no longer needed. Remove it; the (empty) bucket is destroyed by the
targeted apply. Remaining backup buckets: cnpg, velero, secrets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
k.renner added 1 commit 2026-06-25 12:04:09 +00:00
New secrets holding the Hetzner Object Storage keypair, ready for the
per-app backup cutover:
- seed/{paperless,authentik,immich}/hetzner-creds.sops.yaml
  → Secret/hetzner-s3-creds (ACCESS_KEY_ID, ACCESS_SECRET_KEY, REGION=fsn1)
- infrastructure/velero/hetzner-creds.sops.yaml
  → Secret/cloud-credentials-hetzner (cloud INI blob)

These are not referenced by any kustomization yet, so they're inert —
the existing Garage creds keep driving backups until each ObjectStore /
BSL is repointed (staged, paperless-first). Also gitignore
*.plaintext.yaml so the pre-encryption template can never be committed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
k.renner added 1 commit 2026-06-25 12:07:50 +00:00
Stage 1 of the offsite backup migration (paperless-first). Repoint the
pprl-pg ObjectStore from local Garage (s3.nas-1.renner.uno) to Hetzner
fsn1, and switch its credentials to the new hetzner-s3-creds secret.

- destinationPath: s3://k8s-cnpg/pprl-pg -> s3://renner-uno-k8s-cnpg/pprl-pg
- endpointURL: s3.nas-1.renner.uno -> fsn1.your-objectstorage.com
- s3Credentials: barman-cloud-creds -> hetzner-s3-creds

The ObjectStore CRD has no path-style knob; barman/boto3 default (auto)
addressing works against Hetzner's Ceph RGW. Garage creds/secret are left
in place for a one-revert rollback. Validate (WAL archive + base backup +
restore) before broadening to authentik/immich/velero.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
k.renner merged commit e21207c805 into master 2026-06-25 12:10:59 +00:00
Sign in to join this conversation.