Kustomize layout
signal-forge ships a Kustomize base + per-env overlays alongside the deploy-local.sh driver.
Consumers that prefer GitOps (ArgoCD, Flux, Rancher Fleet) or
kubectl apply -k can use the Kustomize path directly. deploy-local.sh is aware of this and will
kubectl apply -k <dir> whenever a directory contains a kustomization.yaml.
Directory layout
mindmap
root["k8s/"]
n1["base/"]
n1a["kustomization.yaml # aggregates every component"]
n2["overlays/"]
n2a["dev/kustomization.yaml # identity overlay — matches base"]
n2b["staging/kustomization.yaml # replicas=3, staging ingress host"]
n2c["prod/kustomization.yaml # replicas=6, required anti-affinity, prod ingress host"]
n3["infra/"]
n3a["kustomization.yaml # namespace + secrets + pdb + netpol + ingress"]
n3b["*.yaml"]
n4["app/{gateway,order,notification,frontend}/"]
n4a["kustomization.yaml # deployment + service"]
n4b["*.yaml"]
n5["datastores/{mysql,postgres,redis,rabbitmq}/"]
n5a["kustomization.yaml"]
n5b["*.yaml"]
The manifest files themselves did not move during the Kustomize refactor. They stayed in their
original directories (k8s/infra/, k8s/app/*/, k8s/datastores/*/). Each subdirectory gained a
small kustomization.yaml listing its local resources; k8s/base/kustomization.yaml then
references those subdirectories.
This matters because:
- Git history on the manifests is intact (no renames).
deploy-local.shcan continue applying per-stage (manifests.infra,manifests.datastores, etc.) — thekustomization.yamlfiles sit alongside the YAML and are auto-detected.- Kustomize’s cross-directory load-restrictor doesn’t fire (each sub-kustomization only loads files from its own directory).
Rendering
# Full stack, no env overrides:
kustomize build k8s/base
# Dev (identity overlay):
kubectl kustomize k8s/overlays/dev
kubectl apply -k k8s/overlays/dev
# Prod (6 replicas, required anti-affinity, prod hostname):
kubectl apply -k k8s/overlays/prod
ArgoCD Application spec targeting staging:
spec:
source:
repoURL: https://github.com/...
path: d-services/11-signal-forge/k8s/overlays/staging
targetRevision: main
Prerequisite for TLS: the Ingress in this layout references a ClusterIssuer
(cert-manager.io/cluster-issuer: signal-forge-ca) that this Kustomize tree does not create —
see the cert-manager-issuer.yaml gotcha below. Apply
k8s/infra/cert-manager-issuer.yaml
with cert-manager already installed in the target cluster before or alongside your GitOps sync, or
certs never provision and the Ingress silently sits without TLS.
What each overlay changes vs. base
dev
The current k3d lab settings are the base (2 replicas, 150m CPU, soft anti-affinity). The dev
overlay is identity plus a signal-forge.environment: dev label. The kustomization file has a
commented-out example showing how to add a patch without having to re-discover the syntax.
staging
gateway-api,order-api,notification-svc→replicas: 3- Ingress host →
signal-forge.staging.example.com(edit before use) - All pods get
signal-forge.environment: staging
prod
gateway-api,order-api→replicas: 6,requests.cpu: 500m,limits.cpu: 2notification-svc→replicas: 4- Ingress host →
signal-forge.example.com(edit before use) - Pod anti-affinity upgraded from
preferredDuringScheduling...(soft) torequiredDuringScheduling...(hard) — prod refuses to co-schedule replicas of the same app on the same node.
How deploy-local.sh interacts with this
apply_stage (in
deploy-local.sh) checks
each configured path:
if [[ -d "$target" && -f "${target%/}/kustomization.yaml" ]]; then
kubectl apply -k "$target" # kustomize-native apply
else
kubectl apply -f "$target" # plain file / dir-of-yamls apply
fi
So conf.yml’s manifests.datastores: [k8s/datastores/mysql/, ...] triggers
kubectl apply -k k8s/datastores/mysql/ automatically — which respects the sub-kustomization,
applies the labels, and obeys any overlay patches at the pathway level.
Overlays are not used by deploy-local.sh today; it targets the per-component
sub-kustomizations directly. If you want deploy-local to honor an overlay, set
manifests.app: [k8s/overlays/dev] and it will apply -k that overlay instead. You lose per-stage
ordering (overlays/* is a single apply call); acceptable for dev, rebuildable CI pipelines.
Patch syntax
Kustomize supports two patch formats. This repo uses strategic merge patches via inline patch:
blocks (vs. separate patch files) because they’re more readable when short:
patches:
- target: { kind: Deployment, name: gateway-api }
patch: |
- op: replace
path: /spec/replicas
value: 6
The op: replace form is JSON Patch RFC 6902, not strategic merge — it’s explicit and surgical. For
anything more complex than a scalar replacement, switch to a separate file in
overlays/<env>/patches/ and reference it via path: instead of patch:.
Gotchas
cert-manager-issuer.yamlis deliberately not ink8s/infra/kustomization.yaml’s resource list, even though the Ingress it backs is part of this tree.k8s/base/kustomization.yamlsets a blanketnamespace: otel-lab, and Kustomize’s namespace transformer has no ideaClusterIssueris a cluster-scoped CRD kind — it stampsnamespace: otel-labonto it anyway (harmless, ignored by the API server for a cluster-scoped object) but, worse, it also overwrites the CA-bootstrapCertificate’s explicitnamespace: cert-managerwithotel-lab, which silently breaks cert-manager’s CA chain (theClusterIssuer’sca.secretNamereference expects that Secret in cert-manager’s own namespace).deploy-local.shalready applies this file as a separate, ungated-by-Kustomize step (install_cert_manager, gated bysecurity.tls.enabled) — GitOps consumers need to do the same:kubectl apply -f k8s/infra/cert-manager-issuer.yamlonce cert-manager is installed, outside thekubectl apply -ksync.commonLabelsis deprecated. This repo uses the replacementlabels:block withpairs:. Kustomize ≥ 5.0 nags oncommonLabels.--load-restrictor=LoadRestrictionsNoneis not required. The sub-kustomization layout means every file loaded by a kustomization.yaml is in its own directory tree. Runningkustomize buildwithout extra flags should always succeed.- ConfigMap generators. Not used anywhere. The
signal-forge-app-envConfigMap is rendered bydeploy-local.shfrom a template (k8s/infra/app-env.yaml.tmpl) rather than via Kustomize’sconfigMapGenerator, because its values come fromconf.yml(Kustomize can’t read arbitrary YAML). If you want a ConfigMap generator, wire it in the overlay.
Local graph
Linked from 2 notes
Kubernetes Infrastructure
Reference for signal-forge's Kubernetes infrastructure — namespaces, secrets, deployments, RBAC, ingress, health probes, and deploy order.
SignalForge Documentation
Documentation hub for the SignalForge OTel Microservices Validation Lab — architecture, services, API, deployment, observability, and operations.
Related notes
Datastore HA — production migration notes
Migration paths from signal-forge's single-replica lab datastores to production HA via CloudNativePG, MySQL/Percona, RabbitMQ, and Redis operators.
Datastores
Reference for signal-forge's four datastores — MySQL, PostgreSQL, Redis, RabbitMQ — covering schemas, credentials, OTel instrumentation, and deploy order.
Container & Pod Hardening
Reference for signal-forge's Kubernetes Pod Security Standards hardening — per-image UIDs, Dockerfile conventions, and the security controls on every workload.
Kubernetes Infrastructure
Reference for signal-forge's Kubernetes infrastructure — namespaces, secrets, deployments, RBAC, ingress, health probes, and deploy order.