[READ-ONLY] Mirror of https://github.com/tiborpilz/infrastructure. Personal Infrastructure managed via terraform & k8s
0

Configure Feed

Select the types of activity you want to include in your feed.

169 46 0

Clone this repository

https://tangled.org/tibor.sh/infrastructure https://tangled.org/did:plc:bnw5wdbehqh2otwayntrublf
git@tangled.org:tibor.sh/infrastructure git@tangled.org:did:plc:bnw5wdbehqh2otwayntrublf

For self-hosted knots, clone URLs may differ based on your setup.



README.md

Infrastructure#

Scaffoldable Kubernetes platform on Hetzner. See PLAN.md for the full design rationale, constraints, and milestone roadmap.

Layers#

The terragrunt/envs/<env>/ directory contains the layered Terragrunt stacks. Apply order is enforced by dependency blocks; terragrunt run --all apply walks the graph correctly.

Layer Purpose
00-machines Hetzner network + VMs. Outputs a generic node inventory.
10-cluster Talos config + bootstrap. Outputs kubeconfig material.
20-argocd Cilium (CNI + Gateway API), Hetzner CCM, Argo CD, AppProjects.
30-networking cert-manager, external-dns, public Gateway.
30-smoke-app Trivial nginx behind the Gateway. End-to-end TLS/DNS proof.
35-platform-data hcloud-csi (hcloud-volumes StorageClass) + CloudNativePG operator.
37-velero Velero (Kopia file-system backup) + Talos etcd snapshot CronJob → Hetzner Object Storage.
40-authentik authentik (OIDC provider) + CNPG Cluster + inlined Valkey.
45-authentik-config General authentik state — placeholder groups. No per-app clients.
50-argocd-oidc Glue layer: creates the authentik objects matching Argo CD's TF-baked OIDC client. See "Argo CD exception" below.

Per-app identity convention#

When a future module deploys a core app that consumes authentik OIDC, it owns BOTH the app's deployment and its authentik client config. Layout inside the module:

# random_password lives in TF state; never in Git
resource "random_password" "oidc_client_secret" {
  length  = 48
  special = false
}

resource "authentik_provider_oauth2" "this" {
  name           = "<app>"
  client_id      = "<app>"
  client_secret  = random_password.oidc_client_secret.result
  redirect_uris  = [{ matching_mode = "strict", url = "https://<app>.<domain>/oauth/callback" }]
  authorization_flow = data.authentik_flow.default_authorization.id
  authentication_flow = data.authentik_flow.default_authentication.id
}

resource "authentik_application" "this" {
  name              = "<app>"
  slug              = "<app>"
  protocol_provider = authentik_provider_oauth2.this.id
}

# Consumed by the app's Helm chart via valuesObject or env
resource "kubernetes_secret" "<app>_oidc" {
  metadata {
    namespace = "<app>"
    name      = "<app>-oidc"
  }
  data = {
    client_id     = authentik_provider_oauth2.this.client_id
    client_secret = random_password.oidc_client_secret.result
    issuer_url    = "${var.authentik_url}/application/o/<app>/"
  }
}

resource "kubectl_manifest" "argo_app_<app>" {
  # Argo CD Application that references the secret above
  ...
}

Why not a central 40-post-config layer? PLAN.md flagged that as awkward. Colocating the identity client with the consumer means adding/removing an app is one module change, the dependency graph stays local, and there's no single layer that knows about every app. Multiple core apps may share one Terragrunt env layer (e.g. 50-core-apps) — they don't each need a dedicated layer.

Argo CD is the exception. Argo CD is deployed before authentik (because authentik runs as an Argo CD Application), so the convention can't apply directly — 20-argocd can't dependency on 40-authentik without a cycle. Instead, 20-argocd generates the OIDC client_secret as a random_password and bakes it into the Helm values; a downstream glue layer 50-argocd-oidc creates the matching authentik objects. Future apps deployed AFTER authentik (Forgejo, Woodpecker, Grafana, ...) follow the documented convention as-is.

What lives in 45-authentik-config? Only state that isn't tied to a specific app: groups, branding, SMTP, MFA policies, default flows. The akadmin password is already pinned upstream by 40-authentik (TF-generated random_password mounted as AUTHENTIK_BOOTSTRAP_PASSWORD).

Reaching the cluster#

After terragrunt run --all apply:

export KUBECONFIG=.kube/hcloud-poc.kubeconfig
kubectl get nodes

authentik bootstrap admin password (first login):

terragrunt --working-dir terragrunt/envs/hcloud-poc/40-authentik output -raw bootstrap_admin_password

Backups#

37-velero runs Velero (Kopia file-system backup) and a Talos etcd snapshot CronJob, both targeting one Hetzner Object Storage bucket per env (backups-<env_name>). Layout: velero/ for cluster + PV backups, etcd/ for raw etcd snapshots.

One-time prerequisite. Hetzner Object Storage S3 access keys aren't yet creatable via the hcloud Terraform provider. Generate them once in the Hetzner Console (Project → Security → Object Storage) and export:

export HCLOUD_S3_ACCESS_KEY=...
export HCLOUD_S3_SECRET_KEY=...

The bucket itself is created by 37-velero via the aws Terraform provider pointed at Hetzner's S3 endpoint — no manual bucket setup needed. (Migrating these creds to SOPS is on the M2 list.)

Defaults.

  • Velero: daily backup at 02:00 cluster time, 14-day retention, all namespaces.
  • etcd snapshots: every 6 hours, 7-day retention.

Restore drill (run quarterly — untested backups are not backups):

# Create a throwaway namespace, back it up, delete, restore.
kubectl create ns smoke-restore
kubectl -n smoke-restore create deploy nginx --image=nginx
velero backup create smoke-$(date +%s) --include-namespaces smoke-restore --wait
kubectl delete ns smoke-restore
velero restore create --from-backup smoke-... --wait
kubectl -n smoke-restore get deploy nginx

velero is in the dev shell (flake.nix).

Local development#

shell.nix / flake.nix pin the toolchain. nix develop (or direnv allow) brings in terragrunt, tofu, talosctl, kubectl, argocd, helm, velero, etc.