This README explains GitOps from first principles, using a Spring Boot application, GitHub Actions (CI), and Argo CD (CD).
The goal is clarity and auditability, not clever automation.
Traditional pipelines:
- CI builds the app
- CI directly deploys to Kubernetes
Problems:
- Cluster state can drift from Git
- Hard to audit who deployed what
- Rollbacks are tool-specific
GitOps flips this model:
Git is the single source of truth for what should be running in the cluster.
The cluster is continuously reconciled to match Git.
Purpose: build artifacts only
lending-app/
src/
Dockerfile
pom.xml
.github/workflows/ci.yml
Responsibilities:
- Compile and test code
- Build container image
- Push image to registry
This repo does NOT deploy to Kubernetes.
Purpose: declare what runs in each environment
lending-gitops/
apps/
lending-app/
base/
overlays/
dev/
qa/
Responsibilities:
- Kubernetes manifests (Deployment, Service, Route)
- Environment-specific configuration
- Image tag / commit hash
This repo represents the desired state of the cluster.
CI is intentionally boring and limited.
Typical flow:
- Build & test Spring Boot app
- Build container image
- Push image to registry
Example outcome:
my-registry/lending-app:1a2b3c
CI stops here.
No kubectl. No helm. No cluster credentials.
Deployment happens via Git change, not CI.
- Developer opens the GitOps repo
- Chooses environment (dev or qa)
- Updates image tag
- Commits or raises a PR
Example (kustomization.yaml):
images:
- name: my-registry/lending-app
newTag: 1a2b3cThis commit now represents:
"This is what should be running in dev."
Argo CD runs inside the cluster and continuously:
- Pulls the GitOps repo
- Renders manifests (plain YAML / Kustomize / Helm)
- Compares Git vs live cluster state
- Applies changes to reconcile the cluster
Argo CD does not build images. Argo CD does not contain business logic.
Developer pushes code
|
v
GitHub Actions (CI)
- build
- test
- push image
|
v
Image registry
|
| (manual Git change)
v
GitOps repo (image tag updated)
|
v
Argo CD detects Git change
|
v
Kubernetes Deployment rolls out
| Responsibility | Owner |
|---|---|
| Build artifacts | CI |
| Decide what runs | Humans via Git |
| Apply to cluster | Argo CD |
- Every deployment is a Git commit
- Who, when, why is recorded
- Rollback =
git revert
- CI has no prod access
- No accidental auto-deploys
- PR approvals enforce control
apps/lending-app/base/
deployment.yaml
service.yaml
kustomization.yaml
apps/lending-app/overlays/dev/
kustomization.yaml # image tag, replicas
apps/lending-app/overlays/qa/
kustomization.yaml # different image tag, replicas
Only the differences live in overlays.
Rollback is boring (by design):
- Revert Git commit in GitOps repo
- Argo CD reconciles cluster back
No special tooling required.
- Git is the only source of truth
- CI and CD are cleanly separated
- Humans control promotion
- Argo CD enforces convergence
This model is common in:
- banks
- regulated enterprises
- multi-team platforms
"Our CI only builds images. Deployment is a Git change in the GitOps repo, and Argo CD reconciles the cluster to match Git."
GitOps is not about speed.
It is about:
- correctness
- traceability
- predictability
Boring on purpose.
Below is a realistic, end-to-end repo layout matching what we discussed.
lending-app/
├── src/main/java/
├── src/test/java/
├── Dockerfile
├── pom.xml
└── .github/
└── workflows/
└── ci.yml
Responsibilities
- Build & test Java code
- Build container image
- Push image to registry
This repo has no Kubernetes manifests.
lending-gitops/
├── apps/
│ └── lending-app/
│ ├── base/
│ │ ├── deployment.yaml
│ │ ├── service.yaml
│ │ └── kustomization.yaml
│ └── overlays/
│ ├── dev/
│ │ └── kustomization.yaml
│ └── qa/
│ └── kustomization.yaml
└── argo-apps/
└── lending-app-dev.yaml
Key points
base/never changes oftenoverlays/*change per environment- image tag changes happen only in overlays
┌────────────┐
│ Developer │
└─────┬──────┘
│ git push
v
┌────────────┐
│ App Repo │ (Spring Boot)
└─────┬──────┘
│ CI: build & push image
v
┌────────────┐
│ Registry │ lending-app:<sha>
└─────┬──────┘
│
│ manual Git change
v
┌────────────┐
│ GitOps Repo│ (image tag updated)
└─────┬──────┘
│
│ watched by Argo CD
v
┌────────────┐
│ Argo CD │
└─────┬──────┘
│ kubectl apply
v
┌────────────┐n│ Kubernetes │
│ Deployment │
└────────────┘
GitOps Repo
apps/lending-app/overlays/dev ---> image: abc123
apps/lending-app/overlays/qa ---> image: xyz999
Promotion = copy known-good image tag from dev to qa
A: You can, but manual updates:
- enforce human approval
- prevent accidental prod deploys
- give cleaner audit trails
Many regulated orgs prefer manual promotion.
A: Argo CD will revert them back to Git state.
This is called self-healing and is a core GitOps feature.
A: Not in plain Git. Common patterns:
- sealed-secrets
- SOPS-encrypted YAML
- external secret operators
GitOps repo stores references, not raw secrets.
A: Usually:
- platform / SRE team owns structure
- app teams own their app folders
This prevents teams from breaking each other.
A: Kubernetes does deployments.
Argo CD only:
- detects Git drift
- applies manifests
A:
git revertthe image-tag commit- push
- Argo CD syncs back automatically
No special rollback tooling required.
- CI builds immutable artifacts
- Git declares desired runtime state
- Argo CD reconciles continuously
- no shared deployment credentials
- no snowflake clusters
- consistent promotion model
- Git is auditable
- rollbacks are deterministic
- environments are explicit
“We separated build from deploy. Git defines what runs, and Argo CD enforces it.”
GitOps works best when it is:
- boring
- explicit
- human-controlled
Argo CD is not magic.
It is a Git-to-cluster reconciliation engine.