DevOps · 03

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.

13
Chapters
1.37
Kubernetes version
kind
Local lab
6
Diagrams
01
Chapter One - Why Orchestration

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 scaleBy hand / with plain DockerWhat Kubernetes gives you
Place containers on machinesYou pick a host and docker run over SSHThe scheduler picks a node that fits CPU, memory and constraints
A process or whole machine diesSomeone is paged and restarts itThe kubelet restarts containers; controllers recreate Pods on other nodes
Run N identical copiesN commands, N port mappingsreplicas: N in a Deployment
Send traffic to healthy copies onlyHand-edited load balancer configA Service with readiness-aware endpoints
Ship a new version safelyStop old, start new, hopeRolling update with health gating and one-command rollback
Configuration and secretsEnv files copied to serversConfigMap and Secret objects injected at start
Capacity changesProvision and configure serversAutoscaling of Pods (and of nodes with an add-on)
Key insight: Docker is a shipping container. Kubernetes is the port: cranes, schedules, customs rules and the harbour master who notices that a container fell off the quay and orders another. You write the manifest, the port does the logistics.
⚠ Adopting Kubernetes before you need it
  • 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.

02
Chapter Two - Architecture

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.

Cluster architecture: a control plane that decides, nodes that obey
Control plane kube-apiserver the only front door; REST + watch etcd the only durable state kube-scheduler unscheduled Pod -> node kube-controller-manager Deployment, Node, Job ... loops cloud-controller-mgr optional: LBs, nodes, routes r/w Node 1 kubelet runs Pods, probes kube-proxy Service rules containerd runs containers Pod A Pod B Pod C Node 2 kubelet runs Pods, probes kube-proxy Service rules containerd runs containers Pod A Pod B Pod C watch + status Everything talks to the API server. Components never call each other directly; they watch the API and write back results.
ComponentRuns onWhat it doesFailure symptom
kube-apiserverControl planeREST front door for all reads and writes; authentication, authorization, admission, validation; the only component that talks to etcdkubectl times out; controllers cannot reconcile. Running Pods keep running
etcdControl planeConsistent key-value store holding every object. Raft-replicated; usually 3 or 5 membersAPI becomes read-only or unavailable; slow disk means slow API
kube-schedulerControl planeWatches Pods with no node, filters nodes that cannot fit, scores the rest, writes the chosen nodeNameNew Pods stay Pending with no events from the scheduler
kube-controller-managerControl planeRuns the built-in controllers: Deployment, ReplicaSet, Job, Node, EndpointSlice, ServiceAccount and moreScaling, rollouts and node-failure handling stop
cloud-controller-managerControl plane (cloud only)Cloud-specific loops: provisions load balancers for type: LoadBalancer, learns node metadataLoadBalancer Services stay <pending>
kubeletEvery nodeWatches Pods assigned to its node, asks the runtime to start containers, runs probes, reports statusNode turns NotReady; its Pods are eventually evicted and recreated
kube-proxyEvery nodePrograms the node's packet rules so Service virtual IPs reach Pod IPs (chapter 06)Service IPs stop working on that node
Container runtimeEvery nodePulls images and runs containers through the CRI. containerd and CRI-O are the common onesPods 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.

Shell - real output from a 1.37 kind cluster (abridged): the control plane is visible as Pods
$ kubectl get pods -n kube-system NAME READY STATUS RESTARTS AGE coredns-559f6c778d-dr578 1/1 Running 0 37s etcd-lab-control-plane 1/1 Running 0 48s kube-apiserver-lab-control-plane 1/1 Running 0 48s kube-controller-manager-lab-control-plane 1/1 Running 0 48s kube-proxy-wptvs 1/1 Running 0 37s kube-scheduler-lab-control-plane 1/1 Running 0 48s
What happens on 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.

03
Chapter Three - The Central Idea

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:

FieldWho writes itMeaning
apiVersion, kindYouWhich API group/version and which type of object
metadataYou (some fields by the system)name, namespace, labels, annotations; the system adds uid, resourceVersion, creationTimestamp
specYouThe desired state
statusControllers and kubeletsThe observed state. Never edit it by hand
The control loop: observe, compare, act, repeat
Desired state spec: replicas: 3 Controller compares forever Actual state status: 2 Pods Ready watch observe act: create 1 Pod API call: create Pod The controller never says "start a Pod on node 3". It only sees "I want 3, I see 2" and creates one. The scheduler and the kubelet then do their own part of the same pattern for that new Pod. Crashes, node loss and manual deletion all look the same to it: actual drifted from desired, so it acts.

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.

