Skip to content
Cloud & Infra Advanced Tutorial

GitOps on Kubernetes with ArgoCD: Declarative Deploys, Auto-Sync, and One-Click Rollbacks

Run ArgoCD on Kubernetes, manage multi-environment deployments with ApplicationSet, automate drift detection, and handle rollbacks correctly for controller-managed applications.

Emeka Okafor
Emeka Okafor
Security Editor · Jun 26, 2026 · 7 min read
GitOps on Kubernetes with ArgoCD: Declarative Deploys, Auto-Sync, and One-Click Rollbacks

What You'll Build

By the end of this tutorial, ArgoCD will be running on your cluster, syncing two environments (staging and production) from a single GitHub repo via ApplicationSet, with automated drift detection and a tested rollback path.

Prerequisites

  • A running Kubernetes cluster with cluster-admin. k3d, kind, EKS, GKE, and AKS all work.
  • kubectl 1.25+ configured against that cluster.
  • ArgoCD CLI 2.8+. On macOS: brew install argocd. On Linux, grab the binary from GitHub releases and put it on your PATH.
  • A GitHub repo containing Kubernetes manifests under manifests/staging/ and manifests/production/. Each directory needs at minimum a Deployment and a Service.

Commands below were validated against ArgoCD 2.9.

1. Install ArgoCD

kubectl create namespace argocd
kubectl apply -n argocd -f \
  https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

Wait for the rollout:

kubectl -n argocd wait --for=condition=available deployment --all --timeout=120s

Grab the initial admin password (flag introduced in v2.4; on older versions use kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d):

argocd admin initial-password -n argocd

Port-forward the server and log in. The tunnel needs a moment to bind, so add a short sleep before the login command:

kubectl port-forward svc/argocd-server -n argocd 8080:443 &
sleep 5
argocd login localhost:8080 --insecure --username admin --password <your-password>

--insecure skips TLS verification, which is fine for a local port-forward. In production, terminate TLS at an ingress or load balancer and drop that flag.

2. Connect Your GitHub Repo

Public repos need no credentials. For a private repo, add a fine-grained personal access token with Contents: Read permission:

argocd repo add https://github.com/yourorg/yourrepo \
  --username git \
  --password <your-github-token>

Verify the connection:

argocd repo list

The CONNECTION STATUS column should read Successful. If it shows Failed, the token scope is wrong or the URL has a mismatch (SSH vs HTTPS, trailing slash). See Troubleshooting below.

3. Deploy with Auto-Sync

Create an Application pointing at your staging manifests. Do this declaratively so ArgoCD's own config lives in Git:

# argocd-apps/staging.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app-staging
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/yourorg/yourrepo
    targetRevision: HEAD
    path: manifests/staging
  destination:
    server: https://kubernetes.default.svc
    namespace: staging
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
kubectl apply -f argocd-apps/staging.yaml

prune: true deletes resources removed from Git. selfHeal: true reconciles out-of-band changes, like someone running kubectl edit directly on the cluster. Both should be on for a real GitOps flow. ArgoCD polls your repo every 3 minutes by default; configure a GitHub webhook pointing at https://<argocd-server>/api/webhook to cut that to near-zero latency.

4. Multi-Environment Promotion with ApplicationSet

Maintaining a separate Application manifest per environment doesn't scale. ApplicationSet (part of ArgoCD core since v2.3) generates Application resources from a template plus a parameter list.

Before applying the ApplicationSet, delete the staging Application you created in step 3. The ApplicationSet controller can't adopt an existing Application it doesn't own, so leaving it in place will cause a conflict:

kubectl delete app my-app-staging -n argocd

Now apply the ApplicationSet:

# argocd-apps/appset.yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: my-app
  namespace: argocd
spec:
  generators:
  - list:
      elements:
      - env: staging
        revision: HEAD
      - env: production
        revision: v1.2.0
  template:
    metadata:
      name: "my-app-{{env}}"
    spec:
      project: default
      source:
        repoURL: https://github.com/yourorg/yourrepo
        targetRevision: "{{revision}}"
        path: "manifests/{{env}}"
      destination:
        server: https://kubernetes.default.svc
        namespace: "{{env}}"
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        syncOptions:
          - CreateNamespace=true
kubectl apply -f argocd-apps/appset.yaml

