πŸ“šBook Signing at KubeCon EU 2026Meet us at Booking.com HQ (Mon 18:30-21:00) & vCluster booth #521 (Tue 24 Mar, 12:30-1:30pm) β€” free book giveaway!RSVP Booking.com Event
Deployments intermediate ⏱ 25 minutes K8s 1.26+

Kustomize Deployments with OpenShift GitOps

Use Kustomize overlays with the OpenShift GitOps Operator (ArgoCD) to manage environment-specific configurations across dev, staging, and production clusters.

By Luca Berton β€’ β€’ πŸ“– 6 min read

πŸ’‘ Quick Answer: Set spec.source.path to your Kustomize overlay directory in an ArgoCD Application. ArgoCD auto-detects kustomization.yaml and runs kustomize build β€” no Helm charts or plugins needed.

The Problem

Managing the same application across multiple environments (dev, staging, production) requires:

  • Shared base manifests with environment-specific overrides
  • No template duplication β€” DRY configuration across clusters
  • GitOps workflow β€” all changes through pull requests
  • Native Kubernetes β€” no proprietary templating languages
  • OpenShift-specific patches β€” Routes, SCCs, resource quotas per environment

Kustomize solves the templating problem. OpenShift GitOps Operator (ArgoCD) solves the delivery problem. Together they provide a complete GitOps pipeline.

The Solution

Repository Structure

my-app/
β”œβ”€β”€ base/
β”‚   β”œβ”€β”€ kustomization.yaml
β”‚   β”œβ”€β”€ deployment.yaml
β”‚   β”œβ”€β”€ service.yaml
β”‚   β”œβ”€β”€ route.yaml
β”‚   └── configmap.yaml
└── overlays/
    β”œβ”€β”€ dev/
    β”‚   β”œβ”€β”€ kustomization.yaml
    β”‚   β”œβ”€β”€ replica-patch.yaml
    β”‚   └── configmap-patch.yaml
    β”œβ”€β”€ staging/
    β”‚   β”œβ”€β”€ kustomization.yaml
    β”‚   β”œβ”€β”€ replica-patch.yaml
    β”‚   └── resource-patch.yaml
    └── production/
        β”œβ”€β”€ kustomization.yaml
        β”œβ”€β”€ replica-patch.yaml
        β”œβ”€β”€ resource-patch.yaml
        β”œβ”€β”€ hpa.yaml
        └── pdb.yaml

Base Manifests

# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
commonLabels:
  app.kubernetes.io/name: my-app
  app.kubernetes.io/managed-by: kustomize
resources:
  - deployment.yaml
  - service.yaml
  - route.yaml
  - configmap.yaml
# 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: registry.example.com/my-app:latest
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 256Mi
          env:
            - name: APP_ENV
              valueFrom:
                configMapKeyRef:
                  name: my-app-config
                  key: APP_ENV
# base/route.yaml
apiVersion: route.openshift.io/v1
kind: Route
metadata:
  name: my-app
spec:
  to:
    kind: Service
    name: my-app
  port:
    targetPort: 8080
  tls:
    termination: edge
    insecureEdgeTerminationPolicy: Redirect

Environment Overlays

# overlays/dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: my-app-dev
namePrefix: dev-
bases:
  - ../../base
patches:
  - path: replica-patch.yaml
  - path: configmap-patch.yaml
images:
  - name: registry.example.com/my-app
    newTag: dev-latest
# overlays/dev/replica-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 1
# overlays/dev/configmap-patch.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: my-app-config
data:
  APP_ENV: "development"
  LOG_LEVEL: "debug"
# overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: my-app-prod
namePrefix: prod-
bases:
  - ../../base
patches:
  - path: replica-patch.yaml
  - path: resource-patch.yaml
resources:
  - hpa.yaml
  - pdb.yaml
images:
  - name: registry.example.com/my-app
    newTag: v2.1.0
# overlays/production/replica-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3
# overlays/production/resource-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  template:
    spec:
      containers:
        - name: my-app
          resources:
            requests:
              cpu: 500m
              memory: 512Mi
            limits:
              cpu: "2"
              memory: 1Gi
# overlays/production/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: prod-my-app
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: prod-my-app
  minReplicas: 3
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

ArgoCD Applications per Environment

# argocd/dev-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app-dev
  namespace: openshift-gitops
  labels:
    env: dev
spec:
  project: default
  source:
    repoURL: https://git.example.com/platform/my-app.git
    targetRevision: develop
    path: overlays/dev
  destination:
    server: https://kubernetes.default.svc
    namespace: my-app-dev
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
---
# argocd/production-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app-prod
  namespace: openshift-gitops
  labels:
    env: production
spec:
  project: production
  source:
    repoURL: https://git.example.com/platform/my-app.git
    targetRevision: main
    path: overlays/production
  destination:
    server: https://kubernetes.default.svc
    namespace: my-app-prod
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
      - PrunePropagationPolicy=foreground

Kustomize with Components (Reusable Patches)

# components/monitoring/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component
patches:
  - target:
      kind: Deployment
    patch: |
      - op: add
        path: /spec/template/metadata/annotations/prometheus.io~1scrape
        value: "true"
      - op: add
        path: /spec/template/metadata/annotations/prometheus.io~1port
        value: "8080"
resources:
  - servicemonitor.yaml
# overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
  - ../../base
components:
  - ../../components/monitoring
  - ../../components/security

OpenShift-Specific Patches

# components/openshift-scc/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component
patches:
  - target:
      kind: Deployment
    patch: |
      - op: add
        path: /spec/template/spec/securityContext
        value:
          runAsNonRoot: true
          seccompProfile:
            type: RuntimeDefault
      - op: add
        path: /spec/template/spec/containers/0/securityContext
        value:
          allowPrivilegeEscalation: false
          capabilities:
            drop: ["ALL"]
# overlays/production/route-patch.yaml
apiVersion: route.openshift.io/v1
kind: Route
metadata:
  name: my-app
  annotations:
    haproxy.router.openshift.io/rate-limit-connections: "true"
    haproxy.router.openshift.io/rate-limit-connections.concurrent-tcp: "100"
spec:
  host: my-app.apps.production.example.com
  tls:
    termination: reencrypt
    certificate: |
      -----BEGIN CERTIFICATE-----
      ...
      -----END CERTIFICATE-----

ArgoCD Kustomize Build Options

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app-prod
  namespace: openshift-gitops
spec:
  source:
    repoURL: https://git.example.com/platform/my-app.git
    path: overlays/production
    kustomize:
      # Override image tag without changing git
      images:
        - registry.example.com/my-app:v2.2.0-hotfix
      # Add common labels
      commonLabels:
        team: platform
      # Add name prefix/suffix
      namePrefix: ocp-
      # Set Kustomize version
      version: v5.3.0
      # Pass build options
      commonAnnotations:
        argocd.argoproj.io/tracking-id: my-app-prod

Preview Kustomize Output in ArgoCD

# See what ArgoCD will apply (dry run)
argocd app manifests my-app-prod --source live

# Compare live vs desired
argocd app diff my-app-prod

# Local kustomize build (same as ArgoCD)
kustomize build overlays/production

# Validate with oc
kustomize build overlays/production | oc apply --dry-run=server -f -
graph TD
    A[Git Repository] --> B[base/]
    A --> C[overlays/dev/]
    A --> D[overlays/staging/]
    A --> E[overlays/production/]
    A --> F[components/]
    
    B --> C
    B --> D
    B --> E
    F --> D
    F --> E
    
    C --> G["ArgoCD App (dev)"]
    D --> H["ArgoCD App (staging)"]
    E --> I["ArgoCD App (prod)"]
    
    G --> J[Dev Cluster]
    H --> K[Staging Cluster]
    I --> L[Prod Cluster]

Common Issues

ArgoCD Not Detecting Kustomize

# ArgoCD auto-detects kustomization.yaml β€” check it exists:
ls overlays/production/kustomization.yaml

# If using kustomization.yml (different extension), ArgoCD still detects it
# If using Kustomization (capital K), also works

# Force Kustomize tool detection in Application spec:
# spec.source.kustomize is enough to signal ArgoCD

Kustomize Version Mismatch

# Check ArgoCD's built-in Kustomize version
argocd version | grep kustomize

# OpenShift GitOps bundles a specific version
# If you need newer features, configure in argocd-cm:
# kustomize.version.v5.3.0: /usr/local/bin/kustomize-5.3.0

# Or set per-app:
# spec.source.kustomize.version: v5.3.0

Strategic Merge Patch vs JSON Patch

# Strategic merge (default) β€” merges maps, replaces lists
patches:
  - path: deployment-patch.yaml

# JSON patch β€” precise array operations
patches:
  - target:
      kind: Deployment
      name: my-app
    patch: |
      - op: add
        path: /spec/template/spec/containers/0/env/-
        value:
          name: NEW_VAR
          value: "added"

Best Practices

  • Keep bases generic β€” no environment-specific values in base manifests
  • Use images in kustomization.yaml to manage image tags (not patches)
  • Prefer components over duplicating patches across overlays
  • Pin targetRevision per environment β€” develop for dev, main for prod
  • Use CreateNamespace=true syncOption so ArgoCD manages namespace lifecycle
  • Validate locally with kustomize build | oc apply --dry-run=server before pushing
  • Don’t mix Helm and Kustomize in the same Application unless using Helm post-rendering
  • Use commonLabels sparingly β€” they’re added to selectors too and can break rolling updates

Key Takeaways

  • ArgoCD auto-detects Kustomize β€” just point spec.source.path to your overlay directory
  • Base + overlays + components = DRY multi-environment config management
  • OpenShift GitOps Operator bundles ArgoCD with Kustomize β€” no extra setup needed
  • Per-environment Applications with different targetRevision and path enable promotion workflows
  • Kustomize components make reusable cross-cutting concerns (monitoring, security) composable
#kustomize #gitops #argocd #openshift #overlays
Luca Berton
Written by Luca Berton

Principal Solutions Architect specializing in Kubernetes, AI/GPU infrastructure, and cloud-native platforms. Author of Kubernetes Recipes and creator of CopyPasteLearn courses.

Kubernetes Recipes book cover

Want More Kubernetes Recipes?

This recipe is from Kubernetes Recipes, our 750-page practical guide with hundreds of production-ready patterns.

Luca Berton Ansible Pilot Ansible by Example Open Empower K8s Recipes Terraform Pilot CopyPasteLearn ProteinLens