Skip to content
Install the CLI

Explanation

Server-side apply and drift

In a live Kubernetes cluster, your deployment tool is rarely the only thing modifying objects. HorizontalPodAutoscalers adjust spec.replicas, operators inject webhook conversion blocks into CRDs, and the Kubernetes API server fills in default values on every pod spec. Kix uses Kubernetes Server-Side Apply to track exactly which fields Kix owns, so it can detect real configuration drift without fighting in-cluster controllers or flagging harmless API server defaults.

When kix deploy applies a manifest, it uses Kubernetes Server-Side Apply with the field manager name kix. Instead of storing a giant “last-applied-configuration” JSON blob in an annotation, the Kubernetes API server records which manager owns each individual field inside metadata.managedFields.

Because ownership is tracked per field rather than per object, Kix and an in-cluster controller can safely co-own the same Deployment:

  • Kix can own the Deployment’s container image, environment variables, and resource requests.
  • A HorizontalPodAutoscaler can own spec.replicas on that same Deployment.
  • Fields that Kix never included in its manifest (such as status or API server defaults) stay outside Kix’s owned field set.

When Kix tries to change a field that another manager currently owns, the Kubernetes API server returns a field ownership conflict instead of silently overwriting the value. That conflict is a useful safety check: it tells you that your Nix manifest and a live controller (or a manual kubectl edit) are both trying to control the same setting.

How you resolve a conflict depends on who should control the field going forward:

  1. Kix should own the field: If someone ran a one-off kubectl edit or you are migrating a resource from Helm to Kix, use --force-conflicts on kix deploy for that transition. A forced Server-Side Apply transfers ownership of the conflicting fields to kix and writes the values from your build. Avoid leaving --force-conflicts enabled on every routine deploy, because it would also steamroll legitimate conflicts from controllers.
  2. A controller should own the field (ownership.cede): Sometimes a resource must include an initial value when it is first created, even though an in-cluster controller will take over that field afterward (for example, spec.replicas on an autoscaled workload, or webhook spec.conversion fields on a CRD). In those cases, the package declares the field path under ownership.cede on scope.mkResource. Kix sends the field when creating the object, and if a later apply conflicts only on declared ceded paths, Kix automatically retries without those fields so the controller keeps ownership.

Making ownership.cede an explicit list of field paths keeps conflicts honest: expected controller takeovers happen smoothly, while an unexpected conflict on any other field still stops the deployment and asks you to decide.

Drift detection uses the same ownership boundary

Section titled “Drift detection uses the same ownership boundary”

Most diff and drift tools compare your desired YAML against the entire live object returned by the cluster, which creates constant noise whenever controllers update status fields or the API server adds default fields.

Kix uses the kix field-ownership entries in metadata.managedFields across both kix drift and kix diff:

  • Right after kix deploy applies a resource, Kix extracts the live fields owned by kix, hashes them, and writes that digest to the kix.run/applied-hash annotation.
  • When you run kix drift, Kix reads the live object, selects the fields owned by kix, hashes their current values, and compares them to kix.run/applied-hash. If a controller updates status, or if a field was cleanly ceded via ownership.cede, Kix ignores it. If someone changes a Kix-owned field out-of-band (even through a subresource like kubectl scale), kix drift flags the exact field that changed.
  • When you run kix diff against a live cluster, Kix compares the desired build against the cluster’s state: resources whose kix.run/identity-hash matches the build are treated as unchanged by the build, changed resources are compared against their kix-owned projection from metadata.managedFields, and a separate drift section checks kix.run/applied-hash and metadata.managedFields to report out-of-band edits and the field manager that made them.

Just as Kix avoids hidden state in the cluster, it avoids hidden inputs at deploy time. Every input that affects a compiled manifest, including frequently updated values like container image digests, lives in a committed file in your repository and is read during evaluation:

  • Content-addressed identity hashes: During evaluation, Kix computes each resource’s kix.run/identity-hash from its serialized JSON in Kix’s local store, and kix deploy uses those hashes (recorded in the cluster’s Activation resource) to determine which objects changed since the last deployment. Reading all inputs during evaluation guarantees that the hashes in your build match the manifests applied to the cluster.
  • Committed JSON image pins (pins.json): To make automated image updates easy without letting a CI bot rewrite Nix code, Kix stores image versions in a structured pins.json file managed by kix pin. Running kix pin set <key> --tag <tag> queries the container registry, records the immutable sha256 digest alongside the tag, and writes the update to pins.json.
  • Verifiable deploy receipts (status.receipt): When kix deploy finishes applying an Activation, it records a deploy receipt on the Activation status subresource containing the activation hash, timestamp, Git commit, locked flake inputs, and container image digests. Because every input was committed before evaluation, kix compliance attest --from-cluster can generate a verifiable SLSA provenance statement directly from the live cluster receipt.