The RK Times
← All posts ArgoCD in Practice: GitOps Concepts and the Commands You Actually Use
Kubernetes

ArgoCD in Practice: GitOps Concepts and the Commands You Actually Use

By Romaan · Oct 3, 2026 · 10 min read · 5 views

"If it isn’t in Git, it isn’t real."

Most teams reach the same wall with Kubernetes. The manifests are in Git, but the cluster is whatever the last person ran kubectl apply on. Nobody can say with confidence what is deployed, and "roll it back" turns into an archaeology exercise.

Argo CD closes that gap. It is a declarative, GitOps continuous delivery tool for Kubernetes: Git holds the desired state, Argo CD continuously compares it against the live cluster, and either reports the drift or corrects it. This post covers just enough of the model to make the CLI make sense, then spends most of its length on the argocd commands you actually reach for.

The model in one minute

Five words carry most of the weight:

  • Application — a mapping from a source (Git repo, path, revision) to a destination (cluster, namespace). This is the unit Argo CD reconciles.
  • Project — a boundary around a group of Applications: which repos they may deploy from, which clusters and namespaces they may deploy to, which resource kinds they may create. default allows everything, which is exactly why you should not ship with it.
  • Sync status — is the live state equal to Git? Synced or OutOfSync. This is a question about drift.
  • Health status — is the workload actually working? Healthy, Progressing, Degraded, Suspended, Missing. This is a question about runtime.
  • Sync — the act of applying Git to the cluster. Optionally with prune (delete live resources that no longer exist in Git) and self-heal (revert manual changes made outside Git).

Those last two statuses are independent, and conflating them is the most common source of confusion. An app can be Synced and Degraded — Git was applied faithfully and the pods are crash-looping. It can also be OutOfSync and Healthy — someone hotfixed production by hand and it is serving traffic beautifully. Argo CD is telling you two different truths.

Diagram: the reconcile loop

   +------------------+         git push        +--------------------+
   |    Developer     |  -------------------->  |   Git repository   |
   +------------------+                         |  (desired state)   |
                                                +--------------------+
                                                          |
                                           webhook, or poll every 3m
                                                          |
                                                          v
   +---------------------------------------------------------------------+
   |                 Argo CD application controller                      |
   |    diff( desired from Git , live from cluster )  -->  sync status    |
   +---------------------------------------------------------------------+
                                                          |
                                                 apply / prune
                                                          |
                                                          v
   +---------------------------------------------------------------------+
   |                 Kubernetes cluster (live state)                     |
   +---------------------------------------------------------------------+

That loop never stops. It is what makes GitOps different from a pipeline that pushes once and forgets: drift introduced an hour after the deploy is still detected.

Getting a CLI you can talk to

Nothing below works until the CLI is pointed at a server. If you installed Argo CD the standard way, the API server is not exposed, so port-forward it and fetch the bootstrap password:

# expose the API server locally
kubectl port-forward svc/argocd-server -n argocd 8080:443

# the initial admin password lives in a secret, base64 encoded
kubectl -n argocd get secret argocd-initial-admin-secret \
  -o jsonpath="{.data.password}" | base64 -d

Login and context

# interactive login against a port-forwarded server with a self-signed cert
argocd login localhost:8080 --username admin --insecure

# login through your identity provider instead of a local account
argocd login argocd.example.com --sso

# when an ingress or proxy in front of Argo CD cannot do gRPC
argocd login argocd.example.com --grpc-web

# which servers am I logged into, and which one is current?
argocd context
argocd context argocd-staging

argocd account update-password
argocd logout argocd.example.com

--grpc-web is worth remembering. The CLI speaks gRPC by default, and plenty of load balancers and corporate proxies quietly mangle it. If commands hang or fail with transport errors while the UI works fine, add --grpc-web before you start debugging anything else.

Working with applications

Looking around

argocd app list
argocd app list -o wide
argocd app list --project payments
argocd app list -l team=platform

# everything about one app: source, destination, and a resource-by-resource tree
argocd app get payments-api

