Lesson 60 of 66 · 18 min read · Updated

GitOps with Argo CD

In the previous two articles you packaged the Catalog API with Helm and with Kustomize. In both cases a person, or a CI job, still ran helm upgrade or kubectl apply -k against the cluster. That has three weaknesses. The CI system needs cluster credentials. Nobody notices when someone runs kubectl edit in production at midnight. And the question "what is running right now?" has no reliable answer, because the cluster may have drifted from anything written down.

GitOps turns this around. Git becomes the single source of truth for what should run, and an agent inside the cluster continuously compares the cluster to Git and fixes any difference. Nobody pushes to the cluster; the cluster pulls from Git. Argo CD is the most widely used GitOps agent for Kubernetes.

In this article we install Argo CD, point it at a Git repository containing the Kustomize overlays and the Helm chart from the previous articles, and watch it deploy, detect drift, heal it, prune deleted resources, and generate one Application per environment automatically. To keep everything on your machine, the Git repository is served by a small Gitea instance inside the cluster. With GitHub or Azure Repos, only the repository URL changes.

We will cover the following topics:

  1. The GitOps model and how Argo CD implements it
  2. Installing Argo CD and the argocd CLI
  3. The Application resource: source, destination and sync policy
  4. Automated sync, prune and selfHeal, and what drift looks like
  5. Deploying Kustomize overlays and Helm charts from Git
  6. How Argo CD runs Helm hooks
  7. ApplicationSets for one Application per environment, and the app-of-apps pattern
  8. Rolling back the GitOps way

Technical Requirements

  1. A Kubernetes cluster and kubectl. The examples run on Docker Desktop's built-in Kubernetes with the kind provisioner (1 control-plane node and 3 workers, Kubernetes v1.36). A plain kind cluster with the same layout behaves the same.
  2. Argo CD v3.5.6 and the matching argocd CLI.
  3. git, and the catalog-api:1.0.0 and catalog-api:1.1.0 images from the Helm article. On a plain kind cluster, load them with kind load docker-image.
  4. The Kustomize folder from the previous article and the Helm chart from the one before it.
  5. About 1 GB of free memory in the cluster for Argo CD and Gitea.

Core Concepts

Push versus Pull Deployments

Push (CI runs kubectl/helm)Pull (GitOps agent)
Who changes the clusterThe CI pipelineAn agent running inside the cluster
Cluster credentialsStored in CINever leave the cluster
Drift (manual changes)Unnoticed until the next deploymentDetected within minutes, optionally reverted
Source of truthWhatever was applied lastThe Git repository
RollbackRe-run an old pipelinegit revert
Audit trailPipeline logsGit history: who, what, when, reviewed by whom

Your CI pipeline still builds and tests the .NET application and pushes the image. What changes is the last step: instead of deploying, CI (or a person) commits the new image tag to the deployment repository, and Argo CD does the rest.

Argo CD's Components

ComponentJob
argocd-serverAPI and web UI, used by the CLI and the browser
argocd-repo-serverClones Git repositories and renders manifests: plain YAML, Kustomize, Helm
argocd-application-controllerCompares rendered manifests with live objects, reports status, runs syncs
argocd-applicationset-controllerGenerates Applications from templates
argocd-redisCache
argocd-dex-serverOptional single sign-on

The repo-server renders Helm charts with helm template. Argo CD does not run helm install, so helm list shows nothing for charts that Argo CD manages. The release history lives in Git and in Argo CD, not in Helm Secrets.

The Application Resource

An Application is a custom resource in the argocd namespace that connects one source to one destination.

FieldMeaning
spec.source.repoURLGit repository (or Helm/OCI repository)
spec.source.targetRevisionBranch, tag or commit to follow, for example main
spec.source.pathFolder inside the repository. Argo CD detects Kustomize or Helm automatically.
spec.destinationWhich cluster and namespace to deploy to
spec.projectAn AppProject that limits which repositories, clusters and namespaces are allowed. default allows everything.
spec.syncPolicyManual or automated sync, plus options

Two statuses describe every Application and every resource in it.

StatusValuesQuestion it answers
Sync statusSynced, OutOfSyncDoes the live object match Git?
Health statusHealthy, Progressing, Degraded, Missing, SuspendedIs the object actually working? A Deployment is Healthy when its rollout completes.

