🔁 Analogy
Old-school deploys are like steering a car by yanking the wheel and hoping — push a change, walk away. GitOps is cruise control with lane-keeping: you set the desired state (“stay at 70 in this lane”), and a controller continuously corrects toward it — if the car drifts, it nudges back automatically. Your job becomes declaring intent in Git, not hand-driving servers; the system keeps reality matching the declaration.
CI vs CD
Two linked disciplines that get a commit safely to production:
- CI (Continuous Integration) — merge often; every change is automatically built and tested so integration breakage surfaces immediately. Answers “does it work?”
- CD — Continuous Delivery keeps the build always releasable (produces a deployable artifact, manual gate to prod); Continuous Deployment ships every passing build to prod automatically. Answers “get it released.”
graph LR Dev["git push"] --> CI subgraph CI["CI pipeline"] B["go build"] --> T["go test -race"] --> V["vet · lint · govulncheck"] --> I["build & push image@digest"] end I --> G["commit new digest to config repo (Git)"] G --> R["GitOps controller reconciles"] R --> K["cluster runs exact artifact"]
A Go CI pipeline
The steps are fast because Go’s toolchain is (sandbox-fenced example):
# .github/workflows/ci.yml (excerpt)
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version: '1.22' }
- run: go build ./...
- run: go test -race -cover ./... # tests + race detector
- run: go vet ./...
- run: golangci-lint run # linters
- run: govulncheck ./... # known-CVE scan
- run: docker build -t app:$GITHUB_SHA . # image tagged by commit
Each step gates the merge: a failure stops the line. (See supply-chain security for why govulncheck belongs here.)
GitOps: Git as the source of truth
In GitOps, the desired state of the cluster lives in a Git repo (Kubernetes manifests, Helm values). An in-cluster controller — Argo CD or Flux — continuously compares live state to Git and reconciles any difference. Deploys become commits/PRs: auditable, reviewable, and revertable with git revert.
See it: the reconcile loop at the heart of GitOps
This playground is a self-contained illustration — a ~30-line model of the reconcile algorithm so you can watch it work. It is not Argo CD’s source and it’s not something you deploy; the real controllers (Argo CD, Flux) run this same loop continuously inside the cluster against the live Kubernetes API. The idea: drive actual state toward the desired state declared in Git, correcting any drift on each tick. This runs here:
package main
import "fmt"
// desired state declared in Git
type Desired struct {
Image string
Replicas int
}
// actual state observed in the cluster
type Actual struct {
Image string
Replicas int
}
// reconcile nudges actual toward desired and reports what it changed.
func reconcile(d Desired, a *Actual) []string {
var actions []string
if a.Image != d.Image {
actions = append(actions, fmt.Sprintf("set image %s -> %s", a.Image, d.Image))
a.Image = d.Image
}
for a.Replicas < d.Replicas {
a.Replicas++
actions = append(actions, fmt.Sprintf("scale up -> %d", a.Replicas))
}
for a.Replicas > d.Replicas {
a.Replicas--
actions = append(actions, fmt.Sprintf("scale down -> %d", a.Replicas))
}
return actions
}
func main() {
desired := Desired{Image: "app@sha256:abc", Replicas: 3}
actual := Actual{Image: "app@sha256:old", Replicas: 1}
// Tick 1: Git changed; controller converges the cluster.
for _, a := range reconcile(desired, &actual) {
fmt.Println("apply:", a)
}
// Tick 2: nothing changed in Git -> nothing to do (idempotent).
fmt.Println("second reconcile actions:", len(reconcile(desired, &actual)))
// Drift! Someone hand-edited the cluster. Next tick reverts it.
actual.Replicas = 5
fmt.Println("drift detected, correcting:", reconcile(desired, &actual))
}
The loop is idempotent (a no-op when already converged) and self-healing (drift gets reverted). Take that exact idea, run it continuously against a real cluster, and compare the Git-declared desired state to live state — that is what Argo CD and Flux are. Your production code stays a normal Go service; the controller is a separate, off-the-shelf component you install, not something you write.
🐹 Go services are a natural fit for this pipeline
Go makes CI/CD pleasant: go build/go test -race are fast and built-in (no external test runner), the output is a single static binary, and a tiny container drops straight into a registry. Stamp the build with version info via -ldflags “-X main.version=$GIT_SHA” so the running service can report exactly which commit it is. Then reference that image by digest in your Git-tracked manifest — full traceability, and rollback is a one-line git revert that the GitOps controller reconciles back to the previous known-good image. The same reconcile loop you saw above is also how Kubernetes itself works.
⚠️ Mutable tags and push-from-CI undercut the model
Two anti-patterns erase GitOps’ benefits. Deploying :latest (or any mutable tag) means you can’t tell what’s actually running and rollback is ambiguous — always pin by digest or unique commit tag. And having CI push with kubectl apply using cluster-admin credentials is both less secure (an external system holds the keys) and not self-healing (it applies once and forgets drift). Prefer the pull model: CI’s job ends at “build image + commit the new digest to the config repo,” and the in-cluster controller takes it from there.
See also
- Deployment strategies — rolling, blue-green, canary releases this delivers.
- Dockerizing Go — building the image the pipeline ships.
- Supply-chain security — why govulncheck & signing belong in CI.
- Argo CD & Flux — the GitOps controllers.
Next: how that image actually rolls out without dropping traffic — deployment strategies.
Related topics
Shipping a new version without dropping traffic — rolling, blue-green, and canary releases, the health-gated promotion that makes them safe, and how readiness probes tie in.
containersDockerizing GoPackaging a Go service into a tiny, secure image — multi-stage builds, scratch/distroless bases, static linking, non-root users, and the version stamping that makes images traceable.
Check your understanding
Score: 0 / 51. What's the difference between Continuous Integration (CI) and Continuous Delivery/Deployment (CD)?
CI (Continuous Integration) is the discipline of merging frequently and automatically building + testing every change, so integration problems surface immediately. CD has two senses: Continuous Delivery keeps the build always releasable (produces a deployable artifact, with a manual gate to push to prod); Continuous Deployment goes further and ships every passing build to production automatically. CI answers 'does it work?'; CD answers 'get it released.'
2. What does a typical CI pipeline for a Go service run?
A solid Go CI pipeline compiles (go build), runs tests including the race detector (go test -race), checks go vet and a linter (golangci-lint), scans for known vulnerabilities (govulncheck), and on success builds a container image and pushes it to a registry — tagged with the commit. Each step is a gate: a failure stops the merge or release. Go's fast compile and built-in test/race tooling make these pipelines quick.
3. What is the core principle of GitOps?
GitOps declares the desired state of your infrastructure/apps (Kubernetes manifests, Helm values) in Git, and an in-cluster controller (Argo CD, Flux) continuously compares live state to that desired state and reconciles any drift. Deploys become Git commits/PRs — auditable, reviewable, and revertable with git revert. The cluster pulls its config rather than a pipeline pushing to it, which is more secure (no external creds into the cluster) and self-healing.
4. Why is the reconciliation/pull model of GitOps more robust than a pipeline pushing kubectl apply?
A push pipeline applies once and forgets; if someone hand-edits the cluster (drift), nothing notices. A GitOps controller runs a continuous reconcile loop: it keeps comparing actual to desired and re-applies until they match, so out-of-band changes are automatically reverted — the cluster self-heals toward Git. It's also more secure: credentials live inside the cluster pulling from Git, instead of a CI system holding cluster admin creds and pushing in.
5. Why pin/stamp the deployed image by immutable digest (or commit SHA), and keep the deploy manifest in Git?
Referencing the image by digest (or a unique commit-SHA tag) in a Git-tracked manifest gives you full traceability — you know exactly which bytes are running — and makes rollback trivial and safe: git revert the commit that bumped the digest and the controller reconciles back to the previous known-good image. Mutable tags like :latest break both properties (you can't tell what's running, and rollback is ambiguous).
Comments
Sign in with GitHub to join the discussion.