⚙️ Infrastructure Intermediate ⏱️ 15 min

Kustomize Overlays for Multi-Environment Deployments

Manage Kubernetes configurations across environments with Kustomize overlays, patches, and ConfigMap generators.

By Victor Robin

Introduction

Managing Kubernetes configurations across multiple environments (Dev, Staging, Prod) can quickly become a maintenance nightmare if you’re duplicating YAML files. Kustomize offers a template-free way to customize application configuration, preserving the base structure while allowing for environment-specific “overlays” to modify behavior. This guide details how to structure a multi-environment deployment using standard Kustomize patterns.

Architecture Overview

The Kustomize workflow separates the “Base” definition (shared across all environments) from “Overlays” (environment-specific modifications). When built, Kustomize merges the layers to produce the final manifests.

flowchart LR
    Base["📄 Base Configuration<br/>(Deployment, Service)"] --> Build("⚙️ Kustomize Build")
    Overlaydev["📝 Dev Overlay<br/>(Patches, Config)"] --> Build
    Overlayprod["📝 Prod Overlay<br/>(Patches, HPA)"] --> Build
    Build --> Output["📦 Rendered Manifests"]

    classDef primary fill:#7c3aed,color:#fff
    classDef secondary fill:#06b6d4,color:#fff
    classDef db fill:#f43f5e,color:#fff
    classDef warning fill:#fbbf24,color:#000

    class Build primary
    class Overlaydev,Overlayprod secondary
    class Base,Output db

Implementation

Directory Structure

apps/
└── archives-api/
    ├── base/
    │   ├── kustomization.yaml
    │   ├── deployment.yaml
    │   ├── service.yaml
    │   └── serviceaccount.yaml
    └── overlays/
        ├── dev/
        │   ├── kustomization.yaml
        │   └── deployment-patch.yaml
        ├── staging/
        │   ├── kustomization.yaml
        │   ├── deployment-patch.yaml
        │   └── hpa.yaml
        └── prod/
            ├── kustomization.yaml
            ├── deployment-patch.yaml
            ├── hpa.yaml
            └── pdb.yaml

Base Configuration

Base Kustomization

# apps/archives-api/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

commonLabels:
  app.kubernetes.io/name: archives-api
  app.kubernetes.io/component: api

resources:
  - deployment.yaml
  - service.yaml
  - serviceaccount.yaml

images:
  - name: archives-api
    newName: registry.bluerobin.local/archives-api
    newTag: latest

Base Deployment

# apps/archives-api/base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: archives-api
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: archives-api
  template:
    metadata:
      labels:
        app.kubernetes.io/name: archives-api
    spec:
      serviceAccountName: archives-api
      containers:
        - name: api
          image: archives-api
          ports:
            - containerPort: 8080
              name: http
          env:
            - name: ASPNETCORE_ENVIRONMENT
              value: Production
            - name: ASPNETCORE_URLS
              value: http://+:8080
          envFrom:
            - secretRef:
                name: archives-api-secrets
          resources:
            requests:
              cpu: 100m
              memory: 256Mi
            limits:
              cpu: 1000m
              memory: 1Gi
          readinessProbe:
            httpGet:
              path: /health/ready
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /health/live
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 10

Base Service

# apps/archives-api/base/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: archives-api
spec:
  selector:
    app.kubernetes.io/name: archives-api
  ports:
    - port: 80
      targetPort: 8080
      name: http

Environment Overlays

Staging Overlay

# apps/archives-api/overlays/staging/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: archives-staging

namePrefix: staging-

commonLabels:
  app.kubernetes.io/instance: staging

resources:
  - ../../base
  - hpa.yaml
  - externalsecret.yaml
  - ingressroute.yaml

patches:
  - path: deployment-patch.yaml
    target:
      kind: Deployment
      name: archives-api

images:
  - name: archives-api
    newName: registry.bluerobin.local/archives-api
    newTag: 20260101-abc1234  # Updated by CI/CD

configMapGenerator:
  - name: archives-api-config
    literals:
      - Environment=staging
      - LogLevel=Debug
      - Otlp__Endpoint=http://signoz-otel-collector.observability.svc.cluster.local:4317

Staging Deployment Patch

