Skip to content

DevOps Cluster

Kubernetes YAML for Beginners: Deployments, Services, Ingress

Published April 11, 2026 · 10 min read

The first time you open a Kubernetes manifest, it feels like reading a legal contract in a language you only half know. There are objects referencing other objects by string labels. There are three places to set "how many replicas." There are nested spec objects inside spec objects, and at the bottom of the YAML, an innocent-looking targetPort: 8080 somehow controls whether your app is reachable at all. This is not because Kubernetes is arbitrary. It is because the manifests describe desired state rather than imperative commands, and the vocabulary to describe desired state took a few years to settle.

This post gives you the mental model that makes the vocabulary stick, then walks through the four objects you actually need to deploy a basic app: Pod (rarely written directly), Deployment, Service, and Ingress. With those four, you can run almost any stateless workload. Everything else in Kubernetes is extension, not replacement.

The mental model: desired state

Kubernetes is a declarative system. You write a YAML file that describes what you want — "three replicas of this image, exposed on port 80, reachable at this URL" — and you submit it to the cluster. The cluster takes responsibility for making reality match. If a replica dies, the cluster starts a new one. If the image changes, the cluster rolls out the new version. If you delete the manifest, the cluster tears down the resources.

That inversion is the whole point. You do not tell Kubernetes how to do anything. You tell it what the world should look like. Everything in a manifest is a declaration about desired state, and the controllers inside the cluster reconcile the actual state toward it continuously.

Once this clicks, the YAML starts making sense. The Kubernetes concepts docs walk through the object model formally; the objects page is the canonical definition of every field you will encounter.

Pods and why you rarely write one directly

A Pod is the smallest schedulable unit in Kubernetes. It contains one or more containers that share networking and (optionally) storage. In almost every real app, you do not write a Pod manifest by hand. You write a Deployment, which creates a ReplicaSet, which creates Pods. If you write a bare Pod and it dies, nothing replaces it — there is no controller watching. That is usually not what you want.

The exception is debugging: a short-lived "please give me a shell in the cluster" Pod is occasionally useful. For everything else, start with a Deployment.

Deployments: the real unit of work

A Deployment manages a set of identical Pod replicas. It tracks rollout history, handles rolling updates and rollbacks, and restarts Pods that fail. This is the manifest you edit most often.

A minimal Deployment:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: myrepo/web:1.2.3
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 512Mi

Things to notice. First, metadata.labels, spec.selector.matchLabels, and spec.template.metadata.labels all have to agree. The selector is how the Deployment finds "its" Pods. If they do not match, the Deployment will not manage the Pods it creates, which is a confusing failure mode. Second, always pin the image tag to a version, never latest, for the same reasons as in Docker — reproducibility. Third, always set resource requests and limits. Without them, the scheduler cannot make good placement decisions and a rogue process can starve the node.

Generate a working Deployment scaffold for common stacks with Kubernetes YAML Generator. It fills in the selector/label matching correctly so you do not have to debug why your Pods are not being picked up. Validate hand-edited YAML with YAML Validator before submitting — a misplaced space can change indentation and silently create a different object than you intended.

Services: stable networking

Pods are ephemeral. They can be killed and recreated at any time, and each new Pod gets a new IP address. If you hard-coded a Pod IP in another service, it would break constantly. Services solve this by providing a stable virtual IP and DNS name that routes to whichever Pods match a label selector.

A minimal Service:

apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:
    app: web
  ports:
    - name: http
      port: 80
      targetPort: 8080
  type: ClusterIP

The selector matches the label on the Deployment's Pods. The port is what the Service exposes inside the cluster; the targetPort is the port the containers actually listen on. They do not have to be the same — it is common for the Service to expose 80 and the container to run on 8080. Mismatching these is the single most common "my app is deployed but I can't reach it" beginner problem.

Four Service types exist. Start with ClusterIP (default — reachable only inside the cluster). Use NodePort and LoadBalancer only if you need external access without going through Ingress. The Services concepts page has the full details.

Ingress: HTTP from outside the cluster