You delete a Pod by hand
▶
The ReplicaSet controller sees 2 Pods where it wants 3 and creates a replacement within moments
A node loses power
▶
The Node controller marks it NotReady, its Pods are evicted after a timeout, and the ReplicaSet controller recreates them elsewhere
You change replicas: 3 to 5
▶
Nobody "scales up"; the ReplicaSet controller sees 3 where it wants 5 and creates 2
You change the image in a Deployment
▶
The Deployment controller creates a new ReplicaSet and shifts replicas gradually from the old one
Declarative apply versus imperative commands
StyleExampleUse for
Imperative commandskubectl create deployment web --image=nginx, kubectl scaleQuick experiments and learning
Imperative object configkubectl create -f file.yaml, kubectl replace -fRare; fails if the object exists
Declarativekubectl 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).

⚠ Editing live objects by hand and forgetting
  • A kubectl edit or kubectl scale on a live object is overwritten by the next apply from 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.
04
Chapter Four - The Smallest Unit

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 sharesA 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 containersCPU and memory accounting (each container has its own requests and limits)
Hostname and (optionally) the process namespaceThe 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.

Why you rarely create a bare Pod

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 wantCreate aBecause
A long-running stateless serviceDeploymentKeeps N replicas running and does rolling updates
A stable identity or per-Pod storageStatefulSetOrdered, named Pods with their own volumes
One Pod on every nodeDaemonSetLog shippers, node agents
Run to completionJob / CronJobBatch work with retries

Bare Pods are fine for a quick debug shell (kubectl run) and for static Pods that the kubelet itself manages.

Multi-container Pod patterns

Put containers in one Pod only when they must share a lifecycle and a network or disk. The classic patterns:

PatternIdeaExample
SidecarA helper that runs alongside the app for its whole lifeLog shipper, service-mesh proxy, certificate refresher
Init containerRuns to completion before the app starts, in orderWait for a dependency, run a schema migration, fetch files into a shared volume
AdapterTranslates the app's output to a standard shapeExpose app metrics in the format the monitoring system expects
AmbassadorProxy for outbound connectionsLocal 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.

YAML - a Pod with an init container and a native sidecar
apiVersion: v1 kind: Pod metadata: name: orders-demo spec: initContainers: - name: wait-for-config image: busybox:1.37 command: ["sh", "-c", "echo checking prerequisites; sleep 2"] - name: log-shipper # native sidecar: init container with restartPolicy Always image: busybox:1.37 restartPolicy: Always command: ["sh", "-c", "while true; do sleep 30; done"] containers: - name: orders image: registry.example.com/shop/orders:1.4.2 ports: - containerPort: 8080
Rule of thumb

One main application container per Pod. Scale by adding Pods, not containers. Containers that need to scale independently belong in separate Deployments.

05
Chapter Five - Stateless Workloads

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.

Deployment web
▶
Owns one ReplicaSet per Pod template version. Changing the template (image, env, resources) creates a new one; changing only replicas does not
ReplicaSet web-69c6f74b8b
▶
The suffix is the pod-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 Pods
Pod web-69c6f74b8b-4xgms
▶
Name = ReplicaSet name + random suffix. Disposable: its name and IP change when it is replaced
Rolling update: replicas 3, maxSurge 1, maxUnavailable 0
start v1 v1 v1 old v1 x3 Ready, new v2 none 1 v1 v1 v1 v2 surge: create 1 new Pod (4 total); old still all Ready 2 v1 v1 v2 new Pod passes readiness: now one old Pod may be removed 3 v1 v1 v2 v2 create next new Pod (surge again) ... v2 v2 v2 repeat until 3 new Pods Ready and 0 old remain Old Pods belong to the old ReplicaSet, new Pods to the new one; the Deployment moves the counts step by step.

The two knobs that shape a rolling update, from the Deployment API reference for 1.37:

FieldMeaningDefault / rounding
maxSurgeHow many Pods above replicas may exist during the update (number or percent)25%; percentages round up; cannot be 0 if maxUnavailable is 0
maxUnavailableHow many Pods may be unavailable during the update25%; percentages round down; cannot be 0 if maxSurge is 0
minReadySecondsA new Pod must stay Ready this long before it counts as available0
progressDeadlineSecondsTime without progress before the rollout is marked failed (ProgressDeadlineExceeded)600
revisionHistoryLimitOld ReplicaSets kept for rollback10

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.

