πŸ“š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 ⏱ 15 minutes K8s 1.25+

ArgoCD Sync Waves for CRD & Operator Ordering

Use ArgoCD sync waves to deploy Custom Resource Definitions before operators and custom resources, preventing CRD race conditions in GitOps pipelines.

By Luca Berton β€’ β€’ Updated February 26, 2026 β€’ πŸ“– 5 min read

πŸ’‘ Quick Answer: Put CRDs at sync wave -5, the operator at wave -3, and custom resources at wave 0. Use ServerSideApply=true for CRDs and set prune: false to prevent accidental CRD deletion.

The Problem

Operators and their CRDs create a chicken-and-egg problem in GitOps:

  1. CRDs must exist before custom resources can be created
  2. Operator must be running before it can reconcile custom resources
  3. ArgoCD applies everything at once by default, causing:
    • no matches for kind "Certificate" in version "cert-manager.io/v1" errors
    • Resources stuck in Unknown health state
    • Sync failures that require manual intervention

The Solution

Step 1: Three-Wave CRD Strategy

# Wave -5: CRDs (must exist before anything)
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: certificates.cert-manager.io
  annotations:
    argocd.argoproj.io/sync-wave: "-5"
    argocd.argoproj.io/sync-options: Replace=true,ServerSideApply=true
# ... full CRD spec
---
# Wave -3: Operator Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cert-manager
  namespace: cert-manager
  annotations:
    argocd.argoproj.io/sync-wave: "-3"
spec:
  replicas: 1
  selector:
    matchLabels:
      app: cert-manager
  template:
    metadata:
      labels:
        app: cert-manager
    spec:
      containers:
        - name: cert-manager
          image: quay.io/jetstack/cert-manager-controller:v1.16.0
---
# Wave 0: Custom Resources (operator must be ready)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
  annotations:
    argocd.argoproj.io/sync-wave: "0"
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: admin@example.com
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
      - http01:
          ingress:
            class: nginx
---
# Wave 1: Resources that depend on the issuer
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: myapp-tls
  namespace: myapp
  annotations:
    argocd.argoproj.io/sync-wave: "1"
spec:
  secretName: myapp-tls
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
    - myapp.example.com

Step 2: ArgoCD Application with CRD Sync Options

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: cert-manager-full
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/gitops-repo.git
    targetRevision: main
    path: cert-manager
  destination:
    server: https://kubernetes.default.svc
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
      - ServerSideApply=true   # Required for large CRDs
      - RespectIgnoreDifferences=true
  ignoreDifferences:
    - group: apiextensions.k8s.io
      kind: CustomResourceDefinition
      jsonPointers:
        - /status

Sync Order Visualization

flowchart TD
    A["Wave -5: CRDs"] --> B["Wave -3: Operator"]
    B --> C["Wave 0: Custom Resources"]
    C --> D["Wave 1: Dependent Resources"]

    A -->|"CRD registered in API server"| B
    B -->|"Operator pods Ready"| C
    C -->|"Operator reconciles CR"| D

    style A fill:#ff9800,color:#fff
    style B fill:#2196f3,color:#fff
    style C fill:#4caf50,color:#fff
    style D fill:#9c27b0,color:#fff

Step 3: Multiple Operators Pattern

When deploying multiple operators with interdependencies:

# Wave -5: ALL CRDs from all operators
# cert-manager CRDs
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: certificates.cert-manager.io
  annotations:
    argocd.argoproj.io/sync-wave: "-5"
---
# Prometheus CRDs
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: prometheuses.monitoring.coreos.com
  annotations:
    argocd.argoproj.io/sync-wave: "-5"
---
# Wave -3: ALL operators (can deploy in parallel)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: cert-manager
  annotations:
    argocd.argoproj.io/sync-wave: "-3"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: prometheus-operator
  annotations:
    argocd.argoproj.io/sync-wave: "-3"
---
# Wave -1: CRs that operators need to reconcile early
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
  annotations:
    argocd.argoproj.io/sync-wave: "-1"
---
# Wave 0: CRs that depend on earlier CRs
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: cert-manager-metrics
  annotations:
    argocd.argoproj.io/sync-wave: "0"

Step 4: PreSync Hook for CRD Readiness

Sometimes ArgoCD proceeds before the API server fully registers CRDs. Add a readiness check:

apiVersion: batch/v1
kind: Job
metadata:
  name: wait-for-crds
  annotations:
    argocd.argoproj.io/hook: PreSync
    argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
    argocd.argoproj.io/sync-wave: "-4"
spec:
  backoffLimit: 10
  template:
    spec:
      restartPolicy: Never
      serviceAccountName: argocd-crd-checker
      containers:
        - name: check
          image: bitnami/kubectl:1.31
          command:
            - /bin/sh
            - -c
            - |
              echo "Waiting for CRDs to be established..."
              kubectl wait --for=condition=Established \
                crd/certificates.cert-manager.io \
                crd/issuers.cert-manager.io \
                crd/clusterissuers.cert-manager.io \
                --timeout=120s
              echo "All CRDs established."

Common Issues

”no matches for kind” Error

The CRD isn’t registered yet when ArgoCD tries to apply the CR. Increase the wave gap or add a PreSync readiness check.

CRD Too Large for Annotations

CRDs with many versions or fields exceed the annotation size limit:

syncOptions:
  - ServerSideApply=true  # Bypasses annotation limits

Pruning Deletes CRDs

Never auto-prune CRDs β€” they contain schema definitions for all resources:

# Per-resource annotation to skip pruning
metadata:
  annotations:
    argocd.argoproj.io/sync-options: Prune=false

Best Practices

  • CRDs at wave -5 or lower β€” give maximum separation from consumers
  • Use ServerSideApply for CRDs β€” avoids annotation size limits
  • Never prune CRDs β€” set Prune=false on CRD resources
  • Separate CRDs from Helm charts β€” use installCRDs: false and manage CRDs explicitly
  • Add readiness checks when CRD registration is slow
  • All operators in the same wave β€” they can deploy in parallel if CRDs are already present

Key Takeaways

  • CRD race conditions are the most common sync failure in operator-heavy GitOps
  • Three-wave pattern: CRDs β†’ Operators β†’ Custom Resources eliminates race conditions
  • Use ServerSideApply and Prune=false for CRD lifecycle management
  • PreSync hooks can validate CRD readiness before operators and CRs are deployed
#argocd #gitops #crds #operators #sync-waves #ordering
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