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) 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-app.yaml ← root "main" Application, applied once by hand (kubectl apply)
platform/apps/ ← ArgoCD watches this path; one Application CRD per service platform/apps/<service>/ ← one self-contained folder per service:
manifests/ ← Helm values + raw manifests referenced by those Applications 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 Each service's `application.yaml` is a single multi-source Application:
readability / staggered rollout): 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 | | Wave | Service | Purpose |
|------|---------|---------| |------|---------|---------|
| 0 | vault | Secrets engine (standalone, manual init/unseal) | | 0 | vault | Secrets engine (standalone, manual init/unseal) (+ HTTPRoute, wave 1 internally) |
| 0 | headlamp | K8s dashboard | | 0 | headlamp | K8s dashboard (+ RBAC for login token, wave 1 internally) |
| 1 | headlamp-config | ClusterRoleBinding for login token | | 1 | external-secrets | Vault → K8s Secret operator (+ ClusterSecretStore, wave 1 internally) |
| 1 | external-secrets | Vault → K8s Secret operator | | 1 | kube-prometheus-stack | Prometheus + Grafana + Alertmanager (+ HTTPRoute, wave 1 internally) |
| 1 | kube-prometheus-stack | Prometheus + Grafana + Alertmanager | | 1 | harbor | Image registry (+ HTTPRoute, wave 1 internally) |
| 1 | harbor | Image registry | | 2 | jenkins | CI (+ HTTPRoute, wave 1 internally) |
| 2 | external-secrets-config | ClusterSecretStore wired to Vault (k8s auth) | | 2 | sonarqube | Code quality, embedded H2 (+ HTTPRoute, wave 1 internally) |
| 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 |
⚠️ 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 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 repos to confirm current versions. Run `helm repo add <name> <url> && helm
search repo <name>/<chart> --versions` before or after first sync and bump 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 ttl=1h
``` ```
Once this is done, `external-secrets-config`'s `ClusterSecretStore` (`vault-backend`) Once this is done, the `ClusterSecretStore` (`vault-backend`, part of the
should show `Valid` — check with `kubectl get clustersecretstore vault-backend -o yaml`. `external-secrets` Application) should show `Valid` — check with
`kubectl get clustersecretstore vault-backend -o yaml`.
Per-service `ExternalSecret` resources (harbor-credentials, gitea-credentials, Per-service `ExternalSecret` resources (harbor-credentials, gitea-credentials,
sonarqube-token, Jenkins creds) aren't created yet — that's a follow-up once 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 is set inside its own database on first boot, so it cannot be swapped via a
`kubectl patch` the way ArgoCD's can. `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 ```bash
kubectl create token headlamp-admin -n headlamp kubectl create token headlamp-admin -n headlamp
@@ -121,10 +129,10 @@ Paste the token into the Headlamp UI login screen.
### Apply HTTPRoutes note ### Apply HTTPRoutes note
Each `*-config` Application creates its own HTTPRoute (unlike Each service creates its own HTTPRoute as part of the same Application
`cluster-bootstrap`'s ArgoCD route, which had to be applied by hand to avoid (unlike `cluster-bootstrap`'s ArgoCD route, which had to be applied by hand to
a chicken-and-egg problem before Envoy existed) — Envoy Gateway is already up avoid a chicken-and-egg problem before Envoy existed) — Envoy Gateway is
by the time this repo syncs, so these are fully GitOps/auto-synced. 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 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` 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 | | Service | Hostname |
|---------|----------| |---------|----------|
| Vault | vault.fireflylab.local |
| Grafana | grafana.fireflylab.local | | Grafana | grafana.fireflylab.local |
| Harbor | harbor.fireflylab.local | | Harbor | harbor.fireflylab.local |
| Jenkins | jenkins.fireflylab.local | | Jenkins | jenkins.fireflylab.local |
| SonarQube | sonarqube.fireflylab.local | | SonarQube | sonarqube.fireflylab.local |
Vault and Headlamp have no HTTPRoute — Vault stays internal-only Headlamp has no HTTPRoute — accessed via `kubectl port-forward` until/unless
(`vault.vault.svc.cluster.local:8200`); Headlamp is accessed via you add a route for it.
`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 repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main targetRevision: main
path: platform/apps path: platform/apps
directory:
recurse: true
include: "*/application.yaml"
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: argocd 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` targetRevision: "0.10.0" # TODO: verify latest via `helm search repo external-secrets/external-secrets --versions`
helm: helm:
valueFiles: valueFiles:
- $values/manifests/external-secrets/values.yaml - $values/platform/apps/external-secrets/values.yaml
- repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git - repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main targetRevision: main
ref: values 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: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: external-secrets namespace: external-secrets
@@ -27,3 +32,4 @@ spec:
syncOptions: syncOptions:
- CreateNamespace=true - CreateNamespace=true
- ServerSideApply=true - ServerSideApply=true
- SkipDryRunOnMissingResource=true
@@ -2,6 +2,8 @@ apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore kind: ClusterSecretStore
metadata: metadata:
name: vault-backend name: vault-backend
annotations:
argocd.argoproj.io/sync-wave: "1"
spec: spec:
provider: provider:
vault: vault:
@@ -3,3 +3,5 @@ kind: ServiceAccount
metadata: metadata:
name: external-secrets-vault-auth name: external-secrets-vault-auth
namespace: external-secrets 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` targetRevision: "1.16.0" # TODO: verify latest via `helm search repo harbor/harbor --versions`
helm: helm:
valueFiles: valueFiles:
- $values/manifests/harbor/values.yaml - $values/platform/apps/harbor/values.yaml
- repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git - repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main targetRevision: main
ref: values ref: values
- repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
path: platform/apps/harbor
directory:
exclude: "{application.yaml,values.yaml}"
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: harbor namespace: harbor
@@ -26,3 +31,4 @@ spec:
selfHeal: true selfHeal: true
syncOptions: syncOptions:
- CreateNamespace=true - CreateNamespace=true
- SkipDryRunOnMissingResource=true
@@ -3,6 +3,8 @@ kind: HTTPRoute
metadata: metadata:
name: harbor name: harbor
namespace: harbor namespace: harbor
annotations:
argocd.argoproj.io/sync-wave: "1"
spec: spec:
parentRefs: parentRefs:
- name: envoy-gateway - 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` targetRevision: "0.31.0" # TODO: verify latest via `helm search repo headlamp/headlamp --versions`
helm: helm:
valueFiles: valueFiles:
- $values/manifests/headlamp/values.yaml - $values/platform/apps/headlamp/values.yaml
- repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git - repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main targetRevision: main
ref: values ref: values
- repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
path: platform/apps/headlamp
directory:
exclude: "{application.yaml,values.yaml}"
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: headlamp namespace: headlamp
@@ -26,3 +31,4 @@ spec:
selfHeal: true selfHeal: true
syncOptions: syncOptions:
- CreateNamespace=true - CreateNamespace=true
- SkipDryRunOnMissingResource=true
@@ -3,11 +3,15 @@ kind: ServiceAccount
metadata: metadata:
name: headlamp-admin name: headlamp-admin
namespace: headlamp namespace: headlamp
annotations:
argocd.argoproj.io/sync-wave: "1"
--- ---
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding kind: ClusterRoleBinding
metadata: metadata:
name: headlamp-admin name: headlamp-admin
annotations:
argocd.argoproj.io/sync-wave: "1"
roleRef: roleRef:
apiGroup: rbac.authorization.k8s.io apiGroup: rbac.authorization.k8s.io
kind: ClusterRole 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` targetRevision: "5.8.0" # TODO: verify latest via `helm search repo jenkins/jenkins --versions`
helm: helm:
valueFiles: valueFiles:
- $values/manifests/jenkins/values.yaml - $values/platform/apps/jenkins/values.yaml
- repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git - repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main targetRevision: main
ref: values ref: values
- repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
path: platform/apps/jenkins
directory:
exclude: "{application.yaml,values.yaml}"
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: jenkins namespace: jenkins
@@ -26,3 +31,4 @@ spec:
selfHeal: true selfHeal: true
syncOptions: syncOptions:
- CreateNamespace=true - CreateNamespace=true
- SkipDryRunOnMissingResource=true
@@ -3,6 +3,8 @@ kind: HTTPRoute
metadata: metadata:
name: jenkins name: jenkins
namespace: jenkins namespace: jenkins
annotations:
argocd.argoproj.io/sync-wave: "1"
spec: spec:
parentRefs: parentRefs:
- name: envoy-gateway - 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` targetRevision: "65.0.0" # TODO: verify latest via `helm search repo prometheus-community/kube-prometheus-stack --versions`
helm: helm:
valueFiles: 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 - repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main targetRevision: main
ref: values 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: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: monitoring namespace: monitoring
@@ -27,3 +32,4 @@ spec:
syncOptions: syncOptions:
- CreateNamespace=true - CreateNamespace=true
- ServerSideApply=true - ServerSideApply=true
- SkipDryRunOnMissingResource=true
@@ -3,6 +3,8 @@ kind: HTTPRoute
metadata: metadata:
name: grafana name: grafana
namespace: monitoring namespace: monitoring
annotations:
argocd.argoproj.io/sync-wave: "1"
spec: spec:
parentRefs: parentRefs:
- name: envoy-gateway - 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` targetRevision: "10.6.0" # TODO: verify latest via `helm search repo sonarqube/sonarqube --versions`
helm: helm:
valueFiles: valueFiles:
- $values/manifests/sonarqube/values.yaml - $values/platform/apps/sonarqube/values.yaml
- repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git - repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main targetRevision: main
ref: values ref: values
- repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
path: platform/apps/sonarqube
directory:
exclude: "{application.yaml,values.yaml}"
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: sonarqube namespace: sonarqube
@@ -26,3 +31,4 @@ spec:
selfHeal: true selfHeal: true
syncOptions: syncOptions:
- CreateNamespace=true - CreateNamespace=true
- SkipDryRunOnMissingResource=true
@@ -3,6 +3,8 @@ kind: HTTPRoute
metadata: metadata:
name: sonarqube name: sonarqube
namespace: sonarqube namespace: sonarqube
annotations:
argocd.argoproj.io/sync-wave: "1"
spec: spec:
parentRefs: parentRefs:
- name: envoy-gateway - name: envoy-gateway
@@ -13,10 +13,15 @@ spec:
targetRevision: "0.30.0" # TODO: verify latest via `helm search repo hashicorp/vault --versions` targetRevision: "0.30.0" # TODO: verify latest via `helm search repo hashicorp/vault --versions`
helm: helm:
valueFiles: valueFiles:
- $values/manifests/vault/values.yaml - $values/platform/apps/vault/values.yaml
- repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git - repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main targetRevision: main
ref: values ref: values
- repoURL: https://gitea.fireflylab.cc/duynguyen/cluster-platform.git
targetRevision: main
path: platform/apps/vault
directory:
exclude: "{application.yaml,values.yaml}"
destination: destination:
server: https://kubernetes.default.svc server: https://kubernetes.default.svc
namespace: vault namespace: vault
@@ -26,3 +31,4 @@ spec:
selfHeal: true selfHeal: true
syncOptions: syncOptions:
- CreateNamespace=true - 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