feat(phase0): bootstrap External Secrets Operator and fix helmfile dual-ownership

Phase 0 groundwork for helmfile→ArgoCD migration:

1. Remove 3 bootstrap releases from helmfile (cert-manager, reloader, ingress-nginx)
   — already managed by terraform/bootstrap-releases.tf; eliminates dual-ownership

2. Bootstrap ESO (External Secrets Operator) as TF-managed release
   — required for all ExternalSecret resources in phases 1-3
   — added to bootstrap-releases.tf + helm-repositories.tf

3. Create ClusterSecretStore connecting ESO to Vault (K8s auth)
   — enables per-namespace/per-release secret injection
   — vault config documented in docs/PHASE0-ESO-VAULT-SETUP.md (manual setup)

4. Fix argocd-bootstrap.tf CA cert copy: use jq instead of sed for cleaner metadata handling

Changes:
- helmfile.yaml.gotmpl: remove cert-manager/reloader/ingress-nginx blocks
- terraform/bootstrap-releases.tf: add external-secrets release
- terraform/helm-repositories.tf: add external-secrets Helm repo
- k8s/external-secrets/clustersecretstore.yaml: ESO→Vault ClusterSecretStore
- k8s/argocd/apps/0-wave-0.yaml: stub wave 0 applications (schema fix, rewrite pending Phase 1)
- docs/PHASE0-ESO-VAULT-SETUP.md: manual ESO-Vault auth setup procedure

Next: Phase 1 will incrementally rewrite ArgoCD Applications + migrate helmfile releases.

Co-Authored-By: Claude Haiku 4.5 <[email protected]>
This commit is contained in:
Story Crater Bot
2026-07-15 14:53:16 -07:00
co-authored by Claude Haiku 4.5
parent 23ec31bd6d
commit e7f3409d0f
10 changed files with 324 additions and 156 deletions
+3 -126
View File
@@ -46,134 +46,11 @@ repositories:
- name: cnpg
url: https://cloudnative-pg.github.io/charts
# ── cert-manager ─────────────────────────────────────────────────────────────
# ── Bootstrap Releases (managed by Terraform) ────────────────────────────────────
# cert-manager, reloader, ingress-nginx, and cilium are bootstrap-managed by
# terraform/bootstrap-releases.tf — do NOT add them here, avoid dual-ownership.
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
# ── CloudNativePG (centralized database) ──────────────────────────────────────
# Single HA cluster (1 primary + 2 replicas) serving Authentik + story-crater-backend.