Instruction file imported from rgordill/couchbase-performance (
.cursor/rules/argocd.mdc). Copyright stays with the author.
ArgoCD Best Practices
Folder Structure
App of Apps Pattern
Use the App of Apps pattern with a root Application in its own directory (e.g., cluster-apps/) that references Kustomize overlays:
# ✅ GOOD - Root Application in cluster-apps/cluster-apps.yaml references overlay
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: cluster-apps
namespace: argocd
spec:
source:
repoURL: https://github.com/org/repo.git
targetRevision: HEAD
path: argocd/cluster-apps/overlays/aws # Environment-specific overlay
Base and Overlays Pattern
- Base directory (
argocd/cluster-apps/base/): Contains common resources referenced by all environments - Overlays directory (
argocd/cluster-apps/overlays/{env}/): Environment-specific customizations (aws, libvirt, etc.)
# ✅ GOOD - Base kustomization.yaml references application directories
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../ingress
- ../../monitoring/base
Application Directory Structure
Each application should have its own directory with:
argocd/
{app-name}/
{app-name}.yaml # Application manifest
kustomization.yaml # Kustomize resources
namespace.yaml # Namespace (if needed)
README.md # Documentation
Application Resource Standards
Required Fields
# ✅ GOOD - Complete Application manifest
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: argocd
finalizers:
- argocd.argoproj.io/finalizer
spec:
project: default
source:
repoURL: https://charts.example.com
chart: my-app
targetRevision: v1.0.0
destination:
server: https://kubernetes.default.svc
namespace: my-app
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: false
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
- PruneLast=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
Sync Waves for Deployment Ordering
Use sync-wave annotations to control deployment order:
# ✅ GOOD - Use sync-wave for ordering
metadata:
annotations:
argocd.argoproj.io/sync-wave: "-3" # Negative = early, positive = late
Common sync waves:
-3: Infrastructure (e.g. storage)-2: Ingress controllers-1: Base services0: Applications (default)1+: Dependent applications
PreSync/PostSync hooks: remove extra RBAC with a post-hook
When an app uses a PreSync hook that requires dedicated RBAC (e.g. reading cluster config, creating a ConfigMap), do not leave that RBAC in the cluster after sync. Add a PostSync hook Job that deletes the PreSync hook RBAC (ServiceAccount, Role, RoleBinding, and any ClusterRole/ClusterRoleBinding). Use a persistent RBAC (separate ServiceAccount and Role/ClusterRole with permission only to delete those hook resources) for the PostSync Job so the cleanup runs on every sync and the cleanup RBAC is not deleted by the post-hook. Delete order: bindings first, then roles, then ServiceAccount. See .cursor/rules/kubernetes.mdc (Ingress host from cluster default domain) for the full pattern.
Ignore Differences
Always ignore webhook CA bundles and status fields that are managed by controllers:
# ✅ GOOD - Ignore controller-managed fields
ignoreDifferences:
- group: admissionregistration.k8s.io
kind: MutatingWebhookConfiguration
jqPathExpressions:
- '.webhooks[]?.clientConfig.caBundle'
- group: admissionregistration.k8s.io
kind: ValidatingWebhookConfiguration
jqPathExpressions:
- '.webhooks[]?.clientConfig.caBundle'
- group: monitoring.coreos.com
kind: Prometheus
jqPathExpressions:
- '.status'
Helm: Use values from a file, not valuesObject
Prefer referencing a values file in the repo over inlining valuesObject in the Application. Use helm.valueFiles so values live in versioned YAML files and stay out of the Application manifest.
Helm: Two sources (chart + values), not a wrapper chart
When the chart is from a Helm repository and values live in Git, use two sources in the same Application: one source for the chart (Helm repo), one for the values (Git). Do not add a wrapper chart in Git that depends on the upstream chart.
# ✅ GOOD - Chart from Helm repo, values from Git (two sources)
spec:
sources:
- repoURL: https://charts.example.com
chart: my-chart
targetRevision: "1.0"
helm:
releaseName: my-release
valueFiles:
- $values/argocd/helm/my-app/values.yaml
- repoURL: https://github.com/org/repo.git
targetRevision: HEAD
ref: values
path: argocd/helm/my-app
- The first source is the Helm chart (repoURL + chart + targetRevision).
- The second source is Git; give it
ref: valuesso the first source can reference files via$values/.... Paths invalueFiles(e.g.$values/values.yaml) are relative to the ref source’spath(or repo root ifpathis omitted). - Use
sources(array), notsource(single), when using this pattern.
# ❌ BAD - Wrapper chart in Git that depends on the upstream chart
spec:
source:
repoURL: https://github.com/org/repo.git
path: argocd/helm/operator # directory with Chart.yaml + dependencies
helm:
valueFiles:
- values.yaml
# ❌ BAD - Large inline valuesObject
spec:
source:
repoURL: https://charts.example.com
chart: my-chart
helm:
valuesObject:
install: { ... }
image: { repository: ..., tag: ... }
# many more keys...
Kustomize
Follow .cursor/rules/kustomize.mdc for all Kustomize usage (use labels and patches; avoid deprecated commonLabels, patchesJson6902, patchesStrategicMerge).
Environment-Specific Configuration
Use Kustomize overlays with ConfigMap generators and replacements:
# ✅ GOOD - Overlay with environment config
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
configMapGenerator:
- name: global-config
namespace: argocd
literals:
- INGRESS_DOMAIN=example.com
replacements:
- source:
kind: ConfigMap
name: global-config
fieldPath: data.INGRESS_DOMAIN
targets:
- select:
kind: Application
name: kube-prometheus-stack
fieldPaths:
- spec.source.helm.valuesObject.grafana.ingress.hosts.[0]
Applications that deploy an Ingress (cluster-specific host)
When an application deploys an Ingress and the host must come from the cluster (e.g. OpenShift ingress domain), use an ApplicationSet with the cluster generator and source.kustomize.patches to inject the host from the Argo CD cluster secret. The cluster secret must have the annotation ingress-domain (e.g. set by a PreSync hook from the cluster’s ingress config). Do not hardcode the host in the Ingress manifest; use a placeholder and patch it at sync time.
-
Cluster secret: Ensure the in-cluster (or target cluster) Argo CD cluster Secret has
metadata.annotations["ingress-domain"]set to the ingress domain (e.g.apps.ocp.example.com). A PreSync Job can set this from the cluster (e.g. OpenShiftingresses.config.openshift.io/cluster.spec.domain). -
ApplicationSet: Use
generators.clusterswith a selector that matches that cluster (e.g.argocd.argoproj.io/cluster-name: in-cluster). In the template, setsource.kustomize.patchesto a JSON patch that replaces the Ingress host and TLS host with"<subdomain>.{{ index .metadata.annotations \"ingress-domain\" }}"(e.g. subdomaingrafana-server,couchbase-admin). -
Ingress in Git: In the app’s Ingress manifest, set
spec.rules[0].hostandspec.tls[0].hosts[0]to a placeholder (e.g.PLACEHOLDER). The ApplicationSet patches replace it so the deployed host is<subdomain>.<ingress-domain>.
# ✅ GOOD - ApplicationSet injects Ingress host from cluster secret annotation
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: my-app
namespace: openshift-gitops
spec:
goTemplate: true
goTemplateOptions: ["missingkey=error"]
generators:
- clusters:
selector:
matchLabels:
argocd.argoproj.io/cluster-name: in-cluster
template:
metadata:
name: 'my-app'
spec:
source:
repoURL: https://github.com/org/repo.git
path: argocd/manifests/my-app
kustomize:
patches:
- target:
kind: Ingress
name: my-app-ingress
patch: |-
- op: replace
path: /spec/rules/0/host
value: "my-subdomain.{{ index .metadata.annotations \"ingress-domain\" }}"
- op: replace
path: /spec/tls/0/hosts/0
value: "my-subdomain.{{ index .metadata.annotations \"ingress-domain\" }}"
destination:
server: '{{.server}}'
namespace: my-namespace
# ✅ GOOD - Ingress in repo uses PLACEHOLDER; ApplicationSet patches replace it
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-ingress
spec:
tls:
- hosts: [PLACEHOLDER]
secretName: my-app-tls
rules:
- host: PLACEHOLDER
http:
paths: [...]
Note: The ApplicationSet cluster generator only exposes secret metadata (annotations, labels) and the standard data keys name, server, project. It does not expose custom secret data keys, so the ingress domain must be stored in the cluster secret’s annotation ingress-domain.
Anti-Patterns
# ❌ BAD - Missing finalizer
metadata:
name: my-app
# Missing finalizers
# ❌ BAD - No sync policy
spec:
# Missing syncPolicy
# ❌ BAD - Hardcoded environment values in base or large valuesObject
spec:
source:
helm:
valuesObject:
ingress:
hosts:
- grafana.prod.example.com # Prefer valueFiles + overlay replacement
# ❌ BAD - Missing ignoreDifferences for webhooks
# Causes sync conflicts with CA bundles
# ❌ BAD - Helm wrapper chart in Git (Chart.yaml with dependency on upstream chart)
# Use two sources instead: one for the chart, one for the values (ref + $values/...)
# ❌ BAD - Kustomize: avoid deprecated commonLabels/patchesJson6902/patchesStrategicMerge (see .cursor/rules/kustomize.mdc)
Documentation
Each application directory should include a README.md with:
- Purpose and features
- Sync wave explanation (if applicable)
- Configuration examples
- Usage instructions