# force Argo CD to re-read the repo instead of trusting its cache
argocd app get payments-api --refresh
argocd app get payments-api --hard-refresh

argocd app get payments-api -o json

The difference between the two refresh flags matters when you are chasing a stale diff. --refresh re-compares against the cached manifests; --hard-refresh throws away the manifest cache and regenerates it, which is what you want after changing a Helm values file or a Kustomize overlay and seeing no change at all.

Creating and changing apps

argocd app create payments-api \
  --repo https://github.com/acme/deploy.git \
  --path envs/staging/payments-api \
  --revision main \
  --dest-server https://kubernetes.default.svc \
  --dest-namespace payments \
  --sync-option CreateNamespace=true \
  --project payments

# turn on continuous reconciliation after the fact
argocd app set payments-api --sync-policy automated --auto-prune --self-heal

# pin a release, override Helm values, change the tracked branch
argocd app set payments-api --revision v1.4.2
argocd app set payments-api --helm-set image.tag=v1.4.2 --helm-set replicas=3
argocd app unset payments-api --helm-set replicas

# remove the app and everything it created
argocd app delete payments-api --cascade

# remove the app but leave the live resources running
argocd app delete payments-api --cascade=false

In a mature setup you will create very few apps this way, because the Application objects themselves live in Git and are managed by an app-of-apps or an ApplicationSet. app create earns its keep for spikes, demos and one-off environments.

Diffing and syncing

# what would change if I synced right now?
argocd app diff payments-api

# compare the cluster against manifests on your laptop, before committing them
argocd app diff payments-api --local ./envs/staging/payments-api

# compare against a specific git revision
argocd app diff payments-api --revision v1.4.2

argocd app sync payments-api
argocd app sync payments-api --prune
argocd app sync payments-api --dry-run

# sync one resource instead of the whole app
argocd app sync payments-api --resource apps:Deployment:payments-api

# recreate instead of patching, for immutable field changes
argocd app sync payments-api --replace

# ignore the diff and apply everything regardless
argocd app sync payments-api --force

# block until the app is actually healthy
argocd app wait payments-api --health --timeout 300
argocd app wait payments-api --sync
argocd app wait payments-api --operation

app sync returns as soon as the operation it started finishes, which is not the same as the workload being ready. In a pipeline you almost always want sync followed by wait --health; otherwise your deploy job goes green while the new pods are still pulling an image that does not exist.

History and rollback

# every sync, with the git revision it deployed
argocd app history payments-api

# go back to a numbered entry from that list
argocd app rollback payments-api 23

One caveat that bites people at exactly the wrong moment: rolling back this way puts the cluster at an old revision while Git still points at the new one. If the app has automated sync enabled, the controller will dutifully drag it forward again within minutes. A rollback is a way to buy time, not a fix — the fix is a commit.

Repositories, clusters and projects

# HTTPS with a token or password
argocd repo add https://github.com/acme/deploy.git \
  --username git --password "$GITHUB_TOKEN"

# SSH deploy key
argocd repo add git@github.com:acme/deploy.git \
  --ssh-private-key-path ~/.ssh/argocd_deploy

# a Helm chart repository
argocd repo add https://charts.bitnami.com/bitnami --type helm --name bitnami

# an OCI registry hosting charts
argocd repo add registry.example.com/charts --type helm --enable-oci

argocd repo list
argocd repo rm https://github.com/acme/deploy.git

Adding an external cluster uses your local kubeconfig contexts, and installs a service account into the target cluster so the controller can act there:

kubectl config get-contexts
argocd cluster add prod-eu --name prod-eu
argocd cluster list
argocd cluster rm prod-eu

Projects are where you stop a misconfigured Application from deploying a repo nobody reviewed into a namespace nobody expected:

argocd proj create payments \
  --dest https://kubernetes.default.svc,payments \
  --src https://github.com/acme/deploy.git

argocd proj list
argocd proj get payments

argocd proj add-destination payments https://kubernetes.default.svc payments-jobs
argocd proj add-source payments https://github.com/acme/charts.git

