kubectl diff: Preview Changes Before Apply
kubectl diff compares your manifests against the live objects and prints a unified diff, exiting 1 when there is a difference.
Running diff before apply turns a deploy review into a real preview: you see exactly which fields the cluster would change.
What it does
kubectl diff sends your manifests to the API server in dry-run mode, retrieves what the merged objects would look like, and prints a diff against the current live state. The exit code is the gate: 0 means no changes, 1 means changes exist, and anything above 1 is an error.
Common usage
kubectl diff -f deploy.yaml
kubectl diff -k ./overlays/prod
# gate a pipeline: succeed only if there are no drifts
kubectl diff -f deploy.yaml && echo "no changes"
# use server-side merge semantics
kubectl diff --server-side -f deploy.yamlOptions
| Flag / Env | What it does |
|---|---|
| -f, --filename | Manifests to compare |
| -k, --kustomize | Diff a kustomize directory |
| --server-side | Use Server-Side Apply merge for the comparison |
| --field-manager=<name> | Field manager to attribute the dry-run apply |
| KUBECTL_EXTERNAL_DIFF | Override the diff program (e.g. colordiff) |
In CI
Mind the exit code: diff returns 1 when there are changes, which set -e treats as failure. Capture it deliberately (for example kubectl diff ... || true to just print, or check $? -eq 1 to detect drift). Set KUBECTL_EXTERNAL_DIFF=diff so output is stable on minimal images.
Common errors in CI
"error: unable to recognize ...: no matches for kind" means a CRD in the manifest is not installed on the cluster, so diff cannot dry-run it; install the CRD first. A non-zero exit that you did not expect is usually just the "changes exist" signal (exit 1), not an error. "exec: \"diff\": executable file not found" appears on slim images without diffutils; install it or set KUBECTL_EXTERNAL_DIFF.
Using this in CI
A runner has no kubeconfig, no cached context, and no interactive auth. Every kubectl invocation in CI needs the context supplied explicitly, and most confusing CI failures here are the command running against the wrong cluster or no cluster at all.
# never rely on the ambient context on a runner
kubectl --context "$KUBE_CONTEXT" -n "$NAMESPACE" get pods
# confirm what you are actually connected to before mutating anything
kubectl config current-context
kubectl cluster-info
# fail fast instead of hanging on an unreachable API server
kubectl --request-timeout=30s get nodes