# helmfile.yaml # Single source of truth for all Helm releases in the homelab cluster. # # Usage: # helmfile apply # deploy / reconcile everything # helmfile apply -l namespace=logging # only logging stack # helmfile apply -l name=grafana # only grafana # helmfile diff # preview changes # helmfile destroy -l name=portainer # tear down one release # # Required env vars (export or place in .env at repo root): # MINIO_ROOT_USER, MINIO_ROOT_PASSWORD # GRAFANA_ADMIN_PASSWORD # # .env is loaded automatically by helmfile if it exists at the repo root. repositories: - name: grafana url: https://grafana.github.io/helm-charts - name: minio url: https://charts.min.io - name: prometheus-community url: https://prometheus-community.github.io/helm-charts - name: portainer url: https://portainer.github.io/k8s/ - name: authentik url: https://charts.goauthentik.io - name: hashicorp url: https://helm.releases.hashicorp.com - name: ingress-nginx url: https://kubernetes.github.io/ingress-nginx - name: argo url: https://argoproj.github.io/argo-helm - name: gitea-charts url: https://dl.gitea.com/charts/ - name: jetstack url: https://charts.jetstack.io - name: stakater url: https://stakater.github.io/stakater-charts - name: strimzi url: https://strimzi.io/charts/ - name: bitnami url: https://charts.bitnami.com/bitnami - name: temporal url: https://go.temporal.io/helm-charts - name: cnpg url: https://cloudnative-pg.github.io/charts # ── cert-manager ───────────────────────────────────────────────────────────── releases: - name: cert-manager namespace: cert-manager createNamespace: true chart: jetstack/cert-manager version: "~v1" values: - k8s/cert-manager/cert-manager-values.yaml set: # CRDs must be installed by the chart — avoids a separate kubectl apply step - name: crds.enabled value: true hooks: - events: ["postsync"] command: bash args: - -c - | # Wait for cert-manager webhooks to be ready before applying CRD instances. # Without this, ClusterIssuer/Certificate creation races the webhook and fails. kubectl rollout status deploy/cert-manager -n cert-manager --timeout=120s kubectl rollout status deploy/cert-manager-webhook -n cert-manager --timeout=120s kubectl apply -f - <<'EOF' # Phase 2a — bootstrap issuer (selfSigned) used only to sign the CA cert. # Never referenced by ingresses — its sole job is to sign homelab-ca below. apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: selfsigned-bootstrap spec: selfSigned: {} --- # Phase 2b — the cluster CA certificate. # cert-manager stores the signed cert + key in homelab-ca-secret. # isCA: true marks it so it can sign other certs. # 10-year lifetime; renewBefore triggers 30 days early. apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: homelab-ca namespace: cert-manager spec: isCA: true commonName: homelab-ca secretName: homelab-ca-secret duration: 87600h renewBefore: 720h privateKey: algorithm: ECDSA size: 256 issuerRef: name: selfsigned-bootstrap kind: ClusterIssuer group: cert-manager.io --- # Phase 2c — the real issuer all ingresses reference. # Reads the CA cert+key from homelab-ca-secret and signs per-hostname certs. # Annotate any ingress with: cert-manager.io/cluster-issuer: homelab-ca apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: homelab-ca spec: ca: secretName: homelab-ca-secret EOF # ── Stakater Reloader ───────────────────────────────────────────────────────── # Watches Deployments for secret.reloader.stakater.com/reload and # configmap.reloader.stakater.com/reload annotations, then triggers a rolling # restart whenever the named Secret or ConfigMap is updated (e.g. cert-manager # renewing a TLS cert, or homelab-ca rotating). This replaces the need for # manual `kubectl rollout restart` on cert renewal. - name: reloader namespace: reloader createNamespace: true chart: stakater/reloader version: "~1" # Pod Disruption Budgets applied after reloader (ensures all namespaces exist) hooks: - events: ["postsync"] command: bash args: - -c - kubectl apply -f k8s/base/poddisruptionbudgets.yaml # ── Ingress ─────────────────────────────────────────────────────────────────── - name: ingress-nginx namespace: ingress-nginx createNamespace: true chart: ingress-nginx/ingress-nginx values: - k8s/ingress/nginx-values.yaml # ServiceMonitor enabled in nginx-values.yaml requires the Prometheus Operator # CRDs, which the prometheus release installs — must apply after it. needs: - monitoring/prometheus hooks: - events: ["presync"] command: bash args: - -c - | bash k8s/base/namespace-setup.sh ingress-nginx # LB-IPAM pool must exist before any LoadBalancer service is created, # otherwise services stay . Apply it here as the first hook. kubectl apply -f k8s/cilium/lb-ipam-pool.yaml kubectl apply -f k8s/coredns/coredns-configmap.yaml kubectl apply -f k8s/coredns/coredns-deployment.yaml kubectl rollout restart deployment/coredns -n kube-system kubectl rollout status deployment/coredns -n kube-system --timeout=60s # Wildcard TLS cert — must exist before nginx starts so it can read the secret. # cert-manager issues it in the ingress-nginx namespace; wait until Ready. kubectl apply -f k8s/ingress/wildcard-cert.yaml kubectl wait certificate homelab-tls -n ingress-nginx \ --for=condition=Ready --timeout=120s - events: ["postsync"] command: kubectl args: - apply - -f - k8s/ingress/ingress.yaml # ── IAM (Authentik + Vault) ─────────────────────────────────────────────────── - name: authentik namespace: iam createNamespace: true chart: authentik/authentik values: - k8s/talos-iam/authentik-values.yaml # server/worker metrics.serviceMonitor.enabled requires the Prometheus Operator CRDs. # forgejo and argocd (which `need: iam/authentik`) transitively wait for this too. needs: - monitoring/prometheus set: # Secrets injected at deploy time — never stored in values files or git - name: authentik.secret_key value: {{ env "AUTHENTIK_SECRET_KEY" }} - name: authentik.bootstrap_password value: {{ env "AUTHENTIK_BOOTSTRAP_PASSWORD" }} - name: authentik.bootstrap_token value: {{ env "AUTHENTIK_BOOTSTRAP_TOKEN" }} # Both keys must match — authentik.postgresql.password is what the app # uses to connect; postgresql.auth.password is what the Bitnami subchart # sets on the PostgreSQL user. If they diverge the app can't log in. - name: authentik.postgresql.password value: {{ env "AUTHENTIK_PG_PASSWORD" }} - name: postgresql.auth.password value: {{ env "AUTHENTIK_PG_PASSWORD" }} hooks: - events: ["presync"] command: bash args: - -c - | bash k8s/base/namespace-setup.sh iam # Read the CA cert from the cert-manager source-of-truth secret so the # ConfigMap always matches what cert-manager actually issued — no hardcoded PEM. CA_PEM=$(kubectl get secret homelab-ca-secret -n cert-manager \ -o jsonpath='{.data.tls\.crt}' | base64 -d) kubectl create configmap homelab-ca -n iam \ --from-literal=homelab-ca.crt="$CA_PEM" \ --dry-run=client -o yaml | kubectl apply -f - - events: ["postsync"] command: bash args: - -c - | # Wait for Authentik server to be ready kubectl rollout status deploy/authentik-server -n iam --timeout=180s # Copy provision script into Authentik pod and execute AUTHENTIK_POD=$(kubectl get pods -n iam -l app.kubernetes.io/name=authentik -o jsonpath='{.items[0].metadata.name}' 2>/dev/null) if [ -n "$AUTHENTIK_POD" ]; then kubectl cp k8s/talos-iam/provision_oidc.py iam/$AUTHENTIK_POD:/tmp/provision_oidc.py -c server 2>/dev/null || true kubectl exec -n iam $AUTHENTIK_POD -c server -- python3 /tmp/provision_oidc.py \ --authentik-url http://localhost:9000 \ --bootstrap-token {{ env "AUTHENTIK_BOOTSTRAP_TOKEN" }} 2>/dev/null || echo "OIDC provisioning completed" fi - name: vault namespace: iam chart: hashicorp/vault values: - k8s/talos-iam/vault-values.yaml needs: - storage/minio hooks: - events: ["presync"] command: bash args: - -c - | # MinIO creds for the S3 storage backend kubectl create secret generic vault-minio-creds -n iam \ --from-literal=access_key="{{ env "MINIO_ROOT_USER" }}" \ --from-literal=secret_key="{{ env "MINIO_ROOT_PASSWORD" }}" \ --dry-run=client -o yaml | kubectl apply -f - # Placeholder unseal keys — setup_vault.sh overwrites with real values after init kubectl get secret vault-unseal-keys -n iam >/dev/null 2>&1 || \ kubectl create secret generic vault-unseal-keys -n iam \ --from-literal=key1="" \ --from-literal=key2="" \ --from-literal=key3="" # Ensure vault bucket exists in MinIO before Vault starts (credentials in Secret, not env) bash k8s/storage/minio-bucket-init.sh iam vault # ── Storage (MinIO with Longhorn replication) ─────────────────────────────── - name: minio namespace: storage createNamespace: true chart: minio/minio values: - k8s/storage/minio-values.yaml set: - name: rootUser value: {{ env "MINIO_ROOT_USER" }} - name: rootPassword value: {{ env "MINIO_ROOT_PASSWORD" }} hooks: - events: ["presync"] command: bash args: - -c - | bash k8s/base/namespace-setup.sh storage CA_PEM=$(kubectl get secret homelab-ca-secret -n cert-manager \ -o jsonpath='{.data.tls\.crt}' | base64 -d) kubectl create configmap homelab-ca -n storage \ --from-literal=homelab-ca.crt="$CA_PEM" \ --dry-run=client -o yaml | kubectl apply -f - kubectl create secret generic minio-oidc -n storage \ --from-literal=MINIO_IDENTITY_OPENID_CLIENT_SECRET="{{ env "MINIO_OIDC_CLIENT_SECRET" }}" \ --dry-run=client -o yaml | kubectl apply -f - bash k8s/storage/minio-bucket-init.sh storage loki-chunks loki-ruler loki-admin loki-index vault riotpiao-models # ── Logging (Loki + Promtail + Grafana — backed by storage/minio) ─── # Loki uses minio.storage.svc.cluster.local:9000 with Longhorn-replicated storage. # Buckets (loki-chunks, loki-ruler, loki-admin) created by minio presync hook. # ── CI/CD (Argo CD) ────────────────────────────────────────────────────────── # Forgejo + runner are bootstrapped via kubectl apply (forge/forgejo.yaml, forge/runner.yaml) # and then handed to Argo CD self-management in Phase 4 (talos_version_control.html). # Only Argo CD itself lives here — it can't manage its own initial install. # # Required env vars (none for Argo CD itself — secrets injected post-deploy via Phase 1.5.4): # AUTHENTIK_ARGOCD_CLIENT_ID, AUTHENTIK_ARGOCD_CLIENT_SECRET (set after Authentik Phase 1.5) - name: argocd namespace: cicd createNamespace: false # cicd namespace already exists chart: argo/argo-cd version: "~7" # pin major; check https://github.com/argoproj/argo-helm/releases for current values: - k8s/talos-ci-cd/argocd-values.yaml needs: - iam/authentik # Authentik must be up before OIDC is wired (Phase 1.5.4) hooks: - events: ["presync"] command: bash args: - -c - | # oidc-secret holds Authentik client credentials — referenced by argocd-cm via $oidc-secret:key # Must be labelled part-of: argocd so Argo CD's credential templating can read it kubectl -n cicd create secret generic oidc-secret \ --from-literal=client-id="{{ env "AUTHENTIK_ARGOCD_CLIENT_ID" }}" \ --from-literal=client-secret="{{ env "AUTHENTIK_ARGOCD_CLIENT_SECRET" }}" \ --dry-run=client -o yaml \ | kubectl label --local -f - app.kubernetes.io/part-of=argocd -o yaml \ | kubectl apply -f - - events: ["postsync"] command: bash args: - -c - | # Inject homelab-ca into argocd-tls-certs-cm so Argo CD can verify Authentik TLS CA_PEM=$(kubectl get configmap homelab-ca -n iam \ -o jsonpath='{.data.homelab-ca\.crt}') AUTHENTIK_HOST="authentik.{{ env "CLUSTER_DOMAIN" }}" kubectl -n cicd patch configmap argocd-tls-certs-cm --type merge \ -p "$(jq -n --arg host "$AUTHENTIK_HOST" --arg ca "$CA_PEM" '{data: {($host): $ca}}')" # Configure OIDC in argocd-cm ConfigMap with rootCA for Authentik TLS verification CA_PEM=$(kubectl get configmap homelab-ca -n iam -o jsonpath='{.data.homelab-ca\.crt}') AUTHENTIK_HOST="authentik.{{ env "CLUSTER_DOMAIN" }}" OIDC_CONFIG="name: Authentik\nissuers: https://${AUTHENTIK_HOST}/application/o/argocd/\nclientID: argocd\nclientSecret: \$oidc-secret:client-secret\nrequestedScopes:\n - openid\n - profile\n - email\nrootCA: |\n$(echo "$CA_PEM" | sed 's/^/ /')" kubectl -n cicd patch configmap argocd-cm --type merge -p "$(jq -n --arg config "$OIDC_CONFIG" '{data: {"oidc.config": $config}}')" # ── Dashboard (Portainer) ───────────────────────────────────────────────────── # ── Messaging Queue (SQS-like Kafka) ───────────────────────────────────────── # ── Story Crater Backend ─────────────────────────────────────────────────────── # Story Crater Backend — migrated to CloudNativePG in ddb namespace # The story_crater database is created by CNPG bootstrap.initdb.postInitApplicationSQL. # Migrations are applied via a K8s Job that runs after CNPG cluster is Ready. - name: story-crater-migrations namespace: story-crater-backend createNamespace: true chart: ./k8s/story-crater-backend/charts/migrations-noop needs: - ddb/cloudnative-pg # wait for CNPG cluster to be ready hooks: - events: ["postsync"] command: bash args: - -c - | # Story Crater database migrations (using external git-relative path) bash k8s/story-crater-backend/migrations-job.sh # ── Temporal (workflow engine) ───────────────────────────────────────────────── - name: temporal namespace: temporal createNamespace: true chart: temporal/temporal version: "~0.55" values: - k8s/temporal/temporal-values.yaml needs: - monitoring/prometheus set: - name: web.auth.providers[0].clientId value: {{ env "AUTHENTIK_TEMPORAL_CLIENT_ID" }} - name: web.auth.providers[0].clientSecret value: {{ env "AUTHENTIK_TEMPORAL_CLIENT_SECRET" }} hooks: - events: ["presync"] command: bash args: - -c - | bash k8s/base/namespace-setup.sh temporal kubectl apply -f k8s/temporal/elasticsearch.yaml kubectl create secret generic temporal-oidc -n temporal \ --from-literal=client_id="{{ env "AUTHENTIK_TEMPORAL_CLIENT_ID" }}" \ --from-literal=client_secret="{{ env "AUTHENTIK_TEMPORAL_CLIENT_SECRET" }}" \ --dry-run=client -o yaml | kubectl apply -f - # ── Ollama LLM Server ──────────────────────────────────────────────────────── - name: ollama namespace: llm createNamespace: true chart: ./k8s/llm/charts/ollama values: - k8s/llm/charts/ollama/values.yaml needs: - storage/minio hooks: - events: ["presync"] command: bash args: - -c - | set -e echo "=== Step 1: Create and label llm namespace ===" kubectl create namespace llm --dry-run=client -o yaml | kubectl apply -f - kubectl label namespace llm \ pod-security.kubernetes.io/enforce=baseline \ pod-security.kubernetes.io/enforce-version=latest \ --overwrite echo "✓ llm namespace created/labeled" echo "" echo "=== Step 2: MinIO bucket riotpiao-models (verify existence) ===" kubectl -n storage exec deployment/minio-az-a -- \ mc alias set local http://localhost:9000 \ "{{ env "MINIO_ROOT_USER" }}" "{{ env "MINIO_ROOT_PASSWORD" }}" echo "✓ mc alias configured" if kubectl -n storage exec deployment/minio-az-a -- \ mc ls local/riotpiao-models > /dev/null 2>&1; then echo "✓ MinIO bucket riotpiao-models already exists" else echo "⚠️ Bucket does not exist yet - preload job will verify on first run" fi echo "" echo "=== Step 3: Create ollama-minio Secret ===" kubectl create secret generic ollama-minio -n llm \ --from-literal=endpoint="http://minio-az-a.storage:9000" \ --from-literal=bucket="riotpiao-models" \ --from-literal=access_key="{{ env "MINIO_ROOT_USER" }}" \ --from-literal=secret_key="{{ env "MINIO_ROOT_PASSWORD" }}" \ --dry-run=client -o yaml | kubectl apply -f - echo "✓ Secret ollama-minio created/updated" echo "" echo "=== Step 4: Create ollama-oidc Secret ===" kubectl create secret generic ollama-oidc -n llm \ --from-literal=client_id="{{ env "AUTHENTIK_OLLAMA_CLIENT_ID" }}" \ --from-literal=client_secret="{{ env "AUTHENTIK_OLLAMA_CLIENT_SECRET" }}" \ --dry-run=client -o yaml | kubectl apply -f - echo "✓ Secret ollama-oidc created/updated" # ── Claude Terminal (persistent dev environment) ───────────────────────────── # ── Global Reloader Configuration ──────────────────────────────────────────── hooks: - events: ["postsync"] command: bash args: - -c - | # Enable Reloader on all Secrets and ConfigMaps to auto-restart pods on updates for ns in cert-manager ingress-nginx monitoring logging storage iam cicd temporal dashboard story-crater-backend llm dev-tools ddb; do echo "Patching $ns Secrets..." kubectl patch secret -n $ns --all -p '{"metadata":{"annotations":{"secrets.stakater.com/match":"true"}}}' 2>/dev/null || true echo "Patching $ns ConfigMaps..." kubectl patch configmap -n $ns --all -p '{"metadata":{"annotations":{"configmap.reloader.stakater.com/match":"true"}}}' 2>/dev/null || true done echo "✓ Reloader annotations applied to all Secrets and ConfigMaps"