Skip to main content
DevOps Insightsguides & answers

Containers · beginner

Reading a Kubernetes YAML You Did Not Write

Short answer

Do not read it top to bottom. kind and name first, then labels and selectors, then image, ports, resources and probes — that order gets you oriented before you get lost. Then confirm the file matches what is actually running, because a template, an autoscaler or an admission webhook is frequently in between.

N
Neeraj Jha
Updated 10 min read221 views
On this page · 14 sections
Diagram: an annotated Deployment manifest showing its kind, replica count, pinned image and missing resource requests.

Somebody has left, or is on holiday, or wrote this two years ago. There are four hundred lines of YAML, it is deployed to production, and you need to change one thing without breaking the rest.

Nobody teaches this. Every Kubernetes tutorial writes a manifest from scratch, which is not the situation you are usually in.

Here is how to read one quickly, in an order that gets you oriented before you get lost.

Who this is for

Somebody has left, and there are four hundred lines of YAML in production that you need to change one line of. You do not need to be able to write a manifest from scratch. This is the other skill, and nobody teaches it.

Read it in this order

Not top to bottom. Manifests are not written to be read linearly, and the important parts are scattered.

1. kind and metadata.name — what is this and what is it called

yaml
kind: Deployment
metadata:
  name: checkout-api
  namespace: production

Most files contain several documents separated by ---. Get the list first:

bash
grep -nE '^(kind|  name):' manifest.yaml

Now you know you are looking at a Deployment, a Service, an Ingress and a ConfigMap rather than one enormous object.

2. Labels and selectors — what is wired to what

This is the part that actually determines behaviour, and it is the part people skip.

yaml
# In the Deployment
spec:
  selector:
    matchLabels:
      app: checkout          # which pods this Deployment owns
  template:
    metadata:
      labels:
        app: checkout        # what the pods it creates will carry
---
# In the Service
spec:
  selector:
    app: checkout            # which pods receive traffic

Three places, one string. If the Deployment's selector and its template labels disagree, the API server rejects the Deployment outright. If the Service's selector disagrees, nothing complains: the Service routes to nothing and you get a Service with no endpoints.

Check them against each other before anything else:

bash
grep -nA3 -E 'selector|labels' manifest.yaml

3. The image — what actually runs

yaml
image: registry.example.com/checkout:1.4.2

Ask two things. Is it pinned, or is it :latest or a moving tag — because if it moves, the manifest does not tell you what is running. And does that tag still exist in the registry, because a tag deleted upstream produces ImagePullBackOff the next time a pod is rescheduled, possibly months after anyone touched this file.

4. Ports — three numbers that must agree

yaml
# Container
ports:
  - containerPort: 8080      # what the container declares (informational)
---
# Service
ports:
  - port: 80                 # what callers use
    targetPort: 8080         # where it forwards — must be what the process listens on

targetPort must be the port the process actually listens on, or you get a Service that resolves and refuses every connection. A numeric containerPort is informational — it can agree with targetPort and both can still be wrong. When targetPort is a name rather than a number, that name has to be defined on the container.

5. Resources — what it reserves and what kills it

yaml
resources:
  requests:   { cpu: "100m", memory: "256Mi" }
  limits:     { cpu: "500m", memory: "512Mi" }

Read these as two different mechanisms, because they are: the request is a reservation the scheduler honours, the limit is a ceiling the kernel enforces. Exceeding the memory limit kills the container — that is OOMKilled, exit code 137.

Two things to notice. If requests and limits are both absent, the pod is scheduled with no reservation and is among the first evicted under node pressure. (If only limits is set, Kubernetes copies it into requests — unless a LimitRange in the namespace supplies a default first.) If requests are much higher than what the thing uses, you are looking at one of the reasons a cluster can be full at 10% utilisation. The distinction itself is worth twenty minutes: requests vs limits.

6. Probes — what decides if it is alive and ready

yaml
readinessProbe:               # decides whether it receives traffic
  httpGet: { path: /healthz, port: 8080 }
livenessProbe:                # decides whether it gets restarted
  httpGet: { path: /healthz, port: 8080 }
  initialDelaySeconds: 30

Read carefully whether they point at the same endpoint. They often do, and that is usually a mistake — a readiness check that fails should remove traffic, and a liveness check that fails restarts the process. Pointing both at one endpoint means a slow dependency can restart your application instead of just draining it. readiness vs liveness is the distinction.

If there is no readiness probe at all, traffic arrives the instant the container starts, which is a common cause of errors during deploys.

7. Everything else, only if relevant

Volumes, affinity, tolerations, securityContext, annotations. Most of it is either boilerplate or a workaround for something specific. Read it when the change you are making touches it.

What the file does not tell you

This is the part that catches people, and it is worth holding in mind the entire time.

It may not be what is running. Somebody may have edited the live object, scaled it, or applied a newer version from a different branch. Always compare:

