DevOps from Zero to Hero: GitOps with ArgoCD
Support this blog
If you find this content useful, consider supporting the blog.
Introduction
Welcome to article fourteen of the DevOps from Zero to Hero series. In the previous article we learned
how to deploy our TypeScript API to an EKS cluster. We ran kubectl apply and helm install commands
to get things running, and that works fine when you are the only person deploying to a single cluster.
But what happens when your team grows, when you have multiple environments, or when someone applies a
quick fix directly in the cluster and forgets to update the YAML files in Git?
That is where GitOps comes in. GitOps is a way of managing your Kubernetes deployments where Git is the single source of truth. Instead of running commands against the cluster, you push changes to a Git repository and a controller inside the cluster picks them up and applies them automatically. No more wondering what is running where. No more manual drift. Everything is tracked, reviewed, and reproducible.
ArgoCD is the most popular GitOps tool for Kubernetes. It is a CNCF graduated project with an excellent web UI, a powerful CLI, and native support for Helm, Kustomize, and plain YAML. In this article we will install ArgoCD on our EKS cluster, deploy our TypeScript API through it, and learn how the whole sync and reconciliation loop works.
If you are already comfortable with GitOps and want to learn about advanced patterns like ApplicationSets, App of Apps, sync waves, multi-cluster management, RBAC, and notifications, check out GitOps with ArgoCD from the SRE series. This article stays beginner-friendly and focuses on getting you from zero to a working GitOps setup.
Let’s get into it.
What is GitOps?
GitOps is an operational model for Kubernetes where you declare what you want running in your cluster in a Git repository, and a controller running inside the cluster continuously makes sure the real state matches the declared state. If someone changes something manually or if a pod crashes and gets recreated with different settings, the controller detects the drift and fixes it.
This is different from the traditional CI/CD approach where a pipeline runs kubectl apply or
helm upgrade at the end of a build. With that push-based model, the CI system needs credentials to
your cluster, drift goes undetected, and there is no easy way to know exactly what is running right
now. With GitOps, the flow is reversed: the controller lives inside the cluster, pulls the desired
state from Git, and handles the apply step itself.
# Traditional push-based CI/CD:
# Developer -> Git push -> CI builds -> CI runs kubectl apply -> Cluster
# (CI needs cluster credentials)
# (manual changes go undetected)
# Pull-based GitOps:
# Developer -> Git push -> Controller detects change -> Controller applies -> Cluster
# (controller lives in the cluster)
# (drift is detected and corrected automatically)
GitOps principles
There are four core principles that define a GitOps workflow:
- Declarative: Your entire system is described as YAML or JSON files in Git. No imperative scripts, no manual steps, no one-off commands. You declare what you want, not how to get there.
- Versioned and immutable: Every change goes through Git, which means every change is versioned, has an author, has a timestamp, and can be reviewed in a pull request. You get a full audit trail for free.
- Pulled automatically: A controller running in your cluster watches the Git repository and pulls changes as they appear. You do not push to the cluster. This is more secure because cluster credentials never leave the cluster.
- Continuously reconciled: The controller does not apply changes once and forget about them. It runs a loop that constantly compares the live state with the desired state. If they differ for any reason, it corrects the drift.
The big win here is that Git becomes the single source of truth. If you want to know what is running in your cluster, look at Git. If you want to roll back, revert a commit. If you want to audit who changed what and when, check the Git history. Everything flows through the same process: commit, push, review, merge, and the controller takes care of the rest.
Why ArgoCD
There are several GitOps tools out there (Flux is another popular one), but ArgoCD has become the go-to choice for most teams. Here is why:
- CNCF graduated: ArgoCD is a graduated project in the Cloud Native Computing Foundation, which means it has passed rigorous security audits and has a large, active community.
- Great web UI: ArgoCD ships with a dashboard where you can see every application, its sync status, health status, and the resource tree. This is incredibly helpful for debugging and for giving visibility to the whole team.
- Kubernetes-native: ArgoCD uses Custom Resource Definitions (CRDs) to define applications. You manage ArgoCD itself with the same tools you use for everything else in Kubernetes.
- Multi-format support: ArgoCD works with plain YAML manifests, Helm charts, Kustomize overlays, Jsonnet, and custom plugins. You do not have to change how you write your manifests.
- CLI and API: Beyond the UI, ArgoCD has a full CLI and a gRPC/REST API for automation and scripting.
Installing ArgoCD on EKS
We are going to install ArgoCD on the EKS cluster we set up in the previous article. The recommended way is using Helm. First, add the Argo Helm repository and create the namespace:
# Add the ArgoCD Helm repository
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update
# Create the argocd namespace
kubectl create namespace argocd
Now create a values file to configure the installation. We will keep it simple for now:
# argocd-values.yaml
configs:
params:
# If you are terminating TLS at the load balancer or ingress,
# set this so ArgoCD does not try to handle TLS itself
server.insecure: true
server:
service:
type: LoadBalancer
This is a minimal configuration. We set server.insecure: true because in a typical EKS setup you
terminate TLS at the load balancer or ingress controller level. We also set the service type to
LoadBalancer so you can access the UI from your browser.
Install ArgoCD with Helm:
helm install argocd argo/argo-cd \
--namespace argocd \
--values argocd-values.yaml \
--wait
# Check that all pods are running
kubectl get pods -n argocd
You should see something like this:
NAME READY STATUS RESTARTS AGE
argocd-application-controller-0 1/1 Running 0 2m
argocd-repo-server-6b7f8d7b4-x9k2l 1/1 Running 0 2m
argocd-server-7c4f8b6d9-m3n8p 1/1 Running 0 2m
argocd-redis-5b6c7d8e9-q4r7s 1/1 Running 0 2m
argocd-applicationset-controller-8f9a1b2c3-t5u6v 1/1 Running 0 2m
argocd-notifications-controller-4d5e6f7a8-w9x0y 1/1 Running 0 2m
Now get the initial admin password and the load balancer URL:
# Get the initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath="{.data.password}" | base64 -d
# Save this password, you will need it to log in
# Get the load balancer URL
kubectl -n argocd get svc argocd-server \
-o jsonpath="{.status.loadBalancer.ingress[0].hostname}"
Open that URL in your browser and log in with username admin and the password you just retrieved.
You should see the ArgoCD dashboard with no applications yet. We will create one shortly.
You can also install the ArgoCD CLI for managing things from the terminal:
# macOS
brew install argocd
# Linux
curl -sSL -o argocd https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
chmod +x argocd
sudo mv argocd /usr/local/bin/
# Log in to your ArgoCD instance
argocd login <load-balancer-url> --username admin --password <your-password> --insecure
Once logged in, change the default password:
argocd account update-password
ArgoCD concepts
Before we create our first application, let’s understand the key concepts. ArgoCD has a handful of building blocks that you will use all the time:
- Application: The fundamental unit in ArgoCD. An Application defines a source (a Git repository with manifests or a Helm chart), a destination (a Kubernetes cluster and namespace), and a sync policy. Each Application represents one deployable unit.
- Project: A logical grouping of Applications with access controls. Projects define which repositories and clusters an Application can use. The
defaultproject allows everything, which is fine for getting started.- Repository: A Git repository that ArgoCD watches. You register repositories with ArgoCD so it knows where to pull manifests from. Public repositories work out of the box. Private repositories need credentials.
- Sync: The process of applying the desired state from Git to the cluster. When ArgoCD detects a difference between what is in Git and what is running in the cluster, it can sync (apply the changes) either automatically or when you click a button.
- Health: ArgoCD understands Kubernetes resource health. A Deployment is healthy when all replicas are available. A Pod is healthy when it is running and ready. A Service is always healthy. ArgoCD shows you the health of every resource in your application.
These five concepts cover 90% of what you need to work with ArgoCD day to day. Let’s put them together by creating our first Application.
Setting up a GitOps repository
The first thing you need is a Git repository that contains your Kubernetes manifests. This is the repository ArgoCD will watch. You can use the same repository as your application code, but the common practice is to have a separate repository for deployment manifests. This separation makes the workflow cleaner: application code changes trigger CI builds that produce new images, and deployment manifest changes trigger ArgoCD syncs.
Let’s create a simple GitOps repository structure:
# Create and initialize the repository
mkdir gitops-repo && cd gitops-repo
git init
mkdir -p apps/task-api
Now create the Kubernetes manifests for our TypeScript API. We will use plain YAML to keep things simple, but remember that ArgoCD also supports Helm charts (which we built in article twelve).
# apps/task-api/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: task-api
# apps/task-api/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: task-api
namespace: task-api
labels:
app: task-api
spec:
replicas: 2
selector:
matchLabels:
app: task-api
template:
metadata:
labels:
app: task-api
spec:
containers:
- name: task-api
image: ghcr.io/your-org/task-api:v1.0.0
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: production
- name: PORT
value: "3000"
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 15
periodSeconds: 20
# apps/task-api/service.yaml
apiVersion: v1
kind: Service
metadata:
name: task-api
namespace: task-api
spec:
selector:
app: task-api
ports:
- port: 80
targetPort: 3000
type: ClusterIP
Commit and push these files to your Git repository:
git add .
git commit -m "Add task-api manifests"
git remote add origin https://github.com/your-org/gitops-repo.git
git push -u origin main
Creating your first ArgoCD Application
Now let’s tell ArgoCD about our application. You can do this through the UI, the CLI, or by applying a YAML manifest. We will use the YAML manifest approach because it is declarative, versionable, and follows the GitOps philosophy.
# application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: task-api
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/your-org/gitops-repo
targetRevision: main
path: apps/task-api
destination:
server: https://kubernetes.default.svc
namespace: task-api
syncPolicy:
syncOptions:
- CreateNamespace=true
Let’s break down what each field means:
- metadata.namespace: Applications always live in the
argocdnamespace, regardless of where they deploy resources.- spec.project: We use
default, which has no restrictions. In a real team setup you would create dedicated projects with scoped access.- spec.source.repoURL: The Git repository ArgoCD watches.
- spec.source.targetRevision: The branch, tag, or commit to track. Using
mainmeans ArgoCD follows the tip of the main branch.- spec.source.path: The directory inside the repository that contains the manifests.
- spec.destination.server: The Kubernetes API server to deploy to.
https://kubernetes.default.svcmeans the same cluster where ArgoCD is running.- spec.destination.namespace: The target namespace for the deployed resources.
- syncPolicy.syncOptions:
CreateNamespace=truetells ArgoCD to create the namespace if it does not exist.
Apply it:
kubectl apply -f application.yaml
If you open the ArgoCD UI now, you will see the task-api application. Its status will be
OutOfSync because we have not synced it yet. Let’s do that.
The sync loop: how ArgoCD detects drift and reconciles
ArgoCD runs a reconciliation loop every three minutes by default. Here is what happens during each cycle:
- Step 1: The Application Controller reads the Application CRD and asks the Repository Server to clone the Git repo and render the manifests from the specified path.
- Step 2: The Repository Server fetches the latest commit from the branch (or tag), reads the YAML files, and returns the rendered manifests. If you are using Helm, it runs
helm template. If you are using Kustomize, it runskustomize build.- Step 3: The Application Controller compares the rendered manifests with the live state of the resources in the cluster. It does a field-by-field comparison to detect any differences.
- Step 4: If there are differences, ArgoCD marks the application as OutOfSync and shows you exactly what changed. Depending on your sync policy, it either waits for you to manually trigger a sync or applies the changes automatically.
Reconciliation loop (every 3 minutes):
Git repository ArgoCD Kubernetes cluster
┌──────────────┐ ┌───────────────┐ ┌──────────────────┐
│ YAML files │───>│ Repo Server │ │ Live resources │
│ (desired │ │ (renders │ │ (actual state) │
│ state) │ │ manifests) │ │ │
└──────────────┘ └───────┬───────┘ └────────┬─────────┘
│ │
v │
┌───────────────┐ │
│ App Controller │<────────────────┘
│ (compares │
│ desired vs │
│ actual) │
└───────┬───────┘
│
OutOfSync? ──> Sync (apply changes)
Synced? ──> Do nothing
This continuous loop is what makes GitOps powerful. If someone runs kubectl edit and changes a
replica count directly in the cluster, ArgoCD will detect the drift and either alert you or fix it
automatically (depending on your configuration).
Manual sync vs auto-sync
When we created our Application above, we did not enable auto-sync. This means ArgoCD will detect changes but wait for you to manually trigger the sync. Let’s do our first manual sync:
# Sync using the CLI
argocd app sync task-api
# Or you can click the "Sync" button in the ArgoCD UI
ArgoCD will apply all the manifests from the Git repository to the cluster. You can watch the progress in the UI or with the CLI:
# Watch the sync progress
argocd app get task-api
# Check that the pods are running
kubectl get pods -n task-api
After the sync completes, the application status should show Synced and Healthy. Now let’s talk about when to use manual sync versus auto-sync.
Manual sync is good for:
- Production environments where you want a human to review and approve every deployment.
- Initial setup when you are getting comfortable with ArgoCD and want to see what it will do before it does it.
- Sensitive applications where you need an extra layer of control.
Auto-sync is good for:
- Development and staging environments where you want changes to be applied as soon as they are merged to the main branch.
- Infrastructure components that should always match what is in Git (monitoring, logging, ingress controllers).
- Teams that have a solid review process and trust that anything merged to main is ready to deploy.
To enable auto-sync, update the Application manifest:
# application.yaml (with auto-sync enabled)
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: task-api
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/your-org/gitops-repo
targetRevision: main
path: apps/task-api
destination:
server: https://kubernetes.default.svc
namespace: task-api
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
The two new fields under automated are important:
- prune: When set to
true, ArgoCD will delete resources from the cluster that no longer exist in Git. If you remove a ConfigMap from your Git repository, ArgoCD removes it from the cluster too. Without this, deleted resources would linger forever.- selfHeal: When set to
true, ArgoCD will revert any manual changes made to the cluster. If someone runskubectl scale deployment task-api --replicas=5directly, ArgoCD will detect the drift and set it back to whatever is declared in Git.
Apply the updated manifest:
kubectl apply -f application.yaml
From now on, every time you push a change to the apps/task-api directory in the main branch,
ArgoCD will automatically apply it to the cluster within three minutes (or sooner if you configure
a webhook).
Deploying the TypeScript API with a Helm chart
In article twelve we created a Helm chart for our TypeScript API. ArgoCD has native Helm support, so you can point an Application directly at a Helm chart in a Git repository. Let’s set that up.
Assuming your GitOps repository has the Helm chart at charts/task-api/, create an Application
that uses it:
# application-helm.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: task-api-helm
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/your-org/gitops-repo
targetRevision: main
path: charts/task-api
helm:
releaseName: task-api
valueFiles:
- values-production.yaml
destination:
server: https://kubernetes.default.svc
namespace: task-api
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
The spec.source.helm section is where the Helm-specific configuration goes. releaseName is the
name Helm uses for the release, and valueFiles points to a values file relative to the chart
directory. You can also inline values directly:
helm:
releaseName: task-api
values: |
replicaCount: 3
image:
repository: ghcr.io/your-org/task-api
tag: v1.2.0
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi
This is how most teams handle deployments in practice: the Helm chart lives in the GitOps repository (or in an OCI registry), and ArgoCD renders and applies it. To deploy a new version, you update the image tag in the values file, commit, push, and ArgoCD takes care of the rest.
Navigating the ArgoCD UI
The ArgoCD web UI is one of its biggest selling points. Let’s walk through what you will see.
Application list view: This is the main page. You see all your applications as cards, each showing the application name, sync status (Synced, OutOfSync, Unknown), health status (Healthy, Degraded, Progressing, Missing), the target revision, and the last sync time. Green means everything is fine. Yellow means something is progressing. Red means something is wrong.
Application detail view: Click on an application to see its resource tree. This is a visual representation of every Kubernetes resource managed by the application. For our task-api, you would see the Deployment, which owns a ReplicaSet, which owns the individual Pods. The Service is shown as a separate node. Each resource shows its health status with a colored icon.
Resource diff view: Click on any resource to see its details. The “Diff” tab shows you exactly what is different between the desired state (from Git) and the live state (in the cluster). This is extremely helpful for debugging sync issues.
Sync status bar: At the top of the detail view, you see the current sync status and a “Sync” button. If the application is OutOfSync, you can click Sync to trigger a manual sync. You can also choose to sync specific resources instead of the entire application.
History and rollback: The “History” tab shows every sync operation with the Git commit that triggered it, the time it happened, and whether it succeeded or failed. You can roll back to any previous sync from here.
Rollback: reverting to a previous state
Things go wrong. A bad image gets deployed, a configuration change breaks something, or a new version has a bug. With GitOps, you have two ways to roll back.
The GitOps way (recommended): Revert the commit in Git. This is the cleanest approach because it keeps Git as the source of truth and creates an audit trail of the rollback:
# Revert the last commit
git revert HEAD --no-edit
git push
# ArgoCD detects the change and syncs automatically (if auto-sync is enabled)
# Or trigger a manual sync:
argocd app sync task-api
The ArgoCD way (for emergencies): Use the ArgoCD CLI or UI to roll back to a previous sync. This is faster but has a caveat: it does not change Git, so if auto-sync is enabled, ArgoCD will eventually re-sync to the latest Git state and undo your rollback:
# View sync history
argocd app history task-api
# Example output:
# ID DATE REVISION
# 3 2026-05-30 10:15:00 abc1234 (main)
# 2 2026-05-29 14:30:00 def5678 (main)
# 1 2026-05-28 09:00:00 ghi9012 (main)
# Roll back to sync ID 2
argocd app rollback task-api 2
If you use the ArgoCD rollback, make sure to also disable auto-sync first, or the controller will re-apply the latest Git state and undo your rollback:
# Disable auto-sync before rolling back
argocd app set task-api --sync-policy none
# Roll back
argocd app rollback task-api 2
# Fix the issue in Git, then re-enable auto-sync
argocd app set task-api --sync-policy automated --self-heal --auto-prune
The key takeaway is that git revert is the preferred way to roll back in a GitOps workflow. It
keeps everything consistent and leaves a clear record of what happened and why.
A typical GitOps workflow
Let’s put it all together and walk through what a typical deployment looks like end to end:
- Step 1: A developer opens a pull request that changes the image tag in the deployment manifest (or the Helm values file) from
v1.0.0tov1.1.0.- Step 2: The team reviews the change. Because it is just a YAML diff in a pull request, it is easy to see exactly what will change in the cluster.
- Step 3: The pull request is merged to main.
- Step 4: ArgoCD detects the new commit within three minutes (or immediately if you have a webhook configured). It compares the new desired state with the live state and finds that the image tag differs.
- Step 5: If auto-sync is enabled, ArgoCD applies the change. The Deployment gets updated, Kubernetes performs a rolling update, and the new pods come up with the
v1.1.0image.- Step 6: ArgoCD marks the application as Synced and Healthy once all pods are running and passing readiness checks.
- Step 7: If something goes wrong, the team reverts the commit in Git and ArgoCD rolls back automatically.
This workflow gives you code review for infrastructure changes, a full audit trail in Git, automatic deployment, automatic drift detection, and easy rollback. That is a lot of value for a relatively simple setup.
Advanced topics: where to go next
Once you are comfortable with the basics covered here, there is a lot more ArgoCD can do. Here is a quick overview of advanced topics:
- App of Apps pattern: Instead of creating Application manifests one by one, you create a parent Application that manages child Applications. This lets you bootstrap an entire cluster with a single Application.
- ApplicationSets: A way to generate multiple Applications from a single template. Useful for deploying the same application across multiple clusters or environments automatically.
- Sync waves and hooks: Control the order in which resources are applied. For example, you can ensure that a database migration Job runs before the Deployment starts.
- RBAC and SSO: Restrict who can see and sync which applications. Integrate with your identity provider for single sign-on.
- Notifications: Send alerts to Slack, email, or other channels when syncs succeed or fail.
All of these topics are covered in depth in GitOps with ArgoCD from the SRE series. That article goes into ApplicationSet generators, sync wave annotations, RBAC policies with AppProjects, notification templates, monitoring ArgoCD with Prometheus, and more. Once you have the basics down from this article, that is a great next step.
Closing notes
GitOps with ArgoCD gives you a deployment workflow that is declarative, versioned, automated, and auditable. Instead of running commands against your cluster and hoping everyone follows the same process, you push changes to Git and let ArgoCD handle the rest. Every change is reviewed in a pull request, tracked in Git history, and automatically applied to the cluster.
In this article we covered what GitOps is and why it matters, installed ArgoCD on an EKS cluster with Helm, learned the core concepts (Application, Project, Sync, Health), created our first Application pointing at a Git repository, understood the reconciliation loop and how ArgoCD detects drift, compared manual sync and auto-sync and when to use each, deployed our TypeScript API using both plain manifests and a Helm chart, explored the ArgoCD UI, and learned how to roll back safely.
The next article will cover monitoring and observability, because deploying applications is only half the battle. You also need to know if they are healthy and performing well.
Hope you found this useful and enjoyed reading it, until next time!
Errata
If you spot any error or have any suggestion, please send me a message so it gets fixed.
Also, you can check the source code and changes in the sources here
$ Comments
Online: 0Please sign in to be able to write comments.