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.
π‘ Quick Answer: Put CRDs at sync wave
-5, the operator at wave-3, and custom resources at wave0. UseServerSideApply=truefor CRDs and setprune: falseto prevent accidental CRD deletion.
The Problem
Operators and their CRDs create a chicken-and-egg problem in GitOps:
- CRDs must exist before custom resources can be created
- Operator must be running before it can reconcile custom resources
- ArgoCD applies everything at once by default, causing:
no matches for kind "Certificate" in version "cert-manager.io/v1"errors- Resources stuck in
Unknownhealth 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.comStep 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:
- /statusSync 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:#fffStep 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 limitsPruning 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=falseBest Practices
- CRDs at wave
-5or lower β give maximum separation from consumers - Use
ServerSideApplyfor CRDs β avoids annotation size limits - Never prune CRDs β set
Prune=falseon CRD resources - Separate CRDs from Helm charts β use
installCRDs: falseand 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
ServerSideApplyandPrune=falsefor CRD lifecycle management - PreSync hooks can validate CRD readiness before operators and CRs are deployed

Recommended
Kubernetes Recipes β The Complete Book100+ production-ready patterns with detailed explanations, best practices, and copy-paste YAML. Everything in one place.
Get the Book βLearn by Doing
CopyPasteLearn β Hands-on Cloud & DevOps CoursesMaster Kubernetes, Ansible, Terraform, and MLOps with interactive, copy-paste-run lessons. Start free.
Browse Courses βπ Deepen Your Skills β Hands-on Courses
Courses by CopyPasteLearn.com β Learn IT by Doing
