Configuration without duplication: variable substitution syntax, postBuild with substitute and substituteFrom, per-environment and multi-cluster configuration strategies, and SOPS and secret operator integration.

In episode 10 you learned to control the deploy order. One problem remains: how do we keep a single set of manifests valid for many clusters and environments, without rewriting YAML? The answer is variable substitution.
This episode covers Flux variable syntax, postBuild with substitute and substituteFrom, configuration strategies, secret management integration, and its best practices.
Flux provides variables that can be used inside rendered manifests. The syntax: a dollar sign, then the variable name between curly braces. Variable values are defined through the postBuild block on the Kustomization. Example usage in a manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
namespace: webapp
spec:
replicas: ${replica_count}
template:
spec:
containers:
- name: webapp
image: registry.internal.acme.com/webapp:${image_tag}
env:
- name: CLUSTER
value: ${cluster_name}The variable value sources:
| Source | Where it's defined | How it's used |
|---|---|---|
| Inline | postBuild.substitute | Directly on the Kustomization |
| ConfigMap | postBuild.substituteFrom | Public values from a ConfigMap |
| Secret | postBuild.substituteFrom | Secret values from a Secret |
There are also built-in variables available automatically: CLUSTER_NAME contains the cluster name from the clusters/ directory used at bootstrap, plus other values you set during flux bootstrap --cluster-domain or in the substitute block.
Tip
Because values are replaced before they're applied, variables are most useful for values that change between environments: replica counts, image tags, namespace names, domains, and non-secret credentials. Don't use them to change the YAML structure.
Variable replacement is enabled through the postBuild block on the Kustomization:
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: webapp
namespace: flux-system
spec:
interval: 10m
path: ./apps/webapp/overlays/prod
prune: true
sourceRef:
kind: GitRepository
name: fleet
postBuild:
substitute:
replica_count: "4"
image_tag: "1.2.0"
cluster_name: "prod-eks-a"
substituteFrom:
- kind: ConfigMap
name: platform-vars
- kind: Secret
name: app-secretsHow postBuild works:
substitute are replaced first.substituteFrom are replaced following the order of the list.Warning
The Secret and ConfigMap for substituteFrom must be in the same namespace as the Kustomization (default flux-system). If a variable is defined in two sources, the one appearing last in the order wins.
While writing files, you can test the substitution result without a cluster using kubectl kustomize:
kubectl kustomize ./apps/webapp/overlays/prodAnd to check the final result after Flux applies it:
kubectl get deployment webapp -n webapp -o yaml | grep replicasNote
Substitution happens on the Flux side, not in the files. Files in the repo keep their placeholders, so always open the kubectl kustomize output to make sure the placeholders are correct before committing.
Variables give freedom, but they need discipline. Four common strategies:
One overlays per environment (dev, staging, prod), each with its own substitute. This continues the overlays pattern from episode 7 — variables add flexibility without adding files.
Use the same Kustomization for all clusters, differentiate values through a per-cluster platform-vars ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: platform-vars
namespace: flux-system
data:
region: ap-southeast-1
domain: cluster-a.internal.acme.comDifferent clusters simply choose different values in their own substitute or ConfigMap.
For multi-tenancy, per-tenant variables are stored in the tenant namespace, and the tenant Kustomization uses substituteFrom with the ConfigMap in its namespace. Tenancy details are covered in episode 12.
Variables can also serve as flags: a true or false value to enable a feature. Set the default value in base, then override it in specific overlays.
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
namespace: webapp
spec:
template:
spec:
containers:
- name: webapp
env:
- name: NEW_PAYMENT
value: ${enable_new_payment}Variables aren't for real secrets. For that, Flux works together with the secret management ecosystem:
| Tool | How it works with Flux |
|---|---|
| SOPS (Mozilla) | Encrypts values inside manifest files; Flux decrypts them during apply using age or KMS |
| Sealed Secrets | Secrets are encrypted into SealedSecrets in Git; the Bitnami controller decrypts them in the cluster |
| External Secrets Operator | Pulls Secrets from AWS, GCP, Vault, or HashiCorp, then syncs them to the cluster |
| Vault | ESO or a sidecar injects secret values when the pod starts |
Example of a Flux Kustomization with SOPS decryption:
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: webapp
namespace: flux-system
spec:
interval: 10m
path: ./apps/webapp/overlays/prod
prune: true
sourceRef:
kind: GitRepository
name: fleet
decryption:
provider: sops
secretRef:
name: sops-ageImportant
The core principle: secrets should never exist in plaintext in Git. SOPS encrypts files, Sealed Secrets encrypts Secrets, ESO moves values directly from the source. Pick one pattern and apply it consistently.
snake_case like image_tag, replica_count, ingress_domain; avoid generic names like value1.README.md or as comments in kustomization.yaml, so it's easy to track.flux get kustomizations
kubectl get deployment webapp -n webapp -o jsonpath='{.spec.replicas}'Tip
A pattern mature teams often use: the kustomization.yaml in clusters/<cluster>/ holds cluster-specific substitute, while substituteFrom stores shared values. The combination of both balances consistency and flexibility.
Configuration is now flexible without becoming messy:
postBuild with substitute and substituteFrom manages value sources declaratively.You now have a complete foundation for single-cluster GitOps. In episode 12 we'll implement multi-tenancy with Flux: the tenancy model, flux create tenant, namespace isolation, and per-tenant service accounts. See you!