# 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 - <`. Grafana stores this encrypted in its own DB (uses `kube-prometheus-stack-grafana`'s secret key), not in git. 4. **Save & test**. Datasource then shows up in Grafana as whatever name you gave it — build a panel with method `POST`, URL path `/budgets//run-query`, and an `ActualQLquery` JSON body, e.g. sum of $ per category: ```json { "ActualQLquery": { "table": "transactions", "groupBy": ["category"], "select": ["category", { "total": { "$sum": "amount" } }] } } ``` Keep the panel's refresh interval long (≥1m) — each call is a real login against `actual_server`, same rate-limit risk hit by the exporter earlier. **Headlamp login token** (ServiceAccount created by `headlamp/rbac.yaml`): ```bash kubectl create token headlamp-admin -n headlamp ``` Paste the token into the Headlamp UI login screen. ### Apply HTTPRoutes note Each service creates its own HTTPRoute as part of the same Application (unlike `cluster-bootstrap`'s ArgoCD route, which had to be applied by hand to avoid a chicken-and-egg problem before Envoy existed) — Envoy Gateway is already up by the time this repo syncs, so these are fully GitOps/auto-synced. Backend service names in each `httproute.yaml` are best-effort based on each chart's naming convention and marked with a `verify with: kubectl get svc` comment — confirm and adjust if a route doesn't resolve. ## Domains (add to local DNS, all → 192.168.1.30) | Service | Hostname | |---------|----------| | Vault | vault.fireflylab.local | | Grafana | grafana.fireflylab.local | | Harbor | harbor.fireflylab.local | | Jenkins | jenkins.fireflylab.local | | SonarQube | sonarqube.fireflylab.local | Headlamp has no HTTPRoute — accessed via `kubectl port-forward` until/unless you add a route for it. ⚠️ Vault's UI/API is now reachable externally via Envoy Gateway (HTTP, no TLS, same as every other service here). Since Vault holds secrets, consider whether that's acceptable for your threat model versus keeping it `kubectl port-forward`/internal-only.