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.
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.
## 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)
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>
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>
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>
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>
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>
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>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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/minioprovider (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-velerowas already taken).variables_hetzner.tf— endpoint/region/keypair vars +enable_hetzner_bucketsgate.providers.tf— addaminueza/minio ~> 3.33; pinunifi ~> 0.49.x..woodpecker/deploy-infrastructure.yml— wireTF_VAR_hetzner_s3_{access,secret}_key.All three buckets are created and in tofu state —
tofu planreportsNo changes.Incidental fix: unifi provider pin
deploy-infrastructure's plan was already broken for everyone: CItofu init -upgrade+~> 0.41pulled unifi0.50.0+, which changeddns_record.ttlto a duration string and rejected the existingttl = 300/60. Bisected:0.49.0is 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
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>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>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>