Shell - a real rollout and a rollout that cannot finish (output abridged)
$ kubectl create deployment web --image=nginx:1.27 --replicas=3 $ kubectl set image deployment/web nginx=nginx:1.28 $ kubectl rollout status deployment/web Waiting for deployment "web" rollout to finish: 1 old replicas are pending termination... deployment "web" successfully rolled out $ kubectl rollout history deployment/web REVISION CHANGE-CAUSE 1 <none> 2 <none> $ kubectl set image deployment/web nginx=nginx:does-not-exist $ kubectl get pods -l app=web NAME READY STATUS RESTARTS AGE web-6f9bcdc979-7lf2h 1/1 Running 0 26s web-6f9bcdc979-th9cr 1/1 Running 0 23s web-6f9bcdc979-znnkp 1/1 Running 0 50s web-85bbdcddc7-f69zj 0/1 ErrImagePull 0 20s $ kubectl rollout undo deployment/web deployment.apps/web rolled back

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.

StrategyBehaviourUse when
RollingUpdate (default)Gradual replacement, bounded by maxSurge / maxUnavailableAlmost always. Old and new versions run side by side, so they must be compatible
RecreateStop all old Pods, then start new ones: downtimeVersions cannot coexist (a schema or a single-writer lock); accept the outage
⚠ A rolling update that quietly takes capacity away
  • With the defaults and a small replica count, or with maxUnavailable above 0, some old Pods are removed before new ones are Ready, so capacity dips. For a service that cannot lose capacity set maxUnavailable: 0 and maxSurge: 1 (as in the YAML in chapter 10), and make sure the cluster has room for the surge Pod.
06
Chapter Six - Networking Inside the Cluster

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.

A Service gives a stable address to a changing set of Pods
client Pod Service orders ClusterIP 10.96.138.147:80 kube-proxy rules on every node EndpointSlice Ready Pod IPs + ports DNS name selector Pod 10.244.0.5 Pod 10.244.0.6 Pod 10.244.0.7 (not ready) The ClusterIP is virtual: no interface owns it. The node rewrites the destination to one ready Pod IP. A Pod that fails readiness is marked not ready in its EndpointSlice and removed from the rotation. The EndpointSlice controller keeps the list current as Pods come and go.
Service typeWhat it createsReachable fromTypical use
ClusterIP (default)A virtual IP inside the clusterInside the cluster onlyService-to-service calls
NodePortA ClusterIP plus the same port on every node (default range 30000-32767)Anything that can reach a node IPDemos, or as the target of an external load balancer
LoadBalancerNodePort plus a request to the cloud for an external load balancerThe internet or VPC via the cloud LB addressExposing one service directly; on EKS this creates an AWS load balancer
Headless (clusterIP: None)No virtual IP; DNS returns the Pod IPs directlyInside the clusterStatefulSets and clients that do their own balancing
ExternalNameA DNS CNAME to an outside nameInside the clusterGiving an external dependency an in-cluster name
Endpoints, EndpointSlices and readiness

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.

Shell - real output (abridged): an EndpointSlice with a Pod that fails readiness
$ kubectl get endpointslices NAME ADDRESSTYPE PORTS ENDPOINTS AGE nr-br67r IPv4 80 10.244.0.16 58s typo-b88kf IPv4 <unset> <unset> 58s web-jtcgl IPv4 80 10.244.0.8,10.244.0.9,10.244.0.10 3m15s $ kubectl get endpointslice -l kubernetes.io/service-name=nr -o jsonpath='{.items[0].endpoints}' [{"addresses":["10.244.0.16"],"conditions":{"ready":false,"serving":false,"terminating":false},"nodeName":"lab-control-plane", ...}]

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).

How kube-proxy implements the virtual IP

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 modeMechanismState in 1.37
iptablesNAT rules in the kernel; random choice among endpointsThe default in 1.37. A future version is documented to change the default to nftables, so set the mode explicitly
nftablesThe same idea on the newer nftables kernel API; documented as a replacement for both iptables and ipvs with better performanceAvailable and recommended in place of ipvs; the docs advise pinning the mode in your kube-proxy config
ipvsKernel IP virtual server with several balancing algorithmsDeprecated since 1.35; docs say support will be disabled by default from 1.40 behind the KubeProxyIPVS feature gate
kernelspaceWindows nodesWindows 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.

