You've built a CI pipeline that tests and pushes images automatically. Now comes a harder question: how do you deploy?
In Chapters 1-4, you learned that CI automates building and testing. But once tests pass, who decides when and how to deploy? If you're running kubectl apply -f deployment.yaml manually after each image push, you're not really automating deployment—you're automating only the preparation for deployment.
This chapter introduces GitOps, a radically different approach: instead of running manual kubectl commands, you declare your desired infrastructure in Git. A controller watches Git and makes the cluster match it automatically. This transforms deployments from imperative commands ("do this, then do that") into declarative declarations ("this is the desired state").
By the end of this chapter, you'll understand why Git-as-truth enables auditability, rollback, and collaboration—and why it's fundamentally safer than manual kubectl apply.
Let's start with what you probably do today: manual deployments.
You have a Kubernetes deployment YAML:
Output:
To deploy a new image, you update the image tag and run:
Output:
Seems simple. But here's what happens at scale:
In a team, this breaks down entirely. If three people are running kubectl commands independently, the cluster becomes a mess of undocumented changes.
When you run kubectl apply, you're being imperative:
Imperative thinking is about steps and sequences. "Do this first, then that." It's how you think when writing bash scripts or manual processes.
Declarative thinking is completely different:
You don't say "create a deployment"; you say "the desired state is a deployment with 3 replicas." Kubernetes's job is to reconcile—to make the cluster match that declaration.
Output:
Imperative requires humans to execute steps. Declarative requires a controller to observe and reconcile continuously.
Here's the GitOps insight: Git is already declarative. Your YAML files declare desired state. But you're treating Git as optional—the real source of truth is the cluster, which you mutate with imperative commands.
Flip that relationship: Make Git the source of truth. Write your desired infrastructure state in Git. A controller watches Git and says:
"What's in Git is the desired state. What's in the cluster is the actual state. They don't match? I'll fix that."
This is radically simpler:
Output:
Notice what just happened:
Compare this to imperative deployments:
You might ask: "Why not store desired state in a database?"
Git has three properties that make it unique for infrastructure:
A database can store current state, but it can't easily show history or allow offline operation. Git excels at both.
Output:
A GitOps controller runs continuously and executes the same loop forever:
This is why we call it reconciliation: the controller is constantly trying to reconcile the gap between desired (Git) and actual (cluster).
Here's a concrete example:
Output:
Controller observes Git, sees replicas: 3. Checks cluster, sees 3 pods running. No action needed.
Now, someone manually scales the deployment:
Output:
The reconciliation loop detects the drift:
Output:
This is the power of reconciliation: the cluster self-heals toward the declared state automatically. No human has to notice the manual change and fix it. The controller notices and fixes it.
"Drift" means the cluster state has drifted away from Git. Here's why it matters:
Scenario 1: Environment variable was manually changed
A developer ssh's into a pod and changes an environment variable to debug an issue:
This works for debugging, but the developer forgets to revert it. Now:
The next time the controller reconciles, it sees the mismatch. It restarts the pod with the Git-declared environment, reverting the debug change. Production is back in sync with Git.
Output:
Scenario 2: Resource was manually deleted
Someone accidentally deletes a service:
Git still declares the service should exist. Controller detects:
Controller recreates the service from Git. Your application is healthy again.
Output:
Scenario 3: Image was manually changed
A desperate developer manually changes the image tag to test a fix:
Git still declares the original image. Drift detected. Controller restarts pods with the Git-declared image.
Output:
In each case, the reconciliation loop acts as a safety net: manual changes automatically drift back toward the declared state. This doesn't prevent mistakes, but it prevents them from persisting.
At this point, you understand the mechanics. But the real shift is philosophical: declarative infrastructure changes how you think about deployments.
Imperative thinking: "How do I change the cluster right now?"
Declarative thinking: "What should the desired state be?"
This is actually more powerful. Instead of worrying about the sequence of commands ("patch this, then update that, then restart pods"), you just describe the end state. The controller figures out how to get there.
For your FastAPI agent:
Imperative approach:
Declarative approach:
Commit, push. Controller does the rest. Rollback is git revert. That's it.
Output:
Here's the full picture side by side:
The shift from manual imperative commands to GitOps reconciliation is transformative. You move from doing deployments to declaring them.
Here's the essence:
This is GitOps: using Git as the source of truth for infrastructure, with a controller enforcing it.
Starting in Chapter 7, you'll install ArgoCD, the controller that watches Git and deploys to your Minikube cluster. But now you understand the philosophy: you'll never run manual kubectl apply again. Instead, you'll commit to Git and let ArgoCD handle deployment.
Ask Claude: "I'm using GitOps with ArgoCD. One of my developers manually scales a deployment to 10 replicas for testing, then forgets to undo it before committing to Git. What happens?"
Evaluate the response:
After Claude provides the explanation, ask: "How would my approach to incident response change if I'm using GitOps instead of manual kubectl?"
Check that:
You built a gitops-deployment skill in Chapter 0. Test and improve it based on what you learned.
Ask yourself:
If you found gaps: