Files
cluster-platform/README.md
T
duynguyen 7a665bdf9d refactor: one self-contained folder per platform service
Each platform/apps/<service>/ now holds its own application.yaml
(multi-source: chart + values + raw-manifest extras), values.yaml, and
any extra manifests (HTTPRoute, RBAC, ClusterSecretStore) together,
replacing the split apps/*.yaml + manifests/*/ + separate *-config
Application pattern.

Root platform-app.yaml now recurses platform/apps/*/application.yaml
only. Extras get a resource-level sync-wave (1) so they still land
after their service's Helm chart within the same Application sync.

Also adds an HTTPRoute for vault (vault.fireflylab.local) - exposed
same as every other service here, accepted as LAN-only exposure.
2026-07-29 23:56:50 +07:00

6.2 KiB

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/<service>/    ← 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 <name> <url> && helm search repo <name>/<chart> --versions before or after first sync and bump if a pin doesn't resolve.

Bootstrap Sequence

Apply the root platform Application

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):

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 <unseal-key>

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):

kubectl exec -it -n vault vault-0 -- sh
export VAULT_TOKEN=<root-token-from-init>

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 - <<EOF
path "kv/data/*" {
  capabilities = ["read"]
}
EOF

vault write auth/kubernetes/role/external-secrets \
  bound_service_account_names=external-secrets-vault-auth \
  bound_service_account_namespaces=external-secrets \
  policies=external-secrets \
  ttl=1h

Once this is done, the ClusterSecretStore (vault-backend, part of the external-secrets Application) should show Valid — check with kubectl get clustersecretstore vault-backend -o yaml.

Per-service ExternalSecret resources (harbor-credentials, gitea-credentials, sonarqube-token, Jenkins creds) aren't created yet — that's a follow-up once you've actually put those secrets into Vault under kv/.

Grafana admin password (chart auto-generates, never in Git):

kubectl get secret -n monitoring kube-prometheus-stack-grafana -o jsonpath='{.data.admin-password}' | base64 -d; echo

Jenkins admin password (chart auto-generates, never in Git):

kubectl exec -n jenkins deploy/jenkins -c jenkins -- cat /run/secrets/additional/chart-admin-password; echo

Harbor admin password — chart ships a default (Harbor12345). Log in to http://harbor.fireflylab.local and rotate it immediately; Harbor's password is set inside its own database on first boot, so it cannot be swapped via a kubectl patch the way ArgoCD's can.

Headlamp login token (ServiceAccount created by headlamp/rbac.yaml):

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.