Deploy Todo App with Kustomize
Deploy Todo Application to Kubernetes using Kustomize
Learn environment-specific deployments with Kustomize overlays.
What You’ll Learn
- Kustomize basics - Built into kubectl, no extra tools needed
- Base + Overlays pattern - DRY (Don’t Repeat Yourself) configuration
- Environment variants - Deploy same app to Docker Desktop, EKS with Docker Hub, and EKS with ECR
- Storage differences - How storage varies between local and cloud
- Service types - NodePort vs LoadBalancer
Prerequisites:
- Completed Exercise 2 (MongoDB Todo App)
- Docker images built and pushed (Exercise 2)
- kubectl configured for Docker Desktop and/or EKS cluster
Understanding the Architecture
Three Deployment Targets
| Environment | Kubernetes | Image Registry | Storage | Service Type |
|---|---|---|---|---|
| docker-desktop | Docker Desktop K8s | Docker Hub | hostpath | NodePort |
| eks-dockerhub | AWS EKS | Docker Hub (public) | EBS (gp3) | LoadBalancer |
| eks-ecr | AWS EKS | AWS ECR (private) | EBS (gp3) | LoadBalancer |
Key Differences to Handle
Storage:
- Docker Desktop: Uses built-in
hostpathprovisioner (no StorageClass needed) - EKS: Requires explicit StorageClass with
ebs.csi.eks.amazonaws.comprovisioner
Networking:
- Docker Desktop: NodePort for local access (http://localhost:30080)
- EKS: LoadBalancer creates AWS ELB for internet access
Image Registry:
- Docker Hub: Public, no authentication needed in cluster
- ECR: Private, requires IAM permissions (handled by EKS node role)
Step 1: Create Directory Structure
Create the Kustomize layout
cd /path/to/todo-app
mkdir -p kubernetes/{base,overlays/{docker-desktop,eks-dockerhub,eks-ecr}}Final structure
todo-app/
βββ kubernetes/
β βββ base/
β β βββ kustomization.yaml
β β βββ namespace.yaml
β β βββ mongodb-statefulset.yaml
β β βββ mongodb-service.yaml
β β βββ mongodb-init-job.yaml
β β βββ webapp-configmap.yaml
β β βββ mongo-express-deployment.yaml
β β βββ mongo-express-service.yaml
β β βββ webapp-deployment.yaml
β β βββ webapp-service.yaml
β β
β βββ overlays/
β βββ docker-desktop/
β β βββ kustomization.yaml
β β βββ webapp-service-patch.yaml
β β βββ mongo-express-service-patch.yaml
β β
β βββ eks-dockerhub/
β β βββ kustomization.yaml
β β βββ storageclass.yaml
β β
β βββ eks-ecr/
β βββ kustomization.yaml
β βββ storageclass.yaml
β
βββ webapp/
βββ docker-compose.yaml
βββ ...Step 2: Create Base Manifests
2.1 Namespace
Create kubernetes/base/namespace.yaml:
apiVersion: v1
kind: Namespace
metadata:
name: todo-app2.2 MongoDB StatefulSet and Service
Create kubernetes/base/mongodb-statefulset.yaml:
# Headless service for StatefulSet
apiVersion: v1
kind: Service
metadata:
name: mongodb-service
namespace: todo-app
spec:
selector:
app: mongodb
ports:
- port: 27017
targetPort: 27017
clusterIP: None # Headless service for stable network identity
---
# StatefulSet with persistent storage
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mongodb
namespace: todo-app
spec:
serviceName: mongodb-service
replicas: 1
selector:
matchLabels:
app: mongodb
template:
metadata:
labels:
app: mongodb
spec:
containers:
- name: mongodb
image: mongo:latest
ports:
- containerPort: 27017
volumeMounts:
- name: mongodb-data
mountPath: /data/db
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
volumeClaimTemplates:
- metadata:
name: mongodb-data
spec:
accessModes: ["ReadWriteOnce"]
# StorageClass will be patched per environment
storageClassName: "" # Empty - will be set by overlay
resources:
requests:
storage: 1GiNote: storageClassName: "" will be overridden by overlays.
2.3 MongoDB Initialization Job
Create kubernetes/base/mongodb-init-job.yaml:
apiVersion: batch/v1
kind: Job
metadata:
name: mongodb-init
namespace: todo-app
spec:
template:
spec:
containers:
- name: mongo-init
image: mongo:latest
command:
- /bin/bash
- -c
- |
sleep 10
mongosh mongodb-service:27017/ToDoAppDb --eval '
db.TodoItems.insertMany([
{
"Id": 1,
"Name": "Learn Kubernetes",
"IsComplete": false
},
{
"Id": 2,
"Name": "Deploy MongoDB",
"IsComplete": true
}
]);
print("Database initialized successfully!");
'
restartPolicy: OnFailure
backoffLimit: 42.4 WebApp ConfigMap
Create kubernetes/base/webapp-configmap.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: webapp-config
namespace: todo-app
data:
MONGODB_HOST: "mongodb-service"
MONGODB_PORT: "27017"
MONGODB_DATABASE: "ToDoAppDb"2.5 WebApp Deployment and Service
Create kubernetes/base/webapp-deployment.yaml:
# Service for WebApp
apiVersion: v1
kind: Service
metadata:
name: todo-webapp-service
namespace: todo-app
labels:
app: todo-webapp
spec:
# Service type will be patched per environment
type: LoadBalancer # Default - will be overridden for docker-desktop
selector:
app: todo-webapp
ports:
- port: 80
targetPort: 8080
---
# WebApp Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: todo-webapp
namespace: todo-app
labels:
app: todo-webapp
spec:
replicas: 2
selector:
matchLabels:
app: todo-webapp
template:
metadata:
labels:
app: todo-webapp
spec:
containers:
- name: webapp
# Image will be replaced by Kustomize overlay
image: todo-app:latest # Placeholder - replaced by overlay
imagePullPolicy: Always
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: webapp-config
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"2.6 Mongo Express Deployment and Service
Create kubernetes/base/mongo-express-deployment.yaml:
# Service for Mongo Express
apiVersion: v1
kind: Service
metadata:
name: mongo-express-service
namespace: todo-app
labels:
app: mongo-express
spec:
# Service type will be patched per environment
type: LoadBalancer # Default - will be overridden for docker-desktop
selector:
app: mongo-express
ports:
- port: 80
targetPort: 8081
---
# Mongo Express Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: mongo-express
namespace: todo-app
spec:
replicas: 1
selector:
matchLabels:
app: mongo-express
template:
metadata:
labels:
app: mongo-express
spec:
containers:
- name: mongo-express
image: mongo-express:latest
ports:
- containerPort: 8081
env:
- name: ME_CONFIG_MONGODB_URL
value: "mongodb://mongodb-service:27017"
- name: ME_CONFIG_BASICAUTH_USERNAME
value: "admin"
- name: ME_CONFIG_BASICAUTH_PASSWORD
value: "pass"
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"2.7 Base Kustomization
Create kubernetes/base/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- mongodb-statefulset.yaml
- mongodb-init-job.yaml
- webapp-configmap.yaml
- mongo-express-deployment.yaml
- webapp-deployment.yamlStep 3: Create Overlay for Docker Desktop
3.1 Docker Desktop Kustomization
Create kubernetes/overlays/docker-desktop/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: todo-app
resources:
- ../../base
# Use Docker Hub image
images:
- name: todo-app
newName: larsappel/todo-app
newTag: latest
# Patch services to use NodePort for local access
patches:
- path: webapp-service-patch.yaml
- path: mongo-express-service-patch.yaml
# Patch MongoDB to use default StorageClass (hostpath)
- target:
group: apps
version: v1
kind: StatefulSet
name: mongodb
patch: |-
- op: replace
path: /spec/volumeClaimTemplates/0/spec/storageClassName
value: hostpath 3.2 WebApp Service Patch
Create kubernetes/overlays/docker-desktop/webapp-service-patch.yaml:
apiVersion: v1
kind: Service
metadata:
name: todo-webapp-service
namespace: todo-app
spec:
type: NodePort
ports:
- port: 80
targetPort: 8080
nodePort: 30080 # Access via http://localhost:300803.3 Mongo Express Service Patch
Create kubernetes/overlays/docker-desktop/mongo-express-service-patch.yaml:
apiVersion: v1
kind: Service
metadata:
name: mongo-express-service
namespace: todo-app
spec:
type: NodePort
ports:
- port: 80
targetPort: 8081
nodePort: 30081 # Access via http://localhost:30081Key differences:
- β
Uses
NodePortinstead ofLoadBalancer(no cloud provider) - β Fixed node ports (30080, 30081) for easy access
- β
Uses default
hostpathStorageClass (built into Docker Desktop) - β Docker Hub image (public)
Step 4: Create Overlay for EKS with Docker Hub
4.1 EKS Docker Hub Kustomization
Create kubernetes/overlays/eks-dockerhub/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: todo-app
resources:
- ../../base
- storageclass.yaml
# Use Docker Hub image
images:
- name: todo-app
newName: larsappel/todo-app
newTag: latest
# Patch MongoDB to use EKS StorageClass and add AWS LoadBalancer annotations
patches:
- target:
group: apps
version: v1
kind: StatefulSet
name: mongodb
patch: |-
- op: replace
path: /spec/volumeClaimTemplates/0/spec/storageClassName
value: ebs-sc
- patch: |-
apiVersion: v1
kind: Service
metadata:
name: todo-webapp-service
namespace: todo-app
annotations:
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
spec:
type: LoadBalancer
- patch: |-
apiVersion: v1
kind: Service
metadata:
name: mongo-express-service
namespace: todo-app
annotations:
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
spec:
type: LoadBalancer 4.2 EKS StorageClass
Create kubernetes/overlays/eks-dockerhub/storageclass.yaml:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ebs-sc
provisioner: ebs.csi.eks.amazonaws.com
volumeBindingMode: WaitForFirstConsumer
parameters:
type: gp3
encrypted: "true"Key differences:
- β
Uses
LoadBalancerservice type (creates AWS ELB) - β
Requires
ebs-scStorageClass for persistent volumes - β AWS-specific annotations for internet-facing load balancer
- β Docker Hub image (public, no auth needed)
Step 5: Create Overlay for EKS with ECR
5.1 EKS ECR Kustomization
Create kubernetes/overlays/eks-ecr/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: todo-app
resources:
- ../../base
- storageclass.yaml
# Use ECR private image
images:
- name: todo-app
newName: 880731366811.dkr.ecr.eu-west-1.amazonaws.com/todo-app
newTag: latest
# Patch MongoDB to use EKS StorageClass and add AWS LoadBalancer annotations
patches:
- target:
group: apps
version: v1
kind: StatefulSet
name: mongodb
patch: |-
- op: replace
path: /spec/volumeClaimTemplates/0/spec/storageClassName
value: ebs-sc
- patch: |-
apiVersion: v1
kind: Service
metadata:
name: todo-webapp-service
namespace: todo-app
annotations:
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
spec:
type: LoadBalancer
- patch: |-
apiVersion: v1
kind: Service
metadata:
name: mongo-express-service
namespace: todo-app
annotations:
service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
spec:
type: LoadBalancer 5.2 EKS StorageClass (Same as Docker Hub)
Create kubernetes/overlays/eks-ecr/storageclass.yaml:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ebs-sc
provisioner: ebs.csi.eks.amazonaws.com
volumeBindingMode: WaitForFirstConsumer
parameters:
type: gp3
encrypted: "true"Key differences:
- β Uses ECR private registry instead of Docker Hub
- β No image pull secrets needed (EKS node IAM role handles auth)
- β Same LoadBalancer and StorageClass as eks-dockerhub
- β Better for production (private registry, vulnerability scanning)
Step 6: Deploy to Docker Desktop
6.1 Verify Kustomize Build
Preview what will be deployed:
kubectl kustomize kubernetes/overlays/docker-desktopThis shows the final YAML after Kustomize applies all patches and overlays.
6.2 Deploy
kubectl apply -k kubernetes/overlays/docker-desktopExpected output:
namespace/todo-app created
service/mongodb-service created
statefulset.apps/mongodb created
job.batch/mongodb-init created
configmap/webapp-config created
service/mongo-express-service created
deployment.apps/mongo-express created
service/todo-webapp-service created
deployment.apps/todo-webapp created6.3 Verify Deployment
# Check all resources
kubectl get all -n todo-app
# Check PVC (should use hostpath)
kubectl get pvc -n todo-app
# Wait for pods to be ready
kubectl wait --for=condition=ready pod -l app=mongodb -n todo-app --timeout=120s
kubectl wait --for=condition=ready pod -l app=todo-webapp -n todo-app --timeout=120s6.4 Access the Application
Todo App:
http://localhost:30080Mongo Express:
http://localhost:30081Login: admin / pass
6.5 Test CRUD Operations
# Get all todos
curl http://localhost:30080/api/todos
# Create a new todo
curl -X POST http://localhost:30080/api/todos \
-H "Content-Type: application/json" \
-d '{"id":3,"name":"Test Kustomize","isComplete":false}'
# Verify
curl http://localhost:30080/api/todosStep 7: Deploy to EKS with Docker Hub
7.1 Switch kubectl Context
# List contexts
kubectl config get-contexts
# Switch to EKS cluster
kubectl config use-context <your-eks-context>
# Verify
kubectl cluster-info7.2 Preview and Deploy
# Preview
kubectl kustomize kubernetes/overlays/eks-dockerhub
# Deploy
kubectl apply -k kubernetes/overlays/eks-dockerhub7.3 Monitor Deployment
# Watch pod creation
kubectl get pods -n todo-app -w
# Check StorageClass was created
kubectl get storageclass ebs-sc
# Check PVC (should use ebs-sc)
kubectl get pvc -n todo-app -o wide
# Check services (LoadBalancer provisioning takes ~2 minutes)
kubectl get svc -n todo-app -w7.4 Get Load Balancer URLs
# Get webapp URL
kubectl get svc todo-webapp-service -n todo-app -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'
# Get Mongo Express URL
kubectl get svc mongo-express-service -n todo-app -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'Access:
# Todo App
WEBAPP_URL=$(kubectl get svc todo-webapp-service -n todo-app -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
echo "Todo App: http://${WEBAPP_URL}"
# Mongo Express
MONGO_EXPRESS_URL=$(kubectl get svc mongo-express-service -n todo-app -o jsonpath='{.status.loadBalancer.ingress[0].hostname}')
echo "Mongo Express: http://${MONGO_EXPRESS_URL}"7.5 Verify
# Test API
curl http://${WEBAPP_URL}/api/todos
# Open in browser
open http://${WEBAPP_URL}
open http://${MONGO_EXPRESS_URL}Step 8: Deploy to EKS with ECR
8.1 Ensure ECR Image Exists
Verify the image was pushed in Exercise 2:
aws ecr describe-images \
--repository-name todo-app \
--region eu-west-1 \
--query 'imageDetails[?imageTags[?contains(@, `latest`)]]'8.2 Update Kustomization with Your Account ID
Edit kubernetes/overlays/eks-ecr/kustomization.yaml and replace the account ID:
images:
- name: todo-app
newName: <YOUR-ACCOUNT-ID>.dkr.ecr.<YOUR-REGION>.amazonaws.com/todo-app
newTag: latest8.3 Deploy
# Preview
kubectl kustomize kubernetes/overlays/eks-ecr
# Deploy
kubectl apply -k kubernetes/overlays/eks-ecr8.4 Verify Image Pull
Check that pods successfully pulled from ECR:
# Describe webapp pod to see image source
kubectl describe pod -l app=todo-webapp -n todo-app | grep -A 5 "Image:"
# Should show ECR URL, not Docker Hub8.5 Test Application
Same as Step 7.5 - get LoadBalancer URLs and test.
Step 9: Understanding the Differences
Compare Deployments
Storage
Docker Desktop:
kubectl get pvc -n todo-app -o custom-columns=NAME:.metadata.name,STORAGECLASS:.spec.storageClassNameOutput: hostpath
EKS:
kubectl get pvc -n todo-app -o custom-columns=NAME:.metadata.name,STORAGECLASS:.spec.storageClassNameOutput: ebs-sc
Explanation:
- Docker Desktop uses local storage (
hostpath) - EKS uses AWS EBS volumes (
gp3SSD) - EBS volumes are network-attached, can move between nodes
- Hostpath is tied to specific node
Services
Docker Desktop:
kubectl get svc -n todo-appOutput: NodePort with ports 30080, 30081
EKS:
kubectl get svc -n todo-appOutput: LoadBalancer with external AWS ELB hostnames
Explanation:
- NodePort: Exposes service on each node’s IP at static port
- LoadBalancer: Creates cloud provider load balancer (AWS ELB)
- NodePort requires port forwarding for external access
- LoadBalancer provides public DNS name automatically
Images
Check deployed image:
kubectl get deployment todo-webapp -n todo-app -o jsonpath='{.spec.template.spec.containers[0].image}'Docker Desktop / EKS-DockerHub:
larsappel/todo-app:latestEKS-ECR:
880731366811.dkr.ecr.eu-west-1.amazonaws.com/todo-app:latestExplanation:
- Docker Hub: Public registry, no authentication needed
- ECR: Private registry, uses EKS node IAM role for authentication
- ECR includes vulnerability scanning
- ECR better for production (private, compliance, scanning)
Step 10: Cleanup
Delete from Docker Desktop
kubectl delete -k kubernetes/overlays/docker-desktop
# Verify namespace is gone
kubectl get ns todo-appDelete from EKS
# Delete application
kubectl delete -k kubernetes/overlays/eks-dockerhub
# OR
kubectl delete -k kubernetes/overlays/eks-ecr
# Delete StorageClass (optional - may be used by other apps)
kubectl delete storageclass ebs-sc
# Verify PVCs are deleted (important to avoid orphaned EBS volumes)
kubectl get pvc --all-namespaces | grep todo-appImportant: Deleting the namespace automatically deletes PVCs, which triggers EBS volume deletion. Verify in AWS Console if needed.
Troubleshooting
Pod ImagePullBackOff on EKS with ECR
Symptom:
kubectl get pods -n todo-app
# Shows ImagePullBackOffCheck:
kubectl describe pod <pod-name> -n todo-app | grep -A 10 EventsCommon causes:
- Wrong ECR URL (check account ID and region)
- Image doesn’t exist in ECR
- EKS node role lacks ECR permissions
Fix:
# Verify image exists
aws ecr describe-images --repository-name todo-app --region eu-west-1
# Check EKS node IAM role has AmazonEC2ContainerRegistryReadOnly policyPVC Stuck in Pending on EKS
Symptom:
kubectl get pvc -n todo-app
# Shows PendingCheck:
kubectl describe pvc -n todo-appCommon causes:
- StorageClass doesn’t exist
- EBS CSI driver not installed (should be automatic with EKS Auto)
- No available AZ
Fix:
# Verify StorageClass exists
kubectl get storageclass ebs-sc
# For EKS Auto Mode, StorageClass should work automatically
# For standard EKS, ensure EBS CSI driver is installedLoadBalancer Stuck in Pending
Symptom:
kubectl get svc -n todo-app
# Shows <pending> for EXTERNAL-IPCheck:
kubectl describe svc todo-webapp-service -n todo-appCommon causes:
- AWS Load Balancer Controller not installed (EKS Auto handles this)
- Service account lacking permissions
- Subnet configuration issues
Wait: LoadBalancer provisioning can take 2-5 minutes.
NodePort Not Accessible on Docker Desktop
Symptom: http://localhost:30080 times out
Check:
# Verify service is NodePort
kubectl get svc -n todo-app
# Check pods are running
kubectl get pods -n todo-appFix:
- Ensure Docker Desktop Kubernetes is running
- Verify NodePort is in valid range (30000-32767)
- Try
kubectl port-forwardas alternative:kubectl port-forward -n todo-app svc/todo-webapp-service 8080:80 # Access at http://localhost:8080
Key Takeaways
Kustomize Benefits
β
DRY Principle: Base manifests shared across environments
β
No Templating: Pure YAML, easier to read than Helm
β
Built-in: No additional tools needed (kubectl -k)
β
Patch Flexibility: Strategic merge, JSON 6902, replacements
β
Environment Variants: Easy to manage dev/staging/prod differences
Environment Differences Summary
| Aspect | Docker Desktop | EKS (Docker Hub) | EKS (ECR) |
|---|---|---|---|
| Storage | hostpath | EBS (gp3) | EBS (gp3) |
| Service | NodePort | LoadBalancer | LoadBalancer |
| Image Registry | Docker Hub | Docker Hub | AWS ECR |
| Authentication | None | None | IAM Role |
| Cost | Free | ELB + EBS costs | ELB + EBS costs |
| Public Access | localhost only | Internet-facing | Internet-facing |
When to Use Each
Docker Desktop (docker-desktop):
- Local development and testing
- No cloud costs
- Fast iteration
- No internet access needed
EKS + Docker Hub (eks-dockerhub):
- Public images
- Multi-cloud compatibility
- Easy sharing (public registry)
- No ECR setup needed
EKS + ECR (eks-ecr):
- Production workloads
- Private images
- Vulnerability scanning
- Compliance requirements
- Better security (private registry)
Next Steps
Now that you’ve mastered Kustomize deployments, you can:
- Add more overlays - Create staging, testing environments
- Use Kustomize components - Reusable pieces across overlays
- Integrate with CI/CD - Automate deployments with GitHub Actions
- Learn Helm - More advanced templating and packaging
- Implement GitOps - ArgoCD for automated sync from Git
Congratulations! π
You’ve successfully deployed the same application to three different environments using Kustomize, understanding how to handle:
- Storage differences (local vs cloud)
- Networking differences (NodePort vs LoadBalancer)
- Registry differences (public vs private)
- Environment-specific configurations
You’re now ready for production Kubernetes deployments!
Reference Commands
Preview before applying
kubectl kustomize kubernetes/overlays/<overlay-name>Deploy
kubectl apply -k kubernetes/overlays/<overlay-name>Delete
kubectl delete -k kubernetes/overlays/<overlay-name>Get resources in namespace
kubectl get all -n todo-appView Kustomize build with diffs
kubectl diff -k kubernetes/overlays/<overlay-name>