TT Lab
Get started
Learn Learning paths Courses

GitOps and Argo CD

Whose Field Is This

Continue in TT Lab

In one sentence

Server-side apply is a device that records an owner for each field of an object, and only if you can read that record (managedFields) can you explain "why my value keeps getting reverted" and Argo CD's managedFieldsManagers ignore rule.

Why this record was needed

The old kubectl apply computed on the client. It put "the manifest I sent last time" whole into the kubectl.kubernetes.io/last-applied-configuration annotation and compared it with the new manifest to decide what to delete. This approach has no concept of an owner. So when two tools handle the same object, they quietly delete each other's fields — right after Argo CD applies, an autoscaler changes replicas, at the next apply that value disappears, and the autoscaler writes it again, going back and forth endlessly. You cannot even tell who is at fault.

Server-side apply moves this calculation to the API server and records which manager owns each field. In kubectl apply --server-side --field-manager=gitops, the gitops is that name.

How it works

The record goes into metadata.managedFields. There is a trap that catches you first here — kubectl get ... -o json hides this field by default. If you don't add --show-managed-fields, it is always null and jq dies.

kubectl -n demo get deploy web -o json --show-managed-fields   | jq -r '.metadata.managedFields[] | .manager + " | " + .operation'
gitops | Apply
kubectl | Update

If you try to write a value into a field you do not own, the server rejects it and tells you in a list which fields belong to whom. If you give --force-conflicts, it goes through, and you must know exactly what happens then — the conflict does not disappear; the ownership moves. The old owner drops out of that field, and the next time the old owner applies its own value again, this time it is blocked. If two automations use force out of habit, it becomes an endless fight.

The opposite direction also differs from intuition. If you remove a field from the manifest, that manager lets go of ownership. But the value does not return to the old owner's value. A field nobody owns becomes the API default. If the manager that had taken replicas away to 5 deletes that line, it becomes 1, not 2.

Subresources are recorded separately too. kubectl scale writes not to the whole object but to the scale subresource, so a separate entry arises in the ownership record with manager kubectl and subresource scale, and the conflict message also tells you that fact. The conflict an autoscaler creates has exactly this shape.

How it connects to Argo CD

Argo CD can use this approach with the sync option ServerSideApply=true. And the ignore rule's managedFieldsManagers picks what to compare not by field path but by this ownership record.

resource.customizations.ignoreDifferences.apps_Deployment: |
  managedFieldsManagers:
  - kubectl
  jsonPointers:
  - /spec/replicas

The advantage is that you can express "do not compare fields the autoscaler wrote" without writing out paths one by one. In exchange, there is a limit — the preview command renders only path rules. What the manager-name rule will actually filter, a person must read the ownership record to judge.

What you see in the field

The most common incident is "the HPA and Argo CD are fighting". Replicas is written in the repository and the HPA writes that value too. If you look at the ownership record, the answer comes right out — two managers are holding the same field. The solution is not force but removing the replicas line from the repository. However, as seen above, the moment you remove it, the value drops to the API default, so the Pods briefly shrink until the HPA recalculates. If you do this during operating hours without knowing that, it becomes an incident.

The second is confusion during migration. If you move an object that had been managed client-side to server-side, the last-applied-configuration annotation remains, and the fields that annotation points to can go out of line with the new ownership record, causing unexpected deletions. So do the migration object by object, checking the ownership record.

The limits of this lab environment

The lab Pod has no Argo CD controller. So you cannot see a ServerSideApply=true synchronization actually running. Instead, what kwok brings up is a real kube-apiserver, so ownership records, conflicts, force, and letting go happen exactly as in production, and the link on the Argo CD side is confirmed by rendering an ignore rule.

What you will do in the next lab

You apply as the manager gitops and read the ownership record. You write the same field as a second manager, hotfix, to create a conflict, take it by force, and then let go to see where the value goes. You compare the annotation that client-side apply leaves, and trigger the subresource conflict that kubectl scale makes. Finally, you render a managedFieldsManagers rule against a real object that holds an ownership record.