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:
- Resource Discovery: It finds all the files listed under
resources(in this case,../../base). - Patching: It applies
patchesStrategicMergeorpatchesJson6902to the resources identified in the previous step.ingress.yamlis merged intodeployment.yamlandservice.yamlbased on common fields likeapiVersion,kind,metadata.name, andmetadata.namespace. - Image Tagging: It finds all containers within the resources and replaces their image tags with the
newTagspecified. - Name Prefixing/Suffixing (if configured): It can add prefixes or suffixes to resource names.
- Common Labels/Annotations (if configured): It can add common labels or annotations to all resources.
- CRDs (if configured): It can handle Custom Resource Definitions.
- 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.