# apps/archives-api/overlays/staging/deployment-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: archives-api
spec:
  replicas: 2
  template:
    spec:
      containers:
        - name: api
          env:
            - name: Environment
              value: staging
          resources:
            requests:
              cpu: 200m
              memory: 512Mi
            limits:
              cpu: 1000m
              memory: 1Gi

Staging HPA

# apps/archives-api/overlays/staging/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: archives-api
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: archives-api
  minReplicas: 2
  maxReplicas: 5
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80

Production Overlay

# apps/archives-api/overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: archives-prod

namePrefix: prod-

commonLabels:
  app.kubernetes.io/instance: prod

resources:
  - ../../base
  - hpa.yaml
  - pdb.yaml
  - externalsecret.yaml
  - ingressroute.yaml

patches:
  - path: deployment-patch.yaml
    target:
      kind: Deployment
      name: archives-api

images:
  - name: archives-api
    newName: registry.bluerobin.local/archives-api
    newTag: 20260101-def5678

configMapGenerator:
  - name: archives-api-config
    literals:
      - Environment=prod
      - LogLevel=Information
      - Otlp__Endpoint=http://signoz-otel-collector.observability.svc.cluster.local:4317

replicas:
  - name: archives-api
    count: 3

Production Deployment Patch

# apps/archives-api/overlays/prod/deployment-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: archives-api
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    spec:
      containers:
        - name: api
          env:
            - name: Environment
              value: prod
          resources:
            requests:
              cpu: 500m
              memory: 1Gi
            limits:
              cpu: 2000m
              memory: 2Gi
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app.kubernetes.io/name: archives-api

Pod Disruption Budget

# apps/archives-api/overlays/prod/pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: archives-api
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app.kubernetes.io/name: archives-api

Strategic Merge Patches

JSON Patch

# apps/archives-api/overlays/staging/json-patch.yaml
- op: add
  path: /spec/template/spec/containers/0/env/-
  value:
    name: FEATURE_FLAG_NEW_SEARCH
    value: "true"

- op: replace
  path: /spec/template/spec/containers/0/resources/requests/memory
  value: 768Mi

Using JSON Patch

# apps/archives-api/overlays/staging/kustomization.yaml
patches:
  - path: json-patch.yaml
    target:
      kind: Deployment
      name: archives-api

ConfigMap and Secret Generators

ConfigMap Generator

# apps/archives-api/overlays/staging/kustomization.yaml
configMapGenerator:
  - name: archives-api-config
    behavior: merge
    files:
      - appsettings.json=config/appsettings.staging.json
    literals:
      - NATS_SUBJECT_PREFIX=staging

Secret Generator (for dev only)

# apps/archives-api/overlays/dev/kustomization.yaml
secretGenerator:
  - name: archives-api-dev-secrets
    literals:
      - DATABASE_PASSWORD=devpassword
    options:
      disableNameSuffixHash: true

Flux Integration

Kustomization Resource

# clusters/bluerobin/apps/archives-api-staging.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: archives-api-staging
  namespace: flux-system
spec:
  interval: 10m
  path: ./apps/archives-api/overlays/staging
  prune: true
  sourceRef:
    kind: GitRepository
    name: bluerobin-infra
  targetNamespace: archives-staging
  healthChecks:
    - apiVersion: apps/v1
      kind: Deployment
      name: staging-archives-api
      namespace: archives-staging
  dependsOn:
    - name: external-secrets
    - name: data-layer

Validation

Build and Preview

# Preview staging configuration
kustomize build apps/archives-api/overlays/staging

# Validate without applying
kubectl apply --dry-run=client -k apps/archives-api/overlays/staging

# Diff against cluster state
kubectl diff -k apps/archives-api/overlays/staging

Conclusion

Using Kustomize overlays allows us to maintain a clean, DRY (Don’t Repeat Yourself) configuration base while accommodating the specific needs of each environment. This approach aligns perfectly with GitOps principles, providing a declarative source of truth that is both flexible and maintainable.

Kustomize structure benefits:

FeatureBenefit
Base + OverlaysDRY configuration management
PatchesEnvironment-specific modifications
GeneratorsConfigMaps and Secrets from files/literals
Name PrefixingClear resource namespacing
Flux IntegrationGitOps-native deployment

Combined with Flux, Kustomize enables declarative, version-controlled Kubernetes deployments.

[Kustomize Documentation] — Kubernetes SIG CLI