Blog · July 1, 2025 · Updated on September 7, 2026 · 12 min read

Kubernetes YAML: Understanding Manifests

Detailed cardboard architecture model of houses, streets and green spaces, photographed from above
Photo: Ron Lach / Pexels

Kubernetes YAML is the text format you use to tell Kubernetes what should run in the cluster. A file like this is called a manifest: it describes an object such as a Deployment or a Service with four required fields (apiVersion, kind, metadata, spec), and Kubernetes keeps working until the cluster matches exactly that state. You describe the goal, not the path to get there.

That last point is exactly why many beginners struggle with YAML. The syntax is quick to learn, but thinking in target states takes longer. And because YAML is very forgiving, there are a few traps that can cost you hours of debugging without a single syntax error ever showing up.

I run Kubernetes clusters in production and wrote the Kubernetes Practical Guide for Rheinwerk Computing (2024). YAML gets its own subchapter there, since every object in the cluster ends up as a YAML file. This article is the short version, with the mistakes that come up most often in day to day operations. You can find all my Kubernetes articles collected on the Kubernetes page.

Declarative configuration: you describe the goal, not the path

If you come from classic programming, you think imperatively: step one, step two, check, step three. For a server that means starting a container, checking whether it is running, restarting it if it crashes, adding a second one under load. You are present at every step yourself.

Kubernetes flips that around. You write in a manifest: "this application should run with three Pods." The ReplicaSet behind it then continuously compares the actual state with your desired state. If a Pod is missing, it starts a new one. If there is one too many, it stops it. This cycle is called the reconciliation loop, and it is the core of declarative configuration: you do not check and correct, the system does.

That gives Kubernetes the properties that make it so pleasant to operate. The same manifest always leads to the same end state, no matter what the cluster looks like right now (idempotence). You can spin up a second environment from the same files (repeatability). And drift gets corrected without you having to do anything (self-healing). There are still limits, though: if the image in the manifest does not exist or the registry cannot be reached, Kubernetes cannot establish the state and reports the error. It finds ways, but it does not perform magic.

Still, kubectl has imperative commands like kubectl run or kubectl scale. They have their place: for quick, small changes, one-off actions, debugging, and development environments. In my production clusters, though, the rule holds: whatever is not a manifest in Git does not exist. If I make an imperative change in an emergency, I pull it into the file afterward. Otherwise it gets silently overwritten on the next apply.

Structure of a Kubernetes manifest: the four required fields

Every Kubernetes manifest has the same basic structure, whether it is a Pod, Deployment, Service or ConfigMap. Four top-level fields are required:

  • apiVersion: which API group and version defines the object. For a Deployment that is apps/v1, for Pod, Service and ConfigMap simply v1.
  • kind: the object type, for example Deployment.
  • metadata: name, namespace, labels and annotations. The name has to be unique within the namespace.
  • spec: the desired state. This is where everything that defines the object lives, and its content differs completely depending on kind.

A fifth field, status, you never write yourself, Kubernetes does. That is where the cluster records the current state, which it compares against your spec. If you read an object out of the cluster with kubectl get deployment shop-web -o yaml, you see this field filled in. It does not belong in your file.

Which fields a spec allows is documented in the API reference on kubernetes.io. It is faster right in the terminal: kubectl explain deployment.spec.template.spec.containers shows you every field of the container object with a description. The documentation uses this same dot notation to point at a single field along with its hierarchy, for example spec.containers[].resources.limits.cpu. The dot separates levels, the square brackets indicate a list.

Kubernetes YAML example: a Deployment line by line

The manifest I write most often is a Deployment. Here is a complete, working example:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: shop-web
  namespace: shop
  labels:
    app: shop-web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: shop-web
  template:
    metadata:
      labels:
        app: shop-web
    spec:
      containers:
        - name: web
          image: nginx:stable
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: "100m"
              memory: "64Mi"
            limits:
              cpu: "500m"
              memory: "128Mi"

