Kubernetes Architecture, Operations, and Troubleshooting Questions
How Kubernetes works, how to run it, and how to debug it. Covers control-plane and node components, the scheduler and API server, cluster design, high availability and multi-cluster topologies, and platform-level operations; the workload primitives (pods, deployments, services, controllers), cluster upgrades, and designing Kubernetes as an internal platform; and the operational depth inside a cluster including pod and service networking, ingress and the CNI model, service mesh, persistent volumes and storage classes, resource requests and limits, and systematically diagnosing scheduling, networking, and storage failures. The full architecture-through-day-two-operations span of Kubernetes.
Describe how Horizontal Pod Autoscaler (HPA) can scale based on a custom metric such as queue length. Which components are required (metrics adapter, exporter), how do you expose the metric to the cluster, and what operational pitfalls should you watch for when autoscaling on custom metrics?
Sample Answer
The Horizontal Pod Autoscaler (HPA) never talks to Prometheus, your app, or a cloud queue directly: it only ever queries one of three Kubernetes metrics APIs (metrics.k8s.io, custom.metrics.k8s.io, external.metrics.k8s.io), so scaling on queue length requires something that exposes the queue's depth through one of those APIs. The standard pipeline is: the application or a sidecar exposes the metric, a metrics system collects it, and a metrics adapter (most commonly prometheus-adapter, or a project like KEDA, Kubernetes Event-Driven Autoscaling, which ships its own adapter and is now the more common current choice specifically for external event sources like queues) is registered with the API server as an aggregated API and translates queries into that metric API's shape.
The three metrics APIs
| API group | What it serves | Tied to a Kubernetes object? |
|---|---|---|
metrics.k8s.io | CPU and memory only, from metrics-server | Yes (per pod/node) |
custom.metrics.k8s.io | Any metric associated with a specific Kubernetes object (a Deployment, a Service) | Yes |
external.metrics.k8s.io | Any metric not tied to a Kubernetes object at all | No |
A message queue's depth (say, a managed queue service or a Kafka consumer-group lag) is not a property of any Kubernetes object, so it belongs under external.metrics.k8s.io and the HPA metric type: External, not Pods or Object.
Required components and how the metric gets exposed
- Instrumentation: the application (preferred) or a sidecar exporter publishes a metric, e.g. a Prometheus gauge
myapp_queue_length{queue="orders"}on/metrics. - Prometheus scrapes it on a scrape job.
prometheus-adapteris deployed with rules mapping a PromQL query to an external metric name Kubernetes will expose, and it registers anAPIService(apiregistration.k8s.io) sokubectl get --raw /apis/external.metrics.k8s.io/v1beta1returns real data. This registration needs its own RBAC: aClusterRolegranting the HPA controller's service account (system:kube-controller-managerreaching through the aggregation layer) permission to read the external metrics API, plus the adapter's own service account needing permission to read Prometheus.- The HPA references the metric by name:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
spec:
minReplicas: 2
maxReplicas: 20
metrics:
- type: External
external:
metric: {name: queue_length_orders}
target: {type: AverageValue, averageValue: "100"}
(autoscaling/v2 has been the stable API since Kubernetes 1.23; the older v2beta2 was removed in 1.26, so any manifest or tooling still referencing it is out of date.)
Operational pitfalls
- A silently wrong zero is worse than a visible failure. If the adapter genuinely can't reach its data source, the HPA does not quietly treat that as "no load": it sets the
ScalingActivecondition toFalsewith reasonFailedGetExternalMetricand shows the metric's current value as<unknown>inkubectl describe hpa, holding replica count steady. The real danger is the opposite case: an adapter that, on a query error, returns a literal0instead of erroring. That looks healthy to the HPA and drives a real scale-down during an actual outage in the metrics path, which is why adapter error-handling is worth testing explicitly rather than assumed. - Cardinality. High-cardinality labels on the underlying metric (one series per customer ID, for instance) can make Prometheus memory blow up long before the HPA ever sees a problem; keep the label set the adapter maps from small and stable.
- Flapping. A noisy queue-length signal causes replica oscillation; use
behavior.scaleDown.stabilizationWindowSeconds(part of theautoscaling/v2HPA behavior fields) or pre-aggregate with a Prometheus recording rule rather than reacting to raw noise. - Latency in the chain. Scrape interval, adapter caching, and the HPA's own sync period all stack up between a real queue-depth change and a scaling action; a 15s scrape interval plus a slow adapter cache can easily add tens of seconds of lag, which matters for a bursty queue.
- Cost. Recomputing an expensive PromQL query on every HPA sync (default every 15 seconds) across many HPAs can meaningfully load a Prometheus instance; pre-aggregate with recording rules for anything non-trivial.
Validate end to end in staging with synthetic queue load before trusting this in production, watching the full chain (producer to queue to exporter to Prometheus to adapter to HPA) rather than any single hop in isolation.
Trade-off note
Hand-rolling prometheus-adapter rules gives full control over the PromQL mapping but is fiddly YAML to maintain; KEDA trades some of that flexibility for purpose-built scalers for dozens of common event sources (queues, streams, schedules) and is usually less operational overhead for exactly this "scale on queue depth" scenario, at the cost of being one more component to run alongside (or instead of) prometheus-adapter.
Explain Kubernetes networking end-to-end: pod IP model, service virtual IPs, kube-proxy modes (iptables vs IPVS), CNI plugins and how they implement pod networking, and practical debugging steps when pods on different nodes cannot reach each other on specific TCP ports. Include commands and sources of truth to inspect.
Sample Answer
Direct answer
Every pod gets a routable IP address from the CNI (Container Network Interface) plugin, with no NAT (network address translation) between pods by default. A Service layers a stable virtual IP and DNS name on top of that, and kube-proxy (or an eBPF-based replacement like Cilium; eBPF lets Linux run sandboxed programs inside the kernel to process packets directly) programs the actual rewriting rules that turn a Service IP into a real pod IP. When two pods on different nodes cannot reach each other on a specific port, the fault sits in exactly one of four layers, checked in this order: the pod's own listener, the kube-proxy/Service plumbing, the CNI's cross-node routing, or a host/cloud firewall. Testing each layer directly beats guessing from symptoms.
Structured elaboration
Pod IP model
The CNI plugin assigns each pod a unique IP from the cluster's pod address range and is responsible for making that IP reachable from every node, either by programming host routes directly (an "underlay" approach, for example Calico in BGP mode, Border Gateway Protocol, the routing protocol routers use to exchange reachability information) or by encapsulating pod traffic in an overlay (VXLAN, a tunneling protocol that wraps one Ethernet frame inside a UDP packet, used by Flannel and by Calico's IP-in-IP mode).
Service virtual IPs and kube-proxy modes
A ClusterIP Service is a stable virtual IP that never corresponds to a real listener; kube-proxy rewrites traffic to it into a real pod IP and port.
| Mode | Mechanism | Status |
|---|---|---|
| iptables | Programs Linux netfilter/iptables NAT rules per Service and endpoint | Still the default |
| IPVS | Uses the kernel's IPVS load balancer, built for large numbers of Services | GA, scales better than iptables (near-constant-time lookup versus iptables' linear rule matching) |
| nftables | Programs nftables rules, the modern successor to iptables | Alpha in 1.29, beta in 1.31, GA in Kubernetes 1.33; not the default even where available, but the direction the project is moving, since the iptables backend is legacy code |
Cilium and some other CNIs bypass kube-proxy entirely, attaching eBPF programs to kernel networking hooks to do the same rewriting job with lower per-packet overhead and without iptables' rule-count scaling problem.
CNI plugins in practice
- Calico: pure L3 routing via BGP (no overlay, lowest overhead, needs a BGP-capable network) or IP-in-IP/VXLAN encapsulation where BGP peering is not available.
- Flannel: a simple VXLAN overlay by default; fewer features than Calico or Cilium.
- Cilium: eBPF-based; can replace kube-proxy, adds L7-aware NetworkPolicy and traffic observability, at the cost of needing a reasonably current kernel.
flowchart LR
A[Pod A on Node 1] -->|packet to Service ClusterIP| KP1[kube-proxy or eBPF dataplane, Node 1]
KP1 -->|rewrite to real Pod IP| CNI1[CNI routing or overlay, Node 1]
CNI1 -->|cross-node route or tunnel| CNI2[CNI routing or overlay, Node 2]
CNI2 --> B[Pod B on Node 2]
FW[Host firewall or cloud security group] -.may block specific ports.-> CNI2
Debugging cross-node connectivity, layer by layer
Reproduce and localize before changing anything:
$ kubectl get pod -o wide -n app
NAME READY STATUS NODE IP
pod-a 1/1 Running node-1 10.244.1.7
pod-b 1/1 Running node-2 10.244.2.9
$ kubectl exec -n app pod-a -- nc -vz 10.244.2.9 8080
nc: connect to 10.244.2.9 port 8080 (tcp) failed: Connection timed out
A timeout, not "connection refused," means the packet is being dropped somewhere in transit rather than rejected by pod-b's listener; that already rules out "the app isn't listening" and points at routing, the CNI, or a firewall.
- Confirm the listener exists:
kubectl exec -n app pod-b -- ss -tlnpshould show something bound to 8080; if not, this was never a networking problem. - Check node-to-node routing for the pod's address range:
$ ip route get 10.244.2.9
10.244.2.9 via 10.0.1.2 dev eth0 src 10.0.1.5
A missing route, or one pointing at the wrong interface, means the CNI failed to program cross-node routing, usually because its daemonset is crashlooping:
$ kubectl get pods -n kube-system -l k8s-app=calico-node
NAME READY STATUS RESTARTS
calico-node-x7k2p 0/1 CrashLoopBackOff 14
- If the failure is specifically Service-IP traffic rather than direct pod-IP traffic, check kube-proxy's own health the same way (
kubectl get pods -n kube-system -l k8s-app=kube-proxy) and inspect its programmed rules (sudo iptables -t nat -S | grep <service-name>orsudo ipvsadm -Ln, depending on mode). - If routing and the CNI both look healthy, check host-level and cloud firewall rules for the specific port. CNI and kube-proxy problems tend to affect all ports uniformly; a single-port failure with everything else in that layer healthy is a strong signal for a firewall rule scoped to that port.
- Capture packets at the suspect boundary to confirm rather than infer:
sudo tcpdump -n -i any host 10.244.2.9 and port 8080on the destination node shows whether the packet arrives at the host at all, before deciding whether the problem is upstream (routing) or downstream (host firewall, pod network namespace).
Trade-offs and pitfalls
- iptables rule count grows linearly with Services and endpoints and becomes a real CPU cost past a few thousand Services; IPVS or nftables scale better, but changing kube-proxy mode cluster-wide is a disruptive change to plan and test, not to flip live.
- The distinction between "Connection timed out" and "Connection refused" is easy to skip past but changes where to look first: refused means the packet arrived and something rejected it (wrong port, no listener); timed out means the packet was dropped somewhere before that.
- MTU (maximum transmission unit) mismatches between overlay encapsulation and the underlying network are a classic cause of connections that work for small packets and hang for large ones. Confirm with a full-size, non-fragmenting ping rather than assuming a working small ping rules out MTU.
Compare managed Kubernetes offerings such as GKE/EKS/AKS with self-managed clusters on provisioned VMs. Discuss trade-offs around operational overhead, upgrade and patching responsibilities, control plane availability, cost components, security responsibilities, and scenarios where self-managed might still be preferable.
Sample Answer
For most teams, a managed control plane (Google Kubernetes Engine/GKE, Amazon Elastic Kubernetes Service/EKS, Azure Kubernetes Service/AKS) is the right default, because the provider absorbs control-plane high availability, etcd operations, and version patching that otherwise consume ongoing senior engineering time. Self-managed clusters on provisioned VMs earn their keep only when a concrete constraint, not a general preference for control, rules the managed option out: a data-residency or regulatory requirement the provider cannot satisfy, hardware or networking the managed offering does not support, or a genuinely large fleet where the provider's per-cluster fee becomes material against staff you would need to hire anyway.
Evaluation matrix
| Dimension | Managed (GKE/EKS/AKS) | Self-managed on VMs |
|---|---|---|
| Operational overhead | Provider runs control-plane HA, etcd backups, and API server scaling; team focuses on node pools, workloads, and policy | Team owns the entire lifecycle: bootstrapping, etcd quorum, HA design, backups, and every runbook |
| Upgrades and patching | Provider orchestrates control-plane upgrades and patches control-plane CVEs; team still handles node images and add-on compatibility | Team designs, tests, and executes every control-plane and node upgrade, including etcd version compatibility |
| Control-plane availability | Typically SLA-backed, multi-zone by default, provider handles API server/etcd failover | Team designs multi-zone etcd quorum and API server redundancy and owns failover testing |
| Cost shape | Per-cluster control-plane fee plus node compute; lower staffing cost | No control-plane fee, but node compute for dedicated control-plane VMs plus the engineering time to operate it reliably |
| Security responsibility split | Provider secures the control plane and hypervisor; team still owns node OS hardening, RBAC (role-based access control, governing who or what can perform which actions against which resources), NetworkPolicy, secrets, and image supply chain | Team owns all of the above, plus control-plane hardening, certificate rotation, and etcd encryption |
The security row matters more than it looks: managed Kubernetes does not remove the shared-responsibility split so much as move its boundary, the same shape as the shared-responsibility model on any IaaS. Workload-level security (RBAC, NetworkPolicy, secrets, supply-chain scanning) is the team's job either way.
A short decision checklist
Answer these before defaulting to self-managed:
- Is there a regulatory or data-residency constraint that no available cloud provider's managed offering can satisfy (for example, a requirement that the control plane run inside a specific on-premises facility with no third-party operational access)?
- Does the workload need node/kernel-level customization (specialized drivers, SR-IOV, custom kernel modules) that managed node pools do not allow?
- Does the footprint include environments with no managed control-plane offering at all (deep edge sites, air-gapped networks, telecom points of presence)?
- At current or planned scale, does the provider's control-plane fee plus its operational savings actually exceed what dedicated in-house Kubernetes operations staff would cost?
If none of these are "yes," managed is the safer default; if one or more are "yes," self-managed (or a lightweight distribution suited to the constraint) deserves serious evaluation.
Worked scenarios
- Compliance-driven case: a bank operating under a regulator that requires its control plane to run inside a specific on-premises data center, with no third-party (including the cloud provider's own control-plane operators) having operational access to cluster internals. This disqualifies every managed offering outright, regardless of cost or operational convenience; the decision is made by question 1 above before cost or overhead ever enters the comparison.
- Edge/scale-driven case: a telecom operator running Kubernetes across dozens of far-edge sites where no cloud provider offers a managed control plane at all. Self-managed, or a purpose-built lightweight distribution, is the only option that reaches the hardware; the comparison in the table above is largely academic here because there is no managed alternative on the table.
Trade-offs and pitfalls
- The most common wrong turn is choosing self-managed "for control" without budgeting the standing SRE headcount it requires; control-plane upgrades, etcd operations, and CVE patching do not stop after go-live, they become a permanent line item.
- Assuming managed Kubernetes removes all security burden is the second most common mistake; node OS hardening, RBAC design, NetworkPolicy, and secret management remain the team's responsibility under either model.
- Very large fleets sometimes justify self-managed purely on the provider fee, but that comparison is frequently done without pricing in the incident risk of an in-house etcd outage against a provider's SLA-backed uptime, which understates the true cost of self-managed at scale.
- Starting managed and only moving specific clusters to self-managed once a concrete constraint actually appears is lower risk than starting self-managed everywhere on a hypothetical future need.
Explain the differences between a Pod, ReplicaSet, Deployment, StatefulSet, and DaemonSet in Kubernetes. For each resource, describe its primary use cases, how it manages lifecycle and scaling, how updates/rollbacks are handled, and provide concrete examples of when you'd choose each resource (stateless web service, per-node agent, stateful database, etc.).
Sample Answer
Pods are the runtime unit; everything else here is a controller deciding how many pods to run and how to replace them. A ReplicaSet just keeps N identical pods alive. A Deployment wraps a ReplicaSet with declarative rolling updates and rollback history. A StatefulSet keeps stable per-replica identity and storage for workloads that care which replica they are. A DaemonSet guarantees exactly one pod per matching node. Job and CronJob run pods to completion, once or on a schedule, rather than keeping them running indefinitely.
Comparison
| Object | Identity model | Storage | Update mechanics | Reach for it when |
|---|---|---|---|---|
| ReplicaSet | anonymous, interchangeable pods | none built in | no native rolling update | almost never directly; it is what a Deployment manages underneath |
| Deployment | anonymous | typically shared, external, or none | rolling update via surge and unavailable settings, with full revision history and rollback | stateless services: web tiers, API servers |
| StatefulSet | stable ordinal identity (pod-0, pod-1, ...) and hostname | one PersistentVolumeClaim per ordinal via volumeClaimTemplates | ordered rolling update by default, highest ordinal first | stateful systems that need to know which replica they are: databases, brokers |
| DaemonSet | one pod per matching node | node-local, if any | rolling update per node, bounded by maxUnavailable | node-level agents: log collectors, CNI (Container Network Interface) plugins, monitoring exporters |
| Job | runs to completion, tracked by completions, parallelism, backoffLimit | ephemeral | retried on failure up to backoffLimit, not a rolling concept | one-off or batch work |
| CronJob | creates a Job on a schedule | ephemeral | governed by concurrencyPolicy (Allow, Forbid, or Replace) | scheduled batch work: nightly reports, cleanup jobs |
Worked example: rolling-update arithmetic
For a Deployment with replicas: 10, maxSurge: 25%, and maxUnavailable: 25%, Kubernetes rounds surge up and unavailable down:
surge=⌈10×0.25⌉=3
unavailable=⌊10×0.25⌋=2
So at the busiest point of the rollout the Deployment can briefly run up to 13 pods (10 plus 3 surge) while guaranteeing at least 8 stay available (10 minus 2 unavailable). This asymmetric rounding is why setting surge and unavailable to the same percentage still lets the total pod count grow slightly during a rollout instead of staying flat.
DaemonSets interact with node taints in a way that is easy to miss: they commonly carry an explicit toleration for the control-plane taint (conventionally node-role.kubernetes.io/control-plane:NoSchedule) so node-level agents like a CNI plugin or kube-proxy itself still run on control-plane nodes that ordinary application pods are excluded from. That toleration is a manifest choice the DaemonSet author makes, not automatic behavior.
Trade-offs and pitfalls
- Choosing Deployment for a workload that actually needs stable identity (a small quorum service, a primary-replica database) forces the team to rebuild that identity logic in application code; StatefulSet gives it for free, at the cost of slower, ordered rollouts.
- A stuck rollout is usually a readiness-probe failure hiding behind the surge and unavailable math: if new pods never pass readiness, the Deployment can stall indefinitely partway through, with old and new pods coexisting. Check pod events and readiness status before assuming a controller bug.
- A PodDisruptionBudget set too strictly (for example,
minAvailableequal to the full replica count) can make a Deployment's own rolling update unable to proceed, because the update's own temporary unavailability trips the budget meant to protect against unrelated disruptions. - DaemonSets bypass normal replica-count thinking: adding nodes silently adds pods and cost. Forgetting a DaemonSet exists across a large node pool is a common source of untracked resource consumption.
Explain liveness, readiness, and startup probes in Kubernetes. For each type describe when it is evaluated, what consequences a failing probe has on pod lifecycle and traffic routing, and list best practices for implementing probes for a typical HTTP-based web service.
Sample Answer
Liveness, readiness, and startup probes all ask whether a container is okay, but each answer drives a different Kubernetes action: a failing liveness probe gets the container restarted, a failing readiness probe gets the pod pulled out of Service traffic without touching the container at all, and a startup probe simply delays the other two until the app has had time to boot.
What each probe gates
| Probe | Evaluated | Consequence on failure | Effect on traffic |
|---|---|---|---|
| Liveness | continuously, after the container starts | kubelet kills the container; it is recreated per the pod's restartPolicy | indirect only, through the restart |
| Readiness | continuously, independent of liveness | pod is marked NotReady and removed from the Service's Endpoints and EndpointSlices, the objects that track which pod IPs actually receive traffic | direct: no new requests are routed to it until it passes again |
| Startup | only until it first succeeds | container is killed and restarted if it fails before ever succeeding; liveness and readiness are not evaluated at all until it does | none directly, but it prevents liveness from killing a still-booting container |
Worked example: sizing a startup budget by workload archetype
What 'booting' means differs a lot by workload, and the startup probe has to be sized for the actual archetype, not guessed at: a machine learning (ML) inference service loading model weights into memory might need several minutes; a batch worker doing asynchronous Java Virtual Machine (JVM) warmup, classloading, and connection-pool initialization for an extract-transform-load (ETL) job might need under a minute; a stateless HTTP handler might be ready in under a second. Whichever number applies, it has to be encoded as periodSeconds times failureThreshold. Budgeting 5 minutes of startup headroom with a 10-second check interval for the ML case:
10×30=300s=5 min
means periodSeconds: 10 and failureThreshold: 30. Too tight in this calculation and the startup probe itself kills a healthy-but-slow container before it ever gets a chance to serve; too loose, and a genuinely stuck container burns minutes before anything reacts.
Trade-offs and pitfalls
- Swapping liveness and readiness is the classic mistake: pointing liveness at a deep dependency check (database reachability) means a transient database blip restarts every application pod at once instead of simply pulling them from rotation, turning a recoverable dependency issue into a self-inflicted outage.
- Using liveness as a substitute for a startup probe on a slow-booting app causes a restart loop before the app ever finishes initializing, since the container never survives long enough to pass a liveness check tuned for steady-state behavior.
- A readiness probe that is too permissive, common with the JVM-async pattern where the process starts accepting connections before its dependency pools are actually warm, reports the pod as ready while real requests still fail; that failure mode never shows up as a probe failure at all, only as user-visible errors.
Unlock Full Question Bank
Get access to all Kubernetes Architecture, Operations, and Troubleshooting interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.