← Back to Guide
GitOps & CI/CD L2 · PRACTICAL ~90 min

Multi-Environment GitOps with Base/Overlay Structure

Implement a multi-environment GitOps repository structure with Kustomize base and overlays for staging and production. Configure Flux with environment-specific clusters, enforce image tag pinning in production, and allow semver ranges in staging.

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

Steps

01

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
02

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
03

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
04

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
05

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
06

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
07

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