Kubernetes Core
Kubernetes is one idea applied many times: you declare the state you want and controllers keep reality equal to it. This page builds the model from the ground up: the control plane and nodes, the reconcile loop, the objects you use daily (Pod, Deployment, Service, Ingress, ConfigMap, Secret, StatefulSet, Job, volumes), a complete Spring Boot manifest validated against a real 1.37 cluster, a five-minute local lab and the exact sequence of a Pod's life and death.
From One Container to a Fleet
Page 02 ended with a container image that runs the same everywhere. That solves packaging. It does not answer the questions you hit the day after the first production deploy: which machine runs it, what restarts it when it crashes at 03:00, how do ten copies share traffic, how do you ship a new version without downtime, and where does the database password come from. A container orchestrator is the system that answers those questions continuously, so people do not have to.
Kubernetes (often written K8s) is the dominant orchestrator. You describe the state you want ("three copies of orders:1.4.2, reachable on port 80, never fewer than two healthy"), and Kubernetes keeps reality matching that description: scheduling Pods onto machines, replacing failed ones, rolling out new versions and wiring up networking.
| Problem at scale | By hand / with plain Docker | What Kubernetes gives you |
|---|---|---|
| Place containers on machines | You pick a host and docker run over SSH | The scheduler picks a node that fits CPU, memory and constraints |
| A process or whole machine dies | Someone is paged and restarts it | The kubelet restarts containers; controllers recreate Pods on other nodes |
| Run N identical copies | N commands, N port mappings | replicas: N in a Deployment |
| Send traffic to healthy copies only | Hand-edited load balancer config | A Service with readiness-aware endpoints |
| Ship a new version safely | Stop old, start new, hope | Rolling update with health gating and one-command rollback |
| Configuration and secrets | Env files copied to servers | ConfigMap and Secret objects injected at start |
| Capacity changes | Provision and configure servers | Autoscaling of Pods (and of nodes with an add-on) |
- A single service on one VM, deployed a few times a month, is usually better served by Docker Compose or a managed container service. Kubernetes adds real operational surface (networking, RBAC, upgrades, observability). It pays off with many services, several teams, frequent deploys, autoscaling needs or a platform you want to standardise.
This page covers the objects you use every day and the machinery behind them. Page 04 covers running them in production: probes, resources, autoscaling, security and troubleshooting. Versions: everything here was checked against Kubernetes 1.37 (docs and a live 1.37 cluster), see the version note in chapter 13.
Control Plane and Nodes
A cluster has two halves. The control plane stores the desired state and makes decisions. Worker nodes run your Pods. The central design rule: every component talks only to the API server. Nothing calls the scheduler or the kubelet directly. Components watch the API for objects they care about and write their results back.
| Component | Runs on | What it does | Failure symptom |
|---|---|---|---|
| kube-apiserver | Control plane | REST front door for all reads and writes; authentication, authorization, admission, validation; the only component that talks to etcd | kubectl times out; controllers cannot reconcile. Running Pods keep running |
| etcd | Control plane | Consistent key-value store holding every object. Raft-replicated; usually 3 or 5 members | API becomes read-only or unavailable; slow disk means slow API |
| kube-scheduler | Control plane | Watches Pods with no node, filters nodes that cannot fit, scores the rest, writes the chosen nodeName | New Pods stay Pending with no events from the scheduler |
| kube-controller-manager | Control plane | Runs the built-in controllers: Deployment, ReplicaSet, Job, Node, EndpointSlice, ServiceAccount and more | Scaling, rollouts and node-failure handling stop |
| cloud-controller-manager | Control plane (cloud only) | Cloud-specific loops: provisions load balancers for type: LoadBalancer, learns node metadata | LoadBalancer Services stay <pending> |
| kubelet | Every node | Watches Pods assigned to its node, asks the runtime to start containers, runs probes, reports status | Node turns NotReady; its Pods are eventually evicted and recreated |
| kube-proxy | Every node | Programs the node's packet rules so Service virtual IPs reach Pod IPs (chapter 06) | Service IPs stop working on that node |
| Container runtime | Every node | Pulls images and runs containers through the CRI. containerd and CRI-O are the common ones | Pods stuck in ContainerCreating |
Two details worth knowing. First, the control plane is itself just software: on a kubeadm-style cluster (and in kind, chapter 10) the API server, scheduler, controller manager and etcd run as static Pods whose manifests sit in /etc/kubernetes/manifests on the control-plane node, started by the kubelet. Second, on managed services such as EKS, GKE or AKS the provider operates the control plane for you; you only manage worker capacity and what you deploy.
kubectl apply
kubectl sends the object to the API server. It authenticates and authorizes you, runs admission checks, validates the schema and stores the object in etcd. That is all the API server does. The scheduler, the Deployment controller and the kubelets then notice the change by watching and act on their own.
Declarative Desired State and Reconciliation
Kubernetes is declarative. You do not issue commands such as "start a container". You submit an object whose spec says what you want, and controllers keep the world equal to it. Every Kubernetes object has the same shape:
| Field | Who writes it | Meaning |
|---|---|---|
apiVersion, kind | You | Which API group/version and which type of object |
metadata | You (some fields by the system) | name, namespace, labels, annotations; the system adds uid, resourceVersion, creationTimestamp |
spec | You | The desired state |
status | Controllers and kubelets | The observed state. Never edit it by hand |
Each built-in controller is a loop of the same three steps: observe (from the API, through a cached watch), compare spec with status, act to close the gap. Because the loop is idempotent and runs continuously, the system is self-healing and level-triggered: it does not matter how the state drifted or whether an event was missed, the next pass sees the difference and fixes it.
NotReady, its Pods are evicted after a timeout, and the ReplicaSet controller recreates them elsewherereplicas: 3 to 5| Style | Example | Use for |
|---|---|---|
| Imperative commands | kubectl create deployment web --image=nginx, kubectl scale | Quick experiments and learning |
| Imperative object config | kubectl create -f file.yaml, kubectl replace -f | Rare; fails if the object exists |
| Declarative | kubectl apply -f dir/ | Everything real: YAML in Git is the source of truth, apply reconciles |
The production habit is manifests in Git, applied by a pipeline (page 05). kubectl diff -f file.yaml shows what apply would change before you do it, and kubectl apply --dry-run=server -f file.yaml asks the real API server to validate the object without storing it (chapter 10).
- A
kubectl editorkubectl scaleon a live object is overwritten by the nextapplyfrom Git, and anything that is not in Git cannot be reproduced after a disaster. Treat the cluster as an output of your manifests, not as the place you edit.
Pods
A Pod is the smallest thing Kubernetes schedules: one or more containers that are always placed on the same node and share a network namespace (one IP, one localhost) and optionally volumes. Containers in a Pod start and die together as a unit of scheduling. Kubernetes does not manage bare containers, only Pods.
| A Pod shares | A Pod does not share |
|---|---|
One IP address and port space (containers reach each other on localhost) | The container filesystems (each image has its own root) |
| Volumes mounted into several containers | CPU and memory accounting (each container has its own requests and limits) |
| Hostname and (optionally) the process namespace | The restart: each container is restarted on its own by the kubelet |
The pause (sandbox) container. When the kubelet starts a Pod, the runtime first creates a Pod sandbox: a tiny container, running the pause image, that does nothing but hold the Pod's network namespace open. Your containers then join that namespace. That is why a single container can crash and restart without the Pod losing its IP. You can see it on a node: the 1.37 kind node has registry.k8s.io/pause version 3.10 in its image list. You never manage it yourself.
A bare Pod is not rescheduled. If its node dies, or someone drains the node, the Pod is gone for good and nothing recreates it. Wrap Pods in a controller instead:
| You want | Create a | Because |
|---|---|---|
| A long-running stateless service | Deployment | Keeps N replicas running and does rolling updates |
| A stable identity or per-Pod storage | StatefulSet | Ordered, named Pods with their own volumes |
| One Pod on every node | DaemonSet | Log shippers, node agents |
| Run to completion | Job / CronJob | Batch work with retries |
Bare Pods are fine for a quick debug shell (kubectl run) and for static Pods that the kubelet itself manages.
Put containers in one Pod only when they must share a lifecycle and a network or disk. The classic patterns:
| Pattern | Idea | Example |
|---|---|---|
| Sidecar | A helper that runs alongside the app for its whole life | Log shipper, service-mesh proxy, certificate refresher |
| Init container | Runs to completion before the app starts, in order | Wait for a dependency, run a schema migration, fetch files into a shared volume |
| Adapter | Translates the app's output to a standard shape | Expose app metrics in the format the monitoring system expects |
| Ambassador | Proxy for outbound connections | Local proxy that handles sharding or retries |
Since Kubernetes 1.33 native sidecar containers are stable: you declare the sidecar under initContainers with restartPolicy: Always. It starts before the main containers, keeps running for the Pod's life, and is shut down after them. That fixes the old problems of a sidecar that was not ready when the app started, or that kept a Job from ever completing.
One main application container per Pod. Scale by adding Pods, not containers. Containers that need to scale independently belong in separate Deployments.
ReplicaSet and Deployment
A ReplicaSet keeps a stated number of identical Pods running. It selects Pods by label and creates or deletes them to match replicas. You almost never write one yourself, because a Deployment owns ReplicaSets and adds the part you really want: versioned, gradual rollouts and rollback.
webreplicas does notweb-69c6f74b8bpod-template-hash, a hash of the template. The ReplicaSet adds that hash as a label on its Pods so sibling ReplicaSets do not fight over Podsweb-69c6f74b8b-4xgmsThe two knobs that shape a rolling update, from the Deployment API reference for 1.37:
| Field | Meaning | Default / rounding |
|---|---|---|
maxSurge | How many Pods above replicas may exist during the update (number or percent) | 25%; percentages round up; cannot be 0 if maxUnavailable is 0 |
maxUnavailable | How many Pods may be unavailable during the update | 25%; percentages round down; cannot be 0 if maxSurge is 0 |
minReadySeconds | A new Pod must stay Ready this long before it counts as available | 0 |
progressDeadlineSeconds | Time without progress before the rollout is marked failed (ProgressDeadlineExceeded) | 600 |
revisionHistoryLimit | Old ReplicaSets kept for rollback | 10 |
With 3 replicas the default 25% gives maxSurge = 1 (0.75 rounded up) and maxUnavailable = 0 (0.75 rounded down). A gate on readiness is what makes rolling updates safe: a new Pod counts as available only after its readiness probe passes (page 04). Without a probe, "running" is treated as "ready" and traffic can reach a Pod that is still starting.
That second rollout is the safety feature at work: the new Pod never became Ready, so the Deployment did not remove any old Pod. Three old Pods kept serving while the broken one sat in ErrImagePull, and kubectl rollout undo returned to the previous ReplicaSet. CHANGE-CAUSE is empty unless you set the kubernetes.io/change-cause annotation.
| Strategy | Behaviour | Use when |
|---|---|---|
RollingUpdate (default) | Gradual replacement, bounded by maxSurge / maxUnavailable | Almost always. Old and new versions run side by side, so they must be compatible |
Recreate | Stop all old Pods, then start new ones: downtime | Versions cannot coexist (a schema or a single-writer lock); accept the outage |
- With the defaults and a small replica count, or with
maxUnavailableabove 0, some old Pods are removed before new ones are Ready, so capacity dips. For a service that cannot lose capacity setmaxUnavailable: 0andmaxSurge: 1(as in the YAML in chapter 10), and make sure the cluster has room for the surge Pod.
Services, EndpointSlices, kube-proxy and DNS
Pods are disposable and their IPs change. A Service is the stable front: a fixed virtual IP and DNS name that load-balances across whichever Pods currently match its label selector and are Ready. Each Pod keeps its own IP, and every Pod can reach every other Pod by IP without NAT (the cluster network model); Services sit on top of that.
| Service type | What it creates | Reachable from | Typical use |
|---|---|---|---|
| ClusterIP (default) | A virtual IP inside the cluster | Inside the cluster only | Service-to-service calls |
| NodePort | A ClusterIP plus the same port on every node (default range 30000-32767) | Anything that can reach a node IP | Demos, or as the target of an external load balancer |
| LoadBalancer | NodePort plus a request to the cloud for an external load balancer | The internet or VPC via the cloud LB address | Exposing one service directly; on EKS this creates an AWS load balancer |
Headless (clusterIP: None) | No virtual IP; DNS returns the Pod IPs directly | Inside the cluster | StatefulSets and clients that do their own balancing |
| ExternalName | A DNS CNAME to an outside name | Inside the cluster | Giving an external dependency an in-cluster name |
Behind each Service, the EndpointSlice controller maintains one or more EndpointSlice objects listing the Pod IPs, ports and a ready condition per endpoint. kube-proxy and other consumers watch those slices. The older Endpoints object still exists but the v1 Endpoints API is deprecated since v1.33 in favour of discovery.k8s.io/v1 EndpointSlice (the 1.37 kubectl prints a warning if you query it). A slice holds up to 100 endpoints by default, so large Services are split into several slices instead of one huge object.
Here nr has a Pod whose readiness probe returns 404, so the endpoint exists with ready: false and receives no traffic. typo has a selector that matches no Pod (app: wbe), so its slice is empty: the classic Service has no endpoints problem (page 04, troubleshooting).
There is no process listening on a ClusterIP. kube-proxy on every node watches Services and EndpointSlices and programs the kernel so that packets sent to the virtual IP are rewritten to one ready Pod IP. The proxy mode decides how:
| kube-proxy mode | Mechanism | State in 1.37 |
|---|---|---|
iptables | NAT rules in the kernel; random choice among endpoints | The default in 1.37. A future version is documented to change the default to nftables, so set the mode explicitly |
nftables | The same idea on the newer nftables kernel API; documented as a replacement for both iptables and ipvs with better performance | Available and recommended in place of ipvs; the docs advise pinning the mode in your kube-proxy config |
ipvs | Kernel IP virtual server with several balancing algorithms | Deprecated since 1.35; docs say support will be disabled by default from 1.40 behind the KubeProxyIPVS feature gate |
kernelspace | Windows nodes | Windows only |
The kind cluster in this chapter reports mode: iptables in its kube-proxy ConfigMap. Some CNI plugins (Cilium, for example) replace kube-proxy entirely, which is why "no kube-proxy Pods" in a cluster is not necessarily a fault.
CoreDNS runs as a Deployment in kube-system and gives every Service a name. Pods are configured to use it through /etc/resolv.conf.
| Name | Resolves to |
|---|---|
orders | The Service orders in the same namespace as the caller (short name, thanks to the search path) |
orders.shop | The Service in namespace shop from anywhere |
orders.shop.svc.cluster.local | The fully qualified name (cluster.local is the default cluster domain) |
cache-0.cache-headless.shop.svc.cluster.local | One specific StatefulSet Pod behind a headless Service |
ndots:5 means any name with fewer than five dots is first tried with each search suffix. A call to an external host such as api.example.com (two dots) can trigger several failed lookups before the real one. Use a trailing dot (api.example.com.) or lower ndots in the Pod's dnsConfig if external DNS volume becomes a problem.
- Never hard-code a Pod IP; it changes on every restart. Always call the Service name. When a Service "has no endpoints", compare
spec.selectorwith the Pod labels character by character, and check that the Pods are Ready.targetPortmust match the port the container really listens on (naming it in the Pod and referring to the name, as the YAML in chapter 10 does, avoids drift).
Ingress versus Gateway API
A Service of type LoadBalancer gives one external address per Service, which gets expensive and awkward when you have dozens of HTTP services. The cluster needs a layer-7 entry point: one address that routes by host name and path to many Services, and terminates TLS. Kubernetes has two APIs for that.
Ingress (networking.k8s.io/v1) | Gateway API (gateway.networking.k8s.io) | |
|---|---|---|
| Status (checked 1.37 docs) | GA but frozen: no plans to remove it, but no new features. The Kubernetes docs recommend Gateway for new work | A separate add-on project, not part of core Kubernetes; the Kubernetes docs recommend it. The latest release I found was v1.7.0 (2026-10-29); v1.6 promoted TCPRoute and UDPRoute to the standard channel |
| Installed by | Nothing by default. You install an Ingress controller; an Ingress without one does nothing | You install the Gateway API CRDs and a controller that implements them (the CRDs are not present in a fresh cluster: kubectl api-resources on a new kind cluster lists no Gateway kinds) |
| Model | One object for rules; controller-specific behaviour via annotations | Role-oriented: GatewayClass (provider), Gateway (listeners, owned by platform team), HTTPRoute (owned by app teams) |
| Beyond host and path | Annotations, different per controller | Header matches, traffic weights, redirects, rewrites in the standard API |
| Protocols | HTTP and HTTPS | HTTP, gRPC, TLS, TCP, UDP routes |
| Cross-namespace | Awkward | Explicit: routes attach to Gateways according to allowed namespaces |
- The Kubernetes project announced the retirement of the community Ingress NGINX controller: best-effort maintenance until March 2026, after which no releases, bug fixes or security updates. Existing installs keep working, but it is a poor base for new clusters. Pick a maintained controller or an implementation of the Gateway API. (Source: the Kubernetes blog post of 2025-11-11; this is separate from the commercial NGINX controllers.)
On EKS the usual choice is the AWS Load Balancer Controller, which provisions AWS load balancers for Ingress or LoadBalancer Services; see the EKS notes on page 04. Choosing between a service mesh, an API gateway and a Gateway API implementation is a design question outside this page; the principle here is that Ingress and Gateway only describe routing, and the controller you install does the work.
ConfigMap, Secret, Namespaces, Labels and Annotations
A ConfigMap holds non-confidential configuration as key-value pairs or whole files. One image can then run in dev and prod with different settings. A Pod consumes it in two ways:
| Consumed as | How | Updates after a change |
|---|---|---|
| Environment variables | env.valueFrom.configMapKeyRef or envFrom.configMapRef | Not updated in a running container; needs a Pod restart |
| Files in a volume | volumes[].configMap mounted at a path | Eventually updated in place by the kubelet (not for subPath mounts) |
Spring Boot maps environment variables to properties automatically (SPRING_PROFILES_ACTIVE, SERVER_PORT, SPRING_DATASOURCE_URL) and can read extra config files from a directory via spring.config.additional-location. The full example in chapter 10 uses both. Because env vars do not refresh, a common pattern is to change the ConfigMap and trigger a rollout (for instance with a checksum annotation in a Helm template, page 04).
A Secret has the same shape as a ConfigMap but is meant for passwords, tokens and keys. The size limit per Secret is 1 MiB. It is handled more carefully by the system (not logged, can be mounted from memory-backed volumes, access controlled separately by RBAC). Read the next warning before you rely on it.
- Secret values are stored base64-encoded in the manifest, and anyone who can
kubectl get secret -o yaml(or read etcd) can decode them withbase64 -d. By default they are stored unencrypted in etcd. Three separate protections are needed: RBAC so only the right identities can read Secrets, encryption at rest for etcd, and not committing Secret manifests to Git in plain form.
- Encryption at rest is configured on the API server with an
EncryptionConfigurationthat lists providers. The documentation coversaescbc,secretbox,aesgcmand KMS providers; with KMS the data-encryption keys are protected by a key held in an external key service. Managed services (EKS, GKE, AKS) offer envelope encryption with a cloud KMS key, enabled in the cluster settings; see the provider's documentation for how. - Immutable ConfigMaps and Secrets (
immutable: true) cannot be changed, only replaced. That protects against accidental edits and reduces load on the API server. - Secrets are only as safe as the nodes and the API access around them. Page 04 compares External Secrets, Sealed Secrets and cloud secret stores.
A namespace is a scope for names and policy inside one cluster. Two namespaces can both contain a Deployment called orders. Namespaces are the unit for RBAC, ResourceQuota, LimitRange, NetworkPolicy and Pod Security labels (page 04). They are an organisational boundary, not a security boundary by themselves: without NetworkPolicies, Pods in different namespaces can talk to each other.
- Common layouts: one namespace per team or per application and environment (
shop-dev,shop-prod), or separate clusters for production. - Not everything is namespaced: Nodes, PersistentVolumes, StorageClasses and Namespaces themselves are cluster-scoped (
kubectl api-resourcesshows a NAMESPACED column). - A default cluster starts with
default,kube-system,kube-publicandkube-node-lease. Do not deploy applications intodefaultorkube-system.
Labels are key-value tags used to select objects; they are how a Service finds Pods and a Deployment finds its ReplicaSet. Annotations are non-identifying metadata that tools read (build info, ingress or controller settings, kubernetes.io/change-cause); you cannot select by them.
| Label key (recommended) | Meaning |
|---|---|
app.kubernetes.io/name | Name of the application, e.g. orders |
app.kubernetes.io/instance | The instance of it, e.g. orders-prod |
app.kubernetes.io/version | Application version |
app.kubernetes.io/part-of, component, managed-by | Grouping, role and tool (Helm, kustomize) |
spec.selectorof a Deployment is immutable after creation, and it must match the Pod template labels. Pick stable labels for it (app.kubernetes.io/name), and keep changing labels such asversionout of it, otherwise the next change forces you to delete and recreate the Deployment.
StatefulSet, DaemonSet, Job, CronJob and Storage
A StatefulSet is like a Deployment for software that needs stable identity and storage: databases, message brokers, anything where "replica 1" is not interchangeable with "replica 2".
| Property | Deployment | StatefulSet |
|---|---|---|
| Pod names | Random (web-6f9bc-th9cr) | Ordinal and stable (cache-0, cache-1) |
| Network identity | Shared Service IP | Per-Pod DNS name through a headless Service (cache-0.cache-headless) |
| Storage | Shared volume or none | volumeClaimTemplates: one PVC per Pod that follows it across restarts |
| Start and stop order | All at once | OrderedReady by default: cache-0, then cache-1, waiting for Ready (Parallel is optional); stopped in reverse |
| PVC cleanup | n/a | Retained by default; persistentVolumeClaimRetentionPolicy can delete them on scale-down or deletion |
A StatefulSet gives you identity and storage. It does not give you replication, backup or failover: that is the database's job, usually delivered by an operator. For production databases, evaluating a managed service first is a sound default.
| Object | Runs | Notes |
|---|---|---|
| DaemonSet | One Pod on every (or every selected) node; new nodes get one automatically | Log collectors, node monitoring agents, CNI and storage drivers. Pods need tolerations to run on tainted nodes |
| Job | Pods until a number of them succeed | completions, parallelism, backoffLimit (default 6), ttlSecondsAfterFinished to auto-clean. Pod restartPolicy must be OnFailure or Never |
| CronJob | A Job on a cron schedule | concurrencyPolicy (Allow, Forbid, Replace), startingDeadlineSeconds, timeZone (otherwise the controller manager's zone), history limits (successful default 3) |
Make batch work idempotent: a CronJob can occasionally run a job twice or miss a run, and a Job retries failed Pods, so the same work may execute more than once.
Container filesystems vanish with the container, so durable data needs a volume. Kubernetes separates "someone provides storage" from "an application needs storage":
| Object | Role |
|---|---|
| PersistentVolume (PV) | A piece of storage in the cluster (a cloud disk, an NFS export). Cluster-scoped |
| PersistentVolumeClaim (PVC) | An application's request: size, access mode, class. Namespaced; Pods mount the claim, never the PV |
| StorageClass | A recipe for creating PVs on demand (dynamic provisioning): provisioner, parameters, reclaim policy, binding mode |
| Access mode | Meaning |
|---|---|
ReadWriteOnce (RWO) | Read-write by a single node (several Pods on that node may share it) |
ReadOnlyMany (ROX) | Read-only by many nodes |
ReadWriteMany (RWX) | Read-write by many nodes (needs a driver that supports it, e.g. network file systems) |
ReadWriteOncePod (RWOP) | Read-write by a single Pod in the whole cluster |
Which modes work depends on the storage driver: a typical block disk supports RWO only, so a Deployment with several replicas on different nodes cannot all mount one RWO volume. In the kind cluster used here the default StorageClass is standard, provisioner rancher.io/local-path, reclaim policy Delete, volume binding mode WaitForFirstConsumer (the volume is created only when a Pod is scheduled, so it lands on the right node or zone).
- Reclaim policy
Deleteremoves the underlying storage when the PVC is deleted;Retainkeeps it for manual cleanup. - A PVC can be expanded if its StorageClass has
allowVolumeExpansion: true(stable since 1.24); it cannot normally be shrunk. - A PVC outlives the Pod that used it. Deleting a StatefulSet does not delete its PVCs unless you configured the retention policy.
A Spring Boot Service, a Local Cluster and kubectl
Put the objects together for a Spring Boot service called orders: two ConfigMaps (environment variables and an application.yaml file), a Deployment with three replicas and a rolling-update policy that never reduces capacity, and a ClusterIP Service. The image name is an example; build and push your own image (page 02).
apps/v1for Deployment,v1for ConfigMap and Service. The selectormatchLabelsand the Pod template labels are identical, and the Service selector matches them too.SPRING_CONFIG_ADDITIONAL_LOCATION=file:/config/tells Spring Boot to read the mountedapplication.yamlin addition to the one in the jar. A ConfigMap key such asapplication.yamlis not a valid environment variable name, so it lives in a separate ConfigMap that is mounted as a file, not passed withenvFrom.- The container port is named
http; the Service usestargetPort: http, so the number lives in one place. requestsandlimitsare set (memory request equals limit, and no CPU limit: the reasoning is in page 04). There are no probes here on purpose, to keep this page focused; page 04 adds them, plus graceful shutdown and a PodDisruptionBudget.
kind (Kubernetes IN Docker) runs a whole cluster as Docker containers, and is the quickest way to practise. Everything below was run on kind v0.33.0 with a node image running Kubernetes v1.37.0 (containerd 2.3), and kubectl v1.37.2. minikube works equally well; the kubectl commands are identical, only cluster creation differs.
kubectl port-forward tunnels a local port to a Pod or Service through the API server: ideal for debugging, not a way to expose a service. The HTTP/1.1 200 OK line is the real first line of the response from that cluster. To try the YAML above without your own image, change image: to a public one and drop the Spring-specific settings, or run kubectl apply --dry-run=server -f app.yaml as validation only.
| Task | Command | Notes |
|---|---|---|
| List things | kubectl get pods -o wide, kubectl get all, -A for all namespaces, -w to watch | -o wide adds node and IP. get all omits many kinds (e.g. ConfigMaps) |
| Inspect one object | kubectl describe pod NAME | The Events section at the bottom explains most scheduling, pulling and probe failures |
| Raw object | kubectl get deploy orders -o yaml | Shows spec and status; also -o json, -o jsonpath=..., -o custom-columns=... |
| Logs | kubectl logs POD -c CONTAINER, -f, --previous, -l app=orders --tail=100 | --previous is the log of the last crashed container, vital for CrashLoopBackOff |
| Shell inside | kubectl exec -it POD -- sh | Minimal images may have no shell; see kubectl debug with an ephemeral container |
| Tunnel | kubectl port-forward svc/orders 8080:80 | Local debugging only |
| Apply | kubectl apply -f file.yaml (or -f dir/, -k overlay/) | Declarative create or update |
| Preview | kubectl diff -f file.yaml | Shows what apply would change. Exit status 1 means differences were found, not an error |
| Validate only | kubectl apply --dry-run=server -f file.yaml | client only checks locally; server runs real admission and schema validation |
| Rollouts | kubectl rollout status|history|undo|restart deployment/orders | restart does a rolling restart (new Pods, same template) |
| Scale | kubectl scale deploy orders --replicas=5 | Overwritten by the next apply if replicas is in your YAML |
| Learn the API | kubectl explain deployment.spec.strategy.rollingUpdate | Documents any field from the live server's schema; add --recursive for the tree |
| Who can do what | kubectl auth can-i delete pods --as=system:serviceaccount:ns:sa | Quick RBAC check (page 04) |
| Events | kubectl get events --sort-by=.lastTimestamp | Cluster-wide timeline of what happened |
Put kubectl describe and kubectl get events first when something does not start, kubectl logs --previous when it starts and crashes, and kubectl explain instead of searching the web for a field name. Add a short alias (alias k=kubectl) and shell completion (source <(kubectl completion zsh)).
Scheduling, Startup and Termination of a Pod
Understanding a Pod's life explains most "why was my request dropped?" and "why is it stuck?" questions.
| Phase / condition | What is happening | What you see |
|---|---|---|
Pending | Accepted by the API but not yet running: waiting for the scheduler, for volumes, or for the image to pull | kubectl get pods shows Pending / ContainerCreating; events explain why |
| Scheduled | The scheduler filtered nodes (resources, selectors, taints, affinity), scored the rest and set spec.nodeName | Event Scheduled: Successfully assigned ... to node |
| Init containers | Run one at a time, each to completion, before the app containers. A failing init container is retried and the Pod shows Init:Error / Init:CrashLoopBackOff | Init:0/2 in STATUS |
| Image pull and start | kubelet asks the runtime to pull (per imagePullPolicy), create and start containers | Events Pulled, Created, Started; failures show ErrImagePull, ImagePullBackOff |
Running, not Ready | Containers run; the startup probe (if any) gates the other probes; the readiness probe decides Ready | READY 0/1; not in Service endpoints |
| Ready | All containers pass readiness; the Pod receives Service traffic | READY 1/1 |
Terminating | Deletion requested; see below | STATUS Terminating until containers have exited |
Succeeded / Failed | Terminal phases for Pods that run to completion (Jobs) | Normal for Job Pods |
Deleting a Pod (directly, by a rollout, a scale-down or a node drain) runs a fixed sequence. The Kubernetes Pod lifecycle documentation describes it, and the 1.37 field documentation confirms the details:
TerminatingterminationGracePeriodSeconds (default 30 seconds). The countdown covers the preStop hook and the shutdown that follows, not only the shutdownpreStop hook runsSTOPSIGNAL if one is defined, otherwise SIGTERM (for containerd and CRI-O). Your process should stop accepting work, finish in-flight work and exitA real measurement on the 1.37 cluster: a container with terminationGracePeriodSeconds: 20, a preStop hook that sleeps 3 seconds, and a shell that traps SIGTERM, sleeps 2 seconds and exits. The logs showed the order below and kubectl delete pod returned after about 7 seconds, well inside the budget (the rest is runtime overhead).
- If your image starts the app through a shell script (
CMD ./start.shthat launchesjava), the shell is PID 1 and may not forward SIGTERM to the JVM. The app is then killed with SIGKILL at the end of the grace period and loses in-flight work. Useexec java ...in the script, or the exec form (CMD ["java", ...]) in the Dockerfile (page 02). Spring Boot handles SIGTERM correctly when it is PID 1.
The Pod restartPolicy (Always by default, OnFailure, Never) tells the kubelet what to do when a container exits. Repeated failures are restarted with an exponential back-off, which is what the CrashLoopBackOff status means: "the container keeps exiting, the kubelet is waiting before the next try". The restart count, the last exit code and the reason are in kubectl describe pod; page 04 turns that into a decision tree. Pod conditions such as PodScheduled, Initialized, ContainersReady and Ready give the same story in machine-readable form.
Questions You Should Be Able to Answer
Crisp answers for common Kubernetes fundamentals questions.
- Q: What problems does Kubernetes solve?
Scheduling containers onto machines, self-healing, scaling, service discovery and load balancing, rolling updates and rollback, and configuration and secret management, all driven from declared desired state. - Q: Name the control-plane components and what each does.
The API server is the only front door and the only component that talks to etcd. etcd stores all state. The scheduler assigns unscheduled Pods to nodes. The controller manager runs the built-in control loops (Deployment, ReplicaSet, Node, Job...). The cloud-controller-manager does cloud-specific work. Nodes run the kubelet, kube-proxy and a container runtime. - Q: What happens when you run
kubectl apply -f deployment.yaml?
kubectl sends the object to the API server, which authenticates, authorizes, runs admission, validates and stores it in etcd. The Deployment controller notices it and creates a ReplicaSet, which creates Pods; the scheduler assigns them to nodes; each kubelet sees its Pods through a watch, has the runtime start the containers and reports status back. - Q: Explain the reconciliation / control loop.
A controller watches objects, comparesspec(desired) withstatus(observed), and acts to close the difference, forever. It is level-triggered and idempotent, so the system self-heals regardless of how the drift happened. - Q: Pod versus container versus Deployment?
A container is one process in an image. A Pod is the smallest scheduled unit: one or more containers sharing network and volumes on one node. A Deployment manages ReplicaSets of identical Pods and provides rolling updates and rollback. - Q: Why not run bare Pods?
Nothing recreates them if the node fails or they are evicted. Use a controller (Deployment, StatefulSet, DaemonSet, Job). - Q: How does a rolling update work, and how do you make it zero-downtime?
A new ReplicaSet is scaled up while the old one is scaled down, bounded bymaxSurgeandmaxUnavailable. Zero-downtime needsmaxUnavailable: 0, a readiness probe so new Pods only count when ready, and graceful shutdown so old Pods finish requests (page 04). - Q: Explain ClusterIP, NodePort, LoadBalancer and headless Services.
ClusterIP is an in-cluster virtual IP; NodePort adds a port on every node; LoadBalancer adds a cloud load balancer; headless has no virtual IP and DNS returns Pod IPs, used with StatefulSets. - Q: How does a Service find its Pods, and how does traffic reach them?
By label selector. The EndpointSlice controller keeps a list of ready Pod IPs, and kube-proxy on each node programs rules (iptables by default in 1.37) that rewrite the virtual IP to one of them. - Q: Ingress versus Gateway API?
Both route external HTTP traffic to Services and need a controller. Ingress is GA but frozen and relies on annotations for advanced behaviour. Gateway API is the recommended newer, role-oriented API with GatewayClass, Gateway and routes, and richer matching in the standard. - Q: Are Kubernetes Secrets secure?
Not by themselves: values are base64-encoded, and by default stored unencrypted in etcd. Add RBAC, encryption at rest (EncryptionConfiguration, usually a KMS provider) and avoid plaintext manifests in Git. - Q: Deployment versus StatefulSet?
Deployment Pods are interchangeable with random names. StatefulSet Pods have stable ordinal names, stable DNS through a headless Service, per-Pod PVCs and ordered start and stop. Neither provides database replication itself. - Q: What are PV, PVC and StorageClass?
A PV is the storage resource, a PVC is an application's request for it, and a StorageClass describes how to provision it dynamically. Pods reference the PVC. - Q: What happens to a Pod when it is deleted?
It is marked Terminating, removed from endpoints in parallel, the preStop hook runs, SIGTERM is sent, and afterterminationGracePeriodSeconds(default 30 s) SIGKILL. The same countdown covers preStop and shutdown. - Q: A Pod is Pending. Where do you look?
kubectl describe podevents: insufficient CPU or memory, unbound PVC, no node matching selector, taints or affinity. Detailed trees are on page 04.
Cheat Sheet and Recap
| Concept | Remember | Detail (Kubernetes 1.37) |
|---|---|---|
| Declarative model | spec is desired, status is observed; controllers reconcile | Store manifests in Git; kubectl apply |
| API server | Only front door and only etcd client | All components watch it |
| Pod | Smallest unit; shared IP and volumes; disposable | Created by controllers, rarely by hand |
| Deployment | ReplicaSet per template version; rolling update; rollback | maxSurge 25% rounds up, maxUnavailable 25% rounds down; history 10 |
| Service | Stable VIP + DNS over Ready Pods selected by labels | ClusterIP, NodePort (30000-32767), LoadBalancer, headless |
| kube-proxy | Programs rules for Service VIPs | iptables default in 1.37; ipvs deprecated since 1.35; nftables available |
| Ingress / Gateway | Layer-7 entry points; both need a controller | Ingress GA but frozen; Gateway API recommended; ingress-nginx retired March 2026 |
| ConfigMap / Secret | Config out of the image; env vars do not refresh, mounted files do | Secrets are base64, add encryption at rest + RBAC; 1 MiB limit |
| StatefulSet | Stable names, DNS, per-Pod PVC; ordered | Does not replicate data for you |
| Job / CronJob | Run to completion on demand or schedule | backoffLimit 6; keep 3 successful Jobs; set timeZone |
| Storage | PV (supply) / PVC (request) / StorageClass (recipe) | RWO, ROX, RWX, RWOP; WaitForFirstConsumer |
| Termination | Terminating, preStop, SIGTERM, SIGKILL; one countdown | terminationGracePeriodSeconds default 30 |
| First debugging steps | describe (events), logs --previous, get events | Details on page 04 |
- Desired state plus reconciliation is the one idea behind everything: you declare, controllers converge, and drift heals itself.
- Everything goes through the API server; components only watch and write back, nothing calls anything else directly.
- Pods are disposable. Use Deployments for stateless, StatefulSets for identity and storage, DaemonSets for per-node, Jobs for batch.
- Services give stable addresses over Ready Pods; a broken selector or a failing readiness probe gives an empty endpoint list.
- Readiness gates rolling updates and traffic; a failed rollout leaves old Pods serving rather than taking the service down.
- Secrets are not encrypted by default. Layer RBAC, encryption at rest and a good source of truth.
- Termination is a single countdown shared by preStop and shutdown, racing with endpoint removal: design for it (page 04).
Checked on 2026-10-31 against the Kubernetes 1.37 documentation (kubernetes.io/docs) and by running a kind v0.33.0 cluster with Kubernetes v1.37.0 and kubectl v1.37.2: all YAML on this page passed kubectl apply --dry-run=server, and the outputs marked real were captured from that cluster. Facts taken from the docs: kube-proxy modes (iptables default in 1.37, ipvs deprecated since 1.35, nftables available), Ingress frozen and Gateway recommended, Endpoints deprecated in 1.33, sidecar containers stable in 1.33, ingress-nginx retirement notice. Gateway API version came from the project's GitHub releases page. Not verified: the exact behaviour of any specific cloud provider or Ingress controller.
- 04 Kubernetes Operations - probes, resources, autoscaling, graceful shutdown, security and a troubleshooting guide.
- 02 Docker - the image and container model that Pods run.
- 05 CI/CD Pipelines - how manifests in Git reach the cluster.
- Cheat sheet: Docker and kubectl - the commands on one page.