DNS names

CoreDNS runs as a Deployment in kube-system and gives every Service a name. Pods are configured to use it through /etc/resolv.conf.

NameResolves to
ordersThe Service orders in the same namespace as the caller (short name, thanks to the search path)
orders.shopThe Service in namespace shop from anywhere
orders.shop.svc.cluster.localThe fully qualified name (cluster.local is the default cluster domain)
cache-0.cache-headless.shop.svc.cluster.localOne specific StatefulSet Pod behind a headless Service
Shell - real output (abridged): the DNS configuration a Pod gets and a lookup
$ kubectl exec deploy/web -- cat /etc/resolv.conf search default.svc.cluster.local svc.cluster.local cluster.local nameserver 10.96.0.10 options ndots:5 $ kubectl run dnstest --image=busybox:1.37 --restart=Never -- nslookup web.default.svc.cluster.local $ kubectl logs dnstest Server: 10.96.0.10 Address: 10.96.0.10:53 Name: web.default.svc.cluster.local Address: 10.96.138.147

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.

⚠ Using Pod IPs, or a selector that does not match
  • Never hard-code a Pod IP; it changes on every restart. Always call the Service name. When a Service "has no endpoints", compare spec.selector with the Pod labels character by character, and check that the Pods are Ready. targetPort must 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).
07
Chapter Seven - Getting Traffic In

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 (one object) versus Gateway API (roles split across objects)
Ingress Ingress host + path rules Ingress controller you install one Services Extra behaviour lives in controller-specific annotations. Gateway API GatewayClass infra provider Gateway listeners, ports, TLS HTTPRoute app team routes Services Infrastructure owner manages GatewayClass and Gateway; application teams attach their own routes. Headers, weights, redirects and rewrites are part of the standard API, not annotations.
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 workA 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 byNothing by default. You install an Ingress controller; an Ingress without one does nothingYou 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)
ModelOne object for rules; controller-specific behaviour via annotationsRole-oriented: GatewayClass (provider), Gateway (listeners, owned by platform team), HTTPRoute (owned by app teams)
Beyond host and pathAnnotations, different per controllerHeader matches, traffic weights, redirects, rewrites in the standard API
ProtocolsHTTP and HTTPSHTTP, gRPC, TLS, TCP, UDP routes
Cross-namespaceAwkwardExplicit: routes attach to Gateways according to allowed namespaces
⚠ Building on ingress-nginx without checking its status
  • 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.)
YAML - an Ingress for the orders Service (valid networking.k8s.io/v1, server dry-run checked; needs a controller to take effect)
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: orders spec: ingressClassName: nginx rules: - host: shop.example.com http: paths: - path: /orders pathType: Prefix backend: service: name: orders port: name: http

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.

08
Chapter Eight - Configuration and Organisation

ConfigMap, Secret, Namespaces, Labels and Annotations

ConfigMap: configuration separate from the image

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 asHowUpdates after a change
Environment variablesenv.valueFrom.configMapKeyRef or envFrom.configMapRefNot updated in a running container; needs a Pod restart
Files in a volumevolumes[].configMap mounted at a pathEventually 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).

Secret: sensitive values, with an important caveat

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.

⚠ Base64 is encoding, not encryption
  • Secret values are stored base64-encoded in the manifest, and anyone who can kubectl get secret -o yaml (or read etcd) can decode them with base64 -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 EncryptionConfiguration that lists providers. The documentation covers aescbc, secretbox, aesgcm and 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.
YAML - create a Secret without writing base64 yourself
apiVersion: v1 kind: Secret metadata: name: orders-db type: Opaque stringData: # stringData is write-only convenience: stored base64-encoded under data SPRING_DATASOURCE_USERNAME: orders SPRING_DATASOURCE_PASSWORD: change-me-example
Namespaces

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-resources shows a NAMESPACED column).
  • A default cluster starts with default, kube-system, kube-public and kube-node-lease. Do not deploy applications into default or kube-system.
Labels, selectors and annotations

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/nameName of the application, e.g. orders
app.kubernetes.io/instanceThe instance of it, e.g. orders-prod
app.kubernetes.io/versionApplication version
app.kubernetes.io/part-of, component, managed-byGrouping, role and tool (Helm, kustomize)
Shell - selecting by label
$ kubectl get pods -l app.kubernetes.io/name=orders $ kubectl get pods -l 'env in (prod,stage),tier!=cache' $ kubectl get pods --show-labels $ kubectl label pod orders-abc123 debug=true # add a label (this Pod only)
⚠ Changing a Deployment selector
  • spec.selector of 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 as version out of it, otherwise the next change forces you to delete and recreate the Deployment.
