Files
homelab/helmfile.yaml.gotmpl
T

474 lines
21 KiB
Go Template

# 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 <pending>. 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"