Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes

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

Sponsored

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 create twice raises an error. Your deployment scripts must handle exits, increasing complexity.
  • State drift – When you run kubectl scale or kubectl edit, those changes live only in the cluster. Your local YAML files become stale, making rollbacks or restores dangerous.
  • No change lineagekubectl create overwrites 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:

  1. The manifest you provide (local file or stdin).
  2. 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

  1. Write a manifest (YAML preferred) with your desired state — apiVersion, kind, metadata, and spec.
  2. Run kubectl apply -f <file> – the client sends the whole manifest to the API server.
  3. API server compares your manifest against the current object (if it exists).
  4. Server-side apply calculates the patch: fields in your manifest are updated; fields absent are left alone unless you explicitly delete them.
  5. Kubernetes stores the merged result and updates managedFields with a new manager entry (by default, kubectl-client-side-apply for older versions, or kubectl for server-side).
  6. 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 apply internally. Understanding how kubectl apply behaves — 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 trackingmanagedFields prevents accidental overwrites.
  • Safe updates – use --prune and --force-conflicts with 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 create for resources that already exist — leads to duplicate errors; always use kubectl apply for declarative management.
  • Forgetting to prune resources removed from YAML — apply without --prune leaves dead objects; use --prune with --dry-run first.
  • Modifying managedFields directly via kubectl edit — that breaks server-side apply tracking; always apply the YAML file.
  • Assuming kubectl apply overwrites all fields — it patches only the fields in your YAML; fields omitted are unchanged unless you use --prune.

Variations

  1. Use kubectl apply -k . with Kustomize to apply environment-specific overlays while keeping base YAML declarative.
  2. Combine kubectl apply --server-side with --field-manager=my-manager to give custom field ownership identifiers.
  3. Use kubectl apply --prune --all to 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 apply underneath 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 --prune ensures only their resources stay.

Key takeaways

  • Declarative config with kubectl apply is 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 apply only patches the fields present in your YAML; omitted fields remain unchanged.
  • Use --dry-run=client before prune to avoid unintended deletions.
  • Mastering kubectl apply is the prerequisite for GitOps workflows and tools like ArgoCD and Flux.
  • Always prefer declarative YAML files over imperative commands for production-grade infrastructure.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.