09
Chapter Nine - Beyond Stateless

StatefulSet, DaemonSet, Job, CronJob and Storage

StatefulSet: Pods with identity

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".

PropertyDeploymentStatefulSet
Pod namesRandom (web-6f9bc-th9cr)Ordinal and stable (cache-0, cache-1)
Network identityShared Service IPPer-Pod DNS name through a headless Service (cache-0.cache-headless)
StorageShared volume or nonevolumeClaimTemplates: one PVC per Pod that follows it across restarts
Start and stop orderAll at onceOrderedReady by default: cache-0, then cache-1, waiting for Ready (Parallel is optional); stopped in reverse
PVC cleanupn/aRetained 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.

DaemonSet, Job and CronJob
ObjectRunsNotes
DaemonSetOne Pod on every (or every selected) node; new nodes get one automaticallyLog collectors, node monitoring agents, CNI and storage drivers. Pods need tolerations to run on tainted nodes
JobPods until a number of them succeedcompletions, parallelism, backoffLimit (default 6), ttlSecondsAfterFinished to auto-clean. Pod restartPolicy must be OnFailure or Never
CronJobA Job on a cron scheduleconcurrencyPolicy (Allow, Forbid, Replace), startingDeadlineSeconds, timeZone (otherwise the controller manager's zone), history limits (successful default 3)
YAML - a nightly CronJob (batch/v1)
apiVersion: batch/v1 kind: CronJob metadata: name: nightly-report spec: schedule: "0 2 * * *" timeZone: "Etc/UTC" concurrencyPolicy: Forbid # skip a run if the previous one is still going jobTemplate: spec: backoffLimit: 2 ttlSecondsAfterFinished: 86400 template: spec: restartPolicy: OnFailure containers: - name: report image: registry.example.com/shop/report:1.0.0

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.

Storage: PersistentVolume, PersistentVolumeClaim, StorageClass

Container filesystems vanish with the container, so durable data needs a volume. Kubernetes separates "someone provides storage" from "an application needs storage":

ObjectRole
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
StorageClassA recipe for creating PVs on demand (dynamic provisioning): provisioner, parameters, reclaim policy, binding mode
Access modeMeaning
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 Delete removes the underlying storage when the PVC is deleted; Retain keeps 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.
YAML - a StatefulSet with a headless Service (apps/v1, server dry-run checked)
apiVersion: v1 kind: Service metadata: name: cache-headless spec: clusterIP: None selector: app: cache ports: - port: 6379 --- apiVersion: apps/v1 kind: StatefulSet metadata: name: cache spec: serviceName: cache-headless replicas: 2 selector: matchLabels: app: cache template: metadata: labels: app: cache spec: containers: - name: redis image: redis:7 volumeMounts: - name: data mountPath: /data volumeClaimTemplates: - metadata: name: data spec: accessModes: ["ReadWriteOnce"] resources: requests: storage: 1Gi
10
Chapter Ten - Hands-On

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).

YAML - app.yaml: ConfigMaps, Deployment and Service (validated: kubectl apply --dry-run=client and --dry-run=server on a 1.37 cluster)
apiVersion: v1 kind: ConfigMap metadata: name: orders-env labels: app.kubernetes.io/name: orders data: SPRING_PROFILES_ACTIVE: "prod" SPRING_CONFIG_ADDITIONAL_LOCATION: "file:/config/" --- apiVersion: v1 kind: ConfigMap metadata: name: orders-files labels: app.kubernetes.io/name: orders data: application.yaml: | orders: page-size: 50 management: endpoint: health: probes: enabled: true --- apiVersion: apps/v1 kind: Deployment metadata: name: orders labels: app.kubernetes.io/name: orders spec: replicas: 3 selector: matchLabels: app.kubernetes.io/name: orders strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 template: metadata: labels: app.kubernetes.io/name: orders spec: containers: - name: orders image: registry.example.com/shop/orders:1.4.2 ports: - name: http containerPort: 8080 envFrom: - configMapRef: name: orders-env volumeMounts: - name: config mountPath: /config readOnly: true resources: requests: cpu: 250m memory: 512Mi limits: memory: 512Mi volumes: - name: config configMap: name: orders-files --- apiVersion: v1 kind: Service metadata: name: orders labels: app.kubernetes.io/name: orders spec: type: ClusterIP selector: app.kubernetes.io/name: orders ports: - name: http port: 80 targetPort: http
  • apps/v1 for Deployment, v1 for ConfigMap and Service. The selector matchLabels and 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 mounted application.yaml in addition to the one in the jar. A ConfigMap key such as application.yaml is not a valid environment variable name, so it lives in a separate ConfigMap that is mounted as a file, not passed with envFrom.
  • The container port is named http; the Service uses targetPort: http, so the number lives in one place.
  • requests and limits are 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.
