Image above from: https://kustomize.io/
When you have to deploy an application to multiple environments like dev, test and production there are many solutions available to you. You can manually deploy the app (Nooooooo! 😉), use a CI/CD system like Azure DevOps and its release pipelines (with or without Helm) or maybe even a “GitOps” approach where deployments are driven by a tool such as Flux or Argo based on a git repository.
In the latter case, you probably want to use a configuration management tool like Kustomize for environment management. Instead of explaining what it does, let’s take a look at an example. Suppose I have an app that can be deployed with the following yaml files:
- redis-deployment.yaml: simple deployment of Redis
- redis-service.yaml: service to connect to Redis on port 6379 (Cluster IP)
- realtime-deployment.yaml: application that uses the socket.io library to display real-time updates coming from a Redis channel
- realtime-service.yaml: service to connect to the socket.io application on port 80 (Cluster IP)
- realtime-ingress.yaml: ingress resource that defines the hostname and TLS certificate for the socket.io application (works with nginx ingress controller)
Let’s call this collection of files the base and put them all in a folder:

Now I would like to modify these files just a bit, to install them in a dev namespace called realtime-dev. In the ingress definition I want to change the name of the host to realdev.baeke.info instead of real.baeke.info for production. We can use Kustomize to reach that goal.
In the base folder, we can add a kustomization.yaml file like so:
apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - realtime-ingress.yaml - realtime-service.yaml - redis-deployment.yaml - redis-service.yaml - realtime-deployment.yaml
This lists all the resources we would like to deploy.
Now we can create a folder for our patches. The patches define the changes to the base. Create a folder called dev (next to base). We will add the following files (one file blurred because it’s not relevant to this post):

kustomization.yaml contains the following:
apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: realtime-dev resources: - ./namespace.yaml bases: - ../base patchesStrategicMerge: - realtime-ingress.yaml
The namespace: realtime-dev ensures that our base resource definitions are updated with that namespace. In resources, we ensure that namespace gets created. The file namespace.yaml contains the following:
apiVersion: v1 kind: Namespace metadata: name: realtime-dev
With patchesStrategicMerge we specify the file(s) that contain(s) our patches, in this case just realtime-ingress.yaml to modify the hostname:
apiVersion: extensions/v1beta1 kind: Ingress metadata: annotations: cert-manager.io/cluster-issuer: letsencrypt-prod kubernetes.io/ingress.class: nginx name: realtime-ingress spec: rules: - host: realdev.baeke.info http: paths: - backend: serviceName: realtime servicePort: 80 path: / tls: - hosts: - realdev.baeke.info secretName: real-dev-baeke-info-tls
Note that we also use certmanager here to issue a certificate to use on the ingress. For dev environments, it is better to use the Let’s Encrypt staging issuer instead of the production issuer.
We are now ready to generate the manifests for the dev environment. From the parent folder of base and dev, run the following command:
kubectl kustomize dev
The above command generates the patched manifests like so:
apiVersion: v1
kind: Namespace
metadata:
name: realtime-dev
---
apiVersion: v1
kind: Service
metadata:
labels:
app: realtime
name: realtime
namespace: realtime-dev
spec:
ports:
- port: 80
targetPort: 8080
selector:
app: realtime
---
apiVersion: v1
kind: Service
metadata:
labels:
app: redis
name: redis
namespace: realtime-dev
spec:
ports:
- port: 6379
targetPort: 6379
selector:
app: redis
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: realtime
name: realtime
namespace: realtime-dev
spec:
replicas: 1
selector:
matchLabels:
app: realtime
template:
metadata:
labels:
app: realtime
spec:
containers:
- env:
- name: REDISHOST
value: redis:6379
image: gbaeke/fluxapp:1.0.5
name: realtime
ports:
- containerPort: 8080
resources:
limits:
cpu: 150m
memory: 150Mi
requests:
cpu: 25m
memory: 50Mi
---
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: redis
name: redis
namespace: realtime-dev
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- image: redis:4-32bit
name: redis
ports:
- containerPort: 6379
resources:
requests:
cpu: 200m
memory: 100Mi
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
kubernetes.io/ingress.class: nginx
name: realtime-ingress
namespace: realtime-dev
spec:
rules:
- host: realdev.baeke.info
http:
paths:
- backend:
serviceName: realtime
servicePort: 80
path: /
tls:
- hosts:
- realdev.baeke.info
secretName: real-dev-baeke-info-tls
Note that namespace realtime-dev is used everywhere and that the Ingress resource uses realdev.baeke.info. The original Ingress resource looked like below:
apiVersion: extensions/v1beta1 kind: Ingress metadata: name: realtime-ingress annotations: kubernetes.io/ingress.class: nginx cert-manager.io/cluster-issuer: "letsencrypt-prod" spec: tls: - hosts: - real.baeke.info secretName: real-baeke-info-tls rules: - host: real.baeke.info http: paths: - path: / backend: serviceName: realtime servicePort: 80
As you can see, Kustomize has updated the host in tls: and rules: and also modified the secret name (which will be created by certmanager).
You have probably seen that Kustomize is integrated with kubectl. It’s also available as a standalone executable.
To directly apply the patched manifests to your cluster, run kubectl apply -k dev. The result:
namespace/realtime-dev created service/realtime created service/redis created deployment.apps/realtime created deployment.apps/redis created ingress.extensions/realtime-ingress created
In another post, we will look at using Kustomize with Flux. Stay tuned!