Kubernetes Basics
Running containers at scale: why orchestration, clusters and nodes, pods, deployments and services, and managed Kubernetes.
Why orchestration?
A single container on a single server is easy to run by hand. A real application quickly becomes dozens of containers on several servers, and then questions pile up: which server has room for this container? What happens when a container crashes at night, or a whole server fails? How do you run five copies instead of two during a busy period? How do you replace version 1.4 with 1.5 without downtime? And how do the containers find each other when their addresses keep changing?
A container orchestrator answers these questions automatically:
- Scheduling: placing each container on a machine with enough free CPU (central processing unit) and memory.
- Self-healing: restarting failed containers and moving work away from failed machines.
- Scaling: running more or fewer copies, by hand or automatically based on load.
- Rolling updates: replacing old versions a few at a time, and rolling back if the new version is unhealthy.
- Service discovery and load balancing: one stable name and address in front of a changing group of containers.
Kubernetes (often written K8s: K, eight letters, s) is the open-source container orchestrator that became the de facto standard. It was started at Google and is now maintained under the CNCF (Cloud Native Computing Foundation). Every major cloud offers it as a managed service.
Clusters, control plane and nodes
A Kubernetes cluster is a set of machines managed together as one system: a control plane that makes decisions, and worker nodes (virtual or physical machines) that run the containers. You never tell a node what to run; you tell the control plane what you want, and it arranges the rest. The Containers & Kubernetes topic goes deeper, from How a Kubernetes Cluster Works to kubectl and Troubleshooting.
The control plane
-
The API server (
kube-apiserver; API stands for application programming interface) is the front door. Every request — from you, from a pipeline, from other components — is an HTTPS (Hypertext Transfer Protocol Secure) call to it. It checks who you are and whether you are allowed (authentication and authorization), validates the object and stores it. - The scheduler watches for pods that have no node yet and picks one, based on the resources they request and any placement rules.
- The controller manager runs the controllers: small control loops that each look after one kind of object and keep reality in line with what was requested (see desired state and reconciliation).
- etcd is a consistent, distributed key-value store that holds the whole cluster state. Losing etcd without a backup means losing the cluster's configuration; in production it runs as three or five members.
Worker nodes
- The kubelet is an agent on each node. It receives the pods assigned to its node, asks the container runtime to start them, runs their health checks and reports their status back to the API server.
- The container runtime actually runs containers; today usually containerd or CRI-O, which run the same container images that Docker builds.
- kube-proxy (or a networking plugin that replaces it) implements services on each node, so traffic for a service address reaches one of its pods.
The main objects
Everything in Kubernetes is an object of a certain
kind, stored by the API server. Each object has a
spec (what you want) and a status (what is
actually the case, filled in by Kubernetes).
Pods
A pod is the smallest unit Kubernetes runs: one container, or a few containers that must always run together on the same node, sharing one IP (Internet Protocol) address and optionally storage volumes. Pods are disposable: when one dies it is not repaired but replaced by a new pod with a new name and a new IP address. That is why you almost never create pods directly.
Deployments
A deployment is an object that
keeps a given number of identical pods (replicas)
running from one pod template, and updates them in a controlled way.
If a pod or its node fails, the deployment's controller creates a
replacement. When you change the template — usually the image tag —
it performs a rolling update: it starts new pods
and removes old ones a few at a time, so the application stays
available. kubectl rollout undo
returns to the previous version.
Services
Because pods come and go, clients cannot use pod addresses. A Kubernetes service is a stable virtual address and DNS (Domain Name System) name in front of a changing set of pods, selected by their labels; traffic sent to the service is spread over the pods that are ready. The main types are:
| Type | Reachable from | Typical use |
|---|---|---|
ClusterIP (default) |
Inside the cluster only | Internal services: an API used by the web front end |
NodePort |
A fixed port (30000–32767 by default) on every node | Testing, or behind an external load balancer you manage |
LoadBalancer |
Outside, through a cloud load balancer | Exposing one service; the cloud creates the load balancer |
Ingress
An ingress is an object with rules
for routing HTTP and HTTPS traffic from outside the cluster to
services, by host name and path: shop.example.com/ to
the web service, shop.example.com/api to the API
service. The rules only work if the cluster runs an
ingress controller (for example Traefik, HAProxy,
F5's NGINX Ingress Controller or a cloud load balancer integration;
the community ingress-nginx project was retired in March 2026),
which does the actual routing and usually terminates TLS (Transport
Layer Security). One ingress can serve many services behind one load
balancer. The Ingress API is feature-frozen; for new clusters the
Gateway API, with a more flexible model, is the recommended
successor (see
the Gateway API).
Namespaces
A namespace divides one cluster into named groups,
such as shop, billing and
monitoring. Names only need to be unique within a
namespace, access rules (RBAC, role-based access control) and
resource quotas can be set per namespace, and deleting a namespace
deletes everything in it. Namespaces are an organizational boundary,
not a strong security boundary.
ConfigMaps and Secrets
Configuration should not be baked into the image, so the same image can run in every environment. A ConfigMap holds ordinary settings (a log level, a feature switch); a Secret holds sensitive values (a database password, an API key). Both can be passed to containers as environment variables or as files.
The values of a Secret are stored base64-encoded, which is an encoding, not encryption: anyone can reverse it.
printf '%s' 'demo-password-123' | base64
echo 'ZGVtby1wYXNzd29yZC0xMjM=' | base64 --decode; echo
ZGVtby1wYXNzd29yZC0xMjM= demo-password-123
A manifest, line by line
Objects are described in
YAML (YAML
Ain't Markup Language) files called manifests. This
one file contains two objects, separated by ---: a
deployment of three web server pods and a service in front of them.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: shop
labels:
app: web
spec:
replicas: 3
selector:
matchLabels:
app: web
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
template:
metadata:
labels:
app: web
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: web
image: registry.example.com/shop/web:1.4.2
ports:
- containerPort: 8080
resources:
requests:
cpu: 250m
memory: 128Mi
limits:
memory: 256Mi
readinessProbe:
httpGet:
path: /healthz
port: 8080
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
---
apiVersion: v1
kind: Service
metadata:
name: web
namespace: shop
spec:
type: ClusterIP
selector:
app: web
ports:
- port: 80
targetPort: 8080
-
apiVersionandkindsay what kind of object this is: aDeploymentfrom theapps/v1API group; the service is in the corev1group. -
metadatagives the object its name, its namespace (shop, which must already exist) and labels. -
replicas: 3is the desired number of pods.selector.matchLabelstells the deployment which pods are its own: those labeledapp: web. It must match the labels in the template. -
strategysets the rolling update: at most one pod below the desired count (maxUnavailable) and at most one extra (maxSurge) during an update. -
templateis the pod template: every pod gets these labels and this spec. -
The pod-level
securityContextrefuses to start the container as root (runAsNonRoot), runs it as an unprivileged user ID, and applies the runtime's default seccomp (secure computing mode) profile, which blocks rarely needed system calls. -
imageis pinned to a specific version tag,1.4.2, neverlatest, so every pod runs the same, known code and a rollback has something to return to. Pinning by digest (@sha256:…) is stricter still. The image is a fictitious example that listens on port 8080 as a non-root user. -
resources.requestsis what the scheduler reserves on a node: a quarter of a CPU core (250m, millicores) and 128 mebibytes of memory.limits.memoryis a hard ceiling: a container that uses more is killed and restarted. Without requests the scheduler places pods blindly. -
The
readinessProbeis a health check: the pod only receives traffic from the service whileGET /healthzon port 8080 succeeds. -
The container-level
securityContextforbids gaining more privileges and drops all Linux capabilities; this application needs none. -
The
Serviceof typeClusterIPselects the pods labeledapp: weband forwards its port 80 to their port 8080. Inside the namespace other pods can now reach it athttp://web.
kubectl basics
kubectl is the command-line tool for
Kubernetes: it turns commands into calls to the API server. It reads
which cluster to use and your credentials from a
kubeconfig file (~/.kube/config), which you
should protect like a password. The sessions below are illustrative.
$ kubectl create namespace shop
$ kubectl apply -f web.yaml
namespace/shop created deployment.apps/web created service/web created
apply -f sends the file's desired state; running it
again after editing the file updates the objects (declarative), and
running it unchanged reports unchanged.
$ kubectl get deployments,pods,services -n shop
NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/web 3/3 3 3 52s NAME READY STATUS RESTARTS AGE pod/web-6f8b9c7d54-2xkqp 1/1 Running 0 52s pod/web-6f8b9c7d54-8mzvn 1/1 Running 0 52s pod/web-6f8b9c7d54-tq4lw 1/1 Running 0 52s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/web ClusterIP 10.96.41.120 <none> 80/TCP 52s
When something is wrong, describe shows an object's
details and, at the bottom, its recent events —
usually the fastest way to find out why a pod does not start.
logs shows what the container wrote to standard output.
$ kubectl describe pod web-6f8b9c7d54-2xkqp -n shop
$ kubectl logs web-6f8b9c7d54-2xkqp -n shop
Name: web-6f8b9c7d54-2xkqp Namespace: shop Node: worker-2/10.0.2.15 Status: Running ... Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 60s default-scheduler Successfully assigned shop/web-6f8b9c7d54-2xkqp to worker-2 Normal Pulled 58s kubelet Container image "registry.example.com/shop/web:1.4.2" already present on machine Normal Created 58s kubelet Created container web Normal Started 58s kubelet Started container web 2026-10-01T09:14:03Z INFO listening on :8080
After changing the image tag in the manifest to
1.4.3 and applying it again, follow the rolling update:
$ kubectl apply -f web.yaml
$ kubectl rollout status deployment/web -n shop
deployment.apps/web configured service/web unchanged Waiting for deployment "web" rollout to finish: 1 out of 3 new replicas have been updated... Waiting for deployment "web" rollout to finish: 2 out of 3 new replicas have been updated... Waiting for deployment "web" rollout to finish: 1 old replicas are pending termination... deployment "web" successfully rolled out
Desired state and reconciliation
The idea behind all of Kubernetes is the same as behind infrastructure as code: you declare the desired state, and software makes it true. The difference is that Kubernetes never stops checking. Each controller runs a reconciliation loop: observe the actual state, compare it with the desired state, act on the difference, and repeat. Nobody has to notice that a pod crashed at 03:00; the next pass of the loop sees two pods where three are wanted and starts a third.
This simulation shows the loop of a deployment controller with three desired replicas. One pod fails, and later someone scales the deployment down:
spec = {"replicas": 3} # desired state, as stored by the API server
pods = {"web-1": "Running", "web-2": "Running", "web-3": "Running"}
counter = {"next": 4}
def reconcile(spec, pods):
"""One pass of the control loop: observe, compare, act."""
for name in sorted(pods):
if pods[name] == "Failed":
del pods[name]
print(f" removed failed pod {name}")
missing = spec["replicas"] - len(pods)
if missing > 0:
for _ in range(missing):
name = f"web-{counter['next']}"
counter["next"] += 1
pods[name] = "Running"
print(f" created {name}")
elif missing < 0:
for name in sorted(pods)[missing:]:
del pods[name]
print(f" deleted {name}")
else:
print(" actual state matches desired state")
for tick in range(1, 6):
if tick == 2:
pods["web-2"] = "Failed"
print(f"tick {tick}: web-2 crashed")
elif tick == 4:
spec["replicas"] = 2
print(f"tick {tick}: someone scaled the deployment to 2")
else:
print(f"tick {tick}:")
reconcile(spec, pods)
print(f" running: {', '.join(sorted(pods))}")
tick 1: actual state matches desired state running: web-1, web-2, web-3 tick 2: web-2 crashed removed failed pod web-2 created web-4 running: web-1, web-3, web-4 tick 3: actual state matches desired state running: web-1, web-3, web-4 tick 4: someone scaled the deployment to 2 deleted web-4 running: web-1, web-3 tick 5: actual state matches desired state running: web-1, web-3
The controller does not care why the numbers differ — a crash, a failed node, a new desired count — it only closes the gap. Real controllers are event-driven rather than ticking on a timer, and a deployment actually manages pods through an intermediate object, the ReplicaSet, but the principle is exactly this.
Managed Kubernetes
Running the control plane yourself — keeping etcd backed up, upgrading every few months, securing the API server — is serious work. Managed Kubernetes services run and upgrade the control plane for you; you manage the worker nodes (or let the service manage those too) and your workloads.
| Provider | Managed Kubernetes service | Hands-off node option |
|---|---|---|
| AWS (Amazon Web Services) | Amazon EKS (Elastic Kubernetes Service) | EKS Auto Mode; pods on AWS Fargate |
| Azure | AKS (Azure Kubernetes Service) | AKS Automatic |
| Google Cloud | GKE (Google Kubernetes Engine) | GKE Autopilot |
Names and options are as at the time of writing. Pricing structures differ: some providers charge a fee per cluster for the control plane, some have a free tier for it, and you always pay for the nodes and load balancers. You remain responsible for what runs in the cluster: images, manifests, access rules, Secrets and keeping up with Kubernetes versions, which are only supported for a limited time.
When Kubernetes is overkill
Kubernetes solves the problems of many containers on many machines, and it brings its own: a large number of concepts, networking and security to configure, upgrades to plan, and people who need to understand it all. For a small team with a handful of services, that overhead often costs more than it saves.
Simpler options run containers without a cluster to manage:
- Serverless container services: Google Cloud Run, Azure Container Apps, and Amazon ECS (Elastic Container Service) with AWS Fargate. You provide an image and settings; the provider handles placement, scaling and healing.
- Platform services and functions: see service models and serverless.
- A single virtual machine with Docker Compose, for small internal tools where a few minutes of downtime is acceptable (see Docker Compose).
Kubernetes pays off with many services, several teams sharing a platform, a need to run the same way on several clouds or on premises, or specific requirements the simpler services cannot meet.