Basics · Hands-on

Reading Kubernetes YAML and Working with kubectl

Every Kubernetes object is written the same way. Learn the four fields every manifest has, just enough YAML to read and write them, how labels and selectors connect objects, namespaces, the difference between imperative commands and kubectl apply, and the kubectl commands you'll use every day.

Module: 03 of 20 Level: Basics · Hands-on Time: 12 min read Published:
All modules in this track (20)

In this module, you'll learn to:

  • Read any manifest using the four fields every object has
  • Write valid YAML, and connect objects with labels and selectors
  • Use namespaces, and work declaratively with kubectl apply

Before you start: have the kind cluster from Your First Cluster running. Every example here works on it.

Principle

Every object has the same shape

Every Kubernetes object, whether it runs your app, routes traffic, or grants a permission, is described with the same four top-level fields. Once you can read one, you can read them all.

apiVersion: apps/v1          # 1. which API group and version defines this kind
kind: Deployment             # 2. what type of object it is
metadata:                    # 3. identity: name, namespace, labels
  name: hello
  namespace: default
  labels:
    app: hello
spec:                        # 4. the desired state: what you want to exist
  replicas: 2
  selector:
    matchLabels:
      app: hello
  template:
    metadata:
      labels:
        app: hello
    spec:
      containers:
        - name: web
          image: nginx:1.27
          ports:
            - containerPort: 80
Scroll sideways to see the full diagram →
FieldWhat it meansYou'll see values like
apiVersionWhich version of the API this object followsv1, apps/v1, networking.k8s.io/v1
kindThe type of objectPod, Deployment, Service, ConfigMap
metadataIts name, namespace, labels, and annotationsname: hello
specWhat you want. You write thisreplicas, image, ports
statusWhat is. The cluster writes this, never youready replicas, pod IP, conditions

That last row is the reconciliation loop again: you write spec, controllers work until status matches it. Run kubectl get deployment hello -o yaml and you'll see both.

YAML

Just enough to read and write manifests

YAML is a way of writing structured data using indentation instead of brackets. Five rules cover nearly every manifest:

  • Indentation is structure. Use spaces, never tabs. Two spaces per level is the convention. A line indented under another belongs to it.
  • key: value makes a map (a set of named fields). Note the space after the colon.
  • A dash - starts a list item. containers: is a list, so each container begins with - name:.
  • --- separates documents, so one file can hold a Deployment and its Service.
  • # starts a comment. Quote values that look like numbers or booleans but must be strings, such as "true" or "8080" in an annotation.
# A map with a nested map and a list
metadata:
  name: hello          # key: value
  labels:              # a nested map
    app: hello
    tier: web
spec:
  containers:          # a list...
    - name: web        # ...whose first item is a map
      image: nginx:1.27
    - name: sidecar    # second item
      image: busybox:1.36
Scroll sideways to see the full diagram →

The most common beginner error is indentation that's one space off, which either fails with a parse error or, worse, silently attaches a field to the wrong parent. kubectl apply --dry-run=server -f file.yaml checks a file against the cluster without changing anything.

Labels

How objects find each other

Kubernetes objects don't reference each other by ID. They use labels (key/value tags in metadata.labels) and selectors (queries that match labels). In the Deployment above, selector.matchLabels: app: hello means "the pods I own are the ones labelled app: hello", and the pod template gives every new pod that label.

A Service finds its pods the same way. That's the glue between almost every object in this track:

Labels and selectors connect objects
Deployment helloselector: app=hello; creates pods with that label
creates
Podslabels: app=hello, tier=web
found by label, not by name or IP
Service helloselector: app=hello; sends traffic to every matching ready pod
# See labels on pods
kubectl get pods --show-labels

# Select pods by label
kubectl get pods -l app=hello
kubectl get pods -l 'tier in (web, api)'

# Add a label to a running pod
kubectl label pod <pod-name> owner=me

If a selector and a label don't match, nothing errors. The Service just has no pods behind it, which is the first thing Services & Cluster Networking teaches you to check.

Namespaces

Sections of a cluster

A namespace groups related objects. Names only have to be unique inside a namespace, so two teams can both have a Deployment called api. Every command runs against one namespace, default unless you say otherwise.

# List namespaces; kube-system holds Kubernetes' own components
kubectl get namespaces

# Create one and work in it
kubectl create namespace shop
kubectl get pods -n shop

# Every namespace at once
kubectl get pods -A

# Make "shop" the default for your current context
kubectl config set-context --current --namespace=shop
Scroll sideways to see the full diagram →

Namespaces are also where permissions and quotas attach, covered in Namespaces, RBAC & Service Accounts.

Two styles

Imperative commands and declarative files

In the last module you used imperative commands: kubectl create deployment, kubectl scale. They're quick for experiments, but nothing records what you did, so nobody can review it or recreate it.

Real work uses the declarative style: write the desired state in a file, keep the file in Git, and run kubectl apply. Apply is safe to repeat. It creates what's missing, changes what's different, and leaves the rest alone.

# Let kubectl write the YAML for you, as a starting point
kubectl create deployment hello --image=nginx:1.27 \
  --dry-run=client -o yaml > hello.yaml

# Create or update everything in the file
kubectl apply -f hello.yaml

# Edit the file (say, replicas: 3), then preview the change before applying it
kubectl diff -f hello.yaml
kubectl apply -f hello.yaml

