# K8s Cluster Platform — Services Bootstraps platform/application services (vault, kube-prometheus-stack, harbor, jenkins, sonarqube, headlamp) onto the cluster using ArgoCD app-of-apps pattern. Run this after `cluster-bootstrap` finishes (ArgoCD, MetalLB, Envoy Gateway, NFS storage must already be up). ## Architecture ``` Git repo (cluster-platform) └── ArgoCD watches platform/apps/*/application.yaml (recursive) → syncs all Applications platform-app.yaml ← root "main" Application, applied once by hand (kubectl apply) platform/apps// ← one self-contained folder per service: application.yaml ArgoCD Application CRD (multi-source: chart + values + extras) values.yaml Helm values for the upstream chart *.yaml any extra raw manifests (HTTPRoute, RBAC, ClusterSecretStore) ``` Each service's `application.yaml` is a single multi-source Application: upstream Helm chart + this repo's `values.yaml` (via `ref: values`) + a third source pointing at the same folder (excluding `application.yaml`/`values.yaml`) for any extra raw manifests. The root `platform-app.yaml` only watches `platform/apps/*/application.yaml` (`directory.recurse: true` + `include` filter) — it never touches `values.yaml` or the extras directly. Ordering uses two independent layers: - **Application-level** `sync-wave` (on `application.yaml`'s `metadata`) — orders services relative to each other. - **Resource-level** `sync-wave` (on the extra manifests themselves, e.g. `httproute.yaml`) — orders a service's own extras (wave `"1"`) after its Helm chart's resources (implicit wave `"0"`), within the same Application. | Wave | Service | Purpose | |------|---------|---------| | 0 | vault | Secrets engine (standalone, manual init/unseal) (+ HTTPRoute, wave 1 internally) | | 0 | headlamp | K8s dashboard (+ RBAC for login token, wave 1 internally) | | 1 | external-secrets | Vault → K8s Secret operator (+ ClusterSecretStore, wave 1 internally) | | 1 | kube-prometheus-stack | Prometheus + Grafana + Alertmanager (+ HTTPRoute, wave 1 internally) | | 1 | harbor | Image registry (+ HTTPRoute, wave 1 internally) | | 2 | jenkins | CI (+ HTTPRoute, wave 1 internally) | | 2 | sonarqube | Code quality, embedded H2 (+ HTTPRoute, wave 1 internally) | ⚠️ Chart `targetRevision` pins in each `application.yaml` are best-effort and marked `TODO: verify latest` — this session had no live access to the Helm repos to confirm current versions. Run `helm repo add && helm search repo / --versions` before or after first sync and bump if a pin doesn't resolve. ## Bootstrap Sequence **Apply the root platform Application** ```bash kubectl apply -f platform-app.yaml ``` Everything else is reached by ArgoCD syncing `platform/apps/` from here. ### Post-sync manual steps **Vault — initialize + unseal** (standalone mode, not auto-unseal): ```bash kubectl exec -n vault vault-0 -- vault operator init -key-shares=1 -key-threshold=1 # save the unseal key + root token shown, then: kubectl exec -n vault vault-0 -- vault operator unseal ``` **Wire Vault up for external-secrets** (kv-v2 mount + kubernetes auth method — this is Vault-internal config, not a k8s resource, so it can't go through ArgoCD; do it once after unseal): ```bash kubectl exec -it -n vault vault-0 -- sh export VAULT_TOKEN= vault secrets enable -path=kv kv-v2 vault auth enable kubernetes vault write auth/kubernetes/config \ kubernetes_host="https://kubernetes.default.svc" vault policy write external-secrets - <