Read it from top to bottom. metadata names the Deployment and places it in the shop namespace. spec holds the target state: three replicas. The selector tells the Deployment which Pods belong to it, namely every Pod with the label app: shop-web. And template is the blueprint for exactly those Pods: its own metadata with the same label, its own spec with the container list. The label in the template has to match the selector, or Kubernetes rejects the manifest.

The nesting stands out: a Deployment contains a Pod template, which in turn has a spec of its own. That is confusing at first, but it is consistent. The Deployment describes how many Pods should run and how they get replaced. The Pod template describes what runs inside each Pod. How the Deployment turns that into rollouts is covered in Kubernetes Deployment: Rollouts Explained.

With kubectl apply -f deployment.yaml you send the file to the cluster. If you later change replicas to five and apply the file again, Kubernetes starts two more Pods. There is nothing else you need to do.

YAML syntax: key-value pairs, lists and indentation

Every YAML file boils down to three building blocks. Key-value pairs are the basic form: replicas: 3. Lists start with a dash at the beginning of the line: containers is a list, each container an entry. And nested structures come from indentation: anything indented under metadata belongs to metadata.

Indentation is therefore not cosmetic, it is the structure itself. Two spaces per level are common in Kubernetes manifests; what matters is that you stay consistent within one file. Tabs are forbidden, YAML only accepts spaces. A typical failure mode: a field lands in the wrong object because of incorrect indentation. That is not a syntax error, the editor reports nothing, and you spend a long time figuring out why a resource limit is not taking effect.

Three small things that make daily work easier: comments start with # and are YAML's biggest advantage over JSON. You can separate several objects in one file with ---, which I use for small applications, while I give larger ones one file per object. And the file extension is .yaml, even though .yml works everywhere. Kubernetes keys are written in camelCase (containerPort, matchLabels), and I stick to that in my own values too.

Common YAML traps: the Norway problem, ports and version numbers

YAML's biggest weakness is its tolerance. You can write strings without quotes, and the parser guesses the type. Most of the time it guesses right. When it does not, you do not get an error, you get a different value.

The best known example is the Norway problem: the country code NO gets read as the boolean false by parsers following the old YAML specification. Specification 1.2 from 2009 narrowed boolean values down to true and false, but many libraries still parse using the old behavior. Kubernetes uses the go-yaml library, where this has been discussed in an open issue for years.

Value without quotes How a parser reads it under the old spec Written safely
NO boolean false "NO"
yes, on, y boolean true "yes"
22:22 a time value or number instead of a port mapping "22:22"
2.1 floating point number 2.1 "2.1"
1.1.0 string (three numbers are not a float) stays safe

In Kubernetes you run into this in one spot especially often: environment variables. The value of an env variable has to be a string. Write value: true without quotes and the cluster rejects the manifest, because it gets a boolean where a string is expected. With value: "true" it works. The same goes for two-part version numbers and anything that looks like a time.

My rule from that: strings that could even remotely look like something else go in quotes. In the Deployment above you see that with "500m" and "128Mi". It never became critical for me, but I would gladly have skipped the debugging time.

Checking Kubernetes YAML: validator, linter and dry run

Before a manifest goes into the cluster, I check it in three stages. The first is the editor: a YAML plugin with the Kubernetes schema flags wrong indentation and unknown fields while you type. A linter like yamllint does the same thing in the pipeline.

The second stage is kubectl itself:

kubectl apply -f deployment.yaml --dry-run=client
kubectl apply -f deployment.yaml --dry-run=server
kubectl diff -f deployment.yaml

--dry-run=client only checks the structure locally, without asking the cluster. --dry-run=server sends the manifest to the API server, which validates it fully, including admission webhooks and policies, without saving anything. That is the most honest Kubernetes YAML validator you have, because it runs exactly the same check as a real apply. kubectl diff on top of that shows you what would actually change in the cluster. The full reference for these commands is in the kubectl documentation.

