GitOps and CI/CD for Kubernetes
The idea in one sentence
Section titled “The idea in one sentence”GitOps makes a Git repository the single source of truth for a cluster’s desired state, with a reconciler running inside the cluster pulling and applying that state instead of a CI job pushing it from outside.
Push-based CI/CD versus pull-based GitOps
Section titled “Push-based CI/CD versus pull-based GitOps”A traditional CI/CD pipeline is push-based: a CI job builds an artifact, then runs kubectl apply or helm upgrade directly against the cluster, using cluster credentials the CI system holds itself. That works, but it means the CI system — often a third-party SaaS product outside your infrastructure boundary — needs standing, and often privileged, access to your cluster.
GitOps flips the direction. A reconciler such as Argo CD or Flux runs inside the cluster it manages, continuously watching a Git repository that holds the desired manifests or Helm values. When the repository changes, the reconciler pulls the new desired state and applies it itself — the cluster reaches out to Git, Git never reaches into the cluster.
# Push-based: CI applies directly, using credentials CI itself holdskubectl apply -f manifests/helm upgrade myapp ./mychart -f values-prod.yaml
# Pull-based (GitOps): CI's job stops at committing to Git;# an in-cluster reconciler (Argo CD / Flux) takes it from theregit commit -am "bump myapp image tag to 1.5.0"git push origin mainWhy the pull-based model earns its keep
Section titled “Why the pull-based model earns its keep”Three concrete benefits fall out of moving the apply step inside the cluster:
- No external system needs cluster-admin credentials. The CI system’s job ends at a
git push; only the in-cluster reconciler, which you control, ever authenticates against the API server with write access. - Drift detection. Because the reconciler continuously compares live cluster state against the Git-declared state, a manual out-of-band
kubectl editorkubectl scalegets noticed — and can be automatically reverted — instead of silently diverging from what Git says should be running. - An inherently auditable history. Every change to desired state is a Git commit: who changed what, when, and why (via the commit message), all in the same history you already use for code review.
A realistic pipeline shape
Section titled “A realistic pipeline shape”Building and deploying with GitOps splits into two repositories (or two clearly separated stages): one that builds the application, and one that declares what should be running.
# Stage 1 — application repo CI: build and publish the imagedocker build -t myregistry/myapp:1.5.0 .docker push myregistry/myapp:1.5.0
# Stage 2 — update the manifests/values repo that GitOps watches# (a small CI step, or a bot, edits the tag and commits)sed -i 's/tag: .*/tag: "1.5.0"/' values-prod.yamlgit commit -am "myapp: bump image tag to 1.5.0"git push origin main
# From here, the GitOps reconciler (not CI) notices the commit and applies it.# In a push-based setup, stage 2 would instead run directly against the cluster:# helm upgrade myapp ./mychart -f values-prod.yamlThe CI system never runs kubectl or helm upgrade against the live cluster in the GitOps flow — its responsibility ends at building the image and committing the new desired state to Git.
flowchart LR dev["Developer"] -->|"push manifest / values change"| git["Git repository (desired state)"] ci["CI pipeline"] -->|"build & push image"| registry["Container registry"] git -->|"watched / polled"| reconciler["In-cluster GitOps reconciler (Argo CD / Flux)"] reconciler -->|"apply diff"| cluster["Live cluster state"] cluster -->|"pulls referenced image"| registry