A local cluster with kind

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.

Shell - create a cluster, deploy, reach the Service (verified commands; output abridged)
$ kind create cluster --name lab $ kubectl cluster-info --context kind-lab $ kubectl get nodes NAME STATUS ROLES AGE VERSION lab-control-plane Ready control-plane 50s v1.37.0 # the example image does not exist, so use a public image for the live demo: $ kubectl create deployment web --image=nginx:1.27 --replicas=3 $ kubectl rollout status deployment/web $ kubectl expose deployment web --port=80 $ kubectl port-forward svc/web 8080:80 & $ curl -sI http://localhost:8080 | head -1 HTTP/1.1 200 OK $ kind delete cluster --name lab

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.

kubectl essentials
TaskCommandNotes
List thingskubectl 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 objectkubectl describe pod NAMEThe Events section at the bottom explains most scheduling, pulling and probe failures
Raw objectkubectl get deploy orders -o yamlShows spec and status; also -o json, -o jsonpath=..., -o custom-columns=...
Logskubectl logs POD -c CONTAINER, -f, --previous, -l app=orders --tail=100--previous is the log of the last crashed container, vital for CrashLoopBackOff
Shell insidekubectl exec -it POD -- shMinimal images may have no shell; see kubectl debug with an ephemeral container
Tunnelkubectl port-forward svc/orders 8080:80Local debugging only
Applykubectl apply -f file.yaml (or -f dir/, -k overlay/)Declarative create or update
Previewkubectl diff -f file.yamlShows what apply would change. Exit status 1 means differences were found, not an error
Validate onlykubectl apply --dry-run=server -f file.yamlclient only checks locally; server runs real admission and schema validation
Rolloutskubectl rollout status|history|undo|restart deployment/ordersrestart does a rolling restart (new Pods, same template)
Scalekubectl scale deploy orders --replicas=5Overwritten by the next apply if replicas is in your YAML
Learn the APIkubectl explain deployment.spec.strategy.rollingUpdateDocuments any field from the live server's schema; add --recursive for the tree
Who can do whatkubectl auth can-i delete pods --as=system:serviceaccount:ns:saQuick RBAC check (page 04)
Eventskubectl get events --sort-by=.lastTimestampCluster-wide timeline of what happened
Shell - kubectl explain, real output (abridged)
$ kubectl explain deployment.spec.strategy.rollingUpdate GROUP: apps KIND: Deployment VERSION: v1 FIELD: rollingUpdate <RollingUpdateDeployment> ... FIELDS: maxSurge <IntOrString> The maximum number of pods that can be scheduled above the desired number of pods. ... Absolute number is calculated from percentage by rounding up. Defaults to 25%. maxUnavailable <IntOrString> The maximum number of pods that can be unavailable during the update. ... Absolute number is calculated from percentage by rounding down. Defaults to 25%.
Habits that save hours

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)).

