kubectl apply vs create vs replace: Declarative or Imperative
8 min readBytePatterns
kubectl apply vs create vs replace: create fails if the object exists, replace overwrites the whole object, apply merges your file into the live state.
kubectl apply vs create vs replace in one line: create makes a new object and fails if it already exists, replace overwrites an existing object with your whole file and fails if it does not exist, and apply creates the object if it is missing or patches it if it is there, by comparing your file with what you last applied and with the live object. The first two are imperative: you tell the API server what to do. apply is declarative: you say what the object should look like, and kubectl works out the patch. Facts come from the Kubernetes documentation listed at the end, as of October 2026.
| Command | Object exists? | What happens to the live object |
|---|---|---|
kubectl create -f | must not | created from the file; otherwise AlreadyExists |
kubectl replace -f | must | replaced by the file, whole; unmentioned fields reset |
kubectl apply -f | either | created, or patched by a three-way merge; other owners' fields kept |
The problem it solves
A Deployment in a cluster is edited by more than one hand. Your file sets the image. The HorizontalPodAutoscaler sets replicas. The API server fills in defaults. Someone ran kubectl edit during an incident. When you now want to roll out a new image, the question is what should happen to all the fields you did not type. create and replace each give a blunt answer. apply gives the one you usually want.
The intuition
Think of each command by how much of the object it claims to own.
createclaims a blank slot. It sends your file as a new object. If an object with that name already exists in the namespace, the server refuses withAlreadyExists, and nothing changes. It is the right verb for one-off objects you make exactly once, such as a Namespace or a Secret generated from a literal.replaceclaims the whole object. It sends your file as the complete new version. Everything in the live object that your file does not mention goes back to its default: thereplicasthe HPA set becomes 1, the annotation someone added disappears. By default the update must carry the currentresourceVersion, so a stale file is rejected with a conflict rather than silently overwriting a newer change;--forcedeletes and recreates instead. If the object does not exist,replacefails.applyclaims only the fields in your file. With client-side apply, kubectl stores the file it applied in the annotationkubectl.kubernetes.io/last-applied-configuration. The next apply compares three things: that last-applied file, the live object, and the new file. A field present in last-applied but missing from the new file is removed, because you owned it and dropped it. A field in the live object that was never in your file is left alone, because something else owns it. Fields in the new file are set. Server-side apply does the same job without the annotation: the API server tracks a field manager per field inmanagedFields, and a second manager setting the same field gets a conflict unless it passes--force-conflicts.
Two consequences follow. First, if your file does say replicas: 3 and the HPA has scaled to 6, every apply resets it to 3, and the HPA scales it back up a minute later: leave replicas out of files for autoscaled Deployments. Second, mixing verbs loses the bookkeeping. An object created with create or changed with edit has no last-applied annotation, or a stale one, so the first apply afterwards cannot tell which fields you dropped; kubectl warns about the missing annotation and patches from the live state. kubectl diff -f shows the patch apply would send before it sends it, and apply --prune deletes objects that match the selector and are no longer in your files, which is why the documentation marks it as something to use with care.
Watch it run
The animation is the object tree these commands act on. A Deployment wants three replicas; its controller creates a ReplicaSet for this version of the Pod template, and the ReplicaSet creates the Pods. A node fails, one Pod is gone, and the ReplicaSet makes a new one with a new UID. Change the image in the template, and the Deployment creates a second ReplicaSet and shifts Pods across, keeping the old one at zero for rollback. Every one of those changes to the Deployment is a kubectl apply of the same file with one line different; the ReplicaSets and Pods are never edited by hand.
Pods, ReplicaSets & Deployments
Step 1 of 10
You write desired state: a Deployment that wants three replicas of api.
The same interactive animation as the lesson — step through it with the controls.
The code
The lesson's Deployment, as a file meant to be applied. replicas is left out on purpose because an HPA will own it:
# illustrative: kubectl apply -f api.yaml, repeatedly, as the file changes
apiVersion: apps/v1
kind: Deployment
metadata: { name: api }
spec:
selector: { matchLabels: { app: api } }
template:
metadata: { labels: { app: api } }
spec:
containers:
- name: api
image: registry.example.com/api:1.5.0 # the one line this release changes
# kubectl diff -f api.yaml shows the patch apply would send
# kubectl apply -f api.yaml creates or patches
# kubectl replace -f api.yaml whole object; replicas would reset to 1
# kubectl create -f api.yaml Error: deployments.apps "api" already exists
A toy model of the three verbs, not the API server. The live object has replicas: 6 set by the HPA. The last-applied file never mentioned replicas, and the new file bumps the image and drops the env block. Apply removes env, sets the image and keeps the HPA's 6; replace sends the file whole, so replicas falls back to the default of 1; create refuses while the object exists:
import copy
# the HPA owns replicas, so the file never mentions it
LAST_APPLIED = {"image": "api:1.4.2", "env": {"LOG_LEVEL": "debug"}}
LIVE = {"image": "api:1.4.2", "env": {"LOG_LEVEL": "debug"},
"replicas": 6, "resourceVersion": "8841"} # replicas: 6 was set by the HPA
NEW_FILE = {"image": "api:1.5.0"} # image bumped, env removed
def apply(last_applied, live, new_file):
"""Toy model of client-side kubectl apply: a three-way merge, not the real patch."""
if live is None:
return dict(new_file) # nothing live: apply creates
result = copy.deepcopy(live)
for key in set(last_applied) - set(new_file): # in last-applied, gone from the file
result.pop(key, None) # ... so remove it from the object
result.update(new_file) # whatever the file says, set
return result # fields only the server set survive
def create(live, new_file):
if live is not None:
return 'Error: deployments.apps "api" already exists'
return dict(new_file)
def replace(live, new_file):
if live is None:
return 'Error: deployments.apps "api" not found'
return dict(new_file, replicas=new_file.get("replicas", 1)) # the file, whole; defaults fill the rest
def show(label, obj):
print(f"{label:8}", {k: obj[k] for k in sorted(obj) if k != "resourceVersion"})
show("live", LIVE)
show("apply", apply(LAST_APPLIED, LIVE, NEW_FILE))
show("replace", replace(LIVE, NEW_FILE))
print("create ", create(LIVE, NEW_FILE))
print("create ", create(None, NEW_FILE), "<- nothing live yet")
print("replace ", replace(None, NEW_FILE))
# live {'env': {'LOG_LEVEL': 'debug'}, 'image': 'api:1.4.2', 'replicas': 6}
# apply {'image': 'api:1.5.0', 'replicas': 6}
# replace {'image': 'api:1.5.0', 'replicas': 1}
# create Error: deployments.apps "api" already exists
# create {'image': 'api:1.5.0'} <- nothing live yet
# replace Error: deployments.apps "api" not found
The same merge shows the two ways people get surprised. If the file does carry replicas: 3, apply dutifully sets it and undoes the HPA. And when someone edited LOG_LEVEL by hand, apply only overrides it if the new file mentions env; a file that dropped env removes the whole block, hand edit included, because last-applied says you owned it:
# the footgun: the file also says replicas: 3 while the HPA has scaled to 6
FILE_WITH_REPLICAS = {"image": "api:1.5.0", "replicas": 3}
LAST_WITH_REPLICAS = {"image": "api:1.4.2", "replicas": 3}
show("apply", apply(LAST_WITH_REPLICAS, LIVE, FILE_WITH_REPLICAS))
# drift: someone ran kubectl edit and set env LOG_LEVEL=trace by hand
drifted = dict(LIVE, env={"LOG_LEVEL": "trace"})
show("apply", apply(LAST_APPLIED, drifted, {"image": "api:1.5.0", "env": {"LOG_LEVEL": "debug"}}))
show("apply", apply(LAST_APPLIED, drifted, {"image": "api:1.5.0"}))
# apply {'env': {'LOG_LEVEL': 'debug'}, 'image': 'api:1.5.0', 'replicas': 3}
# apply {'env': {'LOG_LEVEL': 'debug'}, 'image': 'api:1.5.0', 'replicas': 6}
# apply {'image': 'api:1.5.0', 'replicas': 6}
The real patch works field by field inside nested objects and treats lists by merge keys, which this toy skips; the ownership rule is the same.
The complexity
The cost is in round trips and in what you have to know:
createandreplaceare one request each, butreplaceneeds the complete object, usually fetched first withkubectl get -o yaml, and the rightresourceVersion.- Client-side
applyreads the live object, computes the patch locally and sends it, and stores a copy of your file in an annotation that grows with the object; very large objects can hit the annotation size limit, which is one reason server-side apply exists. - Server-side
applysends the file and lets the server merge, tracking ownership per field inmanagedFields, which makeskubectl get -o yamloutput longer but makes shared ownership explicit.
Where it goes wrong
replicasin an applied file under an HPA. Each deploy resets the count and the autoscaler fights back. Leave the field out, or let the HPA own it under server-side apply.replaceto fix one field. Everything the file omits resets: annotations added by controllers, defaults you never saw, the HPA's count.- Switching from
createoredittoapply. The first apply has no last-applied record, so it cannot remove fields you dropped; kubectl warns, and a stale field survives until you delete it explicitly. apply --prunewith a broad selector. It deletes every matching object that is not in your files, including ones created by other teams with the same label.- Conflicts under server-side apply. A second manager writing the same field gets a
409;--force-conflictstakes ownership and is the moment to ask why two things write that field.
When it shows up in interviews
Usually as a follow-up to "how do you deploy a change?": the interviewer wants apply with files in version control, then probes with "what if the HPA changed replicas?" or "what does replace do to fields you did not specify?". A good answer names the three-way merge and the last-applied annotation, and knows that kubectl diff previews the patch. The objects the files describe are in Pod vs ReplicaSet vs Deployment, and the controller loop that reacts to each apply is the Pods, ReplicaSets and Deployments lesson.
How to say it in an interview
"create is imperative: make this object, fail if it exists. replace is imperative too: overwrite the whole object with my file, so anything the file omits resets, and it fails if the object is missing. apply is declarative: create or patch, computing a three-way diff between what I last applied, what is live and what I have now, so fields I dropped are removed and fields other controllers own, like the HPA's replicas, are left alone. I keep manifests in git and only ever apply them, run kubectl diff first, and keep replicas out of autoscaled Deployments."
Frequently asked questions
What is the difference between kubectl apply and kubectl create?
create sends your file as a new object and fails with AlreadyExists if the name is taken. apply creates the object if it is missing and otherwise patches it, computing the change from your last-applied file, the live object and the new file, so it can be run repeatedly as the file evolves.
What is the difference between kubectl apply and kubectl replace?
replace overwrites the whole live object with your file: fields the file omits fall back to defaults, and it fails if the object does not exist. apply changes only the fields your file owns, keeps fields set by other controllers such as the HPA, and creates the object when it is missing.
Should I use kubectl apply or create?
Use apply for anything described by files in version control, which is almost everything, because the same command works for the first deploy and every later change. Use create for one-off objects you make once, such as a Namespace or a Secret from a literal, where failing on a duplicate is the behaviour you want.
Sources
- Declarative Management of Kubernetes Objects Using Configuration Files — Kubernetes Documentation
- Imperative Management of Kubernetes Objects Using Configuration Files — Kubernetes Documentation
- Server-Side Apply — Kubernetes Documentation
- Kubernetes Object Management — Kubernetes Documentation
- kubectl reference: apply, create, replace, diff — Kubernetes Documentation