Kubernetes Dev & Ops in Practice 6 — Multi-Environment Deployment Strategy (Kustomize + ArgoCD)
Separate dev/staging/prod configuration with Kustomize overlays and manage the entire cluster as GitOps using ArgoCD's App of Apps pattern. The final installment in the Kubernetes Dev & Ops series.
The Core Problem with Multi-Environment Management
You operate the same application across dev, staging, and prod. Each environment differs in:
- Image tag (dev:
latest, prod:1.2.3) - Replicas (dev: 1, prod: 3)
- Domain (
dev.example.comvsexample.com) - Resource requests/limits
- External connection strings (DB URL, Redis URL)
Managing this by copying files leads to drift — a fix in one environment gets missed in another. Kustomize solves this with a shared base + per-environment overlay structure.
1. Kustomize Directory Structure
k8s/
├── base/
│ ├── kustomization.yaml
│ ├── deployment.yaml
│ ├── service.yaml
│ └── configmap.yaml
└── overlays/
├── dev/
│ ├── kustomization.yaml
│ └── patch-deployment.yaml
├── staging/
│ ├── kustomization.yaml
│ └── patch-deployment.yaml
└── prod/
├── kustomization.yaml
├── patch-deployment.yaml
└── patch-hpa.yaml
2. Base Configuration
base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
- configmap.yaml
commonLabels:
app: my-app
managed-by: kustomize
base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 1
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: my-app
image: my-registry/my-app:latest
ports:
- containerPort: 8080
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
envFrom:
- configMapRef:
name: my-app-config
3. Overlay Configuration
overlays/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: development
bases:
- ../../base
patches:
- path: patch-deployment.yaml
images:
- name: my-registry/my-app
newTag: dev-latest
configMapGenerator:
- name: my-app-config
behavior: merge
literals:
- LOG_LEVEL=debug
- DATABASE_URL=postgres://dev-db:5432/myapp
overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: production
bases:
- ../../base
patches:
- path: patch-deployment.yaml
- path: patch-hpa.yaml
images:
- name: my-registry/my-app
newTag: "1.2.3"
configMapGenerator:
- name: my-app-config
behavior: merge
literals:
- LOG_LEVEL=info
- DATABASE_URL=postgres://prod-db:5432/myapp
overlays/prod/patch-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3
template:
spec:
containers:
- name: my-app
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: "2"
memory: 2Gi
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
Preview and Deploy
# Preview rendered output (no deployment)
kubectl kustomize overlays/prod
# Deploy
kubectl apply -k overlays/prod
# Deploy dev
kubectl apply -k overlays/dev
4. ArgoCD App of Apps Pattern
Manage all apps in the entire cluster through a single root ArgoCD Application.
Git Repository Structure
gitops-repo/
├── apps/
│ ├── kustomization.yaml
│ ├── my-app.yaml
│ ├── api-gateway.yaml
│ └── monitoring.yaml
└── manifests/
├── my-app/overlays/prod/
├── api-gateway/overlays/prod/
└── monitoring/overlays/prod/
Root Application (App of Apps)
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root-app
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/gitops-repo
targetRevision: main
path: apps
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
Individual Application Definition
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: production
source:
repoURL: https://github.com/myorg/gitops-repo
targetRevision: main
path: manifests/my-app/overlays/prod
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true # Delete resources removed from Git
selfHeal: true # Revert manual changes automatically
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas # Ignore replicas managed by HPA
5. CI/CD Pipeline Integration
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build and push image
run: |
IMAGE_TAG=${GITHUB_SHA::8}
docker build -t $REGISTRY/my-app:$IMAGE_TAG .
docker push $REGISTRY/my-app:$IMAGE_TAG
echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
- name: Update image tag in GitOps repo
run: |
git clone https://github.com/myorg/gitops-repo
cd gitops-repo/manifests/my-app/overlays/prod
kustomize edit set image my-registry/my-app:$IMAGE_TAG
git config user.email "ci@github.com"
git config user.name "GitHub Actions"
git add .
git commit -m "chore: update my-app image to $IMAGE_TAG"
git push
ArgoCD detects the Git change and automatically reconciles the cluster.
6. Environment Promotion Strategy
[Developer PR]
↓ merge to main
[CI: build image + update dev tag]
↓ ArgoCD auto sync
[dev deployment]
↓ tests pass → manual promotion
[staging tag update PR]
↓ merge
[staging deployment]
↓ QA approval
[prod tag update PR + review]
↓ merge
[prod deployment]
# Promotion script: staging → prod
CURRENT_TAG=$(kustomize cfg grep --path manifests/my-app/overlays/staging | grep newTag | awk '{print $2}')
cd manifests/my-app/overlays/prod
kustomize edit set image my-registry/my-app:$CURRENT_TAG
git add . && git commit -m "chore: promote my-app $CURRENT_TAG to prod"
git push origin -u promote/my-app-$CURRENT_TAG
7. Drift Detection
# Check sync status via ArgoCD CLI
argocd app list
argocd app diff my-app
# Compare cluster vs Git without applying
argocd app sync my-app --dry-run
# Force reconcile
argocd app sync my-app --force
Series Wrap-Up — The Full Architecture
Here’s the complete Kubernetes dev and ops system built across this series.
[Developer Local]
Kind cluster + Skaffold/Tilt → fast dev loop
[Code Structure]
Namespace + RBAC → team and environment isolation
Workload type selection → right unit for the service
[Networking]
ClusterIP + Ingress + NetworkPolicy → explicit traffic control
[Deployment Management]
Helm Chart → templating + version tracking
Kustomize overlay → per-environment config separation
[GitOps]
ArgoCD App of Apps → Git as Single Source of Truth
CI/CD image tag updates → automated deployment pipeline
Each installment builds toward a single coherent flow: code written locally travels through Git and lands in production in a consistent, auditable way.