Sync Policies

SettingEffect
No automated blockArgo CD only reports OutOfSync. A person clicks Sync or runs argocd app sync.
automated: {}A new commit in Git is applied automatically.
automated.prune: trueResources deleted from Git are deleted from the cluster. Off by default, as a safety measure.
automated.selfHeal: trueChanges made directly in the cluster are reverted to match Git.
syncOptions: [CreateNamespace=true]Creates the destination namespace if needed. It is not deleted with the Application.

Argo CD checks Git for new commits every 3 minutes by default (120 seconds plus up to 60 seconds of jitter). A webhook from GitHub or GitLab to argocd-server makes it near-instant. Drift in the cluster is noticed much faster, because the controller watches live objects.

ApplicationSets and App of Apps

Writing one Application per environment, per service, by hand does not scale. Two patterns help:

  1. ApplicationSet: a template plus a generator. The Git directory generator, for example, creates one Application for each folder that matches catalog/overlays/*. Add a folder, get an Application. Delete the folder, the Application and its resources go away. Other generators read a list, the registered clusters, pull requests, or combine several.
  2. App of apps: one parent Application whose source folder contains other Application manifests. Syncing the parent creates the children. It is the simplest way to bootstrap a whole cluster from one repository.

Hands-On: Kubernetes Commands

List Applications with their status

argocd app list

Show one Application, resource by resource

argocd app get catalog-prod

Ask Argo CD to check Git right now

Instead of waiting for the next poll.

argocd app get catalog-prod --refresh

Show the difference between Git and the cluster

Exits with code 1 when there is a difference.

argocd app diff catalog-prod

Sync manually

argocd app sync catalog-prod

Show the sync history

argocd app history catalog-prod

Applications are Kubernetes objects too

kubectl get applications -n argocd
kubectl get events -n argocd --field-selector involvedObject.name=catalog-prod

Step-by-Step Example

The Scenario

A deployment repository, catalog-deploy, holds everything needed to run the Catalog API: the Kustomize base, the dev and prod overlays, the PDB component, and the Helm chart. We install Argo CD, register the repository, and let Argo CD own the production namespace. Then we try to break it.

Step 1 — Install Argo CD

Pin the version instead of using the moving stable tag, so you know what you installed. The --server-side flag is required, because some Argo CD CRDs exceed the 262 KB annotation limit of client-side apply.

kubectl create namespace argocd
kubectl apply -n argocd --server-side --force-conflicts -f https://raw.githubusercontent.com/argoproj/argo-cd/v3.5.6/manifests/install.yaml
kubectl get pods -n argocd
NAME READY STATUS RESTARTS AGE
argocd-application-controller-0 1/1 Running 0 8m15s
argocd-applicationset-controller-58bb5c7479-28cpf 1/1 Running 0 8m16s
argocd-dex-server-dd6b85d9c-56k9h 1/1 Running 0 8m16s
argocd-notifications-controller-6b85c65458-n5rhn 1/1 Running 0 8m15s
argocd-redis-869b85489d-gzrbg 1/1 Running 0 8m15s
argocd-repo-server-5bf66565cd-5zgtt 1/1 Running 0 8m15s
argocd-server-7474bb8f76-kv752 1/1 Running 0 8m15s

Download the CLI for your platform from the release page, for example on an Apple Silicon Mac:

curl -sSL -o argocd https://github.com/argoproj/argo-cd/releases/download/v3.5.6/argocd-darwin-arm64
chmod +x argocd
./argocd version --client --short
argocd: v3.5.6+c2b1e30

On macOS brew install argocd works too, and on Windows download argocd-windows-amd64.exe.

Step 2 — Log In

Expose the API server and UI on your machine. Leave this running in its own terminal:

kubectl port-forward svc/argocd-server -n argocd 8080:443

Get the generated admin password and log in. --insecure is needed because the local server uses a self-signed certificate:

argocd admin initial-password -n argocd
argocd login localhost:8080 --username admin --password <password> --insecure
'admin:login' logged in successfully
Context 'localhost:8080' updated

Open https://localhost:8080 in a browser with the same credentials to see the UI. After you set a real password with argocd account update-password, delete the argocd-initial-admin-secret Secret.

Step 3 — A Git Server Inside the Cluster

If you already have a GitHub repository, skip this step and push the files there. To stay fully local, run Gitea in its own namespace. Its data lives in an emptyDir, which is fine for a lab and wrong for anything else.

apiVersion: v1
kind: Namespace
metadata:
name: gitea
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: gitea
namespace: gitea
spec:
replicas: 1
selector:
matchLabels:
app: gitea
template:
metadata:
labels:
app: gitea
spec:
containers:
- name: gitea
image: gitea/gitea:28.2-rootless
ports:
- name: http
containerPort: 3000
env:
- name: GITEA__security__INSTALL_LOCK
value: "true"
- name: GITEA__database__DB_TYPE
value: sqlite3
- name: GITEA__server__ROOT_URL
value: http://gitea.gitea.svc.cluster.local:3000/
- name: GITEA__service__DISABLE_REGISTRATION
value: "true"
volumeMounts:
- name: data
mountPath: /var/lib/gitea
- name: config
mountPath: /etc/gitea
volumes:
- name: data
emptyDir: {}
- name: config
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: gitea
namespace: gitea
spec:
selector:
app: gitea
ports:
- name: http
port: 3000
targetPort: http
kubectl apply -f gitea.yaml
kubectl -n gitea rollout status deploy/gitea

Create a user and an empty public repository through Gitea's API, using a second port-forward:

kubectl -n gitea exec deploy/gitea -- gitea admin user create --username gitops --password 'ChangeMe-123' --email [email protected] --admin --must-change-password=false
New user 'gitops' has been successfully created!

kubectl -n gitea port-forward svc/gitea 3000:3000

curl -u gitops:ChangeMe-123 -H 'Content-Type: application/json' -d '{"name":"catalog-deploy","private":false,"default_branch":"main"}' http://localhost:3000/api/v1/user/repos

Step 4 — Push the Deployment Repository

Lay out the repository with the Kustomize folder from the previous article under catalog/, and the Helm chart under charts/:

catalog-deploy/
catalog/
base/
components/pdb/
overlays/dev/
overlays/prod/
charts/
catalog-api/
catalog-api-values-dev.yaml

Check that the overlays set namespace: catalog-dev and namespace: catalog-prod, then commit and push:

git init -b main
git add .
git commit -m "Catalog API manifests"
git push http://gitops:ChangeMe-123@localhost:3000/gitops/catalog-deploy.git main

Argo CD runs inside the cluster, so it uses the Service's cluster DNS name, not localhost. Register the repository to confirm Argo CD can reach it. Public repositories work without this step, but it gives you a quick connectivity check, and private repositories need it to store credentials.

argocd repo add http://gitea.gitea.svc.cluster.local:3000/gitops/catalog-deploy.git
argocd repo list
TYPE NAME REPO INSECURE OCI LFS CREDS STATUS MESSAGE PROJECT
git http://gitea.gitea.svc.cluster.local:3000/gitops/catalog-deploy.git false false false false Successful

Step 5 — The Production Application

Write the Application as YAML and keep it in Git too. Creating it with argocd app create works, but then the definition only lives in the cluster.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: catalog-prod
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: http://gitea.gitea.svc.cluster.local:3000/gitops/catalog-deploy.git
targetRevision: main
path: catalog/overlays/prod
destination:
server: https://kubernetes.default.svc
namespace: catalog-prod
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true

The finalizer makes deleting the Application also delete the resources it deployed. Without it, deleting the Application leaves everything running. Kubernetes 1.36 prints a warning that it prefers finalizer names containing a /; the name above is the one Argo CD documents, and the warning is harmless.

kubectl apply -f app-prod.yaml
argocd app get catalog-prod
Name: argocd/catalog-prod
Project: default
Server: https://kubernetes.default.svc
Namespace: catalog-prod
URL: https://localhost:8080/applications/catalog-prod
Source:
- Repo: http://gitea.gitea.svc.cluster.local:3000/gitops/catalog-deploy.git
Target: main
Path: catalog/overlays/prod
SyncWindow: Sync Allowed
Sync Policy: Automated (Prune)
Sync Status: Synced to main (78e5ad8)
Health Status: Healthy

GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE
Service catalog-prod catalog-api Synced Healthy service/catalog-api unchanged
apps Deployment catalog-prod catalog-api Synced Healthy deployment.apps/catalog-api unchanged
ConfigMap catalog-prod catalog-config-hc622gmtk2 Synced
Secret catalog-prod catalog-db-hm4md8f9bf Synced
policy PodDisruptionBudget catalog-prod catalog-api Synced Healthy

Argo CD rendered the Kustomize overlay itself, including the hashed ConfigMap name. Nobody ran kubectl apply.

Step 6 — Drift and Self-Healing

Someone "temporarily" scales production down by hand:

kubectl -n catalog-prod scale deploy catalog-api --replicas=1

Watch the generation, the desired replicas and the ready replicas:

kubectl -n catalog-prod get deploy catalog-api -w -o custom-columns=GENERATION:.metadata.generation,SPEC:.spec.replicas,READY:.status.readyReplicas
GENERATION SPEC READY
2 1 3
3 3 3
3 3 3

Within about two seconds the Deployment was back at three replicas. The events on the Application tell you what happened:

kubectl get events -n argocd --field-selector involvedObject.name=catalog-prod
REASON MESSAGE
OperationStarted Initiated automated sync to '78e5ad8886f959f57868b5ac4aec17114eb2141b'
ResourceUpdated Updated sync status: Synced -> OutOfSync
OperationCompleted Partial sync operation to 78e5ad8886f959f57868b5ac4aec17114eb2141b succeeded
ResourceUpdated Updated sync status: OutOfSync -> Synced

Now turn self-healing off to see drift without the automatic fix. This edits the live Application; in a real setup you would change the YAML in Git.

kubectl -n argocd patch app catalog-prod --type merge -p '{"spec":{"syncPolicy":{"automated":{"selfHeal":false}}}}'
kubectl -n catalog-prod scale deploy catalog-api --replicas=1
argocd app get catalog-prod
...
Sync Status: OutOfSync from main (78e5ad8)
Health Status: Healthy

GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE
apps Deployment catalog-prod catalog-api OutOfSync Healthy deployment.apps/catalog-api configured
ConfigMap catalog-prod catalog-config-hc622gmtk2 Synced
Secret catalog-prod catalog-db-hm4md8f9bf Synced
Service catalog-prod catalog-api Synced Healthy
policy PodDisruptionBudget catalog-prod catalog-api Synced Healthy
argocd app diff catalog-prod
===== apps/Deployment catalog-prod/catalog-api ======
141c141
< replicas: 1
---
> replicas: 3

The Application is OutOfSync but still Healthy: one replica is running fine, it just is not what Git says. Sync by hand, then switch self-healing back on:

argocd app sync catalog-prod
...
Phase: Succeeded
Duration: 1s
Message: successfully synced (all tasks run)

kubectl -n argocd patch app catalog-prod --type merge -p '{"spec":{"syncPolicy":{"automated":{"selfHeal":true}}}}'

Without self-healing, drift is reported. With it, drift is reverted. Many teams start production with reporting only, and turn self-healing on once they trust the setup.

Step 7 — Deploy by Committing

Change the production greeting, and decide that production no longer needs the PodDisruptionBudget component. Both are edits to catalog/overlays/prod/kustomization.yaml:

configMapGenerator:
- name: catalog-config
behavior: merge
literals:
- Catalog__Greeting=Hello from production, deployed by Argo CD

and the components: block is removed. Commit and push:

git commit -am "Prod: new greeting, drop PDB"
git push http://gitops:ChangeMe-123@localhost:3000/gitops/catalog-deploy.git main

Instead of waiting up to three minutes for the next poll, ask for a refresh:

argocd app get catalog-prod --refresh
Sync Status: OutOfSync from main (3ad092c)
Health Status: Healthy

GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE
ConfigMap catalog-prod catalog-config-c92kgm255g OutOfSync Missing
ConfigMap catalog-prod catalog-config-hc622gmtk2 OutOfSync
Secret catalog-prod catalog-db-hm4md8f9bf Synced
Service catalog-prod catalog-api Synced Healthy
apps Deployment catalog-prod catalog-api OutOfSync Healthy
policy PodDisruptionBudget catalog-prod catalog-api OutOfSync Healthy

A few seconds later the automated sync has run:

argocd app get catalog-prod
Sync Status: Synced to main (3ad092c)
Health Status: Healthy

GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE
ConfigMap catalog-prod catalog-config-hc622gmtk2 Succeeded Pruned pruned
policy PodDisruptionBudget catalog-prod catalog-api Succeeded Pruned pruned
Secret catalog-prod catalog-db-hm4md8f9bf Synced secret/catalog-db-hm4md8f9bf configured
ConfigMap catalog-prod catalog-config-c92kgm255g Synced configmap/catalog-config-c92kgm255g created
Service catalog-prod catalog-api Synced Healthy service/catalog-api unchanged
apps Deployment catalog-prod catalog-api Synced Healthy deployment.apps/catalog-api configured

Read the first two lines carefully. Because prune: true is set, Argo CD deleted the PodDisruptionBudget that is no longer in Git, and the old hashed ConfigMap that kubectl apply -k left behind in the previous article. The leftover-ConfigMap problem is solved without ApplySets.

kubectl -n catalog-prod port-forward svc/catalog-api 8082:80
curl http://localhost:8082/
{"service":"catalog-api","version":"1.0.0","environment":"Production","greeting":"Hello from production, deployed by Argo CD","pod":"catalog-api-86f45577d8-k5kdb"}

argocd app history catalog-prod
ID DATE REVISION
0 2026-10-21 11:43:02 +0300 +03 main (78e5ad8)
1 2026-10-21 11:44:13 +0300 +03 main (78e5ad8)
2 2026-10-21 11:44:43 +0300 +03 main (3ad092c)

Every deployment maps to a commit, and the commit tells you who changed what and why.

Step 8 — A Helm Chart as the Source

Point an Application at the chart folder. Argo CD recognises Chart.yaml and renders it with Helm. valueFiles paths are relative to the chart folder and may point elsewhere in the same repository; valuesObject adds inline values on top. Here we turn the migration hook on for dev, to see how Argo CD handles it.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: catalog-helm-dev
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: http://gitea.gitea.svc.cluster.local:3000/gitops/catalog-deploy.git
targetRevision: main
path: charts/catalog-api
helm:
releaseName: catalog-api
valueFiles:
- ../catalog-api-values-dev.yaml
valuesObject:
migrations:
enabled: true
destination:
server: https://kubernetes.default.svc
namespace: catalog-dev
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
argocd app get catalog-helm-dev
...
Path: charts/catalog-api
Helm Values: ../catalog-api-values-dev.yaml
Sync Status: Synced to main (3ad092c)
Health Status: Healthy

GROUP KIND NAMESPACE NAME STATUS HEALTH HOOK MESSAGE
Namespace catalog-dev Running Synced namespace/catalog-dev created
batch Job catalog-dev catalog-api-migrate Succeeded Synced PreSync Reached expected number of succeeded pods
ConfigMap catalog-dev catalog-api Synced configmap/catalog-api created
Service catalog-dev catalog-api Synced Healthy service/catalog-api created
apps Deployment catalog-dev catalog-api Synced Healthy deployment.apps/catalog-api created

The Helm pre-install,pre-upgrade hook became an Argo CD PreSync hook: it runs before every sync, and the Deployment is only applied after it succeeds. And as promised, Helm itself knows nothing about this release:

helm list -n catalog-dev
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION

Argo CD also has its own hook annotations, such as argocd.argoproj.io/hook: PreSync, and sync waves (argocd.argoproj.io/sync-wave: "-1") that order resources within a sync. Use them for plain YAML and Kustomize sources, where Helm hooks are not available.

Delete both Applications before the next step. Thanks to the finalizer, their resources go with them:

kubectl -n argocd delete app catalog-helm-dev catalog-prod
kubectl get all -n catalog-prod
No resources found in catalog-prod namespace.

The namespaces themselves remain. CreateNamespace=true creates a namespace but never deletes it.

Step 9 — One Application per Environment with an ApplicationSet

Instead of one hand-written Application per overlay, let the Git directory generator find the overlays. {{.path.basename}} is the folder name, dev or prod.

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: catalog
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ["missingkey=error"]
generators:
- git:
repoURL: http://gitea.gitea.svc.cluster.local:3000/gitops/catalog-deploy.git
revision: main
directories:
- path: catalog/overlays/*
template:
metadata:
name: 'catalog-{{.path.basename}}'
spec:
project: default
source:
repoURL: http://gitea.gitea.svc.cluster.local:3000/gitops/catalog-deploy.git
targetRevision: main
path: '{{.path.path}}'
destination:
server: https://kubernetes.default.svc
namespace: 'catalog-{{.path.basename}}'
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
kubectl apply -f appset.yaml
argocd app list
NAME CLUSTER NAMESPACE PROJECT STATUS HEALTH SYNCPOLICY CONDITIONS PATH TARGET
argocd/catalog-dev https://kubernetes.default.svc catalog-dev default Synced Healthy Auto-Prune <none> catalog/overlays/dev main
argocd/catalog-prod https://kubernetes.default.svc catalog-prod default Synced Healthy Auto-Prune <none> catalog/overlays/prod main

Now add an environment the GitOps way. Copy the prod overlay to staging, change its namespace to catalog-staging, its label to staging and its greeting, and push:

cp -r catalog/overlays/prod catalog/overlays/staging
git add .
git commit -m "Add staging overlay"
git push http://gitops:ChangeMe-123@localhost:3000/gitops/catalog-deploy.git main

The ApplicationSet controller also polls Git every three minutes. In our test the new Application appeared three minutes after the push, created its namespace, and was Synced and Healthy:

kubectl get applications -n argocd
NAME SYNC STATUS HEALTH STATUS
catalog-dev Synced Healthy
catalog-prod Synced Healthy
catalog-staging Synced Healthy

Nobody wrote an Application for staging. The folder is the declaration.

Step 10 — Roll Back with git revert

In GitOps, a rollback is a commit. Undo the staging change:

git revert --no-edit HEAD
git push http://gitops:ChangeMe-123@localhost:3000/gitops/catalog-deploy.git main

At the next poll the generator no longer finds the staging folder, so the ApplicationSet deletes the catalog-staging Application. Generated Applications carry the resources finalizer, so the Deployment, Service, ConfigMap and Secret go with it:

kubectl get applications -n argocd
NAME SYNC STATUS HEALTH STATUS
catalog-dev Synced Healthy
catalog-prod Synced Healthy

kubectl get all -n catalog-staging
No resources found in catalog-staging namespace.

The same works for a bad release: git revert the commit that bumped the image tag, and Argo CD deploys the previous tag. Argo CD also has argocd app rollback, but it only works when automated sync is off, because otherwise the next sync would immediately re-apply what Git says.

Step 11 — Clean Up

Delete the ApplicationSet; its Applications and their resources follow:

kubectl -n argocd delete applicationset catalog

Uninstall Argo CD with the same manifest you installed it with, which also removes its CRDs and cluster roles, then delete the namespaces:

kubectl delete -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/v3.5.6/manifests/install.yaml
kubectl delete namespace argocd gitea catalog-dev catalog-prod catalog-staging

Summary

  1. GitOps makes Git the source of truth. An agent in the cluster pulls changes and keeps the cluster matching Git; CI no longer needs cluster credentials.
  2. Install Argo CD with a pinned version and kubectl apply --server-side. Argo CD v3.5.6 runs an API server, a repo-server that renders manifests, an application controller, and an ApplicationSet controller.
  3. An Application connects a repository path to a destination namespace. Argo CD detects plain YAML, Kustomize and Helm automatically, and renders Helm with helm template, so helm list shows nothing.
  4. Sync status says whether the cluster matches Git; health status says whether it works.
  5. automated deploys new commits, prune deletes what was removed from Git (including stale hashed ConfigMaps), and selfHeal reverts manual changes within seconds.
  6. Git is polled every 3 minutes by default; use --refresh or a webhook for faster feedback.
  7. Helm hooks become Argo CD PreSync hooks; sync waves order resources for non-Helm sources.
  8. An ApplicationSet with a Git directory generator creates one Application per folder. Adding or deleting a folder adds or removes an environment.
  9. Roll back with git revert. The resources finalizer makes deleting an Application delete its resources, but namespaces created with CreateNamespace=true stay.

That completes the Packaging and GitOps section: Helm to package your services, Kustomize to describe your environments, and Argo CD to make Git the only way anything reaches the cluster.