Server-Side Apply (SSA)
Practical example
An HPA continuously updates spec.replicas on a Deployment while ArgoCD reconciles the rest of the spec from Git; with SSA, each owns only its respective fields, so ArgoCD's sync no longer resets replicas to the manifest's static value on every reconcile.
Prior to SSA, kubectl apply performed a client-side 3-way merge: the client diffed the last-applied-configuration annotation, the live object, and the new manifest, then sent a patch. This approach broke down badly when multiple actors (a human via kubectl, an operator, a HPA, a mutating webhook) modified the same object, because the annotation only tracked one actor’s notion of “desired state” and had no concept of field-level ownership. SSA moves this logic into the API server. Every apply request must declare a field manager identity, and the server persists a managedFields entry in the object’s metadata describing which manager owns which field, using the FieldsV1 encoding (a compressed JSON representation of field paths).
On each apply, the API server computes a 3-way merge using the incoming manifest, the live object, and the set of fields the requesting manager previously owned — not the entire live object. If a field is owned by a different manager and the incoming request tries to change it, the server returns a 409 Conflict unless the request sets force=true, in which case ownership is forcibly transferred. This is a fundamental behavioral shift: SSA is designed to fail loudly on contested fields rather than silently overwrite them, which is the correct default for GitOps controllers reconciling alongside autoscalers or admission mutators.
- List merge semantics: for associative lists (e.g.
containerskeyed byname), SSA uses merge keys defined via OpenAPI extensions (x-kubernetes-list-type: map) rather than positional indices, avoiding the classic client-side merge bug where reordering a list caused spurious diffs. - Shared ownership: multiple managers can co-own a field if they submit identical values; ownership is granular enough to allow, e.g., an HPA to own
spec.replicaswhile a GitOps controller owns everything else in the same Deployment spec — the classic “HPA fight” problem is solved cleanly. - CRD interaction: for SSA to correctly infer list/map merge strategy on custom resources, the CRD’s OpenAPI schema must declare the appropriate
x-kubernetes-*annotations; without them, SSA falls back to treating lists atomically, which can reintroduce clobbering behavior for CRs. - Migration hazard: transitioning an object from client-side apply to SSA does not automatically populate
managedFieldscorrectly; the first SSA apply against an object still carrying the legacy last-applied annotation can produce unexpected conflicts, requiring an explicit ownership migration step (--server-side --force-conflictson first pass, deliberately).
Architecturally, SSA is what makes robust multi-controller reconciliation loops possible at scale. Operators built on controller-runtime increasingly default to SSA (via client.Apply) specifically to avoid the “read-modify-write” race where two controllers reconcile the same object concurrently and one overwrites the other’s patch based on a stale resourceVersion. It also underpins declarative GitOps tooling’s ability to detect drift precisely at the field level rather than the whole-object level, since each field’s owning manager is explicit and queryable via kubectl get -o yaml or the --show-managed-fields flag.
The practical cost is complexity: debugging “why won’t my field update” now requires inspecting managedFields to identify the contesting manager, and naive use of force=true across multiple controllers reintroduces the exact thrashing SSA was built to prevent. Teams operating large multi-controller clusters must treat field-manager naming and ownership boundaries as first-class API contracts, not implementation details, or SSA’s conflict-resolution guarantees degrade into the same nondeterministic overwrite behavior it was designed to replace.