Kubernetes » Operations
This page covers running workloads on a cluster once the fundamentals are in place: using kubectl effectively, packaging applications with Helm, the standard multi-container pod patterns, wiring up observability inside the cluster, troubleshooting by symptom, upgrading clusters safely, and a production readiness checklist. Commands and APIs are current for Kubernetes v1.35–v1.37 and Helm 4.
kubectl
kubectl is a client for the Kubernetes API. Every command ultimately issues REST calls to the API server, so anything it can do can also be done by CI systems, GitOps controllers or your own code.
Contexts and kubeconfig
kubectl reads its configuration from ~/.kube/config, or from the files listed in the KUBECONFIG environment variable (colon-separated; they are merged). A kubeconfig holds clusters (API endpoint and CA), users (credentials, often an exec plugin that fetches short-lived cloud tokens), and contexts that pair a cluster, a user and a default namespace.
kubectl config get-contexts # list; * marks the current one
kubectl config use-context prod-eu # switch cluster
kubectl config set-context --current --namespace=shop # change the default namespace
kubectl --context staging -n shop get pods # one-off override
kubectl auth whoami # which identity the API server sees
kubectl auth can-i create deployments -n shop # check an RBAC permission
Accidentally running a command against the wrong cluster is a classic incident. Mitigations: show the context in your shell prompt, give production contexts distinctive names, and use read-only credentials by default.
Reading State
kubectl get pods -o wide # node and IP columns
kubectl get deploy,sts,ds,svc -n shop # several kinds at once
kubectl get pods -w # stream changes
kubectl get pod web-0 -o yaml # full object, including status
kubectl describe pod web-0 # human summary plus recent events
kubectl events -n shop --for pod/web-0 # events for one object, oldest first
kubectl explain deployment.spec.strategy # built-in API reference
kubectl api-resources # every kind the cluster serves
Output can be shaped for scripts and quick reports:
# Names only
kubectl get pods -o name
# JSONPath: pod name and node, one per line
kubectl get pods -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.nodeName}{"\n"}{end}'
# Custom columns: image of the first container
kubectl get pods -o custom-columns=NAME:.metadata.name,IMAGE:.spec.containers[0].image
# Server-side filtering on fields and labels
kubectl get pods -A --field-selector=status.phase=Pending
kubectl get pods -l 'app.kubernetes.io/name in (web,api)' --sort-by=.status.startTime
Changing State
| Style | Commands | Use for |
|---|---|---|
| Declarative | apply -f, apply -k, diff, delete -f |
Anything that should be reproducible; the normal path |
| Imperative with a manifest | create -f, replace -f |
One-off objects; replace when you need a full overwrite |
| Imperative | scale, set image, patch, edit, label, annotate, rollout |
Emergencies and experiments; the change is not in version control |
kubectl diff -f manifests/ # preview changes against the live cluster
kubectl apply --server-side -f manifests/ # field-ownership-aware apply
kubectl apply -k overlays/prod # Kustomize, built into kubectl
kubectl patch deployment web --type=merge -p '{"spec":{"replicas":5}}'
kubectl rollout restart deployment/web # re-create pods (e.g. to pick up a new Secret)
Generate manifests instead of writing them from scratch with --dry-run=client -o yaml:
kubectl create deployment web --image=nginx:1.29 --dry-run=client -o yaml > web.yaml
kubectl create secret generic db --from-literal=password='s3cret' --dry-run=client -o yaml
kubectl create configmap app-config --from-file=config.yaml --dry-run=client -o yaml
kubectl get secret db -o jsonpath='{.data.password}' | base64 -d # read a Secret value
If a GitOps controller (Argo CD, Flux) manages the cluster, imperative changes are reverted at the next sync; change Git instead.
Debugging Commands
kubectl logs web-0 # current container
kubectl logs web-0 --previous # the container instance that just crashed
kubectl logs -f -l app.kubernetes.io/name=web --all-containers --since=10m --prefix
kubectl exec -it web-0 -c app -- sh # shell in a running container
kubectl port-forward svc/web 8080:80 # local access to a Service or pod
kubectl cp web-0:/tmp/heap.hprof ./heap.hprof
kubectl top pods --sort-by=memory # live usage (needs metrics-server)
kubectl top nodes
Many production images are distroless and contain no shell, so exec is useless. kubectl debug solves this by adding an ephemeral container — with whatever tools you choose — to the running pod, optionally sharing the process namespace of a target container:
# Attach a toolbox to a running pod, sharing the 'app' container's process namespace
kubectl debug -it web-0 --image=busybox:1.37 --target=app
# Copy a crashing pod with a different command so you can inspect it
kubectl debug web-0 -it --copy-to=web-0-debug --container=app -- sh
# Shell on a node: the node's root filesystem is mounted at /host
kubectl debug node/worker-3 -it --image=ubuntu:24.04 --profile=sysadmin
Node Maintenance
kubectl cordon worker-3 # stop scheduling new pods here
kubectl drain worker-3 --ignore-daemonsets --delete-emptydir-data --timeout=10m
# ... patch, reboot, replace ...
kubectl uncordon worker-3
drain cordons the node and then evicts its pods through the Eviction API, which honours PodDisruptionBudgets: if evicting a pod would violate its PDB, drain waits and retries. DaemonSet pods are left in place (they would be recreated immediately), and --delete-emptydir-data acknowledges that emptyDir contents will be lost.
Productivity Tooling
| Tool | Purpose |
|---|---|
Shell completion (kubectl completion bash, or zsh, fish) |
Completes resource names, not just subcommands |
| Krew | kubectl plugin manager (kubectl krew install <plugin>) |
ctx / ns plugins (kubectx, kubens) |
Fast context and namespace switching |
tree, neat, who-can plugins |
Ownership hierarchies, clean YAML output, reverse RBAC lookup |
| k9s | Terminal UI for browsing and operating a cluster |
| stern | Tail logs from many pods and containers at once, with colour per pod |
Helm
Helm packages a set of Kubernetes manifests as a versioned, parameterised chart. Installing a chart with a set of values produces a release; Helm records each release revision (as a Secret in the release’s namespace) so it can upgrade, diff and roll back as a unit.
flowchart LR
C["Chart<br/>templates/ + values.yaml"] --> R["Render<br/>Go templates"]
V["Your values<br/>-f prod.yaml, --set"] --> R
R --> M["Kubernetes manifests"]
M -->|"apply (server-side in Helm 4)"| API["kube-apiserver"]
M --> S[("Release record<br/>Secret sh.helm.release.v1.*")]
| Concept | Meaning |
|---|---|
| Chart | A directory or archive of templates, default values and metadata (Chart.yaml) |
| Values | Configuration merged over the chart’s values.yaml; later -f files and --set flags win |
| Release | One installed instance of a chart in a namespace, with numbered revisions |
| Repository | Where charts are published: an HTTP index or, increasingly, an OCI registry |
Helm 4
Helm 4.0 was released in November 2025, the first major version in six years. Most Helm 3 charts and releases work unchanged. Notable differences:
- Server-side apply is the default for new releases, so Helm participates in field ownership alongside controllers and other tools (releases created by Helm 3 keep client-side apply on upgrade).
- Resource readiness for
--waituses kstatus, the same status logic used by other tooling, giving more accurate “is it ready” answers for complex resources. - Plugins — including post-renderers and getters — run through a new plugin system with an optional WebAssembly runtime.
- Charts can be installed by OCI digest (
oci://registry.example.com/charts/app@sha256:...) for supply-chain pinning. - Flag renames:
--atomicbecomes--rollback-on-failure,--forcebecomes--force-replace(the old names still work with a deprecation warning).
Helm 3 received its final feature release in September 2026 and gets security fixes only until February 2027, so new work should target Helm 4.
Everyday Commands
# Install or upgrade in one idempotent command (the usual CI form)
helm upgrade --install web ./charts/web -n shop --create-namespace \
-f values/prod.yaml --set image.tag=1.8.2 --wait --rollback-on-failure
helm template web ./charts/web -f values/prod.yaml # render locally, apply nothing
helm lint ./charts/web
helm list -A # releases in all namespaces
helm history web -n shop # revisions
helm rollback web 3 -n shop # back to revision 3
helm get values web -n shop # values in effect
helm uninstall web -n shop
# Charts from an OCI registry — no 'helm repo add' needed
helm install cache oci://registry.example.com/charts/redis --version 20.1.0
# Charts from a classic HTTP repository
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm search repo prometheus-community/kube-prometheus-stack --versions
Always pin --version for third-party charts; an unpinned install silently picks up a new major version the next time CI runs. Chart sources also change hands — for example, in August 2025 Bitnami moved most of its free container images to an unmaintained “legacy” repository, leaving many widely used charts pointing at images that no longer receive updates — so keep an internal mirror of charts and images you depend on.
Chart Layout and Templates
charts/web/
├── Chart.yaml # name, version (chart), appVersion (application), dependencies
├── values.yaml # defaults, documented
├── values.schema.json # optional JSON Schema; validates user values
├── templates/
│ ├── _helpers.tpl # named templates (labels, names)
│ ├── deployment.yaml
│ ├── service.yaml
│ └── NOTES.txt # printed after install
└── charts/ # vendored dependencies
# templates/deployment.yaml (excerpt)
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "web.fullname" . }}
labels:
{{- include "web.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
template:
spec:
containers:
- name: web
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
resources:
{{- toYaml .Values.resources | nindent 10 }}
Helm Practices
| Practice | Why |
|---|---|
| Keep one values file per environment in version control | Reviewable, reproducible configuration |
Add a values.schema.json |
Typos in values fail fast instead of rendering silently wrong manifests |
Render and diff before upgrading (helm template, the helm-diff plugin) |
See exactly what will change |
| Pin chart versions and image tags (or digests) | Reproducible installs |
| Do not store secrets in values files | Use External Secrets, Sealed Secrets or SOPS instead |
| Let a GitOps controller run Helm in production | Continuous drift correction and an audit trail |
Helm or Kustomize? Helm templates and versions whole applications and is the standard way to distribute third-party software. Kustomize (built into kubectl apply -k) patches plain YAML with overlays and has no templating language or release state. Many teams use both: Helm for vendor charts, Kustomize overlays for their own services — and Argo CD and Flux support each natively.
Multi-Container Pod Patterns
A pod can hold several containers that share its network namespace and volumes. The patterns below are the standard reasons to use more than one.
| Pattern | Implemented as | Purpose | Examples |
|---|---|---|---|
| Init container | initContainers entry (runs to completion, in order) |
Setup that must finish before the app starts | Schema migration, fetching config, waiting for a dependency |
| Sidecar | initContainers entry with restartPolicy: Always |
Helper that runs for the pod’s whole life | Log shipper, service-mesh proxy, secrets refresher |
| Ambassador | Sidecar that proxies outbound traffic | Hide connection details from the app | Cloud SQL proxy, local API gateway |
| Adapter | Sidecar that translates output | Normalise interfaces | Exporting app stats as Prometheus metrics |
Native Sidecars
Before v1.28, sidecars were ordinary entries in containers, which caused two long-standing problems: there was no guarantee the sidecar started before the app (so a mesh proxy might not be ready for the app’s first request), and a sidecar kept a Job’s pod running forever after the main container finished. Native sidecars — init containers with restartPolicy: Always, stable since v1.33 — fix both.
sequenceDiagram
participant I as init: migrate
participant S as sidecar: log-shipper
participant A as container: app
Note over I,A: Pod start
I->>I: run to completion
S->>S: start (startupProbe passes)
A->>A: start
Note over S,A: both run, the sidecar restarts independently if it crashes
Note over I,A: Pod termination
A->>A: SIGTERM, exit
S->>S: SIGTERM after app has stopped
Init containers and sidecars start in the order listed; a sidecar counts as started once it is running and its startupProbe (if any) passes, and only then does the next entry start. On shutdown, sidecars are stopped after the main containers, in reverse order. In a Job, sidecars do not block completion.
spec:
initContainers:
- name: migrate # classic init container: runs once, must succeed
image: registry.example.com/app:1.8.2
command: ["./migrate", "--up"]
- name: log-shipper # native sidecar
image: fluent/fluent-bit:4.0
restartPolicy: Always
volumeMounts:
- {name: logs, mountPath: /var/log/app}
containers:
- name: app
image: registry.example.com/app:1.8.2
volumeMounts:
- {name: logs, mountPath: /var/log/app}
volumes:
- name: logs
emptyDir: {}
Where possible, prefer writing logs to stdout and letting a node-level agent collect them (below) over a per-pod log sidecar; sidecars cost CPU and memory in every replica.
Observability
Probes answer a binary question — restart or not, route or not (see Health & Resource Management). Operating a system means answering why: why latency rose, which release introduced errors, what a pod logged before it was evicted. That needs the three telemetry signals, correlated.
| Signal | Answers | Cost profile |
|---|---|---|
| Metrics | How much, how fast, how full? Rates, percentiles, saturation | Cheap per series; cost grows with label cardinality |
| Logs | What exactly happened in this request or pod? | Expensive at volume; one record per event |
| Traces | Where did the time go across services? | Sampled; high detail per request |
The platform-neutral concepts — metric types and PromQL, logging architectures, OpenTelemetry and SLOs — are covered in the Observability section. This section is the Kubernetes-specific wiring.
Metrics
Two different things are called “metrics” in Kubernetes:
flowchart LR
subgraph cluster["In-cluster"]
K["kubelet / cAdvisor"] -->|"container CPU & memory"| MS["metrics-server"]
MS -->|"metrics.k8s.io"| HPA["HPA, kubectl top"]
App["App /metrics"] -->|scrape| Prom["Prometheus"]
KSM["kube-state-metrics"] -->|"object state"| Prom
NE["node-exporter"] -->|"host metrics"| Prom
K -->|"cAdvisor, kubelet"| Prom
end
Prom -->|remote_write| LTS[("Long-term store<br/>Thanos / Mimir")]
Prom --> Graf["Grafana"]
Prom --> AM["Alertmanager"] --> Pager["PagerDuty / Slack"]
| Component | Role | Consumed by |
|---|---|---|
| metrics-server | In-memory, latest-value CPU and memory per pod and node; no history | HPA, VPA, kubectl top |
| kube-state-metrics | Converts API object state into series (desired vs available replicas, pod phase, restarts, Job status) | Prometheus alerts and dashboards |
| node-exporter | Host metrics: disk, filesystem, network, load, pressure stall information | Prometheus |
| Prometheus | Scrapes, stores and evaluates alert rules | Grafana, Alertmanager |
metrics-server is not a monitoring system. The usual way to install real monitoring is the kube-prometheus-stack chart, which bundles the Prometheus Operator, Prometheus, Alertmanager, Grafana, node-exporter, kube-state-metrics and a set of default dashboards and alerts:
helm upgrade --install monitoring prometheus-community/kube-prometheus-stack \
-n monitoring --create-namespace
The operator adds CRDs so scrape targets are declared next to the application rather than in a central config file:
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: web
labels:
release: monitoring # must match the Prometheus serviceMonitorSelector
spec:
selector:
matchLabels:
app.kubernetes.io/name: web
endpoints:
- port: metrics # named Service port serving /metrics
interval: 30s
Two checklists cover most instrumentation: RED for request-driven services (Rate, Errors, Duration) and USE for resources (Utilization, Saturation, Errors). Keep label values bounded — status code, route template, method — and never put user IDs, request IDs or raw URLs in metric labels; each unique label combination is a separate time series.
Logs
Container stdout and stderr are written by the runtime to files under /var/log/pods/ on the node and are rotated there. They disappear when the pod is deleted or the node is replaced — precisely when a post-mortem needs them. A node agent, run as a DaemonSet, tails those files, enriches each line with pod metadata and ships it off the node.
flowchart LR
subgraph node["Every node (DaemonSet)"]
F["/var/log/pods/*"] --> Ag["Fluent Bit / Grafana Alloy /<br/>Vector / OTel Collector"]
end
Ag -->|"+ namespace, pod, labels"| B{"Backend"}
B --> L[("Loki")]
B --> ES[("Elasticsearch /<br/>OpenSearch")]
B --> V[("Managed: CloudWatch,<br/>Cloud Logging, ...")]
| Stack | Agent | Store | Trade-off |
|---|---|---|---|
| Loki | Grafana Alloy, Fluent Bit or OTel Collector | Loki on object storage | Indexes only labels, so storage is cheap; queries scan log content |
| EFK / OpenSearch | Fluent Bit (Fluentd for heavy transformation) | Elasticsearch or OpenSearch | Full-text index; powerful queries, but storage- and memory-hungry |
| Vector | Vector agent and aggregator | Any | High-throughput, vendor-neutral routing and transformation layer |
Grafana’s Promtail reached end of life on 2 March 2026; Grafana Alloy is its replacement. Fluent Bit remains the most common lightweight agent. A minimal Fluent Bit pipeline to Loki:
[INPUT]
Name tail
Path /var/log/containers/*.log
multiline.parser cri
Tag kube.*
[FILTER]
Name kubernetes
Match kube.*
Merge_Log On
Keep_Log Off
[OUTPUT]
Name loki
Match kube.*
Host loki-gateway.monitoring.svc
Labels job=fluent-bit, $kubernetes['namespace_name']
Practices matter more than the choice of stack:
- Log structured JSON (
{"level":"error","msg":"payment failed","order_id":123}), not free text. - Propagate a trace or request ID and log it, so one request can be followed across services — and linked to its trace.
- Set retention per level and drop or sample high-volume noise at the agent; logs are usually the most expensive signal.
- Redact secrets and personal data at the agent, before anything reaches long-term storage.
Traces
A trace follows one request across services; each unit of work is a span with a start, duration and parent. Context propagation, normally the W3C traceparent header, carries the trace ID from hop to hop.
OpenTelemetry (OTel) is the standard for instrumentation and transport. Applications emit spans (and increasingly metrics and logs) over OTLP to an OpenTelemetry Collector, typically deployed as a gateway Deployment plus an optional per-node DaemonSet. The OpenTelemetry Operator can also inject auto-instrumentation into pods by annotation. Because the collector decouples instrumentation from storage, backends — Jaeger, Grafana Tempo, or a commercial service — can be changed without touching application code.
# OpenTelemetry Collector (contrib distribution): keep errors and slow traces, sample the rest
receivers:
otlp:
protocols:
grpc: {endpoint: 0.0.0.0:4317}
http: {endpoint: 0.0.0.0:4318}
processors:
tail_sampling:
decision_wait: 10s
policies:
- {name: errors, type: status_code, status_code: {status_codes: [ERROR]}}
- {name: slow, type: latency, latency: {threshold_ms: 500}}
- {name: sample, type: probabilistic, probabilistic: {sampling_percentage: 5}}
batch: {}
exporters:
otlp/tempo:
endpoint: tempo.monitoring.svc:4317
tls: {insecure: true}
service:
pipelines:
traces:
receivers: [otlp]
processors: [tail_sampling, batch]
exporters: [otlp/tempo]
Head sampling decides at the first span and is cheap but can discard the rare failing request; tail sampling (above) buffers whole traces and keeps the interesting ones, at the cost of collector memory and the requirement that all spans of a trace reach the same collector instance.
Alerting
Prometheus evaluates rules and fires alerts; Alertmanager deduplicates, groups, silences and routes them. With the operator, rules are PrometheusRule objects:
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: web-slo
labels:
release: monitoring
spec:
groups:
- name: web.availability
rules:
- alert: WebHighErrorRate
expr: |
sum(rate(http_requests_total{job="web",code=~"5.."}[5m]))
/ sum(rate(http_requests_total{job="web"}[5m])) > 0.05
for: 10m
labels:
severity: page
annotations:
summary: "web 5xx ratio above 5% ({{ $value | humanizePercentage }})"
- alert: PodCrashLooping
expr: increase(kube_pod_container_status_restarts_total[15m]) > 3
labels:
severity: ticket
Page on symptoms users feel — error rate, latency, SLO error-budget burn — and send cause-level signals (CPU, restarts, disk) to tickets or dashboards. Burn-rate alerting against an SLO is described in Observability.
Troubleshooting
Work from the outside in: find what is unhealthy, read what Kubernetes already knows about it (status, conditions, events), then look inside the container.
kubectl get pods -A | grep -Ev 'Running|Completed' # anything not obviously fine
kubectl get pods -A --field-selector=status.phase=Pending
kubectl events -A --types=Warning # recent warnings, cluster-wide
kubectl describe pod <pod> # conditions, last state, events
kubectl logs <pod> --previous # output of the crashed instance
flowchart TD
Start(["Pod not working"]) --> Ph{"STATUS column"}
Ph -->|"Pending"| P1{"Scheduled?<br/>(describe: events)"}
P1 -->|"FailedScheduling"| P2["Insufficient resources,<br/>taints, affinity, spread,<br/>unbound PVC"]
P1 -->|"scheduled"| P3["Pulling images or<br/>running init containers"]
Ph -->|"ImagePullBackOff /<br/>ErrImagePull"| I1["Wrong name or tag,<br/>private registry without<br/>imagePullSecrets, rate limit"]
Ph -->|"CreateContainerConfigError"| C1["Missing ConfigMap / Secret<br/>or key"]
Ph -->|"CrashLoopBackOff"| CL{"Last state<br/>(describe)"}
CL -->|"OOMKilled, 137"| O1["Memory limit too low<br/>or leak"]
CL -->|"Error, exit 1"| O2["App error: logs --previous"]
CL -->|"killed by liveness"| O3["Probe too aggressive<br/>or app wedged"]
Ph -->|"Running, not Ready"| R1["Readiness probe failing:<br/>describe events, app logs"]
Ph -->|"Running + Ready,<br/>no traffic"| S1["Service selector, ports,<br/>NetworkPolicy, DNS"]
Ph -->|"Terminating forever"| T1["Finalizer or<br/>unreachable node"]
Symptom Reference
| Status / symptom | First look | Usual causes | Fix |
|---|---|---|---|
Pending + FailedScheduling |
describe pod events |
Requests exceed free allocatable; untolerated taint; unsatisfiable affinity or spread; PVC unbound or in the wrong zone | Adjust requests or constraints; add nodes; fix StorageClass (WaitForFirstConsumer) |
ImagePullBackOff / ErrImagePull |
describe pod events |
Typo in image or tag; missing imagePullSecrets; registry rate limit; architecture mismatch (exec format error appears later, as a crash) |
Correct reference; create pull secret; mirror images |
CreateContainerConfigError |
describe pod |
Referenced ConfigMap, Secret or key does not exist | Create it or mark the reference optional: true |
CrashLoopBackOff |
logs --previous, last state and exit code |
Application error, bad config, OOM, failing liveness probe | Depends on exit code (below) |
Running but 0/1 Ready |
describe pod (readiness failures) |
App not listening on the probed port or path; dependency down | Fix probe or app; confirm with port-forward |
Evicted |
describe pod, node conditions |
Node memory or disk pressure | Set accurate requests; clean up node disk; see eviction |
Stuck Terminating |
get pod -o yaml (finalizers, node) |
Finalizer whose controller is gone; node unreachable | Fix the controller; as a last resort remove the finalizer or delete --force --grace-period=0 |
Node NotReady |
describe node, kubelet logs (journalctl -u kubelet) |
kubelet or runtime down, disk full, network partition, expired certificates | Repair or replace the node |
Container Exit Codes
| Exit code | Meaning | Typical cause |
|---|---|---|
| 0 | Success | A long-running container that exits 0 still gets restarted under restartPolicy: Always |
| 1, 2 | Application error | Check logs |
| 126 / 127 | Command not executable / not found | Wrong command, missing binary in a slim image |
| 137 | Killed by SIGKILL (128 + 9) | OOMKilled, or liveness failure after the grace period |
| 139 | Segmentation fault (128 + 11) | Native crash; architecture or library mismatch |
| 143 | Terminated by SIGTERM (128 + 15) | Normal shutdown, or liveness restart handled gracefully |
Service Connectivity
When pods are healthy but a Service does not answer, test each hop:
# 1. Does the Service select any Ready pods?
kubectl get endpointslices -l kubernetes.io/service-name=web -o wide
kubectl get pods -l app.kubernetes.io/name=web --show-labels
# 2. Does targetPort match the container's listening port?
kubectl get svc web -o jsonpath='{.spec.ports}'
# 3. Does DNS resolve and the port answer from inside the cluster?
kubectl run nettest --rm -it --restart=Never --image=busybox:1.37 -- \
sh -c 'nslookup web.shop.svc.cluster.local && wget -qO- -T 3 http://web.shop:80/'
# 4. Is a NetworkPolicy blocking it?
kubectl get networkpolicy -n shop
Empty EndpointSlices mean the selector matches nothing or no matching pod is Ready. DNS failures point to CoreDNS (kubectl -n kube-system logs -l k8s-app=kube-dns). Timeouts with correct endpoints usually mean a NetworkPolicy or a pod listening on 127.0.0.1 instead of 0.0.0.0.
Cluster Upgrades
Kubernetes ships three minor releases a year, and each is supported with patches for about a year (v1.35, v1.36 and v1.37 as of September 2026). Clusters therefore need a minor upgrade roughly every four months to stay supported; managed services add “extended support” at extra cost for those that fall behind.
The version skew policy constrains the order:
| Component | Allowed relative to kube-apiserver |
|---|---|
| kube-apiserver instances (HA) | Within one minor version of each other during an upgrade |
| kube-controller-manager, kube-scheduler | Same minor or one older |
| kubelet, kube-proxy | Same minor or up to three older; never newer |
| kubectl | One minor older, same, or one newer |
flowchart LR
A["Check release notes and<br/>deprecated API usage"] --> B["Upgrade control plane<br/>one minor at a time"]
B --> C["Upgrade add-ons<br/>(CNI, CSI, CoreDNS, ingress)"]
C --> D["Roll node pools:<br/>surge new nodes, drain old"]
D --> E["Verify workloads,<br/>then repeat for next minor"]
- Find removed APIs before they bite. Each release may stop serving deprecated API versions. The API server exposes
apiserver_requested_deprecated_apismetrics and returns warnings to clients; tools such asplutoandkubentscan manifests and Helm releases. - Upgrade one minor version at a time for the control plane; nodes may lag, within the skew limits above.
- Replace rather than patch nodes where possible: create nodes on the new version and drain the old ones.
- Protect availability with PodDisruptionBudgets, which
drainand node autoscalers respect:
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: web
spec:
minAvailable: 2 # or maxUnavailable: 1
selector:
matchLabels:
app.kubernetes.io/name: web
unhealthyPodEvictionPolicy: AlwaysAllow # don't let already-broken pods block drains
A PDB that can never be satisfied — minAvailable equal to the replica count, or a single-replica Deployment with maxUnavailable: 0 — blocks every drain and stalls upgrades.
Notable changes to check when upgrading into the current releases include: the Endpoints API deprecation (v1.33) in favour of EndpointSlices; the retirement of the community ingress-nginx controller, which received no further releases or security fixes after March 2026 (migrate to a Gateway API implementation or another controller; ingress2gateway converts manifests); and the deprecation of cgroup v1 in v1.35, from which release the kubelet refuses to start on a cgroup v1 host unless failCgroupV1: false is set — node images must use cgroup v2.
Production Readiness Checklist
| Area | Practice | Why |
|---|---|---|
| Resources | Requests on every container; memory limits; LimitRange defaults and ResourceQuotas per namespace | Predictable scheduling and eviction; no noisy neighbours |
| Right-size from real usage (VPA in recommendation mode, dashboards) | Over-requesting wastes money; under-requesting causes evictions | |
| Availability | ≥ 2–3 replicas, spread across zones with topology spread constraints | Survive node and zone loss |
| PodDisruptionBudgets on every replicated service | Safe drains and upgrades | |
| Readiness probes on everything that takes traffic; startup probes for slow starters | Zero-downtime rollouts | |
Graceful shutdown: handle SIGTERM, preStop delay where needed |
No dropped requests during rollouts | |
| Delivery | Images pinned by tag and digest; manifests in Git, applied by GitOps or CI | Reproducible, auditable changes |
| Progressive delivery (canary or blue/green) for critical services | Limit blast radius of bad releases | |
| Security | RBAC least privilege; no cluster-admin for workloads; short-lived credentials | Contain compromise |
Pod Security Standards restricted where possible; non-root; read-only root filesystem; drop capabilities |
Reduce container-escape impact | |
| Default-deny NetworkPolicies per namespace | Prevent lateral movement | |
| Image scanning and signature verification (e.g. Sigstore cosign with an admission policy) | Supply-chain integrity | |
| Secrets from an external manager; encryption at rest for etcd | Limit secret exposure | |
| Observability | Metrics, centralized logs and traces; dashboards per service | Diagnose after the fact |
| SLO-based alerts routed to an on-call rotation | Actionable paging | |
| Recovery | etcd snapshots (self-managed) and application-level backups (e.g. Velero); tested restores | A backup that has never been restored is a hope |
| Documented upgrade cadence within the supported window | Stay on patched versions |
Security controls are expanded in Workloads & Storage and backups in Stateful Workloads & Persistence.
Certifications
The CNCF and Linux Foundation offer five Kubernetes certifications, all taken online with a remote proctor:
| Certification | Format | Focus | Prerequisite |
|---|---|---|---|
| KCNA — Kubernetes and Cloud Native Associate | Multiple choice | Concepts and the cloud-native ecosystem | None |
| KCSA — Kubernetes and Cloud Native Security Associate | Multiple choice | Security concepts and threat model | None |
| CKAD — Certified Kubernetes Application Developer | Hands-on, live clusters | Building, configuring and exposing applications | None |
| CKA — Certified Kubernetes Administrator | Hands-on, live clusters | Installing, operating and troubleshooting clusters | None |
| CKS — Certified Kubernetes Security Specialist | Hands-on, live clusters | Hardening clusters, supply chain, runtime security | An active CKA |
Holding all five at once earns the “Kubestronaut” title. The performance-based exams reward speed with kubectl: imperative generators (--dry-run=client -o yaml), kubectl explain, and the official documentation (which is available during the exam).
See Also
- Fundamentals — architecture, Pods, Deployments, Services
- Health & Resource Management — probes, requests and limits, eviction, autoscaling
- Workloads & Storage — StatefulSets, DaemonSets, Jobs, RBAC and Pod Security
- Advanced Topics — CRDs and Operators, service mesh, GitOps
- Observability — metrics, logging and tracing in depth; SLOs
- Docker Essentials — container command reference
- CI/CD — automating delivery into the cluster