Before We Begin
Kubernetes can feel overwhelming at first, but if you understand the following concepts you will have a smooth journey. Don't worry — we explain each one briefly below so you don't have to leave this page.
Basic Linux
Comfort with the terminal: ls, cd, cat, file permissions.
Docker / Containers
What an image is, how docker run works, difference between an image and a container.
Basic Networking
IP addresses, ports, DNS, HTTP/S request-response cycle.
YAML
Indentation-sensitive data format. All Kubernetes configs are written in YAML.
Quick YAML Primer
YAML stands for "YAML Ain't Markup Language". It uses indentation to show structure, like Python does for code.
# Lines starting with '#' are comments — ignored by Kubernetes.
name: my-app # string value — no quotes needed unless it contains special chars
version: "1.0" # quoted because it looks like a number; quotes keep it a string
replicas: 3 # integer value — no quotes
labels: # this is a "mapping" (key-value pairs), indented 2 spaces
env: production # key: value (child of 'labels')
team: backend
ports: # this is a "sequence" (list), items start with a dash
- 8080 # first item in the list
- 9090 # second item
What is a Container?
Think of a container like a shipping container on a cargo ship. It packages your application and all its dependencies together in a standardized box that runs the same way on any machine.
🧠 Check Your Understanding — Prerequisites
See Answer & Explanation
Containers share the host OS kernel, which means they don't need to boot a full OS. This makes them much lighter (MBs vs GBs) and faster to start (seconds vs minutes) compared to Virtual Machines.
See Answer & Explanation
Kubernetes accepts both YAML and JSON, but YAML is the community standard because it is more human-readable. All official Kubernetes documentation shows YAML examples.
| symbol mean in a YAML value?See Answer & Explanation
The
| (pipe) character starts a literal block scalar in YAML. Each subsequent indented line becomes part of the string with its newline preserved. This is commonly used in Kubernetes for multi-line config files stored in a ConfigMap.
See Answer & Explanation
A Docker image is like a class in OOP — a static, read-only template. A container is like an instance of that class — a live, running process with its own writable layer on top. You can create many containers from the same image.
See Answer & Explanation
YAML supports two list (sequence) styles: inline flow style
[red, green, blue] or block style using a dash (- red) on separate lines. Both are valid. Kubernetes manifests typically use block style for readability.
What is Kubernetes?
Imagine you run a pizza restaurant. You have 5 chefs making pizzas. If one chef calls in sick, you have to assign their work to the other 4. If it's Friday night and orders spike, you hire more temporary staff. If one oven breaks, you route orders to working ovens. You want all of this to happen automatically without you micromanaging every detail.
Kubernetes does exactly this — but for your containerized applications.
Why do we need it?
Modern applications are made of many small services (microservices), each running in its own container. Managing hundreds of containers manually across multiple servers is painful. Kubernetes automates:
- Scheduling: "Which server should this container run on?"
- Self-healing: "A container crashed — restart it automatically."
- Scaling: "Traffic doubled — spin up more containers."
- Load balancing: "Spread incoming requests across all healthy containers."
- Rolling updates: "Deploy new version without downtime."
- Configuration management: "Inject environment variables and secrets safely."
The Evolution: Bare Metal → VMs → Containers → Kubernetes
Key Kubernetes Terms at a Glance
| Term | Analogy | What it really is |
|---|---|---|
| Cluster | A factory building | A set of machines (nodes) running Kubernetes together |
| Node | A workstation inside the factory | A single machine (VM or physical) in the cluster |
| Pod | A single work desk | The smallest deployable unit; wraps one or more containers |
| Deployment | The HR department managing workers | Declares desired state; ensures Pods are running and updated |
| Service | A reception desk with a stable phone number | Stable network endpoint to reach a group of Pods |
🧠 Check Your Understanding — Introduction
See Answer & Explanation
"K8s" is a numeronym: K + the 8 middle letters (ubernete) + s = Kubernetes. This shorthand is widely used in the cloud-native community.
See Answer & Explanation
Kubernetes manages infrastructure concerns (scheduling, healing, scaling, networking). It has no role in authoring your application code — that's still the developer's job.
See Answer & Explanation
A cluster is the entire Kubernetes environment — it consists of a control plane (master) and one or more worker nodes that collectively run your workloads.
See Answer & Explanation
Google originally created Kubernetes and open-sourced it in 2014, but it was donated to the CNCF in 2016. The CNCF (a sub-foundation of the Linux Foundation) now governs the project with contributions from hundreds of companies.
See Answer & Explanation
Orchestration goes beyond running a single container — it handles scheduling across a fleet of machines, ensuring availability, scaling based on load, service discovery, rolling updates, and more. Kubernetes is the de-facto standard orchestrator.
See Answer & Explanation
Microservices allow independent lifecycle management. If your payment service needs more capacity, you scale just that Deployment — not the entire application. Combined with rolling updates, you can deploy new versions of individual services with zero downtime.
Kubernetes Architecture
A Kubernetes cluster has two main parts: the Control Plane (the brain) and Worker Nodes (the muscles). Think of it as a company: the control plane is the management team and worker nodes are the employees doing the actual work.
Control Plane Components
API Server (kube-apiserver)
The single entry point for all control-plane communications. When you run kubectl apply, you're talking to the API server. It validates requests and updates the state in etcd.
etcd
A distributed key-value store that acts as Kubernetes' "database of truth". Every object's desired and current state is persisted here. If etcd goes down, the cluster cannot function correctly.
Scheduler (kube-scheduler)
When a new Pod needs to run, the scheduler looks at the available nodes, considers CPU/memory constraints, affinities, and taints, then assigns the Pod to the best node.
Controller Manager (kube-controller-manager)
Runs many control loops in background. Each controller watches the cluster state and works to make the actual state match the desired state. Example: the ReplicaSet controller ensures the right number of Pod copies are running.
Worker Node Components
kubelet
An agent running on each worker node. It receives Pod specifications from the API server and ensures the containers described in those specs are actually running and healthy.
kube-proxy
Maintains network rules on each node that allow network communication to and from Pods. It implements the Services concept at the network level.
Container Runtime
The software that actually runs containers (e.g., containerd, CRI-O). Docker was the classic choice but modern clusters use containerd directly.
kubectl command ultimately reads from or writes to etcd through the API server.🧠 Check Your Understanding — Architecture
See Answer & Explanation
The kube-scheduler watches for unscheduled Pods and selects the best node for them based on resource availability, affinity rules, and other constraints.
See Answer & Explanation
etcd is the source of truth. It stores the desired state (what you declared) and the current state. Controllers continuously compare these two and reconcile differences.
See Answer & Explanation
The kubelet is a node-level agent. Every worker node has a kubelet that receives instructions from the API server and manages the lifecycle of Pods and containers on that specific node.
kubectl apply -f deployment.yaml, which component is contacted first?See Answer & Explanation
Every interaction with the cluster goes through the API Server — it is the single front door. It authenticates the request, validates the YAML, and then persists the desired state to etcd. The scheduler and controllers react asynchronously afterwards.
See Answer & Explanation
etcd outage means the API server cannot read or write cluster state. Already-running Pods continue as-is (kubelet still manages them locally), but you cannot create new Pods, scale, deploy, or make any changes until etcd recovers. This is why etcd is run as a clustered HA setup in production.
See Answer & Explanation
The Controller Manager runs control loops (controllers) for each resource type — ReplicaSet controller, Deployment controller, Node controller, etc. Each controller watches the current state in etcd and takes action to match the desired state. This reconciliation loop is the core of Kubernetes' self-healing.
Pods — The Smallest Unit
A Pod is the smallest and most basic deployable object in Kubernetes. Think of a Pod as a wrapper around one or more tightly-coupled containers. While most Pods contain just one container, they can contain multiple containers that need to share storage or network.
localhost.Creating Your First Pod
You can create a Pod using a YAML manifest file (recommended) or the kubectl run command.
# pod.yaml
apiVersion: v1 # Which Kubernetes API group/version this object uses.
# Pods belong to the core "v1" API group.
kind: Pod # The type of Kubernetes object to create.
metadata:
name: my-first-pod # Unique name for this Pod within the namespace.
labels: # Key-value tags. Used by Services/ReplicaSets to
app: my-app # find and group related Pods (via label selectors).
spec: # "spec" describes the DESIRED STATE of the Pod.
containers: # A Pod can hold one or more containers.
- name: nginx-container # Arbitrary name — used in logs and exec commands.
image: nginx:1.24 # Docker image to run. Format: image:tag
# Always pin a specific tag (not "latest") in production.
ports:
- containerPort: 80 # Documents which port the container listens on.
# This is informational only — it does NOT publish the port.
# A Service is needed to actually route traffic here.
# kubectl apply -f <file>
# Reads the YAML file and sends it to the API server.
# Kubernetes creates or updates the described object.
# -f means "from file" (can also be a URL or a directory).
kubectl apply -f pod.yaml
# kubectl get pods
# Lists all Pods in the current namespace.
# Columns: NAME, READY, STATUS, RESTARTS, AGE
# Add -n <namespace> to check a specific namespace,
# or --all-namespaces (-A) to see Pods everywhere.
kubectl get pods
# kubectl describe pod <name>
# Shows the full event log and spec for a Pod.
# Very useful for debugging — look at the "Events:"
# section at the bottom when a Pod won't start.
kubectl describe pod my-first-pod
# kubectl logs <pod-name>
# Prints stdout/stderr from the container.
# Add -f to stream logs in real time (like tail -f).
# Add --previous to see logs of a crashed container.
kubectl logs my-first-pod
# kubectl exec -it <pod-name> -- <command>
# Opens an interactive shell inside a running container.
# -i = keep stdin open | -t = allocate a TTY (terminal)
# -- separates kubectl flags from the command to run.
# Replace /bin/bash with /bin/sh for Alpine-based images.
kubectl exec -it my-first-pod -- /bin/bash
# kubectl delete pod <name>
# Removes the Pod. Kubernetes sends SIGTERM to the container,
# waits for graceful shutdown (30s by default), then force-kills.
# Note: if managed by a Deployment, a new Pod is created immediately.
kubectl delete pod my-first-pod
Pod Lifecycle Phases
| Phase | Meaning |
|---|---|
Pending | Pod accepted by cluster but containers not yet running (may be pulling image) |
Running | At least one container is running, starting, or restarting |
Succeeded | All containers terminated successfully (exit code 0) |
Failed | All containers terminated, at least one with non-zero exit code |
Unknown | Pod state cannot be determined (usually network issue with node) |
🧠 Check Your Understanding — Pods
See Answer & Explanation
Containers in the same Pod share the same network namespace, including IP address and port space. They communicate via localhost, just like two processes on the same machine.
See Answer & Explanation
Pods are ephemeral. Without a controller (Deployment, ReplicaSet), a dead Pod is just gone. Controllers provide self-healing by ensuring a desired number of Pod replicas are always running.
See Answer & Explanation
The sidecar pattern places a helper container alongside the main application container in the same Pod. Common examples: a Fluentd container that collects and ships the app's logs, an Envoy proxy that handles service mesh traffic, or a git-sync container that keeps a config volume up to date.
kubectl get pods and see STATUS: CrashLoopBackOff. What does this mean?See Answer & Explanation
CrashLoopBackOff means the container starts, crashes (exits with non-zero), and Kubernetes restarts it. After each crash the backoff delay doubles (10s → 20s → 40s … up to 5 minutes). Debug with
kubectl logs <pod> --previous to see logs from the crashed container.
See Answer & Explanation
The Succeeded phase means every container in the Pod ran to completion and exited with code 0. This is the expected final state for Job-created Pods. Long-running service Pods (web servers, APIs) should always stay in Running state.
kubectl describe pod my-pod show that kubectl get pod my-pod does not?See Answer & Explanation
kubectl describe is the go-to debugging command. It shows the full spec, current status, container states, resource requests/limits, mounted volumes, env vars, and — most importantly — the Events section at the bottom, which explains exactly why a Pod might be stuck or failing.
ReplicaSets — Ensuring High Availability
A ReplicaSet ensures that a specified number of identical Pod replicas are running at all times. If a Pod crashes or is deleted, the ReplicaSet automatically creates a new one. Think of it as the "safety net" that maintains your desired number of running Pods.
# replicaset.yaml
apiVersion: apps/v1 # ReplicaSets are in the "apps" API group, version v1.
kind: ReplicaSet
metadata:
name: nginx-rs
spec:
replicas: 3 # The desired number of identical Pod copies.
# Controller reconciles: if 2 are running → creates 1 more.
# if 4 are running → deletes 1.
selector: # How the ReplicaSet identifies which Pods it "owns".
matchLabels:
app: nginx # It will manage any Pod that has the label app=nginx.
# ⚠️ selector must match the template's labels exactly.
template: # Blueprint for the Pods this ReplicaSet will create.
metadata:
labels:
app: nginx # Every Pod created from this template gets app=nginx.
# This must match the selector above.
spec:
containers:
- name: nginx
image: nginx:1.24
ports:
- containerPort: 80
🧠 Check Your Understanding — ReplicaSets
replicas: 5 in a ReplicaSet and manually delete 2 Pods, what happens?See Answer & Explanation
The ReplicaSet controller constantly watches the cluster. When it detects fewer Pods than desired, it creates new ones. This is the core self-healing mechanism of Kubernetes.
See Answer & Explanation
Labels are key-value pairs attached to Kubernetes objects. The
selector in a ReplicaSet defines which labels to look for when counting and managing Pods.
replicas: 3. You manually create a 4th Pod with the same labels. What happens?See Answer & Explanation
The ReplicaSet controller only cares about the total count of Pods matching its selector. If it detects 4 when it wants 3, it deletes one — regardless of which Pod created it. This is why you should never create Pods with labels that accidentally match an existing ReplicaSet's selector.
See Answer & Explanation
By default, deleting a ReplicaSet cascades — all Pods it owns are garbage-collected. You can use
--cascade=orphan to leave the Pods running without an owner, but this is rarely desirable in practice.
kubectl scale replicaset nginx-rs --replicas=0 effectively do?See Answer & Explanation
Scaling to 0 replicas terminates all Pods but keeps the ReplicaSet object in the cluster. This is a common pattern for temporarily stopping a workload without losing its configuration — you can scale back up to restart it at any time.
See Answer & Explanation
A Deployment wraps a ReplicaSet and adds critical production features: declarative rolling updates (swap old RS for new RS), rollback history via revision tracking, and the ability to pause a rollout mid-way for canary-style testing. ReplicaSet alone has none of these.
Deployments — The Workhorse
A Deployment is the recommended way to run stateless applications on Kubernetes. It manages a ReplicaSet for you and adds powerful features on top: rolling updates, rollbacks, and scaling. This is the object you will use most frequently in day-to-day Kubernetes work.
# deployment.yaml
apiVersion: apps/v1
kind: Deployment # Manages a ReplicaSet, which in turn manages Pods.
metadata:
name: my-app
spec:
replicas: 3 # Keep 3 Pods running at all times.
selector:
matchLabels:
app: my-app # The Deployment owns Pods with this label.
strategy:
type: RollingUpdate # Default strategy. Alternative: Recreate
# (Recreate kills all old Pods first — causes downtime).
rollingUpdate:
maxSurge: 1 # During an update, allow UP TO 1 extra Pod above replicas.
# So temporarily up to 4 Pods can run (3 + 1 surge).
maxUnavailable: 0 # During an update, NEVER let available Pods drop below 3.
# Combined with maxSurge:1 this gives zero-downtime updates.
template: # Pod blueprint — same as ReplicaSet template.
metadata:
labels:
app: my-app # Must match spec.selector.matchLabels above.
spec:
containers:
- name: app
image: my-app:1.0 # Change this value and apply → triggers rolling update.
ports:
- containerPort: 3000
Essential kubectl Commands
# kubectl apply -f <file>
# Creates or updates the Deployment described in the YAML.
# If the Deployment already exists, Kubernetes calculates
# the diff and applies only what changed (e.g., a new image).
kubectl apply -f deployment.yaml
# kubectl scale deployment <name> --replicas=<n>
# Imperatively adjusts the replica count without editing YAML.
# Kubernetes adds or removes Pods to match the new target.
# For permanent changes, update replicas: in your YAML instead.
kubectl scale deployment my-app --replicas=5
# kubectl set image deployment/<name> <container>=<image:tag>
# Patches the container image, which triggers a rolling update.
# "app" is the container name defined in spec.containers[].name.
# Kubernetes will progressively replace old Pods with new ones.
kubectl set image deployment/my-app app=my-app:2.0
# kubectl rollout status deployment/<name>
# Blocks and shows real-time progress of a rolling update.
# Exits 0 when all replicas are updated, or non-zero on timeout.
# Great to use in CI/CD pipelines to verify deployments succeed.
kubectl rollout status deployment/my-app
# kubectl rollout undo deployment/<name>
# Rolls back to the previous ReplicaSet revision.
# Add --to-revision=<n> to go back to a specific version number.
# Kubernetes simply sets the Deployment's image back to the old one.
kubectl rollout undo deployment/my-app
# kubectl rollout history deployment/<name>
# Shows all saved revisions of this Deployment.
# Each 'kubectl apply' or 'set image' creates a new revision.
# Add --revision=<n> to see the exact spec of a past revision.
kubectl rollout history deployment/my-app
Rolling Update — Zero Downtime Deployment
🧠 Check Your Understanding — Deployments
See Answer & Explanation
A Deployment wraps a ReplicaSet and adds declarative update management. You can push a new image version, watch it roll out progressively, and roll back instantly if something goes wrong.
maxUnavailable: 0 guarantee?See Answer & Explanation
maxUnavailable: 0 means zero old Pods are taken down until an equal number of new Pods are fully ready. This ensures 100% availability at the cost of temporarily using more resources (controlled by maxSurge).
See Answer & Explanation
kubectl rollout undo instantly reverts the Deployment to the previous ReplicaSet revision. It triggers another rolling update in reverse — new version Pods are replaced by old version Pods with zero downtime. Add --to-revision=N to jump to a specific older version.
See Answer & Explanation
The
Recreate strategy terminates all Pods of the old version first, then starts Pods of the new version. This causes downtime equal to the startup time of your new Pods but avoids running two versions simultaneously — useful when the new version is incompatible with the old database schema.
See Answer & Explanation
Each time you trigger a new rollout, Kubernetes creates a new ReplicaSet with the updated Pod template and keeps the old ReplicaSet (scaled to 0). These old ReplicaSets are the rollout history.
revisionHistoryLimit (default: 10) controls how many old ReplicaSets are retained.
kubectl rollout pause deployment/my-app do?See Answer & Explanation
Pausing a rollout lets you do a manual canary check — you push a new version to a fraction of your Pods, verify it works (check logs, metrics, error rates), and then
kubectl rollout resume deployment/my-app to continue replacing the rest.
Services — Stable Networking
Pods are ephemeral — they come and go, and each time a new Pod starts, it gets a new IP address. If you have 3 Pods running your web app, how does a database Pod know which IP to call? This is the problem Services solve.
A Service gives a stable virtual IP (ClusterIP) and DNS name to a group of Pods. kube-proxy load-balances traffic across all healthy Pods matching the Service's selector.
Service Types
| Type | Accessible From | Use Case |
|---|---|---|
| ClusterIP (default) | Only within the cluster | Internal microservice-to-microservice communication |
| NodePort | Outside the cluster via NodeIP:NodePort | Development/testing; exposes a port on every node |
| LoadBalancer | Outside via cloud load balancer | Production workloads on cloud (AWS, GCP, Azure) |
| ExternalName | Maps to an external DNS name | Aliasing external services into the cluster |
# service.yaml — ClusterIP (internal only)
apiVersion: v1
kind: Service
metadata:
name: my-app-svc # Other Pods in the cluster can reach this Service
# by DNS: my-app-svc.<namespace>.svc.cluster.local
# or just "my-app-svc" within the same namespace.
spec:
type: ClusterIP # Default type. Only reachable from within the cluster.
# kube-proxy assigns a stable virtual IP (VIP) to this Service.
selector:
app: my-app # Routes traffic to ALL Pods that have label app=my-app.
# Pods are added/removed from routing dynamically as they
# come and go — the Service always stays at the same VIP.
ports:
- port: 80 # The port clients use to call this Service.
# e.g., http://my-app-svc:80
targetPort: 3000 # The port the container is actually listening on.
# The Service translates: client:80 → container:3000.
---
# LoadBalancer — exposes to the internet via a cloud load balancer
apiVersion: v1
kind: Service
metadata:
name: my-app-lb
spec:
type: LoadBalancer # Tells the cloud provider (AWS/GCP/Azure) to provision
# an external load balancer and assign a public IP.
# The assigned IP appears in: kubectl get svc my-app-lb
# → EXTERNAL-IP column.
selector:
app: my-app
ports:
- port: 80 # Internet-facing port on the cloud load balancer.
targetPort: 3000 # Internal container port traffic is forwarded to.
🧠 Check Your Understanding — Services
See Answer & Explanation
ClusterIP is only accessible within the cluster. It's perfect for internal services like databases that should never be exposed to the internet.
targetPort refers to:See Answer & Explanation
port is what clients call (e.g., 80). targetPort is where the container is actually listening (e.g., 3000). The Service translates between them.
See Answer & Explanation
Kubernetes maintains an Endpoints object for each Service. The Endpoints controller continuously watches for Pods with matching labels and updates the IP list. When a Pod becomes Ready it's added; when it fails a readiness probe it's removed. This is why Services provide stable access despite Pods being replaced.
clusterIP: None) used for?See Answer & Explanation
With
clusterIP: None, no virtual IP is assigned. DNS queries for the Service return the individual Pod IPs directly (A records). StatefulSets use headless Services so each Pod gets a stable DNS name like pod-0.my-svc.default.svc.cluster.local.
See Answer & Explanation
NodePort Services use ports in the 30000–32767 range by default. Each node in the cluster opens that port and forwards traffic to the corresponding Service. You can specify a particular port in this range, or let Kubernetes assign one automatically.
frontend namespace, what DNS name correctly reaches a Service named api in the backend namespace?See Answer & Explanation
The full DNS name format for a Service is
<service>.<namespace>.svc.cluster.local. Within the same namespace you can use just the service name (api). Across namespaces you must include at least the namespace: api.backend or the full FQDN.
Namespaces — Virtual Clusters
Imagine a large office building shared by multiple companies. Each company has its own floor with locked access — they can see each other exists but can't accidentally walk into each other's rooms. Namespaces do the same for Kubernetes resources.
Namespaces provide a way to divide cluster resources between multiple users, teams, or environments (e.g., dev, staging, production) within a single cluster.
# kubectl get namespaces
# Lists all namespaces in the cluster (shorthand: kubectl get ns).
# You'll see the built-in ones: default, kube-system, kube-public.
kubectl get namespaces
# kubectl create namespace <name>
# Creates a new namespace imperatively.
# Alternatively: kubectl apply -f namespace.yaml
# (where the YAML has kind: Namespace)
kubectl create namespace dev
# kubectl apply -f <file> -n <namespace>
# The -n flag targets a specific namespace for this command.
# If omitted, the command uses whatever namespace your
# kubeconfig context is currently set to (often "default").
kubectl apply -f deployment.yaml -n dev
# kubectl get pods -n <namespace>
# Lists Pods only in the specified namespace.
# You must specify -n every time, or change the default below.
kubectl get pods -n dev
# kubectl get pods --all-namespaces (shorthand: -A)
# Lists Pods across ALL namespaces in one view.
# Adds a NAMESPACE column to the output so you can see
# which namespace each Pod belongs to.
kubectl get pods --all-namespaces
# kubectl config set-context --current --namespace=<ns>
# Changes the DEFAULT namespace for your current kubeconfig context.
# After this, you no longer need to pass -n dev on every command.
# Run 'kubectl config view --minify' to verify the active context.
kubectl config set-context --current --namespace=dev
default (for user workloads), kube-system (cluster internals), kube-public (public readable data), and kube-node-lease (node heartbeats). Never delete these!🧠 Check Your Understanding — Namespaces
See Answer & Explanation
The
kube-system namespace holds all Kubernetes system Pods (like CoreDNS, kube-proxy, the scheduler, etc.). You can view them with kubectl get pods -n kube-system.
See Answer & Explanation
Namespaces isolate resource names. Both the
dev and production namespaces can have a Deployment named my-app — they are fully independent objects.
kubectl get pods without any flags, which namespace does it look in?See Answer & Explanation
kubectl commands operate in the namespace configured in the current kubeconfig context. Out of the box, that's the
default namespace. Change it with kubectl config set-context --current --namespace=<ns>, or pass -n <ns> on each command.
frontend namespace call a Service in the backend namespace?See Answer & Explanation
Namespaces scope Kubernetes objects but do NOT provide network isolation by default — all Pods can reach all other Pods and Services across namespaces. Network isolation requires explicit NetworkPolicy objects. Cross-namespace DNS uses the full FQDN.
See Answer & Explanation
kube-public is readable by all users (even unauthenticated). It's used by the cluster bootstrap process to expose cluster info via the cluster-info ConfigMap. In practice it's rarely used directly by application workloads.
ResourceQuota in Kubernetes?See Answer & Explanation
ResourceQuota enforces aggregate limits at the namespace level. For example, "the dev namespace can use at most 4 CPU cores, 8Gi RAM, and 20 Pods total." This prevents one team's namespace from consuming all cluster resources and starving others.
ConfigMaps & Secrets
Hard-coding configuration (database URLs, API keys, feature flags) directly into container images is bad practice. It makes images non-portable and secrets insecure. Kubernetes provides two objects to solve this:
- ConfigMap — Stores non-sensitive key-value configuration data.
- Secret — Stores sensitive data (passwords, tokens, keys) encoded in base64.
# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config # Pods reference this by name.
data: # All values here are plain strings — no base64 encoding.
DATABASE_HOST: "postgres.default.svc.cluster.local"
# Full DNS name of a Service in the cluster.
# Format: <service-name>.<namespace>.svc.cluster.local
APP_PORT: "3000"
LOG_LEVEL: "info"
config.properties: | # The pipe "|" means multi-line string literal.
max-connections=100 # This entire block is stored as a single file-like value.
timeout=30s # Useful for mounting as a config file inside a container.
---
# secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: app-secret
type: Opaque # "Opaque" = generic secret (arbitrary key-value data).
# Other types: kubernetes.io/tls, kubernetes.io/dockerconfigjson
data: # Values MUST be base64-encoded.
# Encode: echo -n "password123" | base64
# Decode: echo "cGFzc3dvcmQxMjM=" | base64 --decode
DATABASE_PASSWORD: cGFzc3dvcmQxMjM= # base64("password123")
API_KEY: c2VjcmV0a2V5 # base64("secretkey")
# ⚠️ base64 is encoding, NOT encryption.
# The real security comes from RBAC restricting who
# can read Secrets, and optionally etcd encryption-at-rest.
Injecting ConfigMaps & Secrets into Pods
spec:
containers:
- name: app
image: my-app:1.0
# ── Method 1: Single env var pulled from a ConfigMap key ──────────────
# The container sees DB_HOST as a normal environment variable.
# It does NOT know it came from a ConfigMap — just like export DB_HOST=...
env:
- name: DB_HOST # Name of the env var inside the container.
valueFrom:
configMapKeyRef:
name: app-config # Name of the ConfigMap object.
key: DATABASE_HOST # Which key from that ConfigMap to use.
# ── Method 2: Single env var pulled from a Secret key ─────────────────
# Kubernetes automatically base64-DECODES the value before injecting it.
# So the container receives the plain-text password, not the encoded form.
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: app-secret # Name of the Secret object.
key: DATABASE_PASSWORD
# ── Method 3: Load ALL keys from a ConfigMap as env vars at once ──────
# Every key in app-config becomes an env var automatically.
# Useful when you have many config values and don't want to list each one.
envFrom:
- configMapRef:
name: app-config
# ── Method 4: Mount a ConfigMap as files inside the container ─────────
# Each key in the ConfigMap becomes a separate file under /etc/config/.
# e.g., /etc/config/LOG_LEVEL, /etc/config/config.properties
# The app can read these files at runtime. Updates propagate automatically.
volumeMounts:
- name: config-volume
mountPath: /etc/config # Directory inside the container where files appear.
volumes:
- name: config-volume # Must match the volumeMount name above.
configMap:
name: app-config # Which ConfigMap to expose as files.
🧠 Check Your Understanding — ConfigMaps & Secrets
See Answer & Explanation
The key distinction is intent and access control. Secrets are base64-encoded (not encrypted by default) and Kubernetes provides mechanisms to restrict who can read them via RBAC. ConfigMaps are plain-text and typically readable by anyone in the namespace.
See Answer & Explanation
Volume-mounted ConfigMaps are updated automatically by the kubelet (typically within 1-2 minutes). However, environment variable injections from ConfigMaps are NOT updated — the Pod must be restarted for those to take effect.
See Answer & Explanation
If a referenced ConfigMap (or Secret) is missing, the Pod gets stuck before the container even starts — the event log shows something like "configmap not found". You can set
optional: true on the reference to allow the Pod to start even if the ConfigMap doesn't exist yet.
password123 on the command line to put it in a Secret?See Answer & Explanation
echo -n "password123" | base64 outputs cGFzc3dvcmQxMjM=. The -n flag is important — it prevents echo from adding a newline character, which would otherwise be included in the encoded value and cause the decoded secret to have a trailing newline.
See Answer & Explanation
Kubernetes recognises specialised Secret types for structured data.
kubernetes.io/dockerconfigjson holds credentials for pulling images from a private registry. You reference it in a Pod with imagePullSecrets. kubernetes.io/tls holds a TLS certificate + key pair (used by Ingress).
envFrom. Later you update the ConfigMap. When do the running Pods see the new value?See Answer & Explanation
Environment variables are set at container startup and are NOT updated while the container runs. To pick up new values, the container must restart. The easiest way is
kubectl rollout restart deployment/my-app, which triggers a rolling restart without downtime.
Volumes & Persistent Storage
Container filesystems are ephemeral — when a Pod dies, any data written inside it is gone. If your database Pod crashes and restarts, you don't want to lose all your data! Volumes solve this by providing storage that outlives a container (and sometimes even a Pod).
Storage Concepts
# PersistentVolumeClaim — a request for storage from the cluster.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc # The Pod will reference this PVC by this name.
spec:
accessModes:
- ReadWriteOnce # This volume can be mounted read-write by ONE node at a time.
# For databases, RWO is the correct choice — only one node
# should write at a time. (RWX requires NFS or similar.)
storageClassName: fast-ssd
# Tells Kubernetes WHICH storage backend to use.
# "fast-ssd" is an example StorageClass name defined by
# your cluster admin. Cloud clusters come with classes like
# "gp2" (AWS), "standard" (GCP), "managed-premium" (Azure).
# Omit storageClassName to use the cluster's default.
resources:
requests:
storage: 5Gi # How much storage you need. Kubernetes finds (or creates)
# a PersistentVolume that satisfies this request.
---
# Pod using the PVC
apiVersion: v1
kind: Pod
metadata:
name: db-pod
spec:
containers:
- name: postgres
image: postgres:15
volumeMounts:
- name: data # Must match the volume name below (not the PVC name).
mountPath: /var/lib/postgresql/data # Where Postgres expects to find its data files.
# Data written here persists even if the Pod is deleted.
volumes:
- name: data # Internal name used to link volumeMount ↔ volume source.
persistentVolumeClaim:
claimName: my-pvc # Name of the PVC defined above.
# Kubernetes mounts the bound PersistentVolume here.
Access Modes
| Mode | Shorthand | Meaning |
|---|---|---|
| ReadWriteOnce | RWO | Mounted read-write by a single node |
| ReadOnlyMany | ROX | Mounted read-only by many nodes |
| ReadWriteMany | RWX | Mounted read-write by many nodes (needs NFS/CephFS) |
🧠 Check Your Understanding — Volumes
See Answer & Explanation
Container filesystems are tied to the container's lifecycle. When the Pod is deleted, all data written inside the container is gone. You must use Volumes (PVC) to persist data beyond a Pod's lifecycle.
See Answer & Explanation
PVC is an abstraction — the app just says "I need 5Gi of ReadWriteOnce storage." Kubernetes then binds the PVC to an available PersistentVolume (or creates one dynamically via StorageClass).
See Answer & Explanation
ReadWriteMany (RWX) allows concurrent read-write access from multiple nodes simultaneously. This requires a distributed filesystem like NFS, CephFS, or Azure Files. Most cloud block storage (AWS EBS, GCP PD) only supports RWO — one node at a time.
See Answer & Explanation
Without a StorageClass, an admin must manually create PersistentVolumes. With dynamic provisioning, when a PVC is created the StorageClass's provisioner (e.g., the AWS EBS CSI driver) automatically creates and binds a matching PV. Cloud clusters come with default StorageClasses pre-configured.
STATUS: Bound. What does this mean?See Answer & Explanation
A PVC transitions from Pending (waiting for a matching PV) to Bound (matched and ready). Once Bound, the PVC can be mounted by a Pod. If no matching PV exists and no StorageClass provides dynamic provisioning, the PVC stays Pending indefinitely.
See Answer & Explanation
PVCs are independent objects from Pods. Deleting a Pod does not delete its PVCs. The PVC stays Bound to its PV and retains all data, ready to be mounted by a replacement Pod. This is precisely why PVCs exist — to persist data across Pod restarts and replacements.
Ingress — Smart HTTP Router
A LoadBalancer Service creates one cloud load balancer per Service — this can get expensive. Ingress is a single entry point that routes incoming HTTP/S traffic to different Services based on the URL path or hostname.
Think of Ingress as a smart reverse proxy or API gateway sitting in front of all your Services.
# ingress.yaml
apiVersion: networking.k8s.io/v1 # Ingress moved to this stable API in K8s 1.19+.
kind: Ingress
metadata:
name: my-ingress
annotations: # Annotations pass extra config to the Ingress Controller.
nginx.ingress.kubernetes.io/rewrite-target: /
# ↑ Strips the path prefix before forwarding to the backend.
# e.g., /api/users → / (the backend only sees /users, not /api/users).
# Different controllers use different annotation keys.
spec:
ingressClassName: nginx # Selects which Ingress Controller handles this object.
# Allows multiple controllers (nginx + traefik) in one cluster.
tls: # Enable HTTPS. The controller terminates TLS here,
- hosts: # so backends only need to handle plain HTTP internally.
- example.com
secretName: tls-secret # A kubernetes.io/tls Secret containing cert.pem + key.pem.
# Create with: kubectl create secret tls tls-secret
# --cert=cert.pem --key=key.pem
rules:
- host: example.com # Only route requests with this Host header.
# Omit host to match ALL hostnames.
http:
paths:
- path: /api # Match requests starting with /api.
pathType: Prefix # Prefix = /api, /api/users, /api/v2/... all match.
# Exact = only /api matches (not /api/users).
backend:
service:
name: api-service # Forward to this Service.
port:
number: 80
- path: / # Catch-all: everything not matched above.
pathType: Prefix # More specific paths (/api) are matched first.
backend:
service:
name: web-service
port:
number: 80
🧠 Check Your Understanding — Ingress
See Answer & Explanation
Each LoadBalancer Service provisions a separate cloud load balancer (which has a cost). Ingress provides one entry point that handles path-based and host-based routing to multiple Services.
See Answer & Explanation
The
Ingress object is just a configuration declaration. You also need an Ingress Controller — a running Pod (like nginx or Traefik) that reads Ingress rules and actually routes the traffic.
pathType: Exact and path: /api will match which requests?See Answer & Explanation
pathType: Exact matches only the exact path string. pathType: Prefix would match /api and all paths that start with /api (like /api/users). Use Exact for strict routing and Prefix for REST API hierarchies.
See Answer & Explanation
TLS termination at the Ingress Controller is the standard pattern. The controller decrypts incoming HTTPS traffic using the certificate stored in a
kubernetes.io/tls Secret, then forwards plain HTTP to the backend. This simplifies backend services — they don't need TLS certificates.
See Answer & Explanation
spec.ingressClassName specifies which IngressClass (and therefore which controller) should handle this Ingress object. Multiple controllers can coexist in one cluster, each watching only the Ingress resources that match their class name.
Health Probes — Self-Healing
How does Kubernetes know if your app is actually working? A container could be running but the app inside might be stuck or not ready to accept traffic. Health Probes let you tell Kubernetes exactly how to check your app's health.
| Probe Type | Question It Answers | Action on Failure |
|---|---|---|
| Liveness | "Is the app alive? Should it be restarted?" | Restarts the container |
| Readiness | "Is the app ready to serve traffic?" | Removes Pod from Service endpoints (stops sending traffic) |
| Startup | "Has the app finished starting up?" | Disables liveness/readiness until startup completes |
spec:
containers:
- name: my-app
image: my-app:1.0
# ── Startup Probe ─────────────────────────────────────────────────────
# Purpose: Give slow-starting apps time to initialize.
# While this probe is running, liveness and readiness probes are PAUSED.
# If it fails more than failureThreshold times → container is killed.
# If it succeeds once → it never runs again; liveness/readiness take over.
startupProbe:
httpGet: # Probe type: send an HTTP GET request.
path: /healthz # The URL path your app exposes for health checks.
port: 8080 # The port to hit (must be containerPort).
failureThreshold: 30 # Number of consecutive failures before giving up.
periodSeconds: 10 # How often to probe (in seconds).
# Total timeout = failureThreshold × periodSeconds
# = 30 × 10s = 5 minutes before killing the container.
# ── Liveness Probe ───────────────────────────────────────────────────
# Purpose: Detect deadlocks or hung processes and restart the container.
# Your /healthz endpoint should verify the app is responsive
# (e.g., can process requests), NOT check external dependencies.
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10 # Wait this many seconds after container start
# before the FIRST liveness check. Gives the app
# time to become ready without failing immediately.
periodSeconds: 15 # Probe interval: check health every 15 seconds.
failureThreshold: 3 # Restart the container after 3 consecutive failures.
# Total tolerated downtime before restart: 3 × 15s = 45s.
# ── Readiness Probe ──────────────────────────────────────────────────
# Purpose: Control when traffic is sent to this Pod.
# A failing readiness probe does NOT restart the container —
# it only removes the Pod from the Service's endpoint list.
# Useful during: startup warm-up, config reload, DB migration, etc.
readinessProbe:
httpGet:
path: /ready # Separate endpoint from /healthz — can check DB
port: 8080 # connections, cache warm-up, etc.
initialDelaySeconds: 5
periodSeconds: 5 # Check every 5s. When it passes again, the Pod
failureThreshold: 2 # is automatically added back to Service endpoints.
🧠 Check Your Understanding — Health Probes
See Answer & Explanation
The Startup Probe was specifically designed for slow-starting apps. While the startup probe is running, liveness probes are disabled, preventing the app from being killed before it finishes initializing.
See Answer & Explanation
A failing readiness probe means "I'm not ready for traffic." Kubernetes keeps the container running (unlike liveness) but removes it from the Service endpoints so no new requests are routed to it.
See Answer & Explanation
Kubernetes probes support three mechanisms: httpGet (HTTP status 200-399 = success), tcpSocket (TCP connection succeeds = success), and exec (command exit code 0 = success). Use
exec for apps that don't expose an HTTP health endpoint, e.g., a Redis container you check with redis-cli ping.
See Answer & Explanation
This is intentional behaviour, but also a design consideration. If the DB goes down, your app Pods fail readiness checks and stop receiving traffic — which looks like an outage even though the app containers are healthy. Many teams prefer readiness probes that only check the app itself, and handle DB unavailability within the app (with retry logic).
initialDelaySeconds in a liveness probe?See Answer & Explanation
Without
initialDelaySeconds, the liveness probe starts immediately after the container begins and can kill a slow-starting app before it's even ready. Set this to slightly longer than your app's typical startup time. For unpredictably slow starts, a Startup Probe is a better solution.
failureThreshold: 3 mean in a liveness probe?See Answer & Explanation
The failure counter resets to zero after any successful probe.
failureThreshold: 3 means the container must fail 3 consecutive probes before action is taken. This prevents flapping — a single transient timeout won't restart your container unnecessarily.
Resource Requests & Limits
Without constraints, a single runaway container could consume all CPU and memory on a node, starving other containers. Kubernetes lets you specify resource requests and limits per container.
spec:
containers:
- name: app
image: my-app:1.0
resources:
requests: # MINIMUM resources guaranteed to this container.
# The Scheduler uses these to decide which node to place the Pod on.
# A node is considered "full" when sum(requests) ≥ node capacity.
memory: "256Mi" # 256 Mebibytes (1 Mi = 1,048,576 bytes).
# Other valid units: Ki, Mi, Gi, Ti (binary)
# K, M, G, T (decimal SI)
cpu: "250m" # 250 millicores = 0.25 of one CPU core.
# "1" = 1 full CPU core. "500m" = 0.5 CPU.
# CPU is a compressible resource — the container can
# burst above its request if the node has spare capacity.
limits: # MAXIMUM resources this container can use.
# Enforced at runtime by the Linux kernel (cgroups).
memory: "512Mi" # If the container tries to use more than 512Mi of RAM,
# the kernel's OOM killer terminates it immediately.
# You'll see: OOMKilled in 'kubectl describe pod'.
cpu: "500m" # If the container tries to use more than 0.5 CPU,
# the kernel throttles it (slows it down).
# Unlike memory, the container is NOT killed for CPU overuse.
🧠 Check Your Understanding — Resources
See Answer & Explanation
The scheduler looks at requests (guaranteed minimums) — not limits — when choosing a node. A node is considered "full" when the sum of all Pod requests exceeds its capacity.
See Answer & Explanation
Memory is not compressible — you can't slow down memory usage. When a container hits its memory limit, the Linux kernel's OOM killer terminates it. Kubernetes then restarts it based on the Pod's restart policy.
250m mean in a CPU resource request?See Answer & Explanation
Kubernetes measures CPU in millicores: 1000m = 1 CPU core. So 250m = 0.25 cores, 500m = 0.5 cores, 2000m = 2 cores (equivalent to writing
2). Millicores allow fine-grained CPU allocation — many small services can share a single core.
LimitRange object used for?See Answer & Explanation
A
LimitRange sets default resource requests and limits for containers in a namespace that don't specify their own. It also enforces min/max boundaries — for example, preventing a container from requesting more than 4 CPU cores. This works together with ResourceQuota for namespace governance.
See Answer & Explanation
Without requests, the Scheduler has no signal about the Pod's resource needs and may place it on a full node. The Pod also gets the lowest Quality of Service (QoS) class: BestEffort — the first to be evicted when the node runs low on memory. Always set requests in production.
See Answer & Explanation
Kubernetes automatically assigns QoS based on your resource settings. Guaranteed: every container has identical requests and limits — this Pod is never evicted due to resource pressure. Burstable: some containers have requests or limits. BestEffort: no requests or limits — evicted first.
StatefulSets — For Stateful Applications
Deployments are great for stateless apps (like web servers), where all Pods are identical and interchangeable. But what about databases like PostgreSQL or Redis clusters? Each instance needs a stable identity, stable network hostname, and stable storage. Enter StatefulSets.
| Feature | Deployment | StatefulSet |
|---|---|---|
| Pod identity | Random names (my-app-xk2pz) | Ordered names (my-db-0, my-db-1, my-db-2) |
| Storage | Shared or none | Each Pod gets its own persistent volume |
| Scaling order | Random | Ordered (0→1→2 for scale-up, 2→1→0 for scale-down) |
| DNS | Random Pod DNS | Stable: my-db-0.my-svc.default.svc.cluster.local |
| Use case | Web servers, APIs | Databases, message queues (Kafka, ZooKeeper) |
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres # Name of a "headless" Service (clusterIP: None) that gives
# each Pod a stable DNS entry:
# postgres-0.postgres.default.svc.cluster.local
# postgres-1.postgres.default.svc.cluster.local
# This lets Pods address each other by name (needed for clustering).
replicas: 3 # Pods are created in order: postgres-0, then postgres-1, then postgres-2.
# Each waits to be Running+Ready before the next starts.
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-secret
key: password
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates: # KEY DIFFERENCE from Deployments:
# Each Pod gets its OWN PVC automatically.
# postgres-0 → PVC "data-postgres-0" (10Gi)
# postgres-1 → PVC "data-postgres-1" (10Gi)
# postgres-2 → PVC "data-postgres-2" (10Gi)
# These PVCs persist even if the StatefulSet is deleted,
# protecting your data from accidental loss.
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
🧠 Check Your Understanding — StatefulSets
See Answer & Explanation
Kafka nodes need stable identities (broker IDs), stable storage (topic partitions), and ordered startup/shutdown. All these are StatefulSet features. A Deployment would create interchangeable Pods without stable identity.
See Answer & Explanation
StatefulSets maintain ordered deployment. New Pods are created sequentially starting from the next index (3, then 4). Each new Pod must be Running and Ready before the next one starts.
See Answer & Explanation
StatefulSets scale down in reverse ordinal order: Pod 4 is terminated first, then Pod 3. Each Pod must be fully terminated before the next one is deleted. This mirrors how distributed systems like database replicas or Kafka brokers expect to be decommissioned — from the "newest" node backward.
See Answer & Explanation
StatefulSet PVCs are intentionally not garbage-collected when the StatefulSet is deleted. This is a safety mechanism — your database data survives even if the StatefulSet is accidentally deleted. You must manually delete PVCs when you truly want to remove the data.
serviceName field in a StatefulSet spec?See Answer & Explanation
The
serviceName must point to a headless Service (clusterIP: None). This Service does not load balance but instead creates a DNS A record per Pod: postgres-0.postgres.default.svc.cluster.local. This stable, predictable hostname is essential for database clustering — nodes need to reach each other by a known address.
DaemonSets — One Pod Per Node
A DaemonSet ensures that exactly one copy of a Pod runs on every node (or a selected subset of nodes). Whenever a new node joins the cluster, the DaemonSet automatically places a Pod on it. When a node is removed, the Pod is garbage-collected.
apiVersion: apps/v1
kind: DaemonSet # Automatically places ONE Pod per node (or per selected nodes).
metadata:
name: log-collector
spec:
selector:
matchLabels:
app: log-collector
template:
metadata:
labels:
app: log-collector
spec:
containers:
- name: fluentd
image: fluentd:v1.16
volumeMounts:
- name: varlog
mountPath: /var/log # Inside the container — fluentd reads logs from here.
volumes:
- name: varlog
hostPath: # hostPath mounts a directory FROM the node's filesystem
path: /var/log # INTO the container. This gives the log-collector access
# to all logs written by processes on that node.
# ⚠️ Use hostPath carefully — it breaks Pod portability and
# can expose sensitive node data. Only use it for infra Pods
# like log collectors and monitoring agents.
🧠 Check Your Understanding — DaemonSets
See Answer & Explanation
The DaemonSet controller continuously watches for new nodes. When a new node joins the cluster, it automatically creates a DaemonSet Pod on it. No manual intervention needed.
See Answer & Explanation
The node-exporter is the classic DaemonSet use case — it must run on every node to scrape that node's metrics. Other common examples: Fluentd/Filebeat for log collection, Cilium/Calico network agents, and security scanners. Web APIs belong in Deployments; databases in StatefulSets; one-time tasks in Jobs.
See Answer & Explanation
You can use
spec.template.spec.nodeSelector to target nodes with specific labels (e.g., only GPU nodes). Taints and tolerations also work — for example, DaemonSet Pods often have tolerations for node.kubernetes.io/not-ready so they get scheduled even on problematic nodes (critical for network plugins).
Jobs & CronJobs — Batch Workloads
Deployments run continuously (web servers, APIs). But sometimes you need to run a task to completion — like a database migration, a report generator, or a one-time data import. That's what Jobs are for.
A CronJob runs a Job on a scheduled interval — just like a Unix cron task, but for Kubernetes.
# job.yaml — run a database migration exactly once
apiVersion: batch/v1
kind: Job # Manages Pods that run to completion (not indefinitely).
metadata:
name: db-migration
spec:
completions: 1 # How many Pods must complete successfully for the Job to be "done".
# Set completions: 5 to process 5 independent work items.
parallelism: 1 # How many Pods can run simultaneously.
# completions:5, parallelism:2 → runs 2 at a time, 3 rounds total.
backoffLimit: 3 # Maximum number of retries before the Job is marked as Failed.
# Each failed Pod = 1 retry. After 3 failures, Kubernetes gives up.
template:
spec:
restartPolicy: OnFailure # OnFailure = restart the container on the SAME Pod.
# Never = create a new Pod for each retry (cleaner logs).
# ⚠️ 'Always' is NOT allowed — it would loop forever.
containers:
- name: migration
image: my-app:1.0
command: ["python", "manage.py", "migrate"]
# command overrides the container image's default ENTRYPOINT + CMD.
# Each element in the list is a separate argument (no shell string splitting).
---
# cronjob.yaml — generate a report every day at midnight
apiVersion: batch/v1
kind: CronJob # Creates a new Job object on a time-based schedule.
metadata:
name: daily-report
spec:
schedule: "0 0 * * *" # Standard cron syntax (5 fields):
# ┌─ minute (0-59)
# │ ┌─ hour (0-23)
# │ │ ┌─ day of month (1-31)
# │ │ │ ┌─ month (1-12)
# │ │ │ │ ┌─ day of week (0-7, 0=Sunday)
# 0 0 * * * → at 00:00 (midnight) every day
# */5 * * * * → every 5 minutes
# 0 9 * * 1 → every Monday at 09:00
jobTemplate: # The Job spec to create on each schedule trigger.
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: report-gen
image: report-generator:1.0
command: ["python", "generate_report.py"]
minute hour day-of-month month day-of-week. Example: "*/5 * * * *" = every 5 minutes. Use crontab.guru to build cron expressions visually.🧠 Check Your Understanding — Jobs & CronJobs
restartPolicy in a Job's Pod template be OnFailure or Never?See Answer & Explanation
The default
restartPolicy: Always is used for long-running services. For Jobs, when the container finishes successfully (exit code 0), you want it to stay done — not restart. Hence OnFailure or Never.
See Answer & Explanation
"*/30 * * * *" means "every 30 minutes." The */n syntax means "every n units." "30 * * * *" would run only at minute 30 of every hour (once per hour).
completions: 10 and parallelism: 3. How does it execute?See Answer & Explanation
Think of it as a work queue:
parallelism is the number of concurrent workers, completions is the total items to process. 3 workers run simultaneously; when one finishes, a new one starts immediately — until the total reaches 10. This pattern is ideal for parallel batch processing.
schedule: "0 9 * * 1". When does it run?See Answer & Explanation
Reading left to right: minute=0, hour=9, day-of-month=*, month=*, day-of-week=1 (Monday). So: at minute 0 of hour 9, on any day of the month, in any month, but only when the day of week is Monday → every Monday at 09:00.
RBAC — Access Control
Role-Based Access Control (RBAC) controls who can do what in your Kubernetes cluster. Without RBAC, any user or service could read secrets, delete Pods, or modify any resource — a serious security risk.
RBAC has four key objects:
- Role — Grants permissions within a specific namespace
- ClusterRole — Grants permissions cluster-wide (or defines reusable roles)
- RoleBinding — Attaches a Role to a User/ServiceAccount in a namespace
- ClusterRoleBinding — Attaches a ClusterRole cluster-wide
# Role: read-only access to Pods in the 'dev' namespace.
# Think of a Role as a JOB DESCRIPTION (what actions are allowed).
apiVersion: rbac.authorization.k8s.io/v1
kind: Role # Namespace-scoped. For cluster-wide: use ClusterRole.
metadata:
namespace: dev # This Role only grants permissions WITHIN the 'dev' namespace.
name: pod-reader # Arbitrary name used by RoleBindings to reference this Role.
rules: # A list of permission rules. Subject gets ALL of these.
- apiGroups: [""] # "" = the core API group (Pods, Services, ConfigMaps, Secrets).
# For apps/v1 resources: apiGroups: ["apps"]
# For batch/v1: apiGroups: ["batch"]
resources: ["pods", "pods/log"]
# Which resource types are covered.
# "pods/log" is a sub-resource (kubectl logs).
# "pods/exec" would allow kubectl exec.
verbs: ["get", "list", "watch"]
# What actions are allowed on those resources.
# get = read one object (kubectl get pod my-pod)
# list = list all objects (kubectl get pods)
# watch = stream changes (kubectl get pods -w)
# create = create new objects
# update = modify existing (kubectl apply)
# patch = partial update
# delete = remove objects
---
# RoleBinding: grants the pod-reader Role TO Alice, in the 'dev' namespace.
# Think of a RoleBinding as an EMPLOYMENT CONTRACT (who gets which job description).
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
namespace: dev
name: alice-pod-reader
subjects: # WHO gets the permissions (can list multiple subjects).
- kind: User # User = human identity authenticated by the cluster.
# Other kinds: Group (team of users), ServiceAccount (for Pods/apps).
name: alice # Must match the username in the user's TLS certificate or OIDC token.
apiGroup: rbac.authorization.k8s.io
roleRef: # WHAT Role/ClusterRole to grant. Immutable after creation.
kind: Role # Use ClusterRole here if referencing a ClusterRole.
name: pod-reader # Name of the Role defined above.
apiGroup: rbac.authorization.k8s.io
🧠 Check Your Understanding — RBAC
See Answer & Explanation
A Role only grants permissions within the namespace where it is defined. A ClusterRole grants permissions across all namespaces or for cluster-scoped resources (like Nodes, PersistentVolumes).
See Answer & Explanation
For namespace-scoped access, use Role + RoleBinding. Using ClusterRole + ClusterRoleBinding would give the CI/CD service account access to all namespaces — more than needed. Prefer the principle of least privilege.
See Answer & Explanation
Every Pod runs with a ServiceAccount (the
default ServiceAccount if not specified). Kubernetes automatically mounts a token for this ServiceAccount inside the Pod at /var/run/secrets/kubernetes.io/serviceaccount/token. The app can use this token to call the Kubernetes API. Use RBAC to control what that ServiceAccount is allowed to do.
kubectl apply and kubectl delete?See Answer & Explanation
kubectl apply needs create (new objects) + update/patch (existing objects). kubectl delete needs delete. Read-only operations use get, list, watch. Grant only what is needed — a developer who only reads logs needs get, list, and pods/log, not delete.
default ServiceAccount in a namespace do via the Kubernetes API?See Answer & Explanation
In a standard cluster, the
default ServiceAccount has no RBAC bindings and therefore cannot perform any meaningful operations on cluster resources. This is intentional — the principle of least privilege. You must explicitly create RoleBindings to grant your app the specific permissions it needs.
See Answer & Explanation
Least privilege is a security principle: if an app only needs to read ConfigMaps, don't give it permission to delete Secrets. If a user only needs to view Pods, don't give them deployment permissions. This limits the blast radius of a compromised token or insider threat. Regularly audit your RBAC bindings.
Helm — The Package Manager for Kubernetes
Deploying a real application might require 10-15 YAML files for Deployments, Services, ConfigMaps, Ingress, Secrets, RBAC, etc. Managing all of these individually is tedious. Helm is the package manager for Kubernetes that bundles all related YAML files into a Chart and lets you install, upgrade, and uninstall applications with a single command.
# brew install helm
# Installs the Helm CLI on macOS via Homebrew.
# Linux: curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
brew install helm
# helm repo add <alias> <url>
# Registers a Chart repository under a local alias.
# Bitnami is a popular repo with production-ready charts
# for PostgreSQL, Redis, Kafka, WordPress, etc.
helm repo add bitnami https://charts.bitnami.com/bitnami
# helm repo update
# Fetches the latest chart index from all registered repos.
# Run this before searching or installing to get fresh versions.
helm repo update
# helm search repo <chart-name>
# Searches locally cached repo indexes.
# Shows available versions, app version, and description.
# Add --versions to list all available chart versions.
helm search repo bitnami/postgresql
# helm install <release-name> <chart> [flags]
# Installs the chart and names this instance "my-postgres".
# --set key=value overrides default values from values.yaml.
# --namespace deploys into a specific namespace (must exist).
# Helm records the full rendered YAML as a "release" in a Secret.
helm install my-postgres bitnami/postgresql \
--set auth.postgresPassword=secret \
--namespace production
# helm upgrade <release-name> <chart> [flags]
# Updates an existing release with new values or a new chart version.
# Helm performs a diff and applies only the changed Kubernetes objects.
# Add --install to create the release if it doesn't exist yet.
helm upgrade my-postgres bitnami/postgresql \
--set image.tag=15.3.0
# helm list -A
# Lists all Helm releases across all namespaces.
# Shows release name, namespace, revision, status, and chart version.
# -A = --all-namespaces (same as kubectl's -A flag)
helm list -A
# helm rollback <release-name> <revision>
# Rolls the release back to a specific revision number.
# Each install/upgrade increments the revision counter.
# Use 'helm history my-postgres' to see all past revisions.
helm rollback my-postgres 1
# helm uninstall <release-name>
# Deletes all Kubernetes objects that were created by this release.
# Also removes Helm's internal release history from the cluster.
helm uninstall my-postgres
🧠 Check Your Understanding — Helm
See Answer & Explanation
When you run
helm install my-postgres bitnami/postgresql, "my-postgres" becomes the Release name. You can install the same Chart multiple times with different Release names (e.g., one for dev, one for prod), each with its own configuration.
See Answer & Explanation
values.yaml contains the default values for a chart's templates. At install or upgrade time you can override them with --set key=value or by supplying your own values file with -f my-values.yaml.
See Answer & Explanation
Helm is designed so you never need to edit a chart's source to customise it. Use
--set replicaCount=5 for simple overrides, or -f production-values.yaml for a full set of environment-specific settings. This keeps the upstream chart upgradeable without merge conflicts.
helm install --dry-run --debug my-release bitnami/nginx do?See Answer & Explanation
--dry-run simulates the install and renders the full YAML output that would be applied to the cluster, without actually applying anything. This is invaluable for reviewing what a chart will create, catching misconfigured values, or generating manifests for GitOps workflows.
See Answer & Explanation
Helm 3 stores each release revision as a Kubernetes Secret of type
helm.sh/release.v1 in the release's namespace. Each upgrade creates a new Secret, and helm history <release> reads these Secrets to show the revision history. This means Helm state lives in-cluster with no external storage needed.
helm upgrade and helm upgrade --install?See Answer & Explanation
helm upgrade --install my-release my-chart is the idiomatic CI/CD command: if the release exists it upgrades it; if not it creates it. This makes your pipeline script work for both first-time deployments and subsequent updates without needing separate install and upgrade commands.
You've completed the Kubernetes Tutorial!
You now understand every major Kubernetes concept from architecture to Helm. The best way to learn is to practice — set up a local cluster and deploy something real!