# cluster-scoped kinds are denied unless you allow them explicitly
argocd proj allow-cluster-resource payments rbac.authorization.k8s.io ClusterRole

# and you can deny dangerous namespaced kinds
argocd proj deny-namespace-resource payments "" Secret

Troubleshooting

This is the set I keep in muscle memory, because it answers "why is this app not Healthy" faster than clicking through the UI.

# every resource the app owns, with sync and health per resource
argocd app resources payments-api

# what Argo CD rendered from Git, vs what is actually in the cluster
argocd app manifests payments-api --source git
argocd app manifests payments-api --source live

# container logs, without switching to kubectl
argocd app logs payments-api --follow
argocd app logs payments-api --container sidecar --tail 100

# a sync is wedged and nothing will proceed until you cancel it
argocd app terminate-op payments-api

# resource-level surgery: restart a rollout, delete a stuck pod
argocd app actions list payments-api --kind Deployment
argocd app actions run payments-api restart --kind Deployment
argocd app delete-resource payments-api --kind Pod --resource-name payments-api-7c9f

argocd version

app manifests --source git against --source live is the single most useful pair here. If the rendered manifest is wrong, the bug is in your chart or overlay. If the rendered manifest is right and the live object disagrees, the bug is in the cluster — a mutating webhook, a conflicting controller, or an admission policy rewriting your spec.

There is also a local UI that does not require exposing the server at all:

argocd admin dashboard -n argocd

# disaster-recovery pair: dump and restore all Argo CD state
argocd admin export -n argocd > argocd-backup.yaml
argocd admin import -n argocd - < argocd-backup.yaml

Driving Argo CD from CI

Interactive login does not belong in a pipeline. Create a dedicated local account, mint a token for it, and pass credentials through the environment:

# once, as an admin
argocd account generate-token --account ci

# in the pipeline
export ARGOCD_SERVER=argocd.example.com
export ARGOCD_AUTH_TOKEN="$ARGOCD_TOKEN"
export ARGOCD_OPTS="--grpc-web"

argocd app sync payments-api --prune
argocd app wait payments-api --health --timeout 600

# fire and forget, when the pipeline should not block on the rollout
argocd app sync payments-api --async

A drift check makes a good scheduled job, and it works because app diff is designed for it:

if argocd app diff payments-api --refresh; then
  echo "cluster matches git"
else
  echo "drift detected" && exit 1
fi

Gotchas worth knowing before they find you

  • app diff exits non-zero when there is a diff. That is deliberate and useful for drift gates, but it means a set -e script will abort on a perfectly normal pending change. Branch on the exit code rather than letting the shell kill the job. Note that an error exits non-zero too, so a strict check should distinguish the codes.
  • Auto-prune deletes more than you expect. Prune removes anything in the destination that Argo CD tracks and Git no longer declares. Rename a resource, move a file out of the app’s path, or point an app at the wrong directory, and the old objects go away. Combine --auto-prune with the Prune=false annotation on anything stateful.
  • Self-heal reverts your emergency kubectl edit. That is the point, but during an incident it feels like the cluster is fighting you. Either commit the change or temporarily disable the sync policy — do not keep re-applying by hand.
  • Sync waves are how you get ordering. The annotation argocd.argoproj.io/sync-wave: "-1" runs a resource before wave 0, and Argo CD waits for each wave to be healthy before starting the next. Use it for CRDs before the operators that need them, and migrations before the deployment that depends on them (as a PreSync hook).
  • Apps stuck Progressing forever are usually a health-check problem. Argo CD derives health from well-known kinds and from Lua health checks for custom resources. A CRD with no health check never reports Healthy, so app wait --health will sit there until it times out.
  • Webhooks beat polling. The default reconcile interval is three minutes, which is a long time to stare at a pipeline. Point a repo webhook at /api/webhook and syncs start within seconds.

Closing

The commands are the easy part. The shift that actually pays off is giving up on imperative deploys: once Git is the only way state changes, "what is running in production" stops being a question anyone has to investigate. The CLI above is mostly there for the days when something has gone wrong and you want an answer without leaving the terminal.


Comments (0)

Be the first to comment.