DevOps from Zero to Hero: GitOps with ArgoCD

2026-05-30 | Gabriel Garrido | 20 min read
Share:

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 default project 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 argocd namespace, 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 main means 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.svc means the same cluster where ArgoCD is running.
  • spec.destination.namespace: The target namespace for the deployed resources.
  • syncPolicy.syncOptions: CreateNamespace=true tells 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 runs kustomize 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 runs kubectl scale deployment task-api --replicas=5 directly, 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.0 to v1.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.0 image.
  • 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: 0

Please sign in to be able to write comments.

2026-05-30 | Gabriel Garrido