Fig. 1. Boundary of responsibility: the pipeline modifies the repository; only the controller changes the cluster.

Most teams that “have GitOps” actually have a pipeline that runs kubectl apply after every commit. That is a reasonable first step, but it is not GitOps, and the difference is not academic. It shows up at the worst possible moment: during an outage, when somebody fixed something by hand on the cluster and forgot about it.

A script: a sequence of steps

A deployment script, whether it is Bash, a pipeline job or an Ansible playbook, is imperative. It performs specific steps in a specific order and then exits. If the state of the environment changes afterwards for any other reason, the script will not notice. It only knows what it did at the moment it ran.

The consequences:

  • A manual change on the cluster stays there until the next pipeline run, which often means the next change to the application code, possibly weeks later.
  • The repository says one thing, the cluster says another, and nobody has a tool that compares the two.
  • Rolling back means re-running the script from an older version, which is not always the same as restoring the old state.

GitOps: a reconciliation loop

In GitOps the repository is the single source of truth for the desired state, and a controller running inside the cluster continuously compares that state with reality and removes the differences. There is no “deployment run”. There is constant reconciliation.

In practice this gives you three things a script cannot:

  1. Drift detection. A manual change on the cluster becomes visible within minutes as a difference between the repository and the actual state. It can be reverted automatically, or at least it is visible.
  2. Self-healing. If somebody deletes a resource, the controller recreates it. If somebody changes the replica count by hand, it goes back to the value in the repository.
  3. Rollback as a Git operation. Returning to the previous version is a git revert. The controller does the rest, and the change history is complete because nothing happened outside the repository.

Where the pipeline’s job ends

GitOps does not eliminate CI. The pipeline still builds the image, runs the tests and publishes the artifact. What changes is the boundary of responsibility: the pipeline does not touch the cluster. Instead it modifies manifests in the repository, for example by bumping the image tag, and stops there. The controller in the cluster handles the deployment.

That boundary simplifies permissions. The pipeline does not need access to the cluster API, and the controller does not need access to the CI system. Compromising one of them does not automatically give control over the other.

# The part of the manifest the pipeline modifies after building the image.
# The GitOps controller notices the change and rolls out the new version.
spec:
  template:
    spec:
      containers:
        - name: app
          image: registry.example.internal/team/app:1.4.2

When a script is enough

Not every environment needs a reconciliation loop. If deployment targets a single server, happens a few times a year, and nobody but the pipeline has access, a script is simpler and sufficient. GitOps starts to pay off when at least one of these is true:

  • more than one person can change the environment,
  • deployments are frequent or involve many components,
  • an audit trail of who changed the configuration and when is required,
  • the environment must be rebuildable from scratch in a predictable time.

Summary

A script answers the question “what to do”. GitOps answers “what the target state should look like” and guards that answer continuously. The first approach is easier to start with, the second is easier to live with. The choice depends on which phase the environment will spend more time in, and the answer is almost always: maintenance.