Kubernetes manifests can get messy fast, especially when managing multiple environments. Kustomize helps you keep things clean by letting you customize YAML files without copying or rewriting them. In this article, we’ll explore how Kustomize simplifies and streamlines Kubernetes configuration.

What does Kustomize do?

let's say you already have a template for deployment, or you can generate sample deployment yaml from this command

1
kubectl create deployment <your deployment name> --image dummy --dry-run=client -o yaml

this will generate a deployment yaml, then you can delete the following lines

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null # this
  labels:
    app: my-app
  name: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  strategy: {} # this
  template:
    metadata:
      creationTimestamp: null # this
      labels:
        app: my-app
    spec:
      containers:
      - image: dummy
        name: my-container
        resources: {} # this
status: {} # this 

Change image

Create kustomization.yaml file

1
2
3
4
5
6
resources:
- deployment.yaml
images:
- name: dummy # this must be match the image in the deployment.yaml
  newName: my-registry/my-image
  newTag: my-tag 

The resources section lists the files or manifests to customize, and the images section updates the deployment image.

1
kubectl kustomize .

The deployment manifest will be updated, and the image name and tag will change according to the kustomization file.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: my-app
  name: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - image: my-registry/my-image:my-tag
        name: my-container

Generators

Kustomize also supports generators, such as secretGenerator and configMapGenerator, which creates resources from the input file(s).

nginx.conf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
events {}

http {
    server {
        listen 80;
        location / {
            return 200 "Hello from ConfigMap Nginx!\n";
        }
    }
}

kustomization.yaml

1
2
3
4
configMapGenerator:
  - name: nginx-config
    files:
      - nginx.conf
1
kubectl kustomize .

This will generate a ConfigMap using the data from nginx.conf.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
apiVersion: v1
data:
  nginx.conf: |
    events {}

    http {
        server {
            listen 80;
            location / {
                return 200 "Hello from ConfigMap Nginx!\n";
            }
        }
    }
kind: ConfigMap
metadata:
  name: nginx-config-59k264tbg4

it's also work for secretGenerator

1
echo "admin:admin123" > credential

kustomization.yaml

1
2
3
4
secretGenerator:
  - name: admin-secret
    files:
      - credential
1
kubectl kustomize .

this will generate Opaque type secret and base64 encoded

1
2
3
4
5
6
7
apiVersion: v1
data:
  credential: YWRtaW46YWRtaW4xMjMK
kind: Secret
metadata:
  name: admin-secret-kh798fmhhc
type: Opaque

Patches

Patches in Kustomize are used to modify or override specific fields in existing resources without changing the original files. For example, we can use a patch to add a ConfigMap volume and mount it into the base Deployment without modifying the original file.

cm-patch.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  template:
    spec:
      volumes:
        - name: nginx-config
          configMap:
             name: nginx-config # must be match with the name from configMapGenerator
      containers:
        - name: my-container
          volumeMounts:
            - name: nginx-config
              mountPath: /etc/nginx/nginx.conf
              subPath: nginx.conf

kustomization.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
resources:
  - deployment.yaml
images:
- name: dummy
  newName: my-registry/my-image
  newTag: my-tag
configMapGenerator:
  - name: nginx-config
    files:
      - nginx.conf
patches:
  - path: cm-patch.yaml
1
kubectl kustomize .

This will generate a Deployment manifest with the ConfigMap volume and mount already included.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
apiVersion: v1
data:
  nginx.conf: |
    events {}

    http {
        server {
            listen 80;
            location / {
                return 200 "Hello from ConfigMap Dev Nginx!\n";
            }
        }
    }    
kind: ConfigMap
metadata:
  name: nginx-config-59k264tbg4
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: my-app
  name: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - image: my-registry/my-image:my-tag
        name: my-container
        volumeMounts:
        - mountPath: /etc/nginx/nginx.conf
          name: nginx-config
          subPath: nginx.conf
      volumes:
      - configMap:
          name: nginx-config-59k264tbg4
        name: nginx-config

Bases & Overlays

Kustomize separates config into bases and overlays. Bases hold reusable resources, while overlays build on them with extra changes. Bases can be local or remote and don’t depend on overlays. Based on what we have done before, now we will use bases and overlays to make it more structured.

example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
.
├── base
│   ├── deployment.yaml
│   ├── kustomization.yaml
│   └── service.yaml
└── overlays
    └── dev
        ├── kustomization.yaml
        ├── nginx.conf
        └── patches
            └── cm-patch.yaml

The base holds the original template manifests, and the overlays contains the changes or patches applied to them.

base/deployment.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: my-app
  name: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - image: dummy
        name: my-container

base/service.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
apiVersion: v1
kind: Service
metadata:
  name: my-app
spec:
  type: ClusterIP
  ports:
    - port: 80
      protocol: TCP
      targetPort: 80
  selector:
    app: my-app

base/kustomization.yaml

1
2
3
resources:
  - deployment.yaml
  - service.yaml

overlays/dev/nginx.conf

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
events {}

http {
    server {
        listen 80;
        location / {
            return 200 "Hello from ConfigMap Nginx!\n";
        }
    }
}

overlays/dev/kustomization.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
namePrefix: dev-
resources:
  - ../../base # reffering to base kustomization
images:
- name: dummy
  newName: nginx
  newTag: alpine

configMapGenerator:
  - name: nginx-config
    files:
      - nginx.conf
patches:
  - path: patches/cm-patch.yaml
  - path: patches/nodeport.yaml

overlays/dev/patches/cm-patch.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  template:
    spec:
      volumes:
        - name: nginx-config
          configMap:
             name: nginx-config
      containers:
        - name: my-container
          volumeMounts:
            - name: nginx-config
              mountPath: /etc/nginx/nginx.conf
              subPath: nginx.conf

overlays/dev/patches/nodeport.yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
  name: my-app
spec:
  selector:
    app: my-app
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
    nodePort: 31232
  type: NodePort
1
kubectl kustomize .
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
apiVersion: v1
data:
  nginx.conf: |
    events {}

    http {
        server {
            listen 80;
            location / {
                return 200 "Hello from ConfigMap Nginx!\n";
            }
        }
    }    
kind: ConfigMap
metadata:
  name: dev-nginx-config-mb9bhc6c66
---
apiVersion: v1
kind: Service
metadata:
  name: dev-my-app
spec:
  ports:
  - nodePort: 32323
    port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: my-app
  type: NodePort
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: my-app
  name: dev-my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - image: nginx:alpine
        name: my-container
        volumeMounts:
        - mountPath: /etc/nginx/nginx.conf
          name: nginx-config
          subPath: nginx.conf
      volumes:
      - configMap:
          name: dev-nginx-config-mb9bhc6c66
        name: nginx-config

You can also apply directly with the following command

1
2
kubectl apply -k ./ # apply
kubectl get -k ./ # get component
1
2
3
4
5
6
7
8
NAME                                    DATA   AGE
configmap/dev-nginx-config-mb9bhc6c66   1      6s

NAME                 TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
service/dev-my-app   NodePort   10.43.194.245   <none>        80:32323/TCP   6s

NAME                         READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/dev-my-app   1/1     1            1           6s

Summary

Kustomize is a simple yet powerful way to tweak your Kubernetes YAMLs without touching the original files. With features like overlays, image overrides, and auto-generated ConfigMaps or Secrets, it makes managing different environments a breeze. It’s a favorite in CI/CD pipelines because it keeps things clean, organized, and easy to maintain even as your app grows.