CGOA — GitOps Certified Associate
The Four OpenGitOps Principles — Why Pull and Not Push
In one line
GitOps is not the name of a deployment tool — it is the name of an operating model. A setup counts as GitOps only if it satisfies all four principles defined by OpenGitOps: Declarative, Versioned and Immutable, Pulled Automatically, and Continuously Reconciled. If even one of the four is missing, what you have is just "a CD pipeline that keeps YAML in Git."
Why this was needed
Ten years ago a deployment usually looked like this. When the CI server finished a build, that same CI server fired off kubectl apply. This is called the push model. It looks simple on the surface, but three things break down.
First, the keys to the cluster live inside CI. A Jenkins or GitHub Actions runner has to hold a kubeconfig or a ServiceAccount token. A runner is a machine that executes code that came from outside. This is where the path comes from by which a single PR workflow uploaded from a fork can gain production deployment rights.
Second, there is no answer to "what is running on the cluster right now?" You have to dig through pipeline logs, and even then anything someone fixed by hand is not in the record.
Third, when the pipeline stops, the state stops too. A push only works when there is an event. If someone changes replicas by hand at 3 a.m., when no event is happening, nobody knows.
The pull model flips all three at once. An agent that runs inside the cluster (Argo CD, Flux) reads Git. CI does not need to be given a single cluster credential. What CI does ends at "build the image, push it to the registry, and commit one line with the tag to the config repository."
How it works
Taking the four principles apart one by one looks like this.
| Principle | Meaning | What happens when it is violated |
|---|---|---|
| Declarative | Describe the desired end state, not the procedure for reaching it | Running the same script twice gives different results |
| Versioned and Immutable | Every state is kept as a commit, and a revision that has already been created is never modified | You cannot answer "since when has it been like this?" |
| Pulled Automatically | The agent fetches approved changes by itself | If a person forgets to deploy, Git and reality drift apart |
| Continuously Reconciled | It keeps comparing and converging regardless of events | A change made by hand survives forever |
The fact that the original wording of the third principle is Pulled Automatically, not "Pushed Automatically," comes up often on the exam. And the "Continuously" in the fourth does not mean "on every commit" but continuously, even without commits. Argo CD runs this loop every 180 seconds by default.
The phrase "Git is the SSOT (Single Source of Truth)" is also often misunderstood. It does not say "all files are in Git"; it is the declaration that "when the cluster and Git differ, Git is right." The key is that the direction is fixed. When you find drift, fixing Git to match the cluster is not GitOps — it is merely after-the-fact documentation.
Declarative does not always win, either. Work where order is the essence — database schema migrations, one-time data fixes, initial certificate issuance — cannot be expressed as an "end state." That is why Argo CD provides a separate imperative escape hatch called hooks (PreSync/PostSync). It is accurate to understand declarative as the default and imperative as something you state explicitly as an exception.
What it looks like in the field
The author's 7-node homelab uses this model as is. Gitea is at 10.0.0.200, Argo CD at 10.0.0.201, Harbor at 10.0.0.202, and Grafana at 10.0.0.203 — addresses taken from the MetalLB L2 pool 10.0.0.200-215. The network is handled by Cilium 1.20 eBPF without kube-proxy, and kube-prometheus-stack and CloudNativePG run on top of it. The whole stack was not pushed in from outside the cluster; it is the result of being pulled from inside.
There was one incident here that shows both the power and the limits of declarative management. This cluster's controlPlaneEndpoint is not a VIP or a DNS name but is pinned to the physical IP 10.0.0.120 of the first control plane. Even after the control plane was later expanded to 3 nodes and there were 3 etcd members, if that first node dies, neither kubectl nor the 7 kubelets can connect at all. This is because the apiserver certificate SAN does not include any other node's IP, so TLS verification fails from the start.
What does this have to do with GitOps? It shows that a value that is not written in a declaration file is not managed. controlPlaneEndpoint is decided once, at the moment the cluster is created, and after that it is neither in Git nor a target of reconciliation. That is why changing it is hell. In practice, a sense for telling apart the area you can manage with GitOps from the area outside it, such as cluster bootstrap, matters far more.
What to read next
The very next reading pins down exam words such as desired state, drift, reconciliation, and convergence with precise definitions. In the module after that, you build the skeleton of a real GitOps repository under /root/cgoa-repo/ and use kubectl kustomize to see with your own eyes how the per-environment render results diverge.