bash
kubectl get deployment checkout-api -n production -o yaml > live.yaml
diff <(yq -P 'sort_keys(..)' manifest.yaml) <(yq -P 'sort_keys(..)' live.yaml) | head -40

Expect noise — the cluster adds status, creationTimestamp, resourceVersion and defaults for everything you left out. What you are looking for is a difference in something the file sets explicitly.

It may be a template. If this is Helm or Kustomize, the file is an input rather than the output. Render it before reading:

bash
helm template ./chart -f values-production.yaml     # what Helm would apply
kubectl kustomize ./overlays/production             # what Kustomize would apply

Reading the template and reasoning about the output is how people end up debugging a value that gets overridden two layers up.

It may not be the only thing acting on the object. An autoscaler changes replicas, so the number in the file may be irrelevant and overwriting it can cause a scale-down. A mutating webhook or service mesh can inject containers that are not in the file at all.

A quick orientation script

Four commands that answer "what is this" faster than reading:

bash
# What objects, and what are they called
grep -nE '^(kind|  name|  namespace):' manifest.yaml

# Every image, so you know what actually runs
grep -nE 'image:' manifest.yaml

# The wiring: labels, selectors, ports
grep -nE 'app:|selector|targetPort|containerPort' manifest.yaml

# Does it parse and would the cluster accept it — without applying anything
kubectl apply --dry-run=server -f manifest.yaml

That last one is the most underused command in this list. --dry-run=server sends the manifest to the API server, which validates it against the real schema and the admission controllers, and applies nothing. It catches a deprecated API version, a typo in a field name, and a policy that would reject it — before you find out the interesting way.

If you want a second opinion on what the manifest gets wrong before you touch it, the manifest analyzer checks it in your browser.

Before you change anything

Find out if it is in Git and whether that copy is authoritative. If the cluster is managed by GitOps, editing the live object is either reverted by the controller — Flux corrects drift on every reconcile interval, Argo CD only once self-heal is turned on — often while you are still wondering whether your change worked, or left quietly out of sync until the next sync from Git overwrites it.

Change one thing. Multiple simultaneous edits to an unfamiliar manifest make the failure impossible to attribute.

Apply to a non-production namespace first, if one exists that is close enough to be meaningful.

Know how to undo it. For a Deployment that is kubectl rollout undo deployment/<name>, and it is worth having it typed out in another terminal before you apply.

When it will not even parse

Added 2 November 2026. Before you can read a file you did not write, it has to parse, and the three errors you will meet most say less than they seem to. Reproduced with Helm 3.18.2, whose messages come from the same Go YAML library kubectl uses:

  • yaml: line N: mapping values are not allowed in this context: a colon followed by a space inside a value that is not quoted, two keys on one line, or a key indented further than its neighbours.
  • yaml: line N: did not find expected key: a key one or more spaces out of line. The number pointed at the line before the mistake both times it was reproduced, so look at line N + 1.
  • yaml: line N: found character that cannot start any token: a tab in the indentation, which looks exactly like spaces. cat -et FILE (macOS) or cat -A FILE (Linux) shows it as ^I. An unquoted value starting with @ gives the same message without a line number.

When the line number does not help, a second parser often does. PyYAML, if it is installed, names the line and column of the mistake itself:

bash
python3 -c 'import sys,yaml; yaml.safe_load(open(sys.argv[1]))' deployment.yaml

Each error, with its causes and the commands to confirm them: mapping values are not allowed, did not find expected key and found character that cannot start any token.

Common misreadings

Assuming replicas in the file is the live count. An HPA may own it.

Assuming the absence of a field means a safe default. No resources block means no limits — unless a LimitRange in the namespace fills in defaults, which only the live object will show — and no limits is not conservative: it means the container can consume whatever the node has.

Reading limits as the reservation. It is the ceiling. requests is the reservation, and the scheduler only looks at requests.

Trusting the file's indentation of a list. YAML is unforgiving here, and not always loudly. A list item indented one space too far under a mapping is usually a parse error, which is the lucky case. The silent ones are a string item indented too far, which is folded into the previous string (- --a followed by an over-indented - --b parses as the single argument --a - --b), and a key indented one level too shallow, which quietly moves to the enclosing mapping.

Not checking the namespace. Half a debugging session can go on a manifest that is being applied somewhere other than where you are looking.

Tagged with

Enjoyed this article?

Get more DevOps insights delivered to your inbox.

Get new posts by email

Subscribe to get an email when a new blog post is published. Skip anytime.

No spam, unsubscribe anytime.

N

Written by

Neeraj Jha

Platform administrator and lead writer.

View all posts →
Terms used here (1)

Short definitions for the vocabulary on this page, in case any of it is new. Each links to the full entry.

Service Mesh
A dedicated infrastructure layer for managing service-to-service communication within a microservices architecture. Service meshes like Istio and Linkerd provide traffic management, mutual TLS, observability, and resilience features without changing application code.

All 59 terms in the glossary

Discussion

0 comments

Sign in to join the conversation.

Be the first to comment

Start a conversation about this post

Share: