apply makes the cluster match a file. delete removes objects. Between them they cover most of how real workloads are managed.
The important habit is that manifests live in version control and apply is how they reach the cluster, so the repository, not the cluster, is the source of truth.
kubectl apply -f deployment.yaml
kubectl apply -f ./manifests/ # every file in a directory
kubectl apply -f ./manifests/ -R # and subdirectories
kubectl apply -k ./overlays/production # a Kustomize overlayIt creates the object if absent and updates it if present, which makes it safe to run repeatedly. The same command works on first deploy and every deploy after.
Several objects in one file, separated by ---, are applied in order:
apiVersion: v1
kind: Service
metadata:
name: payments-api
# ...
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: payments-api
# ...| Command | Behaviour | When |
|---|---|---|
create | Fails if the object exists | One-off creation, or generating YAML |
apply | Creates or updates, merging changes | Nearly always |
replace | Overwrites the whole object; fails if absent | Rare (discards fields set by controllers) |
patch | Changes named fields only | Scripted, targeted edits |
The mechanism behind apply is a three-way merge between the last applied configuration, your file, and the live object. That is why it can change one field without discarding values written by controllers, and why the annotation kubectl.kubernetes.io/last-applied-configuration exists on objects you have applied.
It also explains a common surprise: removing a field from your manifest removes it from the cluster, because the merge can see it was there before and is not now. A field you never declared at all is left alone.
Mixing apply with kubectl edit or console changes muddles that history and produces confusing results. Pick one.
kubectl diff -f deployment.yaml--- live
+++ merged
spec:
- replicas: 3
+ replicas: 5
template:
spec:
containers:
- name: api
- image: ghcr.io/acme/payments-api:1.4.2
+ image: ghcr.io/acme/payments-api:1.5.0This is the closest equivalent to terraform plan, and it is worth running before any change to a production namespace.
kubectl apply -f deployment.yaml --dry-run=server--dry-run=server sends the request to the API server, which runs validation and admission controllers without persisting anything. It catches schema errors, policy rejections and missing CRDs that client-side validation cannot see. --dry-run=client only formats locally. Useful for generating YAML, not for validating it.
kubectl apply -f deployment.yaml --server-sideThe newer mechanism. Instead of relying on a last-applied annotation, the API server records field ownership for every manager that writes to the object. If two controllers try to own the same field, you get an explicit conflict rather than a silent overwrite:
Apply failed with 1 conflict: conflict with "hpa-controller": .spec.replicas
That message is genuinely useful. Here it is telling you a HorizontalPodAutoscaler owns the replica count and your manifest should not declare it.
--force-conflicts takes ownership anyway, which is sometimes correct and should be a decision rather than a reflex.
kubectl delete -f deployment.yaml # the safest form
kubectl delete deployment payments-api
kubectl delete pods -l app=payments-api
kubectl delete pods --all -n stagingPrefer delete -f with the same file you applied. Deleting by name is easy to get wrong, particularly when the name is similar across namespaces.
Deleting a Deployment deletes its ReplicaSets and their Pods, through owner references.
# Delete the Deployment but keep the Pods running
kubectl delete deployment payments-api --cascade=orphan--cascade=orphan is occasionally useful, migrating Pods to a new controller, for instance, and leaves Pods that nothing manages, so use it deliberately.
Deleting a Pod managed by a Deployment does not remove it: the controller notices the shortfall and creates a replacement. That is how you restart a single Pod.
kubectl delete namespace stagingThis removes everything in it. Deployments, Services, Secrets, ConfigMaps and PersistentVolumeClaims. With a StorageClass whose reclaim policy is Delete, the underlying disks and their data go too. It is among the most destructive single commands available.
An object stuck in Terminating is almost always waiting on a finalizer. A marker saying "some controller must clean up before this can go".
kubectl get pod <name> -o jsonpath='{.metadata.finalizers}'For Pods, the usual cause is a node that has gone away, so the kubelet cannot confirm the containers stopped:
kubectl delete pod <name> --grace-period=0 --forceThis tells the API server to forget the Pod without confirmation. On a node that is merely unreachable rather than dead, the containers may still be running, which for a StatefulSet member holding a volume can mean two instances writing to the same data. Force-delete after establishing the node is genuinely gone, not before.
For other objects, removing a finalizer by hand is a last resort:
kubectl patch pvc <name> -p '{"metadata":{"finalizers":null}}' --type=mergeThe correct fix is nearly always to remove whatever is blocking cleanup. A Pod still using a PVC, or an operator that was uninstalled before its custom resources were deleted.
kubectl apply -f ./manifests/ --prune -l app=payments-api--prune deletes objects matching the selector that are no longer in the manifests, which is how you make apply genuinely declarative. It is powerful and easy to misuse. A wrong label selector deletes more than you intended. In practice, most teams get this behaviour from ArgoCD or Flux instead, where it is scoped and previewed properly.
| Task | Command |
|---|---|
| Apply a file | kubectl apply -f file.yaml |
| Apply a directory | kubectl apply -f ./manifests/ -R |
| Preview | kubectl diff -f file.yaml |
| Validate fully | kubectl apply -f file.yaml --dry-run=server |
| Field-ownership apply | kubectl apply -f file.yaml --server-side |
| Delete what you applied | kubectl delete -f file.yaml |
| Delete by label | kubectl delete pods -l app=<label> |
| Keep the children | kubectl delete deployment <n> --cascade=orphan |
| Force a stuck Pod | kubectl delete pod <n> --grace-period=0 --force |
| Inspect finalizers | kubectl get <res> <n> -o jsonpath='{.metadata.finalizers}' |
apply creates or updates and is safe to repeat. create fails on existing objects; replace overwrites wholesale.kubectl diff -f before applying to anything that matters.--dry-run=server validates against admission controllers; --dry-run=client only formats.Delete reclaim policy destroys the data.<MCQExercise exercise={{ id: 'k8s-applydel-field-removed', title: 'A setting that disappeared', description: 'A Deployment manifest previously set nodeSelector. Someone removes those lines and runs kubectl apply. The live Deployment no longer has a nodeSelector. Is that expected?', options: [ { id: 'a', text: 'Yes. Apply uses a three-way merge and can see the field was previously applied and is now absent, so it removes it' }, { id: 'b', text: 'No. Apply only adds and updates fields, so removal indicates a bug' }, { id: 'c', text: 'Yes, but only because nodeSelector is a mutable field; most fields would persist' }, { id: 'd', text: 'No. The field must have been removed by a controller, not by apply' }, ], correctId: 'a', explanation: 'apply compares the last-applied configuration, your current file and the live object. A field present in the first and missing from the second is treated as a deliberate removal. This is what makes apply genuinely declarative. The file describes the complete intended state. Fields you never applied, such as those set by controllers or defaulted by the API server, are left untouched.', wrongExplanations: { b: 'Add-only behaviour would make it impossible to remove anything declaratively, and the manifest would drift further from reality with every change.', c: 'The behaviour comes from the merge algorithm rather than from any per-field property. It applies to any field you previously applied.', d: 'No controller manages nodeSelector. The last-applied annotation records that your own manifest set it, which is precisely how apply knows to remove it.', }, }} />
<MCQExercise exercise={{ id: 'k8s-applydel-force-delete', title: 'Force-deleting on a partitioned node', description: 'A StatefulSet Pod is stuck Terminating because its node is unreachable. What is the risk of kubectl delete pod --grace-period=0 --force?', options: [ { id: 'a', text: 'If the node is only partitioned rather than dead, the container keeps running and a replacement Pod may write to the same volume' }, { id: 'b', text: 'The Pod's PersistentVolumeClaim is deleted along with it' }, { id: 'c', text: 'The StatefulSet is left without a controller and stops creating replacements' }, { id: 'd', text: 'Nothing. Force deletion is always safe once a Pod is Terminating' }, ], correctId: 'a', explanation: 'Force deletion tells the API server to forget the Pod without waiting for the kubelet to confirm the containers stopped. If the node is merely unreachable, those containers are still running with the volume attached, and the StatefulSet will create a replacement with the same identity. Two processes writing the same data is exactly the corruption StatefulSet ordinality exists to prevent.', wrongExplanations: { b: 'PVCs from a volumeClaimTemplate deliberately outlive their Pods, that is what makes StatefulSet storage durable. Force deletion does not touch them.', c: 'The StatefulSet controller is unaffected and will create a replacement Pod immediately, which is the source of the risk rather than a mitigation.', d: 'For a Deployment Pod on a genuinely dead node it is usually fine. For a StatefulSet on a partitioned node it is the one case where it can cause data loss.', }, }} />
kubectl diff -f manifest.yaml computes what applying that file would change and prints a unified diff against the live objects, without modifying anything. It is the nearest equivalent to terraform plan and worth running before any change to a production namespace. Pair it with kubectl apply --dry-run=server, which sends the request to the API server for full validation including admission controllers but does not persist it.
With server-side apply the API server records which manager owns each field, so two controllers writing the same field produce an explicit conflict instead of silently overwriting each other. A message like conflict with "hpa-controller": .spec.replicas is telling you that an autoscaler owns the replica count and your manifest should not declare it. It is the more robust mechanism and is worth adopting, particularly where several controllers touch the same objects. --force-conflicts overrides ownership and should be a considered decision.
Yes. Owner references make deletion cascade from Deployment to ReplicaSets to Pods. --cascade=orphan keeps the children, which is occasionally useful when migrating workloads between controllers but leaves Pods that nothing manages. Note the reverse is not true: deleting a Pod that belongs to a Deployment simply causes the controller to create a replacement, which is exactly how you restart a single Pod.
Almost always a finalizer, and for Pods the usual cause is an unreachable node. The API server will not remove the Pod until the kubelet confirms the containers stopped, and a node that is down never will. kubectl delete pod <name> --grace-period=0 --force removes it from the API. Be careful with StatefulSet Pods: if the node is merely partitioned rather than dead, the container is still running with its volume attached, and a replacement Pod writing the same data can corrupt it.
kubectl apply -f ./manifests/ --prune -l app=myapp deletes objects matching the label selector that are no longer present in the applied manifests, which is what makes apply fully declarative rather than additive. It is genuinely useful and genuinely dangerous, because a wrong or overly broad selector deletes more than intended. Most teams get the same behaviour from ArgoCD or Flux, where pruning is scoped to an application and previewed before it runs.
Debug pods with kubectl logs (including --previous), kubectl exec, port-forward and kubectl cp. Handle distroless images with kubectl debug.
Learn how kubeconfig works, switch clusters with kubectl config use-context, set a default namespace, merge kubeconfig files and avoid the wrong cluster.
Learn what a Kubernetes Pod is, what its containers share, why pod IPs change, how to write one, and how to read the statuses you see when debugging.