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.
This commit is contained in:
2026-07-29 23:56:50 +07:00
parent a1cff1dfb9
commit 7a665bdf9d
30 changed files with 133 additions and 169 deletions
+43 -30
View File
@@ -10,33 +10,40 @@ storage must already be up).
```
Git repo (cluster-platform)
└── ArgoCD watches platform/apps/ → syncs all Applications
└── ArgoCD watches platform/apps/*/application.yaml (recursive) → syncs all Applications
platform-app.yaml ← root Application, applied once by hand (kubectl apply)
platform/apps/ ← ArgoCD watches this path; one Application CRD per service
manifests/ ← Helm values + raw manifests referenced by those 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)
```
Sync waves (no hard interdependencies between services yet — grouped for
readability / staggered rollout):
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) |
| 0 | headlamp | K8s dashboard |
| 1 | headlamp-config | ClusterRoleBinding for login token |
| 1 | external-secrets | Vault → K8s Secret operator |
| 1 | kube-prometheus-stack | Prometheus + Grafana + Alertmanager |
| 1 | harbor | Image registry |
| 2 | external-secrets-config | ClusterSecretStore wired to Vault (k8s auth) |
| 2 | kube-prometheus-stack-config | Grafana HTTPRoute |
| 2 | harbor-config | Harbor HTTPRoute |
| 2 | jenkins | CI |
| 2 | sonarqube | Code quality (embedded H2, no external Postgres) |
| 3 | jenkins-config | Jenkins HTTPRoute |
| 3 | sonarqube-config | SonarQube HTTPRoute |
| 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 `platform/apps/*.yaml` are best-effort and
⚠️ 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
@@ -88,8 +95,9 @@ vault write auth/kubernetes/role/external-secrets \
ttl=1h
```
Once this is done, `external-secrets-config`'s `ClusterSecretStore` (`vault-backend`)
should show `Valid` — check with `kubectl get clustersecretstore vault-backend -o yaml`.
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
@@ -112,7 +120,7 @@ kubectl exec -n jenkins deploy/jenkins -c jenkins -- cat /run/secrets/additional
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-config`):
**Headlamp login token** (ServiceAccount created by `headlamp/rbac.yaml`):
```bash
kubectl create token headlamp-admin -n headlamp
@@ -121,10 +129,10 @@ Paste the token into the Headlamp UI login screen.
### Apply HTTPRoutes note
Each `*-config` Application creates its own HTTPRoute (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.
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`
@@ -134,11 +142,16 @@ comment — confirm and adjust if a route doesn't resolve.
| Service | Hostname |
|---------|----------|
| Vault | vault.fireflylab.local |
| Grafana | grafana.fireflylab.local |
| Harbor | harbor.fireflylab.local |
| Jenkins | jenkins.fireflylab.local |
| SonarQube | sonarqube.fireflylab.local |
Vault and Headlamp have no HTTPRoute — Vault stays internal-only
(`vault.vault.svc.cluster.local:8200`); Headlamp is accessed via
`kubectl port-forward` until/unless you add a route for it.
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.
+3
View File
@@ -9,6 +9,9 @@ spec:
repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
path: platform/apps
directory:
recurse: true
include: "*/application.yaml"
destination:
server: https://kubernetes.default.svc
namespace: argocd
@@ -1,22 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: external-secrets-config
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "2"
spec:
project: default
source:
repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
path: manifests/external-secrets-config
destination:
server: https://kubernetes.default.svc
namespace: external-secrets
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- SkipDryRunOnMissingResource=true
@@ -13,10 +13,15 @@ spec:
targetRevision: "0.10.0" # TODO: verify latest via `helm search repo external-secrets/external-secrets --versions`
helm:
valueFiles:
- $values/manifests/external-secrets/values.yaml
- $values/platform/apps/external-secrets/values.yaml
- repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
ref: values
- repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
path: platform/apps/external-secrets
directory:
exclude: "{application.yaml,values.yaml}"
destination:
server: https://kubernetes.default.svc
namespace: external-secrets
@@ -27,3 +32,4 @@ spec:
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
- SkipDryRunOnMissingResource=true
@@ -2,6 +2,8 @@ apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: vault-backend
annotations:
argocd.argoproj.io/sync-wave: "1"
spec:
provider:
vault:
@@ -3,3 +3,5 @@ kind: ServiceAccount
metadata:
name: external-secrets-vault-auth
namespace: external-secrets
annotations:
argocd.argoproj.io/sync-wave: "1"
-22
View File
@@ -1,22 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: harbor-config
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "2"
spec:
project: default
source:
repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
path: manifests/harbor-config
destination:
server: https://kubernetes.default.svc
namespace: harbor
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- SkipDryRunOnMissingResource=true
@@ -13,10 +13,15 @@ spec:
targetRevision: "1.16.0" # TODO: verify latest via `helm search repo harbor/harbor --versions`
helm:
valueFiles:
- $values/manifests/harbor/values.yaml
- $values/platform/apps/harbor/values.yaml
- repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
ref: values
- repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
path: platform/apps/harbor
directory:
exclude: "{application.yaml,values.yaml}"
destination:
server: https://kubernetes.default.svc
namespace: harbor
@@ -26,3 +31,4 @@ spec:
selfHeal: true
syncOptions:
- CreateNamespace=true
- SkipDryRunOnMissingResource=true
@@ -3,6 +3,8 @@ kind: HTTPRoute
metadata:
name: harbor
namespace: harbor
annotations:
argocd.argoproj.io/sync-wave: "1"
spec:
parentRefs:
- name: envoy-gateway
-22
View File
@@ -1,22 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: headlamp-config
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "1"
spec:
project: default
source:
repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
path: manifests/headlamp-config
destination:
server: https://kubernetes.default.svc
namespace: headlamp
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- SkipDryRunOnMissingResource=true
@@ -13,10 +13,15 @@ spec:
targetRevision: "0.31.0" # TODO: verify latest via `helm search repo headlamp/headlamp --versions`
helm:
valueFiles:
- $values/manifests/headlamp/values.yaml
- $values/platform/apps/headlamp/values.yaml
- repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
ref: values
- repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
path: platform/apps/headlamp
directory:
exclude: "{application.yaml,values.yaml}"
destination:
server: https://kubernetes.default.svc
namespace: headlamp
@@ -26,3 +31,4 @@ spec:
selfHeal: true
syncOptions:
- CreateNamespace=true
- SkipDryRunOnMissingResource=true
@@ -3,11 +3,15 @@ kind: ServiceAccount
metadata:
name: headlamp-admin
namespace: headlamp
annotations:
argocd.argoproj.io/sync-wave: "1"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: headlamp-admin
annotations:
argocd.argoproj.io/sync-wave: "1"
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
-22
View File
@@ -1,22 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: jenkins-config
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "3"
spec:
project: default
source:
repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
path: manifests/jenkins-config
destination:
server: https://kubernetes.default.svc
namespace: jenkins
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- SkipDryRunOnMissingResource=true
@@ -13,10 +13,15 @@ spec:
targetRevision: "5.8.0" # TODO: verify latest via `helm search repo jenkins/jenkins --versions`
helm:
valueFiles:
- $values/manifests/jenkins/values.yaml
- $values/platform/apps/jenkins/values.yaml
- repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
ref: values
- repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
path: platform/apps/jenkins
directory:
exclude: "{application.yaml,values.yaml}"
destination:
server: https://kubernetes.default.svc
namespace: jenkins
@@ -26,3 +31,4 @@ spec:
selfHeal: true
syncOptions:
- CreateNamespace=true
- SkipDryRunOnMissingResource=true
@@ -3,6 +3,8 @@ kind: HTTPRoute
metadata:
name: jenkins
namespace: jenkins
annotations:
argocd.argoproj.io/sync-wave: "1"
spec:
parentRefs:
- name: envoy-gateway
@@ -1,22 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: kube-prometheus-stack-config
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "2"
spec:
project: default
source:
repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
path: manifests/kube-prometheus-stack-config
destination:
server: https://kubernetes.default.svc
namespace: monitoring
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- SkipDryRunOnMissingResource=true
@@ -13,10 +13,15 @@ spec:
targetRevision: "65.0.0" # TODO: verify latest via `helm search repo prometheus-community/kube-prometheus-stack --versions`
helm:
valueFiles:
- $values/manifests/kube-prometheus-stack/values.yaml
- $values/platform/apps/kube-prometheus-stack/values.yaml
- repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
ref: values
- repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
path: platform/apps/kube-prometheus-stack
directory:
exclude: "{application.yaml,values.yaml}"
destination:
server: https://kubernetes.default.svc
namespace: monitoring
@@ -27,3 +32,4 @@ spec:
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
- SkipDryRunOnMissingResource=true
@@ -3,6 +3,8 @@ kind: HTTPRoute
metadata:
name: grafana
namespace: monitoring
annotations:
argocd.argoproj.io/sync-wave: "1"
spec:
parentRefs:
- name: envoy-gateway
-22
View File
@@ -1,22 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: sonarqube-config
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "3"
spec:
project: default
source:
repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
path: manifests/sonarqube-config
destination:
server: https://kubernetes.default.svc
namespace: sonarqube
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- SkipDryRunOnMissingResource=true
@@ -13,10 +13,15 @@ spec:
targetRevision: "10.6.0" # TODO: verify latest via `helm search repo sonarqube/sonarqube --versions`
helm:
valueFiles:
- $values/manifests/sonarqube/values.yaml
- $values/platform/apps/sonarqube/values.yaml
- repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
ref: values
- repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
path: platform/apps/sonarqube
directory:
exclude: "{application.yaml,values.yaml}"
destination:
server: https://kubernetes.default.svc
namespace: sonarqube
@@ -26,3 +31,4 @@ spec:
selfHeal: true
syncOptions:
- CreateNamespace=true
- SkipDryRunOnMissingResource=true
@@ -3,6 +3,8 @@ kind: HTTPRoute
metadata:
name: sonarqube
namespace: sonarqube
annotations:
argocd.argoproj.io/sync-wave: "1"
spec:
parentRefs:
- name: envoy-gateway
@@ -13,10 +13,15 @@ spec:
targetRevision: "0.30.0" # TODO: verify latest via `helm search repo hashicorp/vault --versions`
helm:
valueFiles:
- $values/manifests/vault/values.yaml
- $values/platform/apps/vault/values.yaml
- repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
ref: values
- repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
path: platform/apps/vault
directory:
exclude: "{application.yaml,values.yaml}"
destination:
server: https://kubernetes.default.svc
namespace: vault
@@ -26,3 +31,4 @@ spec:
selfHeal: true
syncOptions:
- CreateNamespace=true
- SkipDryRunOnMissingResource=true
+22
View File
@@ -0,0 +1,22 @@
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: vault
namespace: vault
annotations:
argocd.argoproj.io/sync-wave: "1"
spec:
parentRefs:
- name: envoy-gateway
namespace: envoy-gateway-system
hostnames:
- "vault.fireflylab.local"
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
# verify with: kubectl get svc -n vault (UI served on same port as API when ui.enabled)
- name: vault
port: 8200