Argo CD is the most widely adopted implementation of GitOps on Kubernetes — a controller that runs inside your cluster, continuously compares live state to what's declared in Git, and reconciles the difference.
- A working Kubernetes cluster with
kubectlaccess. - A Git repository containing Kubernetes manifests, a Helm chart, or a Kustomize overlay for at least one application.
- Familiarity with basic Kubernetes objects (Deployments, Services) — Argo CD manages them, it doesn't replace understanding them.
Installing Argo CD
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yamlArgo CD ships its own UI, CLI, and API server, all running as pods inside the argocd namespace it just created. Access the UI by port-forwarding the API server service, or exposing it via an Ingress/LoadBalancer for real team use:
kubectl port-forward svc/argocd-server -n argocd 8080:443The core object: an Application
Everything in Argo CD revolves around the Application custom resource — it points at a Git source and a cluster/namespace destination, and declares how the two should be kept in sync.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/my-org/my-app-config.git
targetRevision: main
path: environments/production
destination:
server: https://kubernetes.default.svc
namespace: my-app
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=trueTwo fields inside automated do most of the practical work:
prune: true— resources removed from Git get deleted from the cluster too, not just left behind as orphans.selfHeal: true— this is what enforces GitOps principle 4 (continuous reconciliation). Without it, Argo CD only syncs on a Git change; with it, it also corrects manual drift automatically, even if Git itself hasn't changed.
The sync lifecycle
The last step matters: Argo CD doesn't consider a sync "done" just because kubectl apply succeeded. It waits for real Kubernetes health signals — a Deployment's rollout actually completing, a Service having endpoints — before marking the Application Healthy. A sync that applies cleanly but never becomes healthy (a crash-looping pod, for instance) stays visibly flagged, not silently "successful."
App of Apps: managing many applications declaratively
Once you have more than a handful of services, manually creating Application objects for each one stops scaling. The "App of Apps" pattern solves this by making a parent Argo CD Application whose entire job is to manage a directory of other Application manifests:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: platform-apps
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/my-org/platform-config.git
targetRevision: main
path: apps
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: trueAdding a new service becomes: add one YAML file describing its Application to the apps/ directory, commit, and the parent Application's own reconciliation loop picks it up and creates it — the entire fleet of services stays declarative, including the fact that they exist at all.
Sync waves: controlling apply order
By default, Argo CD applies resources roughly in Kubernetes-recommended order (namespaces before deployments, for instance), but real dependencies — a database migration Job that must finish before the application Deployment starts — need explicit ordering via sync wave annotations:
metadata:
annotations:
argocd.argoproj.io/sync-wave: "-1"Lower numbers sync first. A migration Job annotated -1 and the application Deployment left at the default 0 guarantees the migration completes before the Deployment is applied.
Multi-cluster and multi-environment patterns
A single Argo CD instance can manage many clusters — the destination.server field in each Application points at whichever cluster it targets, registered via argocd cluster add. Combined with a config repo layout like environments/staging and environments/production (from the GitOps article), one Argo CD install can drive an entire fleet: staging apps synced automatically on every merge, production apps requiring a manual sync click or a separate promotion PR, using the exact same underlying mechanism with a different syncPolicy.
ApplicationSets: generating Applications from a template
App of Apps works well when you're hand-writing each Application, but it doesn't scale cleanly to "one Application per environment per service" across a growing matrix. ApplicationSet solves this by generating Application resources from a template plus a generator — most commonly a Git directory generator or a cluster list generator:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: services
namespace: argocd
spec:
generators:
- git:
repoURL: https://github.com/my-org/services-config.git
revision: main
directories:
- path: services/*
template:
metadata:
name: "{{path.basename}}"
spec:
project: default
source:
repoURL: https://github.com/my-org/services-config.git
targetRevision: main
path: "{{path}}"
destination:
server: https://kubernetes.default.svc
namespace: "{{path.basename}}"
syncPolicy:
automated:
prune: trueAdding a new service becomes: add a new directory under services/ in the config repo. The ApplicationSet controller notices it on its next reconciliation and generates a full Application for it automatically — no manual Application YAML to write per service, and no risk of one getting forgotten.
Projects: RBAC and blast-radius control
An Argo CD AppProject scopes what a set of Applications is even allowed to do — which source repos they can pull from, which destination clusters/namespaces they can deploy into, and which Kubernetes resource kinds they're permitted to manage:
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: team-payments
namespace: argocd
spec:
sourceRepos:
- "https://github.com/my-org/payments-*"
destinations:
- server: https://kubernetes.default.svc
namespace: "payments-*"
clusterResourceWhitelist: []clusterResourceWhitelist: [] denies cluster-scoped resources (ClusterRoles, CustomResourceDefinitions) entirely for this project — a team's Applications are restricted to namespaced resources within their own namespace pattern, so a misconfigured or malicious manifest in their config repo can't touch anything outside their own blast radius, independent of whatever RBAC the underlying Kubernetes cluster itself enforces.
Notifications: closing the feedback loop
A GitOps deployment happening automatically in the background is only trustworthy if the team actually finds out when it fails. Argo CD Notifications (built into the controller) sends alerts on sync/health state transitions to Slack, email, or a webhook, configured declaratively alongside the rest of the setup:
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-notifications-cm
namespace: argocd
data:
trigger.on-sync-failed: |
- when: app.status.operationState.phase in ['Error', 'Failed']
send: [slack-sync-failed]
template.slack-sync-failed: |
message: "Sync failed for {{.app.metadata.name}}: {{.app.status.operationState.message}}"Without this, the only signal a sync failed is someone noticing the Argo CD UI shows a red status — fine for a small team watching closely, not reliable once deployments are frequent and the team is larger than whoever happens to have the dashboard open.
Comparing Argo CD to Flux
Argo CD isn't the only mature GitOps controller — Flux (also a CNCF project) implements the same four OpenGitOps principles with a different architectural emphasis: Flux is more modular (separate controllers for source, Kustomize, Helm, and notifications, composed together) and has no bundled UI by default, while Argo CD ships a full web UI and a more monolithic, opinionated Application model out of the box.
| Aspect | Argo CD | Flux |
|---|---|---|
| UI | Full web UI included by default | No bundled UI — CLI and Grafana dashboards, or a separate UI project |
| Architecture | More monolithic — one Application CRD drives everything | Modular — separate controllers per concern (source, Kustomize, Helm) |
| Multi-tenancy | AppProjects | Kubernetes-native RBAC + tenant-scoped controllers |
| Best for | Teams wanting a visual dashboard and simpler mental model | Teams wanting minimal footprint and deep GitOps-toolkit composability |
Neither is objectively better — teams that want a visible dashboard for less Kubernetes-fluent stakeholders tend to prefer Argo CD; platform teams building a highly customized internal developer platform on top of GitOps primitives often prefer Flux's more composable pieces.
Common mistakes
- Enabling
selfHeal: truein production before the team trusts it. It's genuinely powerful, but it means any manualkubectlchange gets silently reverted — a legitimate emergency hotfix applied by hand disappears on the next reconciliation unless it's also committed to Git. - Putting Helm
values.yamlsecrets or raw credentials directly in the config repo Argo CD watches. Argo CD syncs exactly what's in Git, including anything sensitive left there in plaintext — use Sealed Secrets or an external secrets operator instead. - Forgetting
prune: trueand assuming removing a resource from Git deletes it from the cluster. Without pruning, deleted manifests just become orphaned, undeclared resources that Argo CD no longer tracks or manages. - Treating "Synced" as equivalent to "working." Synced only means the last apply succeeded — check the separate Health status (Healthy/Degraded/Progressing) before assuming a deployment is actually serving traffic correctly.
Practical rollout patterns
- Use manual sync (not
automated) for production Applications until the team has enough operational trust in what auto-sync will do — flip it to automated once change volume and confidence justify it. - Pair Argo CD with a real CI pipeline that only updates the image tag in the config repo — never let CI apply directly to the cluster; that reintroduces the exact push-based credential exposure GitOps exists to remove.
- Use
sync-waveannotations explicitly for any real ordering dependency (migrations, CRDs before the resources that use them) rather than hoping default ordering happens to work. - Set resource-level health checks for custom resources Argo CD doesn't understand natively — without one, a CRD-based resource can report "Healthy" the instant it's applied, regardless of its actual state.
Related reading
- What Is GitOps? Principles, Workflow, and Why Teams Adopt It — shares tags: devops, kubernetes (same category).
- Kubernetes ConfigMaps and Secrets: A Practical Guide — shares tags: kubernetes, devops.
- How to Debug CrashLoopBackOff in Kubernetes — shares tags: kubernetes, devops.
- CI/CD Pipelines Explained: From Commit to Production — shares tags: devops, git.
- Karpenter: Faster, Simpler Kubernetes Autoscaling on AWS — shares tags: kubernetes, cloud.