Kubernetes manifest checklist before kubectl apply
What to check before kubectl apply: apiVersion drift, indentation, resource limits, probes, image tags, and the YAML values that parse but misbehave.
kubectl apply is forgiving in the worst way. A manifest that is syntactically valid and structurally plausible is accepted, and the problems show up minutes later as a pod stuck in Pending, a rollout that never becomes ready, or a service that quietly routes to nothing. Most of those problems are visible in the file. This is what to check before the file leaves your machine.
1. It parses, and it parses the way you think
Start with YAML Validator. Indentation errors and tabs are the obvious failures. The subtle one is a document that parses to the wrong shape: a key indented one level too deep becomes a child of the wrong parent, and Kubernetes ignores fields it does not recognise in that position. resources: nested under image: instead of alongside it is valid YAML and silently applies no limits.
The fastest way to see the real structure is YAML to JSON. Braces do not lie about nesting the way indentation can.
2. apiVersion and kind are current
APIs are removed on a schedule. extensions/v1beta1 Deployments and Ingresses have been gone since 1.16 and 1.22 respectively; batch/v1beta1 CronJobs since 1.25; policy/v1beta1 PodSecurityPolicy since 1.25. A manifest copied from an old tutorial applies fine on an old cluster and fails on the current one with no matches for kind.
Kubernetes Manifest Validator checks apiVersion/kind pairs against a current schema, which catches the drift before the cluster does.
3. Values that YAML reinterprets
Three fields are routinely broken by YAML 1.1 scalar rules:
- Ports written as
port: 80are fine; atargetPortgiven a name must be a string, and"8080"and8080are different values to some controllers. - Environment values.
value: trueis a boolean, and Kubernetes rejects it (valuemust be a string). Quote everyenvvalue:value: "true". - Anything that looks like a time or a version:
"22:22","3.10","0755".
The rule is simple: if a field is documented as a string, quote it. YAML Validator in YAML 1.1 mode warns about each ambiguous scalar.
4. Image tags are pinned
image: nginx means nginx:latest, which means a different image on every node that pulls at a different time, and no way to roll back to what was running yesterday. Pin a tag, or better, a digest:
image: nginx:1.27.2@sha256:...
Set imagePullPolicy deliberately. The default is IfNotPresent for a pinned tag and Always for latest, which is a trap in both directions.
5. Resources are set, and limits are honest
A container without resources.requests is scheduled as if it needs nothing, and the scheduler will happily pack thirty of them onto a node that can run ten. A container without limits can consume the node. Set both:
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "1"
memory: "512Mi"
A memory limit lower than the application's real working set produces OOMKilled restarts that look like application crashes. Measure before setting; do not copy the numbers from another service.
6. Probes exist and point at the right thing
Without a readiness probe, a pod receives traffic the instant its container starts, including the seconds before the application is listening. Without a liveness probe, a deadlocked process runs forever. Add both, and make sure the paths are real: a readiness probe against /health on an application that serves /healthz fails every check, the pod never becomes ready, and the rollout hangs at 0/3.
Startup probes cover slow-starting applications; without one, a generous initialDelaySeconds on the liveness probe is the fallback.
7. Selectors match labels
A Deployment's spec.selector.matchLabels must match spec.template.metadata.labels, and a Service's spec.selector must match the pod labels it is meant to route to. A typo in any of the three produces a Service with zero endpoints, which is the classic "the app is running but nothing can reach it." Check with kubectl get endpoints <service> after applying — or check the labels in the file before.
8. Security context is not the default
The default is root, a writable root filesystem, and privilege escalation allowed. Most applications need none of those:
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
Pod Security Admission in restricted mode enforces this on newer clusters; setting it in the manifest means the manifest works there too.
The checklist
- Valid YAML; structure confirmed in the JSON view
apiVersion/kindcurrent for the target cluster version- String fields quoted, especially
envvalues and ports - Image tag or digest pinned; pull policy explicit
- Requests and limits set from measurement
- Readiness and liveness probes with correct paths
- Selectors match labels
- Security context set
Then kubectl apply --dry-run=server -f manifest.yaml for the checks only the API server can do, and the commands for the rollout on the kubectl cheat sheet.