Cloud10 min read

Getting Started with Kubernetes: A Beginner's Guide

Learn Kubernetes from first principles: what the control loop actually does, the objects you need on day one, a production-shaped first deployment, and an honest look at when you shouldn't use it at all.

Zeeshan Shahid
Zeeshan Shahid
January 5, 2025
Share:
Getting Started with Kubernetes: A Beginner's Guide

Most Kubernetes tutorials hand you a YAML file and tell you to run kubectl apply. It works, something starts, and you learn nothing about why. The concepts that actually make Kubernetes make sense — reconciliation, labels, the difference between a request and a limit — get skipped because they aren't a command you can copy.

This guide goes the other way. It starts with the one idea the whole system is built on, then introduces objects only as you need them.

What Kubernetes Actually Does

Kubernetes (abbreviated K8s — eight letters between the K and the s) is an open-source container orchestration platform. It originated at Google and is now maintained by the Cloud Native Computing Foundation.

That description is accurate and almost useless. Here's the version that helps.

Kubernetes is a declarative reconciliation system. You don't tell it what to do; you tell it what you want to be true. "I want three copies of this container running." Kubernetes writes that down, then runs a control loop forever:

  1. Read the desired state (what you declared).
  2. Observe the actual state (what's running).
  3. If they differ, take an action to close the gap.
  4. Repeat.

Every meaningful behaviour people attribute to Kubernetes falls out of this loop. Kill a pod and a new one appears — not because something detected a failure and responded, but because actual state stopped matching desired state and the loop did what it always does. A node dies and its workloads reappear elsewhere for the same reason. A rolling update is just the loop being fed a new desired state and walking toward it in increments.

Key Takeaway

Kubernetes has no "restart the thing" feature. It has a loop that continuously drives actual state toward declared state, and self-healing is what that looks like from the outside. Once this clicks, the API stops feeling arbitrary — every object is a description of desired state plus a controller trying to satisfy it.

This is also why kubectl apply is preferred over imperative commands like kubectl run. Applying a manifest updates the declared state. Imperative commands mutate the cluster directly and leave nothing behind that describes what you intended.

Do You Actually Need Kubernetes?

Worth asking before you invest weeks. Kubernetes solves real problems, but it charges rent whether or not you have those problems.

You probably don't need it if:

  • You run one or two services and deploy a few times a week. A managed platform — Vercel, Netlify, Railway, a cloud provider's container service — will do the job with a fraction of the concepts.
  • Nobody on the team has operational capacity to own it. A cluster is infrastructure you now maintain: upgrades, node pools, ingress, certificates, RBAC.
  • Your bottleneck is product, not deployment. Kubernetes doesn't ship features.

It starts paying for itself when:

  • You have many services that need a consistent deployment and networking model.
  • You need bin-packing across a fleet — many workloads sharing pooled machines efficiently.
  • Multiple teams need self-service deploys against shared infrastructure with real isolation.
  • You need portability across environments, or you're already committed to the CNCF ecosystem.
Managed clusters remove some work, not most of it

EKS, GKE, and AKS manage the control plane — the API server and etcd stop being your problem. Your workloads, networking, ingress, RBAC, resource tuning, and upgrade cadence remain entirely yours. "Managed Kubernetes" is a meaningfully smaller commitment than self-hosted, but it is not a small one.

If you're still here, the rest of this is worth your time.

The Objects You Need on Day One

Kubernetes has dozens of object types. You need five to deploy something real.

Pods

A Pod is the smallest deployable unit — one or more containers that are scheduled together and share a network namespace and storage volumes.

Sharing a network namespace has a concrete consequence: containers in the same pod reach each other on localhost, and they cannot both bind the same port. This is what sidecars are built on — a log shipper or proxy alongside your app, talking to it over loopback.

Most pods have exactly one container. Reach for a second only when the two processes genuinely must share a lifecycle and a network namespace.

You will almost never create a pod directly. A bare pod is not managed by anything — if it dies, it stays dead. Nothing is reconciling it. You create Deployments, which create pods for you.

Deployments

A Deployment declares desired state for a set of identical pods: which image, how many replicas, what config. It manages ReplicaSets under the hood, and a rolling update works by creating a new ReplicaSet and shifting replicas from old to new incrementally.

This is also what makes rollback cheap. The old ReplicaSet is still there, scaled to zero, so kubectl rollout undo reverses direction rather than redeploying from scratch.

Services

Pods are ephemeral. They get replaced, rescheduled, and reassigned IPs constantly. Anything holding a pod IP is holding a value with an expiry date it can't see.

A Service is a stable virtual IP and DNS name in front of a set of pods, chosen by label selector. Hit the Service; it routes to whichever pods currently exist and are ready.

The types you'll meet:

| Type | What it does | When to use | |------|--------------|-------------| | ClusterIP | Internal-only virtual IP (the default) | Service-to-service traffic inside the cluster | | NodePort | Opens a port on every node | Mostly a building block; rarely used directly | | LoadBalancer | Provisions a cloud load balancer | Exposing a service externally on a managed cloud | | ExternalName | Maps to an external DNS name | Pointing at something outside the cluster |

For HTTP, you usually want an Ingress instead: one load balancer, routing many hostnames and paths to many services. Ingress objects do nothing on their own — they're inert config until an ingress controller (ingress-nginx, Traefik, or a cloud-native one) is installed to act on them. This trips up nearly everyone once.

ConfigMaps and Secrets

Config that varies by environment doesn't belong in your image. ConfigMaps hold non-sensitive key/value config; Secrets hold sensitive values and are the same idea with different handling.

Secrets are base64-encoded, not encrypted

Base64 is an encoding, not a cipher. By default, Secret data is stored in etcd unencrypted and anyone with read access to Secrets in a namespace can decode it trivially. Encryption at rest is a cluster-level configuration you have to enable, and RBAC is what actually restricts access. Teams handling genuinely sensitive material typically integrate an external secret manager such as HashiCorp Vault rather than relying on Secrets alone.

Namespaces

Namespaces partition a cluster into virtual sub-clusters, scope names, and give you a handle for quotas and access policy.

They are not a security boundary by themselves. By default, a pod in one namespace can talk to a pod in another over the network. Isolation requires NetworkPolicies for traffic and RBAC for API access. Namespaces organize; they don't protect.

Your First Deployment

Here's an nginx Deployment written the way you'd actually want it in a cluster — not the two-field version tutorials usually show.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1          # one extra pod allowed during the update
      maxUnavailable: 0    # never drop below the desired replica count
  template:
    metadata:
      labels:
        app: nginx        # must match spec.selector.matchLabels
    spec:
      containers:
        - name: nginx
          image: nginx:1.27.3   # pin a real version; never use :latest
          ports:
            - containerPort: 80
          resources:
            requests:
              memory: "64Mi"
              cpu: "100m"
            limits:
              memory: "128Mi"
          readinessProbe:
            httpGet:
              path: /
              port: 80
            initialDelaySeconds: 3
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /
              port: 80
            initialDelaySeconds: 10
            periodSeconds: 15
          securityContext:
            runAsNonRoot: true
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true

Apply it and watch the rollout:

kubectl apply -f nginx-deployment.yaml
kubectl rollout status deployment/nginx-deployment
kubectl get pods -l app=nginx

Then give it a stable address:

apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  type: ClusterIP
  selector:
    app: nginx      # matches pods by label, not by Deployment name
  ports:
    - port: 80        # the port the Service listens on
      targetPort: 80  # the port on the pod

Any pod in the cluster can now reach it at nginx-service, or nginx-service.default.svc.cluster.local in full. From your laptop:

kubectl port-forward service/nginx-service 8080:80
Services find pods by label, never by Deployment

The single most common beginner bug: a Service whose selector doesn't match any pod's labels. Kubernetes won't error — the selector matched zero pods, which is a valid answer. You get a Service with no endpoints and connections that hang or refuse. When something is unreachable, run kubectl get endpoints my-service first. Empty output means your labels don't line up.

Probes: The Part Everyone Gets Wrong

Three probe types, doing three different jobs. Confusing them causes outages.

  • readinessProbe — "can this pod serve traffic right now?" On failure, the pod is removed from the Service's endpoints but keeps running. This is your traffic gate.
  • livenessProbe — "is this container wedged?" On failure, the kubelet kills and restarts the container. This is a last resort for deadlocks.
  • startupProbe — "has this thing finished booting?" Liveness and readiness are suspended until it passes. Use it for slow-starting apps rather than inflating liveness timeouts.

The classic failure: pointing a liveness probe at an endpoint that checks the database. The database gets slow, every liveness probe fails at once, Kubernetes restarts every pod simultaneously, and the restart storm makes the incident considerably worse. Nothing was wrong with the containers.

Rule of thumb: readiness probes may check dependencies. Liveness probes should check only whether this process is functioning. A liveness probe that fails should mean "restarting this container will help" — if that isn't true, it shouldn't be a liveness probe.

Requests and Limits

Two numbers, two entirely different mechanisms.

Requests are for the scheduler. A pod requesting 100m CPU is placed on a node with 100m unreserved. Requests are a reservation, and the scheduler only ever looks at them — not actual usage.

Limits are enforced at runtime, and CPU and memory behave differently:

  • CPU is compressible. Exceed the limit and the container is throttled — slowed, not killed.
  • Memory is not. Exceed the limit and the container is OOMKilled. No warning, no grace period.

That asymmetry drives a widely-used guideline: always set memory limits, and be cautious with CPU limits. A throttled container that could have used idle CPU is a self-inflicted latency problem, whereas unbounded memory growth threatens the whole node.

Requests and limits also determine a pod's QoS class, which decides who gets evicted when a node runs out of memory:

  • Guaranteed — requests equal limits for every container. Evicted last.
  • Burstable — requests set, lower than limits. Evicted in the middle.
  • BestEffort — nothing set at all. Evicted first.

Leaving resources unset doesn't mean "no constraints." It means BestEffort — first in line to die under pressure, and invisible to the scheduler, which will happily overcommit the node.

Debugging: The Commands That Matter

Most Kubernetes debugging is four commands.

# What's the cluster's own story? Start here. Almost always here.
kubectl describe pod my-pod

# What did the app say?
kubectl logs my-pod
kubectl logs my-pod --previous       # logs from the container that just crashed
kubectl logs -l app=nginx --tail=50  # across all matching pods

# Recent cluster events, newest last
kubectl get events --sort-by=.metadata.creationTimestamp

# Get inside
kubectl exec -it my-pod -- /bin/sh

kubectl describe pod is the one to reach for reflexively. The Events section at the bottom explains most failures in plain language — image pull errors, failed scheduling, probe failures, OOM kills.

Reading the common pod states:

  • Pending — not scheduled. Usually no node has enough unreserved capacity for your requests, or a volume can't be attached. describe names the reason.
  • ImagePullBackOff — wrong image name or tag, or missing registry credentials.
  • CrashLoopBackOff — the container starts and exits repeatedly. Kubernetes backs off between restarts. This is a symptom, not a cause; kubectl logs --previous shows what the dying container said.
  • OOMKilled — hit the memory limit. Either the limit is too low or the app is leaking.
  • Running but not Ready — the readiness probe is failing. The container is alive; it's saying it can't serve.

And when a deploy goes wrong:

kubectl rollout undo deployment/nginx-deployment
kubectl rollout history deployment/nginx-deployment

Practices Worth Adopting Early

  • Pin image tags. nginx:latest means different things on different days, which makes rollbacks meaningless and "works on my node" a real phenomenon. Pin a version, or better, a digest.
  • Set resource requests on everything. Otherwise you're BestEffort and the scheduler is flying blind.
  • Use maxUnavailable: 0 for user-facing services so a rollout never dips below the replica count.
  • Label consistently. Labels are how Services, selectors, and your own tooling find things. The app.kubernetes.io/* labels from the recommended-labels convention are a reasonable default.
  • Add PodDisruptionBudgets before your first node drain, not after. A PDB tells Kubernetes the minimum availability to preserve during voluntary disruptions like upgrades.
  • Keep manifests in Git. Declarative state belongs in version control. This is the premise GitOps tooling like Argo CD and Flux builds on, and it's worth adopting even without them.
  • Don't run as root. runAsNonRoot, allowPrivilegeEscalation: false, and a read-only root filesystem cost nothing on a greenfield workload and are painful to retrofit later.

Where to Go Next

Once deployments and services feel routine:

  • Helm — templating and packaging for manifests. The moment you're maintaining near-identical YAML for staging and production, you want Helm or Kustomize.
  • Horizontal Pod Autoscaler — scales replicas on CPU, memory, or custom metrics. Requires resource requests to be set, which is one more reason to set them.
  • NetworkPolicies — default-deny traffic rules. Note that they require a CNI plugin that implements them; on a cluster whose CNI ignores them, a NetworkPolicy silently does nothing.
  • Prometheus and Grafana — the conventional open-source monitoring pair for clusters. Datadog is a common commercial alternative if you'd rather buy than run.
  • StatefulSets and operators — for databases and other stateful workloads, which have genuinely different requirements from stateless apps.

Getting a Cluster to Practice On

Don't learn on a cloud cluster. Run one locally, break it freely, delete it, start again:

# kind — Kubernetes in Docker, fast and disposable
brew install kind
kind create cluster --name learning

kubectl cluster-info --context kind-learning
kubectl apply -f nginx-deployment.yaml

# Wipe it and start over whenever you like
kind delete cluster --name learning

minikube and k3d are equally good starting points, and Docker Desktop ships a single-node cluster you can enable in settings. Any of them will teach you the same lessons for free.

The thing to internalize: you're not learning commands. You're learning to describe a desired state precisely enough that a control loop can reach it — and to read the cluster's explanation when it can't.

Resources

Tags:KubernetesCloudContainersDevOpsDocker
Zeeshan Shahid

Zeeshan Shahid

Founder, DevPages

Zeeshan builds and maintains DevPages, a hand-curated directory of developer tools. He writes about the tools in the catalog and the trade-offs between them.

22 articles published

Related Articles