Skip to main content
KBY Technologies Logo
cd ../lexicon
sys/docs/lexicon/server-side-apply-ssa.md
Lexicon Entry

Server-Side Apply (SSA)

Server-Side Apply is a Kubernetes API server feature that tracks per-field ownership of an object's manifest, allowing multiple controllers or actors to declaratively co-manage the same resource without clobbering each other's changes. It replaces client-side 3-way diffing with a server-computed merge based on ownership metadata stored in the object itself.

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. containers keyed by name), 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.replicas while 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 managedFields correctly; 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-conflicts on 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.