KubernetesVII · Declarative Resource ManagementDeclarative resource management
kubectl diff and server-side diff — preview before apply
What you'll learn
- Run kubectl diff to preview manifest changes against the live cluster
- Distinguish client-side and server-side diff and dry-run
- Wire kubectl diff into code review and CI pipelines
- Recognise the cases where diff cannot tell the full story (admission webhooks, controllers)
Prerequisites
Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16
The first rule of any apply workflow is “look before you
leap.” kubectl diff is the look — it shows exactly what
apply would change, field by field. This lesson covers the
diff modes, when each is the right choice, and how to wire
diff into the change-management pipeline.
kubectl diff — the local preview
kubectl diff -f manifest.yaml runs the same three-way merge
that apply would, then prints the resulting change as a
unified diff. It does not write to the cluster; it sends
the merge result to a dry-run=server API request and
compares what the API server would store against what it
currently has.
kubectl diff -f deployment.yaml
kubectl diff -f manifests/
kubectl diff -f https://example.com/manifest.yaml
Output is a unified diff:
--- // LIVE Deployment/web (before)
+++ // LIVE Deployment/web (after)
@@ -8,7 +8,7 @@
name: web
spec:
- replicas: 3
+ replicas: 5
selector:
matchLabels:
app: web
template:
@@ -22,7 +22,7 @@
- name: nginx
- image: nginx:1.27.1
+ image: nginx:1.27.2
The diff is colour-coded when the terminal supports it
(--server-side=false is implicit). It is also the output
that code-review tools consume; many GitOps controllers use
the same diff format.
Client-side vs server-side dry-run
Two dry-run modes exist; they answer different questions:
| Mode | What runs | What it catches |
|---|---|---|
--dry-run=client | kubectl-side merge only | Schema errors, last-applied-configuration conflicts, client-side validation |
--dry-run=server | kubectl merge + API server admission + validation + defaulting | All of the above + admission policy, schema on the server, mutating webhooks, defaulting |
kubectl apply -f manifest.yaml --dry-run=client # local check
kubectl apply -f manifest.yaml --dry-run=server # full pipeline, no persist
kubectl diff -f manifest.yaml # same as --dry-run=server
--dry-run=client runs entirely on the kubectl workstation.
It does not contact the API server at all. It catches:
- Schema errors (typos, wrong field types)
- Merge conflicts that kubectl’s client-side logic can detect
- Static analysis errors (e.g., mismatched
apiVersionfor akind)
--dry-run=server runs the full admission pipeline against
the API server. It catches everything client catches plus:
- Admission webhook rejections (PodSecurity, OPA, etc.)
- Mutating webhook defaults
- ResourceQuota violations (often)
- Server-side schema strictness
- Webhook-allowed dry-run requests
Production discipline: always use --dry-run=server for the
final CI gate. Client-side dry-run is fast and catches
typos, but it cannot see what the admission controllers will
do.
Diff in code review
The production workflow for manifest changes:
- Engineer edits the manifest in a feature branch.
- CI runs
kubectl apply --dry-run=server -f manifests/— fails if any manifest is invalid. - Engineer opens a PR. The reviewer runs
kubectl diff -f manifests/against a staging cluster. - Reviewer approves based on the diff.
- Merge triggers a deploy to staging, then prod.
The diff in step 3 is the artefact the reviewer reads. It should show:
- Exactly the fields the engineer intended to change
- No accidental side effects (e.g., the engineer changed
replicaswhile editingimage)
If the diff shows unexpected changes, the engineer should fix the manifest before merging. This is the discipline that keeps changes scoped.
Diff in CI pipelines
The standard CI gate:
# .github/workflows/manifest-validate.yml
name: Validate manifests
on: [pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: azure/setup-kubectl@v4
- name: Server-side dry-run
run: |
kubectl apply --dry-run=server -f manifests/
- name: Diff against staging
run: |
kubectl diff -f manifests/ --context staging
env:
KUBECONFIG: ${{ secrets.STAGING_KUBECONFIG }}
Server-side dry-run is fast (no waiting for controllers); it catches schema and admission errors. The diff step requires credentials to the staging cluster; secrets management should restrict those credentials to read-only.
Diff with kubectl-neat
Default kubectl diff output includes server-defaulted
fields (status, metadata.resourceVersion, managedFields,
creationTimestamp). These fields change every apply and
make the actual changes hard to see.
kubectl-neat (a krew plugin) strips these fields before
diffing:
kubectl diff -f deployment.yaml | kubectl neat
Output is a clean diff of only the fields the operator changed. This is the right tool for code review.
When diff cannot tell the whole story
kubectl diff is powerful but it has blind spots:
- Generated fields. Some admission webhooks add fields based on inputs that are not visible to kubectl (e.g., a webhook that adds an annotation based on a label value). These fields appear in the diff as “removed” because the manifest does not declare them, but the live state has them from the previous webhook run.
- Status fields.
kubectl diffdoes not diffstatus, but controllers writestatus. The diff is silent on status changes. - Side effects of controllers. Applying a manifest that
creates a Deployment causes the Deployment controller to
create ReplicaSets and Pods.
diffdoes not show those downstream creations. - Resource quota.
diffshows the change but does not show whether the change will fit inside the namespace’s ResourceQuota.
For these cases, you need more than diff. Production
pipelines combine diff with:
- Admission webhook dry-run testing
- Pre-prod clusters with the same admission and quota configuration
- Manual review of controller-generated changes
Cross-course references
- The Terraform course part
XVI-Terraform-Plan-Reviewcovers plan output as a code-review artefact;kubectl diffis the same idea at the cluster level. - The Ansible course part
XXV-Ansible-CheckDiffcovers--checkmode;--dry-run=serveris the same idea for Kubernetes. - The GitOps course (CIII-Kubernetes-GitOps) covers ArgoCD’s diff view; it implements the same three-way merge.
Quiz
Knowledge check · 4 questions
Q1. Which dry-run mode runs the full admission pipeline (including mutating webhooks) but does not persist the change?
Q2. All admission webhooks respect the dry-run flag and skip side effects during --dry-run=server.
Q3. A CI pipeline runs `kubectl apply --dry-run=server -f deployment.yaml` against a staging cluster. The pipeline succeeds. The same manifest is applied to production and is rejected with `forbidden: exceeded quota`. Diagnose.
Deployment manifest has `replicas: 50` and `resources.requests.cpu: 4` per replica. Staging has ResourceQuota `cpu: 200`. Production has ResourceQuota `cpu: 100`. Staging CI succeeds; production apply fails.
Q4. What two dry-run checks should a CI pipeline run for every manifest change, and what does each catch?
Passing score: 75%. Answers are checked in this browser.
Production discipline
- Always run
--dry-run=serverin CI. Client-side dry-run is faster but cannot see what admission webhooks will do. - Pipe
diffthroughkubectl neatfor code review. Strip defaulted fields so the diff shows only the change. - Make staging’s quota and admission mirror production. Otherwise dry-run in staging does not predict production outcomes.
- Diff is necessary but not sufficient. A clean diff does not validate intent. Human review is required for changes that scale, delete, or re-architect.
- Test admission webhooks for dry-run correctness. Webhooks that ignore the dry-run flag will execute side effects during a CI dry-run, which can corrupt state.