Skip to main content

workspace (component)

A Kustomize component that drops one TofuWorkspace custom resource, named workspace, into an app's namespace. Crossplane turns that resource into an OpenTofu workspace that owns the app's external resources — the restic buckets on Hetzner and Backblaze, the Backblaze application key scoped to them, and the *.int.kueber.eu DNS records for its internal routes — so they are declared next to the app and unwound with its namespace.

It is the second component most apps pull in, right after app-network-policy: backups reads the credentials it produces, and http-internal routes get their DNS record from it.

What it composes​

  • TofuWorkspace workspace (platform.kueber.eu/v1alpha1) from base/, with the talos overlay patching in the state location — bucket crossplane-talos-workspaces-… on Hetzner object storage, key state.tfstate — and the internal DNS target (dns.address 192.168.105.151, the internal gateway, plus the NetBird zone id).
  • Namespace labels secrets.tofu-workspace: "true" and secrets.netbird: "true", patched onto the app's Namespace, so External Secrets delivers the object-storage and NetBird credentials the workspace needs into the namespace.
  • Secret tofu-encryption — supplied by the app's wrapper (talos/workspace/secret-workspace.yaml, SOPS-encrypted): the passphrase that encrypts the OpenTofu state at rest.

What the composition renders from that (k8s/platform/configs/talos/crossplane/composition-tofu-workspace.yaml):

  • a per-namespace ProviderConfig and Workspace for provider-opentofu, state in the S3 backend above;
  • for every k8up Schedule in the namespace labelled crossplane-workspace: "true", a bucket named after the Schedule's spec.backend.s3.bucket: minio_s3_bucket on the Hetzner endpoint, b2_bucket plus a bucket-scoped b2_application_key on Backblaze;
  • for every internal HTTPRoute, a netbird_dns_record A record pointing at dns.address;
  • the outputs written to the workspace-outputs connection Secret — the Backblaze key id and secret that schedule-backblaze in backups reads;
  • Usage objects that keep the ProviderConfig and the credential Secrets alive until the workspace's tofu destroy has run.

How an app uses it​

# k8s/apps/<app>/talos/workspace/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component
components:
- ../../../../components/workspace/talos
resources:
- secret-workspace.yaml # tofu-encryption, SOPS

and in the overlay's kustomization.yaml, components: [network-policies, workspace, backups, …]. The wrapper must be included from the overlay that also holds namespace.yaml, because the component patches the Namespace by kind.

Why one workspace per app​

  • External resources follow the app. A bucket or DNS record that only exists because an app exists should be declared with the app, not in a central tofu environment that has to be edited on every add and remove.
  • Credentials stay scoped. Each app gets its own Backblaze application key, restricted to its own bucket; the cluster-wide key that mints them can create buckets and keys but not delete them (backup location: Backblaze).
  • Deletion is guarded. Backup buckets render with prevent_destroy = spec.retainBuckets (default true), so a careless prune cannot take the backups with it.

Operational notes​

  • Deleting the namespace deletes everything in it — except that the TofuWorkspace unwinds external resources through tofu destroy, and its backup buckets are prevent_destroy-guarded. Decommissioning an app therefore starts with emptying its bucket and setting spec.retainBuckets: false on the TofuWorkspace; only then does the namespace terminate cleanly. The composition's Usages keep the ProviderConfig and credential Secrets alive until the destroy has run. Details in IaC layers.
  • The bucket suffix (-<app>-<8 hex> in the app's backups/transformer-bucket-name.yaml) is what makes bucket names unique across apps and clusters; it is generated once per app and never changed, or the workspace would create a fresh, empty bucket.
  • Realtime compositions are off in Crossplane, so a new Schedule or HTTPRoute is picked up on the next reconcile interval, not instantly (Crossplane).
  • The component exists for the talos cluster only; edge apps have no k8up and no internal routes, so they carry no workspace.

Composition source​

There is no README.md in k8s/components/workspace/talos/; the manifests themselves are short — see k8s/components/workspace/talos and the XRD/Composition under k8s/platform/configs/talos/crossplane.