Flux’s postbuild substitutions let you inject dynamic values into your Kubernetes manifests right before they’re applied, and the most surprising thing is that they don’t require a separate templating engine like Helm or Kustomize.

Let’s see it in action. Imagine you have a Deployment manifest that needs a specific image tag that changes with each build.

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-app

        image: ghcr.io/my-org/my-app:{{ .TAG }} # <-- Variable here!

        ports:
        - containerPort: 8080

And you have a Kustomization resource in Flux that points to this manifest.

# kustomization.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
  name: my-app-deployment
  namespace: flux-system
spec:
  interval: 1m
  path: ./manifests # Directory containing deployment.yaml
  prune: true
  sourceRef:
    kind: GitRepository
    name: my-flux-repo
  validation: client
  postBuild:
    substitutions:
      TAG: "v1.2.3" # <-- The value we want to inject

When Flux reconciles this Kustomization, it will read deployment.yaml, find the {{ .TAG }} placeholder, and replace it with "v1.2.3" before applying the manifest to Kubernetes. The resulting manifest applied to the cluster will look like this:

# Applied manifest (after substitution)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-app
        image: ghcr.io/my-org/my-app:v1.2.3 # <-- Substituted value
        ports:
        - containerPort: 8080

This capability is built directly into the Flux controller, meaning you don’t need to run kustomize build or helm template as a separate step in your CI/CD pipeline. Flux handles it during its reconciliation loop. You define your base manifests and then use the postBuild.substitutions field in your Kustomization to provide the dynamic values.

The magic behind this is Flux’s internal Go templating engine, which is used for substitutions. The syntax {{ .VARIABLE_NAME }} is standard Go template syntax. The values provided in postBuild.substitutions are key-value pairs. The keys become the variable names (prefixed with . when used in templates), and the values are the strings that will replace the placeholders.

This is incredibly powerful for managing environment-specific configurations, image tags, or any other dynamic parameter without cluttering your core YAML with complex templating logic. You can even use multiple substitutions within a single manifest.

For example, to set both an image tag and a replica count:

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:

  replicas: {{ .REPLICAS }}

  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-app

        image: ghcr.io/my-org/my-app:{{ .TAG }}

        ports:
        - containerPort: 8080
# kustomization.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
  name: my-app-deployment
  namespace: flux-system
spec:
  interval: 1m
  path: ./manifests
  prune: true
  sourceRef:
    kind: GitRepository
    name: my-flux-repo
  validation: client
  postBuild:
    substitutions:
      TAG: "v1.2.4"
      REPLICAS: "3"

The postBuild.substitutions field can also accept a list of substitutions, allowing you to define multiple key-value pairs. This is especially useful when you have many variables to inject.

One subtle but powerful aspect of postbuild substitutions is their interaction with Kustomize. If your path points to a directory containing a kustomization.yaml file, Flux will first process Kustomize overlays and then apply its own postbuild substitutions. This means you can use Kustomize for structural changes and Flux substitutions for dynamic values, creating a flexible layering of configuration. The substitutions happen after Kustomize has done its work, ensuring your injected values are applied to the final generated manifests.

You can also use environment variables from the Flux controller pod to dynamically set these substitution values. This is done by referencing environment variables within your Kustomization resource. For instance, if you have an environment variable IMAGE_TAG set in the Flux controller’s deployment, you can reference it like this:

# kustomization.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
  name: my-app-deployment
  namespace: flux-system
spec:
  interval: 1m
  path: ./manifests
  prune: true
  sourceRef:
    kind: GitRepository
    name: my-flux-repo
  validation: client
  postBuild:
    substitutionsFrom:
      - kind: ConfigMap
        name: my-app-config
        vars:
          - name: TAG
            objRef:
              kind: ConfigMap
              name: flux-env-vars # Assuming a ConfigMap named flux-env-vars
              key: APP_IMAGE_TAG

This allows you to manage sensitive or frequently changing values outside your Git repository, perhaps through CI/CD pipeline variables that update a ConfigMap.

The next step in managing dynamic configurations within Flux often involves exploring Flux’s built-in capabilities for secrets management, such as integrating with external secret stores or using Flux’s own sealed secrets.

Want structured learning?

Take the full Flux course →