Use kubectl apply
Learn to use kubectl apply with declarative config in this Kubernetes tutorial. Understand the core concept, walk through hands-on exercises, compare options, and troubleshoot edge cases. Step 47 of the Kubernetes learning path.
Focus: use kubectl apply with declarative config
Picture this: you've hand-crafted a perfect Deployment manifest for your application, but when you run kubectl create, Kubernetes throws an error because a resource with that name already exists. Or worse — someone on your team manually scaled a ReplicaSet, and now your YAML is out of sync with the cluster's actual state. These frictions vanish when you adopt declarative management with kubectl apply. Instead of issuing imperative commands (create, replace, delete), you write your desired state in YAML or JSON and let Kubernetes compute the diff to make the cluster match. This approach, powered by server-side apply, brings GitOps workflows, rollback safety, and collision detection to every resource you manage.
The problem this lesson solves
Manual kubectl operations work for experiments, but they break down fast in team environments. Here's what happens when you rely on kubectl create or kubectl replace:
- Idempotency failure – Running
kubectl createtwice raises an error. Your deployment scripts must handle exits, increasing complexity. - State drift – When you run
kubectl scaleorkubectl edit, those changes live only in the cluster. Your local YAML files become stale, making rollbacks or restores dangerous. - No change lineage –
kubectl createoverwrites fields without recording who set them or why. Debugging mysterious overrides becomes a guessing game.
Declarative config with kubectl apply solves all three. You store the intended state in version control (Git), and kubectl apply reconciles the cluster to that state, every time — no matter what happened before.
Core concept / mental model
Think of kubectl apply as a two-way diff engine. It compares:
- The manifest you provide (local file or stdin).
- The current live object in the cluster.
For every field, it calculates the minimal change to make the cluster match your manifest. Fields not present in your file are left untouched (unless removed via kubectl apply --prune). This is fundamentally different from kubectl create (which rejects existing objects) or kubectl replace (which overwrites the entire object, potentially wiping out fields managed by other controllers).
Declarative vs. imperative: a quick grid
| Approach | Command | Behavior | Use case |
|---|---|---|---|
| Declarative | kubectl apply -f file.yaml |
Create or update; patches only diff | CI/CD, GitOps, team environments |
| Imperative create | kubectl create -f file.yaml |
Fails if object exists | One-shot resource creation |
| Imperative replace | kubectl replace -f file.yaml |
Replaces entire object, deletes missing fields | Emergency overrides (rarely safe) |
Server-side apply (Kubernetes 1.18+, GA in 1.22)
Since Kubernetes 1.18, kubectl apply uses server-side apply by default (K8s 1.22+ with kubectl ≥1.24). The server computes the diff and stores ownership metadata in the object's managedFields. This means:
- Two team members can apply different fields to the same Deployment without clobbering each other.
- Field ownership is tracked per field, per user/agent.
- Remove a field by moving its ownership to another manager.
How it works step by step
- Write a manifest (YAML preferred) with your desired state —
apiVersion,kind,metadata, andspec. - Run
kubectl apply -f <file>– the client sends the whole manifest to the API server. - API server compares your manifest against the current object (if it exists).
- Server-side apply calculates the patch: fields in your manifest are updated; fields absent are left alone unless you explicitly delete them.
- Kubernetes stores the merged result and updates
managedFieldswith a new manager entry (by default,kubectl-client-side-applyfor older versions, orkubectlfor server-side). - Return success – the updated object is returned.
If the object does not exist, kubectl apply creates it. That's why you can run the same kubectl apply command in CI/CD every deployment — it's idempotent.
The imperative trap illustrated
# Imperative: creates NGINX Deployment
kubectl create deployment nginx --image=nginx:1.21 --replicas=3
# Later, apply a YAML with replicas: 5 and image: nginx:1.25
kubectl apply -f nginx-deploy.yaml
With kubectl create, the second command would fail unless you first delete the object. With kubectl apply, it works seamlessly — the cluster updates to replicas: 5 and image: nginx:1.25 without manual cleanup.
Hands-on walkthrough
Let's build a real Deployment manifest and apply it twice to see idempotency in action.
Step 1: Create your first declarative manifest
# nginx-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-declarative
labels:
app: nginx
spec:
replicas: 2
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.21
ports:
- containerPort: 80
Step 2: Apply the manifest
kubectl apply -f nginx-deploy.yaml
Expected output:
deployment.apps/nginx-declarative created
Check the pods are running:
kubectl get pods -l app=nginx
NAME READY STATUS RESTARTS AGE
nginx-declarative-7f9c7c8b7f-abc12 1/1 Running 0 10s
nginx-declarative-7f9c7c8b7f-xyz34 1/1 Running 0 10s
Pro tip: Always include
---at the top of a multi-resource YAML file. It helps viewers (and linters) parse multiple documents.
Step 3: Modify and re-apply (idempotent update)
Update the YAML to change the image and scale:
spec:
replicas: 3
template:
spec:
containers:
- image: nginx:1.25
Apply again:
kubectl apply -f nginx-deploy.yaml
Expected output:
deployment.apps/nginx-declarative configured
Verify the changes:
kubectl get deploy nginx-declarative -o jsonpath='{.spec.replicas}'
3
kubectl get deploy nginx-declarative -o jsonpath='{.spec.template.spec.containers[0].image}'
ginx:1.25
Step 4: Apply an identical manifest (no-op)
Apply the same file again:
kubectl apply -f nginx-deploy.yaml
Expected output:
deployment.apps/nginx-declarative unchanged
This is the power of declarative config: Kubernetes detects no diff and does nothing. Your CI/CD pipeline can safely run kubectl apply hundreds of times.
Step 5: (Bonus) View managed fields
kubectl get deploy nginx-declarative -o yaml | head -50 | grep -A5 managedFields
You'll see entries like:
managedFields:
- manager: kubectl
operation: Update
apiVersion: apps/v1
fieldsType: FieldsV1
fieldsV1: {...}
This metadata enables advanced GitOps tools (ArgoCD, Flux) to detect drift and resolve conflicts.
Compare options / when to choose what
| Tool / approach | Declarative? | Use case |
|---|---|---|
kubectl apply |
Yes | General purpose, GitOps, multi-environment |
kubectl create --save-config |
Partial | One-time creation with config later editable |
kubectl set image, kubectl scale |
No (imperative) | Quick dev fixes or emergency scale-ups |
| Helm | Yes (templated) | Reusable charts with config values |
Kustomize (via kubectl -k) |
Yes | Environment-aware overlays on base YAML |
When to choose kubectl apply:
- You want pure YAML management without templating.
- You don't need Helm's chart packaging or lifecycle hooks.
- Your team prefers GitOps with tools like ArgoCD (which uses kubectl apply under the hood).
When to choose Helm or Kustomize: - You need environment-specific overrides (staging vs. prod). - You're distributing applications as packages. - You need to manage release history and rollbacks (Helm).
Pro tip: Even if you use Helm, Helm itself calls
kubectl applyinternally. Understanding howkubectl applybehaves — including the no-op for unchanged resources — helps you debug Helm releases faster.
Troubleshooting & edge cases
1. kubectl apply reports "NotFound" when you expect an update
Cause: The manifest's metadata.name is wrong or the namespace doesn't exist.
Fix:
# Check exact name
kubectl get deployments --all-namespaces | grep your-app
# Ensure the namespace exists
kubectl get ns
If the namespace is missing, create it imperatively first (kubectl create ns my-ns) or add a Namespace resource to your YAML.
2. Fields are not being updated
Symptom: You modify a field like replicas: 5, but after apply, replicas stays at 3.
Common cause: The field is being overridden by a mutating admission controller (e.g., VPA, Istio, or a custom webhook).
Diagnosis:
kubectl get deploy nginx-declarative -o yaml | grep -A2 managedFields
Look for a manager other than kubectl. If you see horizontal-pod-autoscaler or custom-webhook, that controller is setting replicas.
Solution:
- For HPA/VPA: Add the field to the .spec.ignoreOverrides (if supported) or accept its value.
- For custom webhooks: Check admission logs.
3. Unexpected resource deletion after kubectl apply --prune
Symptom: Running kubectl apply --prune -f . deletes a Service you intended to keep.
Cause: The prune command removes resources that are not listed in the applied files. If you forgot to include a resource in your file, prune deletes it.
Fix: Always dry-run first:
kubectl apply --prune -f . --dry-run=client
Review the output for resources that would be pruned. Add any missing YAML files.
4. Server-side apply conflict error
Error: Apply failed with 1 conflict: conflict with "another-agent"
Cause: Two different kubectl clients (or controllers) have managed different fields, and your apply tries to set a field already claimed by another manager.
Solution:
# Force overwrite
kubectl apply --force-conflicts -f file.yaml
Use --force-conflicts carefully – it overrides ownership and may cause other managers to lose track of their fields.
What you learned & what's next
You now understand the core power of declarative configuration with kubectl apply:
- Idempotency – run the same command in CI/CD without error.
- State reconciliation – Kubernetes automatically computes diffs.
- Ownership tracking –
managedFieldsprevents accidental overwrites. - Safe updates – use
--pruneand--force-conflictswith understanding.
This model is the foundation for GitOps tools and production-grade deployments. In the next lesson, "Manage Kubernetes resources with ConfigMaps and Secrets," you'll apply this same declarative pattern to configuration data — storing environment variables, TLS certificates, and config files outside of your container images.
Master kubectl apply, and you've mastered the sustainable Kubernetes workflow.
Practice recap
Now practice by writing your own Deployment manifest with at least 4 replicas and two containers (nginx and a busybox sidecar). Apply it twice — the second time should say 'unchanged'. Then modify the image tag, re-apply, and verify with kubectl describe deploy. For extra credit, add a ConfigMap reference and apply again.
Common mistakes
- Using
kubectl createfor resources that already exist — leads to duplicate errors; always usekubectl applyfor declarative management. - Forgetting to prune resources removed from YAML — apply without
--pruneleaves dead objects; use--prunewith--dry-runfirst. - Modifying
managedFieldsdirectly viakubectl edit— that breaks server-side apply tracking; always apply the YAML file. - Assuming
kubectl applyoverwrites all fields — it patches only the fields in your YAML; fields omitted are unchanged unless you use--prune.
Variations
- Use
kubectl apply -k .with Kustomize to apply environment-specific overlays while keeping base YAML declarative. - Combine
kubectl apply --server-sidewith--field-manager=my-managerto give custom field ownership identifiers. - Use
kubectl apply --prune --allto sync the cluster state to the YAML files in a directory, deleting any resources not in those files.
Real-world use cases
- A CI/CD pipeline that runs
kubectl apply -f k8s/on every Git push to continuously reconcile the cluster state. - A multi-environment GitOps setup (ArgoCD) that uses
kubectl applyunderneath to sync staging and production manifests from separate Git branches. - A team sharing a single cluster where each microservice team owns a
team/directory;kubectl apply --pruneensures only their resources stay.
Key takeaways
- Declarative config with
kubectl applyis idempotent — it creates or updates without errors on re-run. - Server-side apply (default in modern K8s) tracks field ownership via
managedFields, preventing accidental overwrites. kubectl applyonly patches the fields present in your YAML; omitted fields remain unchanged.- Use
--dry-run=clientbefore prune to avoid unintended deletions. - Mastering
kubectl applyis the prerequisite for GitOps workflows and tools like ArgoCD and Flux. - Always prefer declarative YAML files over imperative commands for production-grade infrastructure.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.