Declarative kubectl
Generate, diff, apply
You finished the previous lesson with an export in your pocket: do='--dry-run=client -o yaml'. This lesson turns it into a working method: never write YAML from scratch; generate it with kubectl, review it, version it and apply it. It's the declarative workflow on imperative steroids.
Work from the dev-machine tab.
Mission 1: the manifest factory
You're going to generate three manifests without creating anything in the cluster, all in a manifests directory. First, prepare the ground:
mkdir -p /home/laborant/manifests && cd /home/laborant
export do='--dry-run=client -o yaml'
How do I generate a Deployment without creating it in the cluster?
kubectl create deployment api --image=ghcr.io/iximiuz/labs/nginx:alpine --replicas=3 $do > manifests/deployment.yaml
How do I generate a Service for that Deployment that doesn't exist yet?
kubectl expose -f manifests/deployment.yaml --port=80 --target-port=80 $do > manifests/service.yaml
Notice the nuance: kubectl expose deployment api would query the cluster (and api doesn't exist yet), but expose -f reads the file. The whole flow happens on your disk.
How do I generate a ConfigMap from a .env file?
printf 'LOG_LEVEL=debug\nAPP_COLOR=azul\n' > .env
kubectl create configmap web-config --from-env-file=.env $do > manifests/configmap.yaml
Open manifests/configmap.yaml in the IDE tab: each line of the .env has become a key. The same technique works for Secrets and CronJobs:
kubectl create secret generic db-credentials --from-literal=user=admin --from-literal=password=s3cr3t $do > secret.yaml
kubectl create cronjob limpieza --image=alpine --schedule="0 2 * * *" $do > cronjob.yaml
(These two aren't part of the mission; generate as many as you like, practice is free.)
Mission 2: diff before you apply
Now, to the cluster. But the professional workflow has an intermediate step most people skip and later regret:
How do I preview what would change before applying?
kubectl diff -f manifests/
Since everything is new, the diff shows three whole objects in green. Boring now, a lifesaver once the cluster has been alive for months. Two variants of diff it helps to know:
kubectl apply -f manifests/deployment.yaml --dry-run=server
KUBECTL_EXTERNAL_DIFF="diff -u --color=always" kubectl diff -f manifests/
The first validates against the real API without creating anything (it catches errors the client-side dry-run can't, such as admission webhooks or quotas). The second changes the tool that renders the diff: KUBECTL_EXTERNAL_DIFF accepts the command and its arguments, and kubectl hands it two directories to compare. Here we use the plain old diff with color; if your machine has colordiff or delta, they go in its place, but they have to be installed, or kubectl fails with executable file not found in $PATH.
How do I apply every manifest in a directory?
kubectl apply -f manifests/
With -R it descends recursively into subdirectories. And the deploy-and-wait pattern, the one that waits for everything to converge before moving on (the heart of any pipeline):
kubectl apply -f manifests/ && kubectl rollout status deployment/api
The rest of the lifecycle, in three questions
How do I delete everything defined in a directory, or by label?
kubectl delete -f manifests/
kubectl delete pods,services -l app=api
(Don't run them now: the next unit needs these objects alive.)
How do I wait for a resource to reach a condition before moving on?
kubectl wait deployment/api --for=condition=Available --timeout=120s
And how do I export the YAML of an existing resource?
kubectl get deployment api -o yaml > deployment-exportado.yaml
Open it and you'll see why it won't do as-is for a repository: it carries status, metadata.uid, resourceVersion, creationTimestamp and the annotations the cluster itself adds. None of that was written by you and none of it should be versioned.
There are plugins that clean up that junk (kubectl-neat is the best known; it's installed with krew and doesn't ship out of the box), but the professional habit is a different one: what goes into the repository is generated with --dry-run=client -o yaml, as in Mission 1. Exporting is for inspecting a live object; for versioning, you generate.
💡 There is also kubectl apply --prune, which removes from the cluster whatever is no longer in your files. It's powerful and destructive in equal measure (always try it with --dry-run=server first), and its classic --prune-allowlist mechanism has been in alpha for years: Kubernetes is moving toward the ApplySet model to replace it. If you're interested, check its status in your cluster's version before betting on it.
Manifests generated, diffed and applied. The next unit answers the question that's probably already on your mind: what about when I need the same manifests with variations per environment?
Kustomize and patches
You have a set of base manifests working. Now imagine the inevitable request: "we need the same thing in production, but with 5 replicas and a prefix on the names". The naive solution is to copy the YAML files and edit them, and with it come the duplicated files that silently drift apart. The solution built into kubectl is called Kustomize.
Unlike Helm (next lesson), Kustomize doesn't use templates: it starts from a set of untouched base manifests and applies transformations to them, declared in a kustomization.yaml file and organized in overlays per environment.
Mission 3: the production overlay
Set up the classic structure and put your base in place (the manifests from the previous unit):
cd /home/laborant
mkdir -p k8s/{base,overlays/{dev,prod}}
cp manifests/deployment.yaml manifests/service.yaml k8s/base/
cat << 'EOF' > k8s/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
EOF
And now the production overlay, in k8s/overlays/prod/kustomization.yaml:
cat << 'EOF' > k8s/overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namePrefix: prod-
replicas:
- name: api
count: 5
labels:
- pairs:
env: tienda
includeSelectors: true
EOF
The YAML, explained in questions and answers
Why does resources point at ../../base instead of at files?
Because an overlay consumes another complete kustomization, not loose manifests. The base doesn't even know overlays exist: it stays untouched and shared by every environment.
What exactly does namePrefix do?
It renames every generated resource (api becomes prod-api) and, the important part, also updates the references between them: the renamed Service still points at the right Pods. That reference tracking is what you don't get with a find and replace.
Do replicas and labels modify my deployment.yaml?
They don't touch a single byte of the base: they're transformations applied on the fly to the in-memory copy. replicas changes the count of the Deployment called api, and labels adds the label to every resource.
And what's that includeSelectors: true about?
About the dangerous part. By default, labels only writes into the objects' metadata; with includeSelectors: true it also writes into the Deployment's selector and into the labels of its Pod template. Here we want it, because the overlay produces a new Deployment (prod-api) and its three places must be consistent.
On a Deployment that already exists, on the other hand, it's a bomb: the selector is immutable, so the API rejects the change and the rollout is left half done. That's why the old commonLabels (which always did both things, without asking) is deprecated, and why its replacement makes you ask for the dangerous behavior by hand.
And what if I need a change that has no transformer of its own?
That's what patches are for: partial YAML files merged into the base (you'll see the mechanics in Mission 4). Kustomize can also generate ConfigMaps and Secrets from the kustomization itself (configMapGenerator, secretGenerator) and group reusable features into components.
The workflow has two commands, and the order matters:
How do I preview the final YAML Kustomize would generate?
kubectl kustomize k8s/overlays/prod/
Go over the output: prefixed names, 5 replicas, new labels, and your base untouched on disk.
How do I apply an overlay directly?
kubectl apply -k k8s/overlays/prod/
Notice that api (the base applied in the previous unit) and prod-api (the overlay) now live side by side: two environments from the same origin, in the same practice cluster.
💡 kubectl kustomize and apply -k use the version of Kustomize bundled with kubectl, which sometimes runs one version behind the standalone kustomize binary. If one day you need a very recent feature, install the binary separately.
Mission 4: patches for one-off changes
Not everything deserves an overlay. For surgical tweaks on live objects there is kubectl patch, in three dialects. Apply these two to the api Deployment (the base one):
How do I change the number of replicas with a JSON patch?
kubectl patch deployment api --type='json' -p='[{"op":"replace","path":"/spec/replicas","value":5}]'
How do I add an annotation with a merge patch?
kubectl patch deployment api --type='merge' -p='{"metadata":{"annotations":{"deploy-time":"2026-07-09"}}}'
The third dialect, the strategic merge patch, is the one kubectl apply uses under the hood: it merges a partial YAML with knowledge of the schema (it knows, for instance, to merge container lists by name instead of replacing them). A file like this one, applied with patch --patch-file, would add an environment variable without declaring the rest of the container:
spec:
template:
spec:
containers:
- name: nginx
env:
- name: LOG_LEVEL
value: "debug"
Summary
- The method: generate with
$do, review withdiffand apply by directory. What goes into the repository is generated; from the cluster you only export to look. - Kustomize derives environments from an untouched base: overlays with
namePrefix,replicas,labelsand patches. - Preview (
kubectl kustomize) before applying (apply -k), always. kubectl patchin three dialects (json, merge, strategic) for precise one-off tweaks.
- Previous lesson
- Imperative kubectl
- Next lesson
- Helm