Objective
A production GitOps setup needs to manage differences between environments (replicas, resource limits, image tags, config) without duplicating manifests. The base/overlay pattern with Kustomize is the standard approach. This exercise builds a full two-environment structure and demonstrates how Flux's ImageAutomation component can automate image tag updates differently per environment.
Prerequisites
- Flux v2 bootstrapped from exercise d3-e1
- Two namespaces (or two clusters) for staging and production
- Container registry with your test image and multiple tags
- Flux CLI installed
Steps
Build the repository structure
# Full directory structure apps/ ├── base/ │ └── nginx/ │ ├── deployment.yaml # base Deployment │ ├── service.yaml # base Service │ └── kustomization.yaml # lists base resources ├── overlays/ │ ├── staging/ │ │ ├── kustomization.yaml # patches for staging │ │ ├── replicas-patch.yaml │ │ └── resources-patch.yaml │ └── production/ │ ├── kustomization.yaml # patches for production │ ├── replicas-patch.yaml │ ├── resources-patch.yaml │ └── hpa.yaml # HPA only in production clusters/ ├── staging/ │ └── nginx-app.yaml # Flux Kustomization → staging overlay └── production/ └── nginx-app.yaml # Flux Kustomization → prod overlay
Write the base manifests
# apps/base/nginx/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: nginx spec: replicas: 1 selector: matchLabels: { app: nginx } template: metadata: labels: { app: nginx } spec: containers: - name: nginx image: nginx:1.25.3 # {"$imagepolicy": "flux-system:nginx"} ports: - containerPort: 80 resources: requests: { cpu: 50m, memory: 64Mi } limits: { cpu: 200m, memory: 128Mi } --- # apps/base/nginx/kustomization.yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - deployment.yaml - service.yaml
Write the staging overlay
# apps/overlays/staging/kustomization.yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: staging namePrefix: staging- bases: - ../../base/nginx patches: - path: replicas-patch.yaml target: kind: Deployment name: nginx images: - name: nginx newTag: latest # staging tracks latest (semver ~1.25) --- # apps/overlays/staging/replicas-patch.yaml apiVersion: apps/v1 kind: Deployment metadata: name: nginx spec: replicas: 2 # staging: 2 replicas
Write the production overlay with pinned tags
# apps/overlays/production/kustomization.yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: production namePrefix: prod- bases: - ../../base/nginx patches: - path: replicas-patch.yaml target: { kind: Deployment, name: nginx } - path: resources-patch.yaml target: { kind: Deployment, name: nginx } resources: - hpa.yaml images: - name: nginx newTag: 1.25.3 # production: PINNED exact tag --- # apps/overlays/production/replicas-patch.yaml apiVersion: apps/v1 kind: Deployment metadata: name: nginx spec: replicas: 5 # production: 5 replicas
Configure Flux ImagePolicy for each environment
Flux's Image Automation tracks image registries and updates the Git repo when new tags are published. Different ImagePolicies enforce different tag selection strategies per environment.
# staging-image-policy.yaml — allows semver range apiVersion: image.toolkit.fluxcd.io/v1beta2 kind: ImagePolicy metadata: name: nginx-staging namespace: flux-system spec: imageRepositoryRef: name: nginx policy: semver: range: ">=1.25.0 <2.0.0" # allows any 1.25.x+ minor --- # production-image-policy.yaml — locked to exact tag apiVersion: image.toolkit.fluxcd.io/v1beta2 kind: ImagePolicy metadata: name: nginx-production namespace: flux-system spec: imageRepositoryRef: name: nginx policy: semver: range: "1.25.3" # pinned: only this exact version
Create Flux Kustomization resources per environment
# clusters/staging/nginx-app.yaml apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: nginx-staging namespace: flux-system spec: interval: 1m path: ./apps/overlays/staging prune: true sourceRef: kind: GitRepository name: flux-system --- # clusters/production/nginx-app.yaml apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: nginx-production namespace: flux-system spec: interval: 5m # less frequent in production path: ./apps/overlays/production prune: true sourceRef: kind: GitRepository name: flux-system
Validate the overlay differences
# Verify kustomize builds correctly before pushing kubectl kustomize apps/overlays/staging | \ grep -E "replicas:|image:|namespace:" # Should show: namespace: staging, replicas: 2, image: nginx:latest kubectl kustomize apps/overlays/production | \ grep -E "replicas:|image:|namespace:" # Should show: namespace: production, replicas: 5, image: nginx:1.25.3 # Push and verify both environments deploy git add -A && git commit -m "feat: add multi-env overlays" && git push kubectl get deployments -n staging kubectl get deployments -n production # Check replica counts match overlay definitions kubectl get deployment -n staging -o jsonpath='{.items[*].spec.replicas}' # Expected: 2 kubectl get deployment -n production -o jsonpath='{.items[*].spec.replicas}' # Expected: 5
Success Criteria
Further Reading
- Kustomize bases and overlays — kubectl.docs.kubernetes.io/references/kustomize/kustomization
- Flux multi-tenancy — fluxcd.io/flux/guides/repository-structure
- Flux Image Automation — fluxcd.io/flux/guides/image-update
- Managing manifests at scale — fluxcd.io/flux/guides/managing-manifests-at-scale