11
Chapter Eleven - Lifecycle

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 / conditionWhat is happeningWhat you see
PendingAccepted by the API but not yet running: waiting for the scheduler, for volumes, or for the image to pullkubectl get pods shows Pending / ContainerCreating; events explain why
ScheduledThe scheduler filtered nodes (resources, selectors, taints, affinity), scored the rest and set spec.nodeNameEvent Scheduled: Successfully assigned ... to node
Init containersRun one at a time, each to completion, before the app containers. A failing init container is retried and the Pod shows Init:Error / Init:CrashLoopBackOffInit:0/2 in STATUS
Image pull and startkubelet asks the runtime to pull (per imagePullPolicy), create and start containersEvents Pulled, Created, Started; failures show ErrImagePull, ImagePullBackOff
Running, not ReadyContainers run; the startup probe (if any) gates the other probes; the readiness probe decides ReadyREADY 0/1; not in Service endpoints
ReadyAll containers pass readiness; the Pod receives Service trafficREADY 1/1
TerminatingDeletion requested; see belowSTATUS Terminating until containers have exited
Succeeded / FailedTerminal phases for Pods that run to completion (Jobs)Normal for Job Pods
Pod lifecycle: from scheduling to termination
Startup Pending scheduling Init containers one by one Pull image create + start Running startup probe Ready readiness passes Pod is added to the EndpointSlice only once Ready (when all containers pass readiness). Termination (delete, rollout, node drain, scale-down) Pod marked Terminating removed from endpoints (in parallel, asynchronous) preStop hook runs SIGTERM to PID 1 of each container SIGKILL when grace expires One countdown for everything: terminationGracePeriodSeconds (default 30 s) starts when the Pod is marked Terminating, before the preStop hook runs. Endpoint removal and SIGTERM race each other: some clients still send requests after SIGTERM (page 04, graceful shutdown).
What happens when a Pod is deleted

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:

The API marks the Pod Terminating
▶
A single grace-period countdown starts, from terminationGracePeriodSeconds (default 30 seconds). The countdown covers the preStop hook and the shutdown that follows, not only the shutdown
Endpoint removal happens in parallel
▶
The EndpointSlice controller marks the Pod terminating and kube-proxy updates rules on every node. This is asynchronous and can lag, so a client may still send a request to the Pod after it has begun shutting down
The preStop hook runs
▶
An exec, HTTP or sleep action. It runs before the TERM signal. It is not called if the container crashes or exits by itself
SIGTERM is sent to the main process of each container
▶
The runtime sends the image's STOPSIGNAL if one is defined, otherwise SIGTERM (for containerd and CRI-O). Your process should stop accepting work, finish in-flight work and exit
SIGKILL at the end of the grace period
▶
Anything still running is killed. The docs note a small extra grace (2 seconds) if a preStop hook is still running when the period expires

A 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).

Shell - real log output of that experiment
started preStop running got SIGTERM exiting
⚠ PID 1 swallows SIGTERM
  • If your image starts the app through a shell script (CMD ./start.sh that launches java), 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. Use exec 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.
Restart behaviour and the CrashLoopBackOff state

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.

12
Chapter Twelve - Interview

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, compares spec (desired) with status (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 by maxSurge and maxUnavailable. Zero-downtime needs maxUnavailable: 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 after terminationGracePeriodSeconds (default 30 s) SIGKILL. The same countdown covers preStop and shutdown.
  • Q: A Pod is Pending. Where do you look?
    kubectl describe pod events: insufficient CPU or memory, unbound PVC, no node matching selector, taints or affinity. Detailed trees are on page 04.
13
Chapter Thirteen - Summary

Cheat Sheet and Recap

Cheat sheet
ConceptRememberDetail (Kubernetes 1.37)
Declarative modelspec is desired, status is observed; controllers reconcileStore manifests in Git; kubectl apply
API serverOnly front door and only etcd clientAll components watch it
PodSmallest unit; shared IP and volumes; disposableCreated by controllers, rarely by hand
DeploymentReplicaSet per template version; rolling update; rollbackmaxSurge 25% rounds up, maxUnavailable 25% rounds down; history 10
ServiceStable VIP + DNS over Ready Pods selected by labelsClusterIP, NodePort (30000-32767), LoadBalancer, headless
kube-proxyPrograms rules for Service VIPsiptables default in 1.37; ipvs deprecated since 1.35; nftables available
Ingress / GatewayLayer-7 entry points; both need a controllerIngress GA but frozen; Gateway API recommended; ingress-nginx retired March 2026
ConfigMap / SecretConfig out of the image; env vars do not refresh, mounted files doSecrets are base64, add encryption at rest + RBAC; 1 MiB limit
StatefulSetStable names, DNS, per-Pod PVC; orderedDoes not replicate data for you
Job / CronJobRun to completion on demand or schedulebackoffLimit 6; keep 3 successful Jobs; set timeZone
StoragePV (supply) / PVC (request) / StorageClass (recipe)RWO, ROX, RWX, RWOP; WaitForFirstConsumer
TerminationTerminating, preStop, SIGTERM, SIGKILL; one countdownterminationGracePeriodSeconds default 30
First debugging stepsdescribe (events), logs --previous, get eventsDetails on page 04
∑ Kubernetes core - what to remember
  • 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).
Version note

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.

Next in this section