Nuno
Nuno

I Added GitOps, Then Took It Out

Argo CD's self-heal reverted manual canary releases within seconds. The fix was not a better GitOps setup but matching how the team really ships.
Vander Nunes
June 09, 2026
XInstagramYouTubeLinkedIn
A canary release arrow being pulled back by a git branch

In May 2026 I added Argo CD to a small startup's k3s cluster. Shortly after, I removed it on purpose. It was reverting the team's canary releases within seconds of them starting.

I still like GitOps. This is a post about where it doesn't fit, and how I should have spotted that before installing it.

The setup

I was moving a bare-metal platform from hand-run shell scripts to Terraform. The cluster already ran Argo Rollouts for canary releases, and that part worked well. The engineer who shipped the app had a simple, tag-driven routine: build an image, then run kubectl argo rollouts set image to start a canary with the new tag, watch it, promote it.

Git held the Kubernetes manifests, but git wasn't the source of truth for which image was running. The manifests said newTag: latest. The real version lived in the cluster, set by hand.

While I was rebuilding everything as code, adding Argo CD looked like a natural upgrade. Manifests in git, a controller that keeps the cluster matching them, drift corrected automatically. I turned on selfHeal: true, because that's most of the point.

What happened

When a canary release started, within seconds Argo CD saw the Rollout's image differ from git. Git said latest. The cluster said the new tag. Self-heal did its job and put it back.

The rollout controller saw its spec change mid-canary and aborted the steps. The release never progressed. From the engineer's side, a deploy they had run many times before now silently undid itself.

Nothing was broken in either tool. Each one was doing exactly what it was configured to do. They just disagreed about who owned the image tag.

The options

There were a few ways to keep Argo CD:

  • Turn off self-heal. Then Argo CD only syncs when someone asks. That's most of the value gone, and drift becomes a status colour people learn to ignore.
  • Tell Argo CD to ignore the image field. It works, but now the one thing you most want tracked in git is the one thing git doesn't track.
  • Move releases into git. Every canary starts by committing a new tag, and Argo CD applies it. This is the GitOps answer, and it's the right one for many teams.

The third option is the clean one. It also meant changing how the team shipped, adding a commit and a sync to every release, for a small team that was happy with its release flow and had no problem with it. I'd be swapping a routine they trusted for one I preferred.

Why I took it out

On 25 May 2026 I removed Argo CD. Releases stayed manual and tag-driven through Argo Rollouts, the way they had been before I arrived.

That sounds like a step back. I don't think it was. GitOps solves a specific problem: several people or pipelines changing a cluster, and nobody sure what's running. This team had one engineer doing the releases, a clear canary routine and a small number of services. The problem GitOps solves wasn't the problem they had.

What they did need was the infrastructure underneath in code: servers, bootstrap, secrets plumbing, databases, DNS. That's where the hand-run scripts had been, and that's where Terraform went. The release step on top could stay manual without hurting anyone.

What it cost

Not much in time, because it came out within days. It cost some trust, though. The engineer's first experience of my "upgrade" was a deploy that undid itself. If I'd left it in and asked them to adapt, I would have been optimising for my architecture diagram, not their week.

I also had to be honest in the documentation afterwards. "Drift is corrected automatically" was in early drafts of the design. It isn't true anymore, and saying so matters more than the diagram looking modern.

What I'd do differently

Before adding any reconciler, I'd ask one question: how does a new version reach production today, step by step? Then I'd look for anything in that answer that changes the cluster outside git. A kubectl set image, a Helm upgrade from a laptop, a patch run during an incident. Each of those will fight a self-healing controller.

If the answer is "a person runs a command", there are two honest choices. Move that step into git first, with the team, and then add GitOps. Or leave GitOps out and put the effort where the risk actually is.

I did it the other way round: added the controller, then found the conflict. It was cheap here. On a bigger team, with more people releasing, it would have been a bad afternoon.

When GitOps is the right call

I'd still add it, and early, when:

  • several people or pipelines deploy to the same cluster;
  • nobody can say with confidence what version is running where;
  • releases already start with a commit, or the team wants them to;
  • you need an audit trail of every change for compliance.

If none of those apply, a well-understood manual release with a canary is fine. Boring and understood beats elegant and fought.

The takeaway

Fit the delivery model to how the team actually ships, then automate that. Don't install a controller that assumes a workflow nobody agreed to.

If you're deciding how releases should work on your cluster, I'm happy to look at it with you. See Kubernetes & Platform.