As Kubernetes environments grow, manually running deployment commands becomes harder to manage. Teams need a clear way to understand what should be running, what has changed and whether the cluster still matches the intended configuration.

After working with CI/CD pipelines, Terraform, Kubernetes and Python automation, I started looking more closely at how deployment state itself could be managed in a more predictable way.

This led me towards GitOps.

The idea is simple: Git becomes the source of truth for application deployment configuration, while a GitOps controller continuously compares the desired state in Git with the actual state running inside Kubernetes.

In this article, I will explore how I use Argo CD to create a safer and more controlled Kubernetes deployment workflow.

GitOps Flow

Developer

Git Repository

Kubernetes Manifests

Argo CD

Kubernetes Cluster

Continuous Reconciliation

The Problem with Direct Kubernetes Deployments

A traditional CI/CD pipeline may build an application and then run:

kubectl apply -f deployment.yaml

This works, but over time several questions appear.

Direct deployment commands can gradually make the CI/CD system responsible for both building software and controlling cluster state.

GitOps separates these responsibilities more clearly.

What Is GitOps?

GitOps is an operational model where the desired state of a system is stored in Git.

For Kubernetes, this means deployment configuration can be defined through files such as:

A GitOps controller watches the repository and compares the configuration in Git with the resources running inside Kubernetes.

Git

Desired State

Argo CD

Kubernetes
Actual State

Compare + Reconcile

Why I Like the GitOps Model

The biggest advantage for me is visibility.

If Git contains the desired configuration, I can review:

This creates an auditable deployment history.

My view: GitOps changes the deployment conversation from "Which command was run?" to "Which configuration change was approved and committed?"

Separating Application Code from Deployment Configuration

One GitOps pattern I find useful is separating the application source repository from the deployment configuration repository.

For example:

cloud-native-api/
├── src/
├── tests/
├── Dockerfile
└── Jenkinsfile

cloud-native-api-gitops/
├── dev/
├── test/
└── prod/

The application repository focuses on:

The GitOps repository focuses on:

Creating the Kubernetes Manifest

Our application Deployment might look like this:

apiVersion: apps/v1
kind: Deployment

metadata:
  name: cloud-native-api

spec:
  replicas: 2

  selector:
    matchLabels:
      app: cloud-native-api

  template:
    metadata:
      labels:
        app: cloud-native-api

    spec:
      containers:
        - name: cloud-native-api
          image: myregistry/cloud-native-api:1.0.0

          ports:
            - containerPort: 8080

          readinessProbe:
            httpGet:
              path: /health/ready
              port: 8080

          livenessProbe:
            httpGet:
              path: /health/live
              port: 8080

The important part is that this configuration is committed to Git rather than being generated manually during deployment.

Creating an Argo CD Application

Argo CD needs to know which Git repository contains the desired state and where that configuration should be deployed.

An Argo CD Application can look like this:

apiVersion: argoproj.io/v1alpha1
kind: Application

metadata:
  name: cloud-native-api
  namespace: argocd

spec:
  project: default

  source:
    repoURL: https://git.example.com/cloud-native-api-gitops.git
    targetRevision: main
    path: prod

  destination:
    server: https://kubernetes.default.svc
    namespace: cloud-native-api

  syncPolicy:
    automated:
      prune: true
      selfHeal: true

    syncOptions:
      - CreateNamespace=true

This configuration tells Argo CD:

Understanding Automated Synchronisation

The configuration:

syncPolicy:
  automated:
    prune: true
    selfHeal: true

enables automated reconciliation.

Self Heal

If someone manually changes a managed Kubernetes resource and that resource no longer matches Git, Argo CD can restore the desired state.

Git Says 2 Replicas

Someone Manually Changes Cluster to 5

Argo CD Detects Drift

Reconciles Back to 2

Prune

If a managed resource is removed from Git, pruning allows Argo CD to remove that resource from the Kubernetes environment during synchronisation.

Important: automated pruning and self-healing are powerful features. I would introduce them carefully and test them before enabling them for critical production applications.

Understanding Sync Status

Argo CD continuously compares desired and actual state.

One of the important statuses is:

Synced
The Kubernetes resources match the desired configuration stored in Git.

Another status is:

OutOfSync
The running Kubernetes resources differ from the configuration stored in Git.

This makes configuration drift much easier to identify.

Health Status Is Different from Sync Status

One useful distinction in Argo CD is that synchronisation and application health are not the same thing.

An application may be:

Synced + Healthy
Git and Kubernetes match, and the application is operating correctly.

Synced + Degraded
Git and Kubernetes match, but the application itself has a problem.

OutOfSync + Healthy
The application may still be running, but the cluster does not match the intended configuration.

This separation is useful because successful deployment configuration does not automatically mean the application itself is healthy.

Updating an Application Version

Suppose the application currently runs:

image: myregistry/cloud-native-api:1.0.0

A new pipeline produces:

myregistry/cloud-native-api:1.1.0

Instead of the pipeline directly deploying version 1.1.0 to Kubernetes, the GitOps repository can be updated:

image: myregistry/cloud-native-api:1.1.0

Once that change is merged, Argo CD detects the new desired state and synchronises Kubernetes.

CI Pipeline Builds 1.1.0

GitOps Repository Updated

Pull Request Review

Merge

Argo CD Detects Change

Kubernetes Rolls Out 1.1.0

CI and GitOps Have Different Responsibilities

This separation became one of the most useful concepts for me.

CI can focus on creating a deployable artifact:

Source Code

Build

Test

Security Checks

Container Image

GitOps can focus on deployment state:

Container Version

Git Configuration

Review and Approval

Argo CD

Kubernetes

CI builds the software.

GitOps manages how the approved software configuration reaches the environment.

Managing Multiple Environments

A GitOps repository can also represent multiple environments.

For example:

gitops/
│
├── dev/
│   ├── deployment.yaml
│   └── service.yaml
│
├── test/
│   ├── deployment.yaml
│   └── service.yaml
│
└── prod/
    ├── deployment.yaml
    └── service.yaml

Each Argo CD Application can point to a different folder.

Git Repository

dev → Development Cluster
test → Test Cluster
prod → Production Cluster

Reducing Duplication with Kustomize

Copying complete manifests for each environment can eventually create duplication.

One approach is using Kustomize with a base configuration and environment overlays.

gitops/
│
├── base/
│   ├── deployment.yaml
│   ├── service.yaml
│   └── kustomization.yaml
│
└── overlays/
    ├── dev/
    │   └── kustomization.yaml
    │
    ├── test/
    │   └── kustomization.yaml
    │
    └── prod/
        └── kustomization.yaml

The base contains shared application configuration.

The overlays contain environment-specific differences.

Example Production Overlay

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - ../../base

replicas:
  - name: cloud-native-api
    count: 4

images:
  - name: myregistry/cloud-native-api
    newTag: 1.1.0

Development may use two replicas while production uses four.

The underlying application structure remains consistent.

Rollback Becomes a Git Operation

One of the GitOps concepts I find powerful is that rollback can become a configuration change.

Suppose:

Version 1.0.0 = Stable
Version 1.1.0 = Problem

The Git configuration can be reverted from:

newTag: 1.1.0

back to:

newTag: 1.0.0

Once the change is merged, Argo CD reconciles the environment back to the previous desired state.

GitOps Does Not Remove the Need for Testing

GitOps provides deployment control, but it does not automatically prove that a release is correct.

I still want the delivery process to include:

GitOps controls how the desired state reaches Kubernetes.

It does not replace software quality practices.

Keeping Secrets Out of Git

One thing GitOps does not mean is storing every value directly inside a Git repository.

I would never recommend storing passwords or API tokens like this:

DATABASE_PASSWORD: "ProductionPassword123"

Git should contain desired configuration, but sensitive data requires additional protection.

Depending on the environment, secrets can be handled through approaches such as:

GitOps and Platform Engineering

This is where I started seeing the connection between GitOps and platform engineering.

Developers should not always need deep Kubernetes knowledge to release an application.

A platform team can create reusable deployment patterns while application teams work through a simpler interface.

Developer

Application Change

CI Pipeline

Container Image

GitOps Configuration

Argo CD

Kubernetes Platform

The developer interacts with a controlled delivery path rather than manually changing cluster resources.

From Pipelines to Continuous Reconciliation

Traditional pipelines are event-driven.

A pipeline runs, performs actions and then finishes.

Commit

Pipeline Starts

Deployment Runs

Pipeline Ends

GitOps introduces a different idea.

The controller continuously checks whether reality still matches the desired state.

Git Desired State

Continuous Comparison

Kubernetes Actual State

That concept of continuous reconciliation is one of the areas that made GitOps feel different from traditional deployment automation to me.

A More Mature Delivery Workflow

Developer

Application Repository

CI Build + Tests + Security Checks

Container Registry

GitOps Repository Update

Pull Request Review

Merge

Argo CD Reconciliation

Kubernetes Deployment

Health and Observability

What I Learned from GitOps

One of the biggest lessons for me was that deployment automation is not only about making releases faster.

It is also about making change easier to understand.

GitOps provides a model where deployment state becomes:

It also encourages teams to reduce direct manual changes inside Kubernetes environments.

This moved my thinking beyond individual CI/CD pipelines and towards building more standardised delivery platforms for development teams.

Final Architecture

Developer

Application Source Repository

CI Pipeline

Container Registry

GitOps Repository

Pull Request + Approval

Argo CD

Continuous Reconciliation

Kubernetes Cluster

Application Workloads

Conclusion

In this article, I explored how GitOps and Argo CD can make Kubernetes deployments more predictable and easier to control.

We covered:

For me, GitOps was an important step in moving from individual automation workflows towards thinking about how a delivery platform should behave.

Instead of asking developers to understand every Kubernetes command, we can create a controlled path where changes are reviewed in Git and the platform continuously ensures that the environment matches the approved configuration.

My next step: Once deployments became more predictable through GitOps, I started looking at another challenge: how developers could use these capabilities without needing to understand every infrastructure detail. In the next article, I will explore building self-service deployment workflows for development teams and how this begins to move DevOps towards platform engineering.