The third stage is the CI pipeline without cluster access. There a schema tool like kubeconform takes over, checking manifests against the Kubernetes API schemas without needing a reachable cluster. What additionally kicks in for me in production are policies through Gatekeeper, which reject manifests without resource limits, for example. That is a topic of its own, though.

Anchors, aliases and the Kustomize or Helm question

YAML can avoid repetition: an anchor &name marks a value or a whole object, an alias *name reuses it elsewhere, and <<: *name merges the fields of one object into another. What matters is where these shortcuts end: anchors, aliases and merge keys are resolved by the YAML parser before anything reaches Kubernetes. The API server then receives a finished JSON structure with no trace of them. That also means kubectl get -o yaml never gives your anchors back, and that a merge key is not a Kubernetes feature you can rely on inside the cluster. So I rarely need that in Kubernetes manifests. Where it helps me daily is the pipeline definition in GitLab CI, where script blocks repeat across many jobs.

For Kubernetes, the repetition question looks different: you have the same Deployment for development, test and production, with different replica counts, different images, different resources. Anchors do not help there, since the environments live in separate files. That is what Kustomize is for, layering base manifests with patches, and Helm, which generates finished manifests from templates and values. I compare when each tool makes sense in Kustomize vs Helm: When to Use Which?.

And once your manifests live in Git, the next step is an obvious one: a tool like ArgoCD reads them from the repository and keeps the cluster in sync with it. Git then becomes the desired state, and the reconciliation loop reaches all the way into your repository.

Frequently asked questions

What is the difference between a Kubernetes manifest and YAML?

YAML is the file format, a manifest is the content: the description of a Kubernetes object with apiVersion, kind, metadata and spec. Every manifest is YAML (or JSON), but not every YAML file is a manifest. A GitLab pipeline or a Docker Compose file is also YAML, just with a completely different structure.

Do I have to use YAML, or does JSON work too?

The Kubernetes API server speaks JSON internally, and kubectl converts your YAML file before sending it. So you can also write manifests as JSON and apply them with kubectl apply -f. In practice almost nobody does that, because YAML allows comments, needs fewer brackets, and is far more readable for humans.

How do I find out which fields an object has?

With kubectl explain, followed by the path in dot notation, for example kubectl explain pod.spec.containers.resources. The command reads the schemas directly from your cluster and shows the type and description for every field. For an overview of all objects, the API reference on kubernetes.io is the right place.

Which Kubernetes YAML validator makes sense?

For a quick check, kubectl apply --dry-run=server, because the API server runs the same validation there as during a real apply. For a CI pipeline without cluster access, a schema checker like kubeconform. And in the editor, a YAML plugin with the Kubernetes schema that flags wrong indentation immediately. No single tool catches everything, but the three stages together catch the bulk of it.

Is the file called .yaml or .yml?

Both work, kubectl does not care. The YAML documentation recommends .yaml, and I stick to that across all projects. What matters is that you settle on one extension within a repository, so search patterns in pipelines and editors reliably find every file.

Where to go next

Once you know the four required fields and the traps, the sensible next step is understanding the most important manifest properly: Kubernetes Deployment: Rollouts Explained shows how the Pod template turns into rollouts and rollbacks. As soon as you maintain several environments, it is worth looking at Kustomize vs Helm: When to Use Which?, and once your manifests live in Git, GitOps with ArgoCD: Deploy from Git is the way to get them into the cluster automatically.

In full detail, from the YAML basics through anchors to version control and Kustomize, this is covered in chapter 4 of my Kubernetes Practical Guide.

Kevin Welter

Kevin Welter

Developer, IT architect, author of technical books (Kubernetes, cloud infrastructures) and speaker. Runs his business with an AI workforce of eight AI employees and shows solo business owners in his community how to hire their first AI employee.

More about AI employees

Kubernetes from the basics to a production-ready cluster

Get the book