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.yamlThis works, but over time several questions appear.
Who changed the deployment?
Which version of the configuration is running?
Was the cluster changed manually?
Does production still match the source repository?
How do we identify configuration drift?
How do we safely restore the previous configuration?
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:
Deployments
Services
ConfigMaps
Ingress resources
Horizontal Pod Autoscalers
Other Kubernetes configuration
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:
What changed
Who changed it
When it changed
Why it changed
Which pull request approved it
Which previous version can be restored
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:
Source code
Tests
Application build
Container creation
The GitOps repository focuses on:
Deployment configuration
Environment-specific values
Application versions
Kubernetes configuration
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: 8080The 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=trueThis configuration tells Argo CD:
Which repository to monitor
Which branch to use
Which folder contains the manifests
Which Kubernetes cluster to target
Which namespace should receive the application
Understanding Automated Synchronisation
The configuration:
syncPolicy:
automated:
prune: true
selfHeal: trueenables 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.0A new pipeline produces:
myregistry/cloud-native-api:1.1.0Instead of the pipeline directly deploying version 1.1.0 to Kubernetes, the GitOps repository can be updated:
image: myregistry/cloud-native-api:1.1.0Once 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.yamlEach 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.yamlThe 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.0Development 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 = ProblemThe Git configuration can be reverted from:
newTag: 1.1.0back to:
newTag: 1.0.0Once 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:
Unit tests
Integration tests
Container scanning
Manifest validation
Application health checks
Monitoring after deployment
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:
External secret managers
Cloud-native secret stores
Encrypted GitOps secrets
Managed identities
Workload identities
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:
Version controlled
Reviewable
Auditable
Reproducible
Continuously reconciled
Easier to roll back
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:
Git as the deployment source of truth
Desired state and actual state
Argo CD Applications
Automated synchronisation
Self-healing
Configuration drift detection
Pruning
Sync and health status
Environment management
Kustomize overlays
Git-based rollback
Separation between CI and deployment
GitOps as a foundation for platform engineering
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.

Join the conversation! Your thoughts help the community grow.