# Delete everything the file describes
kubectl delete -f hello.yaml
Scroll sideways to see the full diagram →

Later in the track, Helm & Kustomize handle the same files across several environments, and GitOps with Argo CD runs apply for you whenever Git changes.

Cheat sheet

The kubectl commands you'll use every day

CommandWhat it does
kubectl get <kind>List objects. Add -o wide for more columns, -o yaml for everything, -w to watch
kubectl describe <kind> <name>Human-readable detail, including recent events. The first place to look when something's wrong
kubectl logs <pod>A container's output. -f follows it, --previous shows the last crashed run
kubectl exec -it <pod> -- shA shell inside a running container
kubectl apply -f <file>Create or update from a file
kubectl delete -f <file>Delete what a file describes
kubectl get events --sort-by=.lastTimestampWhat the cluster has been doing recently, newest at the bottom
kubectl explain <kind>.<field>Built-in documentation for any field, such as kubectl explain deployment.spec.replicas
kubectl port-forward <svc-or-pod> 8080:80Reach something in the cluster from your laptop
kubectl config get-contextsWhich clusters you can talk to, and which one is current
Try it

Deploy an app and its Service from one file

Save this as hello.yaml, a Deployment and a Service in one file:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello
  labels:
    app: hello
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hello
  template:
    metadata:
      labels:
        app: hello
    spec:
      containers:
        - name: web
          image: nginx:1.27
          ports:
            - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: hello
  labels:
    app: hello
spec:
  selector:
    app: hello
  ports:
    - port: 80
      targetPort: 80
kubectl apply -f hello.yaml
kubectl get deployment,service,pods -l app=hello

# The pod IPs behind the Service
kubectl get endpointslices -l kubernetes.io/service-name=hello

# Break the link on purpose: in hello.yaml, change the Service's selector
# to app: hellooo, apply again, and re-run the endpointslices command.
# The Service now has no pods behind it. Change it back and apply again.

# Clean up
kubectl delete -f hello.yaml
Scroll sideways to see the full diagram →
Implementation notes
  • Start from generated YAML. --dry-run=client -o yaml and the examples in the Kubernetes docs are better starting points than writing manifests from memory.
  • Don't copy status or generated fields back into your files. kubectl get -o yaml output includes status, uid, resourceVersion, and timestamps. Strip them before saving a manifest.
  • Use the same label keys everywhere. The common convention is app.kubernetes.io/name and app.kubernetes.io/instance; short labels like app work fine as long as everyone agrees on them.
  • kubectl edit changes the live object only. It's handy in a lab, but the change isn't in any file, so the next apply from Git quietly undoes it.
Recap

Key terms

  • Manifest: a YAML file describing one or more objects you want to exist.
  • spec and status: what you want (you write it) and what is (the cluster writes it).
  • Label: a key/value tag on an object, such as app: hello.
  • Selector: a query that matches labels; how Deployments and Services find their pods.
  • Namespace: a named section of a cluster; names only need to be unique inside one.
  • Declarative: describing the desired state in files and applying them, rather than issuing one-off commands.
Check yourself

Pop quiz

Five questions: three on the ideas in this module, and two scenarios where you apply them. The order changes every time you take it, and 4 out of 5 passes.

5 questions. Decide on your answer to each, then check it.

  1. Concept

    Which field of an object is written by the cluster, never by you?

    1. spec
    2. metadata
    3. status
    4. apiVersion
    Show answer

    status

    You write spec, the desired state. Controllers write status, the actual state, and keep working until the two match.

  2. Scenario

    Your Service's selector says app: web, but your Deployment's pods are labelled app: hello. You apply both. What happens?

    1. kubectl rejects the Service with a validation error
    2. The Service renames the pods' labels to match
    3. Both are created, but the Service has no pods behind it, so requests fail
    4. The Service sends traffic to every pod in the namespace instead
    Show answer

    Both are created, but the Service has no pods behind it, so requests fail

    Objects find each other through labels and selectors. A mismatch isn't an error; the Service simply matches nothing, which is why "no endpoints" is the first thing to check.

  3. Concept

    In a manifest, what does a line starting with - mean?

    1. A comment
    2. An item in a list
    3. A field that's been removed
    4. The start of a new document
    Show answer

    An item in a list

    A dash starts a list item, such as each container under containers:. Comments start with #, and --- separates documents in one file.

  4. Concept

    Why prefer kubectl apply -f with files in Git over commands like kubectl create and kubectl scale?

    1. apply is faster
    2. Imperative commands don't work on real clusters
    3. The files are a reviewable, repeatable record of what should be running
    4. apply skips the API server's checks
    Show answer

    The files are a reviewable, repeatable record of what should be running

    Imperative commands leave no record. Files in Git can be reviewed, diffed, and re-applied safely, which is the foundation for everything from Helm to GitOps.

  5. Scenario

    You run kubectl get pods and see nothing, but you know your app is running. What's the most likely reason?

    1. The pods are still being scheduled
    2. The pods are in a different namespace from your current one
    3. kubectl only shows pods you created yourself
    4. The pods have no labels
    Show answer

    The pods are in a different namespace from your current one

    Commands run against one namespace, default unless you say otherwise. Try kubectl get pods -A, or add -n <namespace>.

Questions about this learning path?

I'm happy to go into more detail on any of this: how the pieces fit together, ingress costs, RBAC design, or how I triage incidents.

anupojuprudhvi@gmail.com