An Ingress is the rules layer for HTTP(S) traffic entering the cluster. It maps hostnames and paths to Services. A single Ingress controller (nginx-ingress, traefik, or a cloud-managed alternative) actually handles the traffic; the Ingress objects are just rules.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web
spec:
  rules:
    - host: example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port:
                  number: 80
  tls:
    - hosts: [example.com]
      secretName: example-com-tls

If you are running nginx-ingress, the controller terminates TLS using the referenced Secret and proxies to your Service. If you are writing the nginx configuration by hand outside of Kubernetes (for a traditional host), Nginx Config Generator scaffolds reasonable defaults with the security headers modern browsers expect.

For authoring and validating the resulting YAML, YAML Validator catches indentation errors. For configuration files that describe build or deploy pipelines alongside your manifests, Dockerfile Generator and Docker Compose Generator handle the container-side definitions your Deployment will reference.

Beginner pitfalls

Label/selector mismatches

If spec.selector.matchLabels does not match spec.template.metadata.labels, the Deployment will fail to adopt its Pods. Always make them identical. Copy-paste is fine here; precision matters more than DRY.

Port confusion

Service port, target port, container port — all three can be different numbers and the manifest stays valid. The rule: targetPort on the Service must equal containerPort on the Pod. Service port can be anything you want clients to call.

Missing resource requests

No requests means the scheduler assigns Pods without considering node capacity, and limits protect against runaway containers. Always set both, even if you guess for the first version.

Using latest tags

A rolling update with image: myapp:latest cannot actually roll out anything — the image reference has not changed, so Kubernetes thinks nothing needs to update. Always pin to a specific tag or digest.

Forgetting readiness probes

Without a readinessProbe, Kubernetes routes traffic to your Pod the moment the container starts, before your app has finished initializing. Add a readiness probe that checks an actual health endpoint and the rolling update will wait until new Pods are genuinely ready before retiring old ones. The probes documentation has the exact fields.

Adjacent tools worth bookmarking

Tools that pair well with Kubernetes manifests: Cron Expression Builder for CronJob schedules, JSON Formatter for inspecting kubectl get -o json output, Base64 Encoder/Decoder because Secret values are base64-encoded in the manifest, and Password Generator for high-entropy values that populate Secrets at rest.

Related pillar guide

This cluster post is part of the developer tools track. For the broader foundation on choosing and using free developer tools, see The Complete Guide to Free Online Tools.

FAQ

Do I need to write YAML by hand or can I use Helm?

Learn to read and write YAML first, then adopt Helm (or Kustomize, or both) once you have real repetition. Helm is fantastic for managing many similar charts, but it hides the YAML from you, and the hidden YAML is exactly what you need to debug when things go wrong.

What is the difference between Deployment and StatefulSet?

Deployment is for stateless workloads where Pods are interchangeable. StatefulSet is for stateful workloads where each Pod has a stable identity and persistent storage — databases, for example. If your app is stateless, use Deployment. If it needs stable hostnames and per-Pod storage, StatefulSet.

How does Service discover Pods across namespaces?

Services are namespace-scoped. A Service in namespace A cannot directly select Pods in namespace B. Clients in other namespaces reach the Service by its fully-qualified DNS name: web.default.svc.cluster.local.

Can I use Kubernetes locally for development?

Yes — minikube, kind, and k3d all run a real Kubernetes cluster on your laptop. For most local development, Docker Compose is simpler, but if you want to test manifests end-to-end, a local K8s cluster is invaluable.

What is the difference between a ConfigMap and a Secret?

Both store configuration data. Secrets are base64-encoded (not encrypted by default) and intended for sensitive data. Treat Secrets as "data that should not appear in logs"; they are not a substitute for a real secret manager but they are a stepping stone.

Closing thought

Kubernetes is intimidating until you realize that 90% of day-to-day manifests are variations on the four objects above. Write a minimal Deployment, wire a Service to it, and expose it through Ingress. Delete and recreate them until the YAML stops feeling like a puzzle. The advanced patterns — operators, CRDs, service meshes — all rest on this same foundation.