Kubernetes Essentials
The core objects and how they relate, probes, requests and limits, rollouts, and an order for debugging a Pod that will not run.
Core objects and how they relate
Kubernetes stores objects that describe the state you want, and controllers work to make the cluster match it. Seven objects cover most of a small application.
| Object | What it is and how it relates |
|---|---|
| Pod | One or more containers with shared storage and network resources. Pods are generally not created directly: workload resources such as a Deployment create them. |
| ReplicaSet | Maintains a stable set of replica Pods. A Deployment manages ReplicaSets, so you rarely touch one. |
| Deployment | Declarative updates for Pods and ReplicaSets. The configuration of each revision is stored in its ReplicaSets. |
| Service | Exposes an application running as one or more Pods, chosen with a label selector. The default type, ClusterIP, is reachable only inside the cluster. |
| ConfigMap | Non-confidential key-value data. Pods can read it as environment variables, command-line arguments or files in a volume. |
| Secret | A small amount of sensitive data such as a password, token or key. Stored unencrypted in etcd by default. |
| Namespace | Isolates groups of resources within one cluster. Names must be unique inside a namespace, not across namespaces. Nodes and PersistentVolumes are not namespaced. |
Add -n shop to commands, or save the namespace in your context with kubectl config set-context --current --namespace=shop. A forgotten namespace shows an empty list.
Labels are the glue. A Deployment’s .spec.selector must match .spec.template.metadata.labels or the API rejects it, and the selector is immutable after creation in apps/v1. Do not create other Pods whose labels match its selector: Kubernetes does not stop you, and the Deployment then thinks it created them.
A compact manifest set
One file, five objects. The image name is a placeholder.
---
apiVersion: v1
kind: Namespace
metadata:
name: shop
---
apiVersion: v1
kind: ConfigMap
metadata:
name: web-config
namespace: shop
data:
LOG_LEVEL: info
---
apiVersion: v1
kind: Secret
metadata:
name: web-secret
namespace: shop
stringData:
DB_PASSWORD: change-me
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: shop
spec:
replicas: 3
selector:
matchLabels:
app: web
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
minReadySeconds: 10
progressDeadlineSeconds: 300
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: registry.example.com/shop/web:1.4.2
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: web-config
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: web-secret
key: DB_PASSWORD
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi
startupProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 5
failureThreshold: 30
livenessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 5
failureThreshold: 3
---
apiVersion: v1
kind: Service
metadata:
name: web
namespace: shop
spec:
selector:
app: web
ports:
- port: 80
targetPort: 8080- The image has a specific tag. The documentation advises avoiding
:latestin production because it is harder to track which version runs and to roll back. stringDatatakes plain strings, which are merged intodata. Never commit a real secret value to git. Base64 is obscured, not encrypted.- The Service listens on
port80 and forwards to the container’stargetPort8080.
Without a cluster, yamllint shop.yaml checks syntax and style only, and kubectl create deployment web --image=... --replicas=3 --dry-run=client -o yaml prints a starting manifest. Against a real cluster, kubectl apply --dry-run=server -f shop.yaml submits the request without persisting it, and the default --validate=strict checks the schema. Client-side kubectl apply --dry-run=client still contacts the API server, so it fails when no cluster is reachable.
Probes: startup, liveness, readiness
Probes are checks the kubelet runs against a container. Each answers a different question, so a failure has a different consequence.
| Probe | Question | When it fails |
|---|---|---|
startupProbe | Has the application finished starting? | Until it succeeds, liveness and readiness probes are not run. If it fails, the kubelet kills the container and the restart policy applies. |
livenessProbe | Should this container be restarted? | The kubelet restarts the container. It is meant to catch a deadlock, where the application runs but cannot make progress. |
readinessProbe | Can this container take traffic now? | The EndpointSlice controller removes the Pod’s IP address from the EndpointSlices of all matching Services, so it stops receiving Service traffic. Readiness probes run for the whole life of the container. |
The mechanisms are exec, grpc, httpGet and tcpSocket. Documented defaults: initialDelaySeconds 0, periodSeconds 10, timeoutSeconds 1, successThreshold 1 (it must be 1 for liveness and startup) and failureThreshold 3. Check the documentation for your version.
- Startup. The documentation suggests one when the container usually needs more than
initialDelaySeconds + failureThreshold × periodSecondsto start. In the manifest,periodSeconds: 5timesfailureThreshold: 30allows about 150 seconds. - Liveness. The documentation says to use it with caution: it must truly indicate unrecoverable failure such as a deadlock, and a wrong setup can cause cascading failures. Reasoning, not from the docs: a check that fails because a database is down cannot be fixed by restarting this container, so it is a poor liveness check.
- Readiness is not only for startup. The documentation notes it is also useful later, for example when recovering from temporary faults or overloads.
Requests versus limits
Requests and limits are set per container. Different components use them, and that is the whole difference.
| Field | Used by | Effect |
|---|---|---|
requests | kube-scheduler | The sum of the requests of the containers on a node must be less than the node’s capacity. A Pod whose requests fit nowhere stays Pending. |
limits.cpu | kubelet, runtime, kernel | Enforced by CPU throttling. |
limits.memory | kubelet, runtime, kernel | Enforced by out of memory (OOM) kills. The kernel may terminate a container that exceeds its limit, and the kubelet restarts it if it can be restarted. |
CPU is measured in CPU units: 0.1 equals 100m. Memory is measured in bytes, with suffixes such as M or the power-of-two Mi. If you set a limit but no request, and nothing applies a default request, Kubernetes uses the limit as the request. The manifest requests 100m CPU and 128Mi memory and limits memory to 256Mi. It sets no CPU limit, which is a policy choice.
- Choose numbers from measurement.
kubectl top podshows current usage but needs the metrics-server service. - The failures look different. Too-large requests give
Pendingwith aFailedSchedulingevent. Too-small memory limits give restarts and anOOMKilledreason.
Rollouts and rollbacks
A Deployment’s rollout is triggered if and only if its Pod template (.spec.template) changes, for example a new container image or new labels. Scaling does not trigger a rollout.
$ kubectl set image deployment/web web=registry.example.com/shop/web:1.4.3 -n shop
$ kubectl rollout status deployment/web -n shop
$ kubectl rollout history deployment/web -n shop
$ kubectl rollout history deployment/web -n shop --revision=2
# back to the previous revision, or to a specific one
$ kubectl rollout undo deployment/web -n shop
$ kubectl rollout undo deployment/web -n shop --to-revision=2kubectl rollout status exits with 0 when the rollout completed and non-zero when the Deployment exceeded its progress deadline, so it works as a CI gate. It also accepts --timeout and --watch=false.
| Field | Meaning | Default |
|---|---|---|
strategy.type | RollingUpdate or Recreate. | RollingUpdate |
rollingUpdate.maxUnavailable | Maximum number of Pods that can be unavailable during the update. A number or a percentage, rounded down. | 25% |
rollingUpdate.maxSurge | Maximum number of Pods that can be created over the desired number. A number or a percentage, rounded up. | 25% |
minReadySeconds | How long a new Pod must be ready, with no container crashing, before it counts as available. | 0 |
progressDeadlineSeconds | How long to wait for progress before Progressing becomes False with reason ProgressDeadlineExceeded. | 600 |
revisionHistoryLimit | Old ReplicaSets kept for rollback. | 10 |
Those are the documented defaults, so check the documentation for your version. maxUnavailable and maxSurge cannot both be 0. The manifest uses maxSurge: 1 and maxUnavailable: 0: with 3 replicas, at most 4 Pods exist during the update and the number of available Pods never drops below 3, at the price of room for one extra Pod. “Available” depends on readiness, so a missing readiness probe makes rollouts less safe.
Undo is limited by history. Each revision lives in an old ReplicaSet, and revisionHistoryLimit caps how many are kept. Advice, not from the docs: use undo as a fast mitigation, then fix the manifest in git, because applying the old bad manifest again brings the bad template back.
Debugging order
The Kubernetes documentation starts with the same first step: look at the Pod, then at its recent events. Follow one order and stop at the first clear answer.
kubectl get pods -n shopshows the status of every Pod. Add-wto watch changes.kubectl describe pod NAME -n shopshows the Pod’s current state and recent events, including the scheduler’s reasons. For the last termination reason and exit code, readlastState.terminatedunderstatus.containerStatusesinkubectl get pod NAME -n shop -o yaml.kubectl logs NAME -n shop, and--previousfor the previous container’s crash log. Add-c CONTAINERfor a multi-container Pod.kubectl events --for pod/NAME -n shoplists events for that Pod only. The docs also showkubectl get events.- Only then get inside.
kubectl exec -it NAME -n shop -- shworks when the image includes a shell.kubectl debug -it NAME -n shop --image=busybox:1.28 --target=CONTAINERadds an ephemeral debug container (stable since v1.25, per the docs).kubectl debug NAME -it --image=ubuntu --share-processes --copy-to=NAME-debugdebugs a copy, and--copy-to=NAME-debug --container=CONTAINER -- shreplaces the command of a crashing container in the copy.
| Status | What it means and where to look | Typical causes and fixes |
|---|---|---|
Pending | Not scheduled onto a node. Read the scheduler’s messages in the Events of describe. | Not enough CPU or memory in the cluster, or a hostPort. Adjust requests, add nodes or free capacity. Docs example: FailedScheduling with Insufficient memory. |
ImagePullBackOff | The image could not be pulled. Kubernetes retries with an increasing delay, capped at 300 seconds. Check the Events, image name and tag. | Wrong name or tag, image not pushed, or a private registry without an imagePullSecret, which must be in the Pod’s namespace and of type kubernetes.io/dockerconfigjson or kubernetes.io/dockercfg. Try pulling by hand. |
CrashLoopBackOff | The container keeps crashing and the kubelet restarts it with delays of 10s, 20s, 40s and so on, capped at 300s. The timer resets after the container runs successfully for 10 minutes. Read logs --previous and lastState.terminated. | Commonly an application error at start, bad config, insufficient resources or a failing probe. Fix the cause, because waiting does not. |
OOMKilled | Terminated for exceeding the memory limit. Shown as lastState.terminated.reason in get pod -o yaml. The docs example shows exit code 137 and the container alternating between OOMKilled and Running as the kubelet restarts it. | Raise the memory limit if the application really needs it, or fix the leak or the load. |
For a full walk-through of one case, see the CrashLoopBackOff tutorial in the tutorials section.
Cheat sheet and checklist
| Task | Command |
|---|---|
| Apply a file | kubectl apply -f shop.yaml |
| List Pods (and watch) | kubectl get pods -n shop (-w) |
| Pod state and events | kubectl describe pod NAME -n shop |
| Logs, current and crashed | kubectl logs NAME, kubectl logs NAME --previous, -c CONTAINER |
| Events for one Pod | kubectl events --for pod/NAME -n shop |
| Shell in a container | kubectl exec -it NAME -- sh |
| Debug container / copy | kubectl debug -it NAME --image=busybox:1.28 --target=CONTAINER, --copy-to=NAME-debug |
| Change the image | kubectl set image deployment/web web=IMAGE:TAG |
| Wait for a rollout | kubectl rollout status deployment/web |
| Revisions | kubectl rollout history deployment/web (--revision=N) |
| Roll back | kubectl rollout undo deployment/web (--to-revision=N) |
| Resource usage | kubectl top pod -n shop (needs metrics-server) |
Before you ship
- The image tag is pinned. No
:latest. - Every container has requests. Memory limits come from measurement.
- A readiness probe exists, slow starters have a startup probe, and liveness only reports failures a restart can fix.
- Secret values are not in git. Secrets are unencrypted in etcd by default, so restrict access and read the Secrets documentation.
- CI runs
kubectl rollout statusand treats a non-zero exit as a failed deploy.
Official documentation
Keep going
How this guide was checked: the manifest passed yamllint and every Deployment field survived kubectl’s decoding into its built-in types (kubectl client v1.37.0, no cluster). Flags and examples were compared with the built-in help of that client. Definitions, defaults and behaviours come from the kubernetes.io pages listed above, read in their source Markdown in the kubernetes/website repository. There is no cluster output in this guide because none was available, and messages and defaults can change between Kubernetes versions, so check the documentation for yours.
benmabrouk.fr: free DevOps and SRE learning resources, written from production experience.