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

158 lines
6.2 KiB
Markdown

# 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**
```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 <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):
```bash
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):
```bash
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):
```bash
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`):
```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.