Staging always tracks HEAD. Production is pinned to the git tag v1.2.0. Promoting to production means cutting a new tag and updating the revision field in this file. Commit that change, and ArgoCD handles the rest. The revision separation is exactly what prevents staging changes from silently rolling into production.

For more realistic setups, replace the flat manifest directories with Kustomize overlays. ArgoCD's source block supports Kustomize natively with no extra config.

5. Rollback

You can inspect deployment history for any ArgoCD-managed application:

argocd app history my-app-production
ID  DATE                           REVISION
0   2024-01-15 09:23:11 +0000 UTC  v1.1.0
1   2024-01-20 14:05:44 +0000 UTC  v1.2.0

Here's the catch with ApplicationSet-managed applications: the ApplicationSet controller continuously reconciles every Application it owns. If you ran argocd app rollback my-app-production 0, the controller would immediately overwrite your change and push the application back to v1.2.0. The CLI rollback command simply doesn't work in this setup.

The correct approach is to update the revision in the ApplicationSet manifest itself. Edit appset.yaml, change the production element's revision from v1.2.0 to v1.1.0:

# appset.yaml — production element after edit
- env: production
  revision: v1.1.0

Then apply (or better, commit and push if ArgoCD is also managing its own config):

kubectl apply -f argocd-apps/appset.yaml

The ApplicationSet controller picks up the new revision and updates the generated Application. ArgoCD syncs it immediately. This change belongs in a commit, not a one-off kubectl apply: it gives you a clear audit trail and keeps the repo as the actual source of truth.

Once you understand the root cause, fix forward in Git. A rollback is just a deploy of an older known-good version, and it should go through the same change process as any other deploy.

Verify It Works

Push a change to a manifest in manifests/staging/ and either wait up to 3 minutes or force a sync:

argocd app sync my-app-staging

Expected output:

TIMESTAMP                  GROUP  KIND        NAMESPACE  NAME    STATUS  HEALTH
2024-01-20T14:00:00+00:00  apps   Deployment  staging    my-app  Synced  Healthy

Check both environments at once:

argocd app list

Both apps should show Synced and Healthy. OutOfSync means Git has diverged from the cluster state (expected briefly after a push). Degraded means the workload itself is unhealthy, which is a signal to check your pods, not ArgoCD.

Troubleshooting

ComparisonError: failed to get cluster info ArgoCD can't reach the destination cluster. For in-cluster deployments, https://kubernetes.default.svc is always correct. For remote clusters, register them first with argocd cluster add <kubectl-context-name> and use the URL ArgoCD assigns.

Sync stuck in Progressing / OutOfSync after applying Something in the cluster isn't becoming Healthy: a pending PVC, a failing readiness probe, an image that won't pull. Run argocd app get <name> --show-operation for the full resource diff, then kubectl describe on the stuck resource.

permission denied applying resources to a namespace The default ArgoCD project whitelists all destination namespaces. If you created a custom project, check its allowed destinations. The ArgoCD service account also needs RBAC permission in the target namespace; CreateNamespace=true in syncOptions only creates the namespace, it doesn't grant permissions.

Repo shows Unknown or Failed connection status Token is misconfigured or the repo URL doesn't match exactly. Run argocd repo get https://github.com/yourorg/yourrepo to see the raw error. Remove and re-add with argocd repo rm <url> followed by argocd repo add.

Next Steps

  • Secrets: ArgoCD deliberately doesn't manage secrets. Pair it with External Secrets Operator or Sealed Secrets to keep credentials out of Git while staying declarative.
  • Notifications: ArgoCD Notifications (built into v2.4+) posts Slack messages or fires webhooks on sync failures, health changes, or degraded status.
  • Progressive delivery: Argo Rollouts integrates directly with ArgoCD to add canary and blue-green strategies, driven by the same GitOps commit flow.
  • Multi-cluster at scale: Swap the list generator in your ApplicationSet for the cluster generator to fan deployments out across every registered cluster automatically.
Emeka Okafor
Written by
Emeka Okafor · Security Editor

Emeka has spent over a decade tracking threat actors, vulnerability disclosures, and the evolving landscape of application security, bringing a sharp continent-spanning perspective to his reporting. He's known for translating dense CVE advisories into clear, actionable context that developers and security teams alike actually read.

Discussion 0

Join the discussion

Sign in or create an account to comment and vote.

No comments yet

Be the first to weigh in.

Related Reading