Kustomize, while powerful for managing Kubernetes configurations, can sometimes feel like a black box when it comes to generating manifests from your local directory. The core problem is that Kustomize doesn’t just copy files; it transforms them based on a kustomization.yaml file. This transformation process is where the magic, and sometimes the confusion, happens.

Let’s say you have a directory structure like this:

my-app/
├── base/
│   ├── deployment.yaml
│   └── service.yaml
└── overlays/
    └── production/
        ├── kustomization.yaml
        └── ingress.yaml

And your my-app/overlays/production/kustomization.yaml looks like this:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - ../../base

patchesStrategicMerge:
  - ingress.yaml

images:
  - name: my-docker-repo/my-app
    newTag: v1.2.3

When you run kustomize build my-app/overlays/production, you’re not just getting the files from base/ and ingress.yaml dumped into a single output. Kustomize performs several steps:

  1. Resource Discovery: It finds all the files listed under resources (in this case, ../../base).
  2. Patching: It applies patchesStrategicMerge or patchesJson6902 to the resources identified in the previous step. ingress.yaml is merged into deployment.yaml and service.yaml based on common fields like apiVersion, kind, metadata.name, and metadata.namespace.
  3. Image Tagging: It finds all containers within the resources and replaces their image tags with the newTag specified.
  4. Name Prefixing/Suffixing (if configured): It can add prefixes or suffixes to resource names.
  5. Common Labels/Annotations (if configured): It can add common labels or annotations to all resources.
  6. CRDs (if configured): It can handle Custom Resource Definitions.
  7. Output: Finally, it serializes all these modified resources into a single YAML stream.

The most surprising thing about Kustomize’s build process is that it doesn’t actually create any files on disk unless you explicitly tell it to. The kustomize build command, by default, just prints the resulting YAML to standard output. It’s a pipeline, not a file copier.

Let’s see it in action. Imagine my-app/base/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-app
        image: my-docker-repo/my-app:latest
        ports:
        - containerPort: 8080

And my-app/base/service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: my-app-service
spec:
  selector:
    app: my-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
  type: ClusterIP

And my-app/overlays/production/ingress.yaml (which will be patched onto the deployment):

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - host: myapp.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: my-app-service
            port:
              number: 80

Now, run the build command from the root of your project (my-app/):

kustomize build overlays/production

The output will be a single YAML document containing the merged and modified resources:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-deployment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - image: my-docker-repo/my-app:v1.2.3 # <-- Tag changed!
        name: my-app
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: my-app-service
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 8080
  selector:
    app: my-app
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
  name: my-app-ingress # <-- Patched in!
spec:
  rules:
  - host: myapp.example.com
    http:
      paths:
      - backend:
          service:
            name: my-app-service
            port:
              number: 80
        path: /
        pathType: Prefix

Notice how the image tag in the Deployment is now v1.2.3 and the Ingress resource has been added. The ingress.yaml was merged into the Deployment object because Kustomize’s patchesStrategicMerge uses strategic merge patch by default. It looks for matching apiVersion, kind, metadata.name, and metadata.namespace to determine where to apply the patch. Since ingress.yaml doesn’t have apiVersion, kind, metadata.name, or metadata.namespace that match the Deployment or Service, Kustomize treats it as a top-level resource to be added to the output. This is a common point of confusion: patches that don’t have matching identifying fields are simply appended as new resources.

The mental model you need is that kustomization.yaml is a recipe. kustomize build is the chef executing that recipe. It takes raw ingredients (resources), modifies them (patches, images), and presents the final dish (YAML output). You can pipe this output directly to kubectl apply -f - or save it to a file.

A crucial, often overlooked, aspect of Kustomize’s patching mechanism is how it handles patches that don’t have a name and namespace matching existing resources. If you have a patch file that specifies a name and namespace that doesn’t exist in your base, Kustomize will not error out by default. Instead, it will simply append that patch as a new, separate resource to the output. This behavior can be surprising if you expect it to apply the patch to a specific existing resource and then discard the patch itself. For example, if you had an Ingress resource defined in ingress.yaml and it didn’t have a name that matched anything in your base directory, it would be added as a new Ingress object to the final output, rather than being merged into an existing object.

To understand the exact transformations applied, you can use kustomize build --output <directory> to dump the intermediate and final outputs into a directory, allowing you to inspect each step.

Want structured learning?

Take the full Kustomize course →