System design, trade-offs, and large-scale patterns
Quick Answer
Native sidecars are init containers with `restartPolicy: Always`, so Kubernetes starts them during initialization but keeps them running for the lifetime of the Pod. Their readiness can affect Pod readiness, and for Jobs they do not prevent completion once the main container exits.
Detailed Answer
Think of a restaurant opening for dinner. The kitchen can start cooking only after the exhaust fan and point-of-sale terminal are running, but those support systems must keep working after the first meal is served. A native sidecar container is that support system: it starts before or alongside the main work, keeps running, and is not the reason the restaurant exists.
In Kubernetes, native sidecars solve the long-standing awkwardness of treating log shippers, mesh proxies, and credential refreshers as normal containers. A normal app container has no special startup relationship to the main workload. A traditional init container runs to completion before app containers start. A native sidecar sits between those ideas: it is listed under initContainers, but restartPolicy: Always tells Kubernetes to keep it alive as a supporting container.
Internally, kubelet processes init containers in order. When it reaches an init container with restartPolicy: Always, it starts that container and continues once the sidecar has started successfully. The app containers then start while the sidecar remains running. If the sidecar has a readiness probe, Kubernetes includes that probe when calculating whether the Pod is Ready. During shutdown, Kubernetes terminates app containers first, then sidecars, which is important for log flushing and proxy drain behavior.
At production scale, this changes rollout and observability practices. Teams should monitor sidecar restart count, readiness probe failures, log shipping lag, and whether the sidecar adds startup latency. For Jobs, the sidecar no longer keeps the Job alive forever after the main container has completed, which removes a common operational trap in batch workloads that need a helper process.
The non-obvious gotcha is that native sidecars are still part of the Pod resource budget and lifecycle. A slow or unhealthy sidecar can block readiness for an otherwise healthy app, and a sidecar with an aggressive readiness probe can cause endpoints to flap under dependency pressure. Architects should treat sidecars as production dependencies with SLOs, not invisible helper containers.
Code Example
# Apply a Deployment that starts a log shipper as a native sidecar before the API container
kubectl apply -f payments-api-native-sidecar.yaml
# Verify the sidecar appears under init container status while continuing to run
kubectl get pod -l app=payments-api -n payments -o jsonpath='{.items[0].status.initContainerStatuses[0].state}'
# Check whether sidecar readiness is affecting the Pod Ready condition
kubectl describe pod -l app=payments-api -n payments | grep -A6 'Conditions'
# payments-api-native-sidecar.yaml
apiVersion: apps/v1 # Uses the stable Deployment API for a replicated service
kind: Deployment # Manages rollout and replacement of payments-api Pods
metadata:
name: payments-api # Realistic production service name
namespace: payments # Keeps the service isolated from other teams
spec:
replicas: 3 # Runs three Pods so one sidecar issue does not stop all traffic
selector:
matchLabels:
app: payments-api # Connects the Deployment to its Pods
template:
metadata:
labels:
app: payments-api # Allows Service and observability selectors to find Pods
spec:
initContainers:
- name: logshipper # Supporting container that tails application logs
image: alpine:3.20 # Small image for a simple tailing process
restartPolicy: Always # Makes this init container a native sidecar
command: ['sh','-c','tail -F /var/log/payments/access.log'] # Keeps log streaming active
volumeMounts:
- name: app-logs # Mounts the same log volume as the app container
mountPath: /var/log/payments # Path where the app writes logs
readinessProbe:
exec:
command: ['sh','-c','test -d /var/log/payments'] # Marks sidecar ready only after the shared path exists
periodSeconds: 5 # Checks readiness frequently during rollout
containers:
- name: api # Primary business container
image: registry.company.com/payments-api:2.8.4 # Versioned production image
ports:
- containerPort: 8080 # API listens on port 8080 inside the Pod
volumeMounts:
- name: app-logs # Shares log files with the sidecar
mountPath: /var/log/payments # Write location for request logs
volumes:
- name: app-logs # Empty volume shared by the app and sidecar
emptyDir: {} # Lives only for the Pod lifetimeInterview Tip
A junior engineer typically answers that a sidecar is just another container in the same Pod, but for a senior/architect role, you need to show lifecycle reasoning. Explain why `restartPolicy: Always` under `initContainers` matters, how kubelet starts and stops those containers, how readiness can include the sidecar, and why batch Jobs no longer hang because the helper process keeps running. Strong answers also mention resource accounting, drain behavior, startup latency, and sidecar observability.
◈ Architecture Diagram
┌──────────┐
│ Init run │
└────┬─────┘
↓
┌──────────┐
│ Sidecar │
└────┬─────┘
↓
┌──────────┐
│ App API │
└────┬─────┘
↓
┌──────────┐
│ Ready │
└──────────┘💬 Comments
Quick Answer
ValidatingAdmissionPolicy lets the API server enforce declarative CEL rules in-process, avoiding many webhook latency and availability risks. It is best for deterministic validation of request objects, while webhooks are still needed for external lookups, mutation, or logic that cannot be expressed safely in CEL.
Detailed Answer
Think of a building lobby with two security checks. One guard can read a badge and a printed rule list immediately, while another guard must call a remote office for approval. ValidatingAdmissionPolicy is the fast local rule list; a validating webhook is the remote phone call. Both can be useful, but the second one can delay every person entering the building if the phone line is slow.
In Kubernetes, admission control is the checkpoint between an API request and persisted cluster state. ValidatingAdmissionPolicy gives cluster administrators a native way to reject invalid resources with Common Expression Language, or CEL, which is a safe expression language designed for policy checks. Instead of operating a separate webhook service, TLS certificates, network path, scaling rules, and timeout behavior, the API server evaluates the expression directly.
The flow is precise. A ValidatingAdmissionPolicy defines match constraints and validation expressions. A ValidatingAdmissionPolicyBinding connects that policy to the resources and namespaces where it applies and specifies actions such as Deny. Optional parameter resources can make one abstract policy reusable across teams. When a matching CREATE or UPDATE request reaches admission, Kubernetes evaluates the CEL expression against the incoming object. If the expression returns false, the configured failure behavior decides whether the request is rejected.
In production, this is ideal for rules like requiring labels, limiting replica counts, blocking privileged settings, or enforcing image registry patterns. Operators should monitor admission rejection rates, API server latency, policy rollout changes, and namespace selectors. Policies should be rolled out gradually because one bad rule can block deployments across many services. Use audit or warning-style validation before deny when the blast radius is unknown.
The gotcha is treating CEL as a total replacement for admission webhooks. CEL should not call external systems and cannot mutate resources. If policy needs live inventory from a CMDB, vulnerability scanner, signing service, or custom approval database, a webhook or controller may still be required. Architects should prefer CEL for fast predictable (deterministic) checks and reserve webhooks for cases where the policy genuinely needs external state.
Code Example
# Create a policy that blocks oversized Deployments in shared namespaces
kubectl apply -f vap-replica-limit.yaml
# Bind the policy only to namespaces labeled environment=shared
kubectl apply -f vap-replica-limit-binding.yaml
# Test the policy with a dry-run Deployment request
kubectl create deployment payments-api --image=registry.company.com/payments-api:2.8.4 --replicas=8 -n payments --dry-run=server
# vap-replica-limit.yaml
apiVersion: admissionregistration.k8s.io/v1 # Uses the stable admission policy API
kind: ValidatingAdmissionPolicy # Declares reusable validation logic
metadata:
name: replica-limit.platform.example.com # Globally unique policy name
spec:
failurePolicy: Fail # Fails closed if the policy cannot be evaluated
matchConstraints:
resourceRules:
- apiGroups: ['apps'] # Matches workload resources in the apps API group
apiVersions: ['v1'] # Applies to stable v1 Deployments
operations: ['CREATE','UPDATE'] # Checks new and changed Deployments
resources: ['deployments'] # Limits the policy scope to Deployments
validations:
- expression: 'object.spec.replicas <= 5' # CEL rule limiting replica count
message: 'shared namespaces may run at most five replicas per Deployment' # Clear operator-facing error
---
apiVersion: admissionregistration.k8s.io/v1 # Uses the matching binding API
kind: ValidatingAdmissionPolicyBinding # Connects the policy to namespaces
metadata:
name: replica-limit-shared-namespaces # Binding name for shared environments
spec:
policyName: replica-limit.platform.example.com # References the policy above
validationActions: ['Deny'] # Rejects matching requests that fail validation
matchResources:
namespaceSelector:
matchLabels:
environment: shared # Applies only to namespaces with this labelInterview Tip
A junior engineer typically answers that admission webhooks validate Kubernetes objects, but for a senior/architect role, you need to show control-plane reliability tradeoffs. Explain why in-process CEL policies reduce webhook outage and latency risk, how bindings scope policy, why failurePolicy matters, and where CEL stops being appropriate. A mature answer separates predictable (deterministic) object validation from external business approval and describes how to roll out enforcement without breaking every deployment pipeline at once.
◈ Architecture Diagram
┌──────────┐
│ API Req │
└────┬─────┘
↓
┌──────────┐
│ Match │
└────┬─────┘
↓
┌──────────┐
│ CEL Rule │
└────┬─────┘
↓
┌─────┬────┐
│Deny │Save│
└─────┴────┘💬 Comments
Quick Answer
Dynamic Resource Allocation models devices through DeviceClasses, ResourceClaims, ResourceClaimTemplates, and ResourceSlices so the scheduler can allocate a suitable device and place the Pod on a node that can access it. Architects must monitor ResourceClaim status, driver health, scheduler behavior, and kubelet device metrics because the device driver becomes part of the scheduling control plane.
Detailed Answer
Think of a hospital assigning specialized equipment to surgeries. A room is not enough; the scheduler must know whether the right scanner, technician, and access path are available before booking the operation. Dynamic Resource Allocation, or DRA, gives Kubernetes a similar model for GPUs, network adapters, accelerators, and other devices that cannot be represented well by a simple integer resource count.
DRA exists because extended resources are too blunt for many modern devices. A workload may need a GPU with specific memory, a network device with a particular capability, or a device pool managed by a vendor driver. DeviceClass describes categories of devices. ResourceClaim says what a workload needs. ResourceClaimTemplate lets Kubernetes generate per-Pod claims. ResourceSlice advertises devices attached to nodes so the scheduler can reason about where the Pod can run.
The internal workflow crosses multiple components. A driver creates ResourceSlices for its device pools. A workload references a ResourceClaim or ResourceClaimTemplate. If a template is used, the resourceclaim-controller creates a claim. The scheduler filters ResourceSlices, allocates a matching device, updates the ResourceClaim status, and schedules the Pod to a compatible node. The kubelet and device driver on that node then prepare device access for the container.
Production adoption requires more than enabling a feature. Platform teams should watch unschedulable Pods, ResourceClaim conditions, ResourceSlice inventory freshness, kubelet device metrics, driver logs, and device health status where available. They should test behavior during device failure, node drain, driver restart, and cluster upgrade. DRA also introduces RBAC and security questions because drivers may report device status and influence scheduling decisions.
The subtle gotcha is assuming allocation means uniform capacity across replicas. Some DRA features allow prioritized alternatives, so different Pods in the same ReplicaSet may receive different device types if the workload permits it. If the application assumes identical hardware, performance or correctness can drift between replicas. Architects need clear device classes, workload tolerances, and dashboards that expose which claim was allocated to which Pod.
Code Example
# Inspect available DeviceClasses published for platform users
kubectl get deviceclasses
# Check ResourceClaims waiting for allocation in the ml-platform namespace
kubectl get resourceclaims -n ml-platform
# Describe a pending training Pod to see resource claim scheduling events
kubectl describe pod fraud-model-trainer-0 -n ml-platform
# dra-claim-template.yaml
apiVersion: resource.k8s.io/v1 # Uses the Dynamic Resource Allocation API group
kind: ResourceClaimTemplate # Lets Kubernetes create a per-Pod ResourceClaim
metadata:
name: gpu-large-template # Names the reusable claim template
namespace: ml-platform # Keeps claims in the workload namespace
spec:
spec:
devices:
requests:
- name: training-gpu # Logical request name used by the Pod
exactly:
deviceClassName: nvidia-h100 # Requests a class exposed by the device driver
---
apiVersion: v1 # Standard Pod API
kind: Pod # Single training workload Pod
metadata:
name: fraud-model-trainer-0 # Realistic ML training workload
namespace: ml-platform # Matches the ResourceClaimTemplate namespace
spec:
resourceClaims:
- name: training-device # Claim handle referenced by the container
resourceClaimTemplateName: gpu-large-template # Creates a claim from the template
containers:
- name: trainer # Main training container
image: registry.company.com/fraud-trainer:4.3.1 # Versioned workload image
resources:
claims:
- name: training-device # Mounts the allocated device into this containerInterview Tip
A junior engineer typically answers that DRA is for GPUs, but for a senior/architect role, you need to show scheduler and driver integration awareness. Explain the relationship between DeviceClass, ResourceClaim, ResourceClaimTemplate, and ResourceSlice; describe how the scheduler allocates before Pod placement; and call out driver trust, stale inventory, and observability. A strong answer also mentions what happens when claims are missing, how generated claims follow Pod lifecycle, and why replicas may not receive identical devices.
◈ Architecture Diagram
┌──────────┐
│ Driver │
└────┬─────┘
↓
┌──────────┐
│ ResSlice │
└────┬─────┘
↓
┌──────────┐
│ Claim │
└────┬─────┘
↓
┌──────────┐
│ Scheduler│
└────┬─────┘
↓
┌──────────┐
│ Pod Node │
└──────────┘💬 Comments
Quick Answer
Gateway API separates infrastructure ownership from route ownership through resources such as GatewayClass, Gateway, and HTTPRoute. Platform teams can own listener and infrastructure policy, while application teams attach routes within allowed namespaces and hostnames.
Detailed Answer
Think of a shopping mall. The mall operator controls entrances, fire doors, and security rules, while each store controls its own sign and product layout. Traditional Ingress often turns the mall operator into the person editing every store sign. Gateway API gives the building owner and store owner separate but connected responsibilities.
Gateway API was designed to make Kubernetes traffic management more expressive and role-oriented than the original Ingress API. Ingress is useful but often overloaded with controller-specific annotations, which blur ownership and make portability harder. Gateway API introduces clearer resources for infrastructure providers, cluster operators, and application teams so each group can manage the layer it actually owns.
The flow starts with GatewayClass, which identifies the implementation or controller. A Gateway represents a deployed data-plane entry point with listeners such as HTTPS on port 443. HTTPRoute, GRPCRoute, or other route resources define application-level routing rules and attach to a Gateway when allowed by namespace and listener policy. The controller reconciles those resources into load balancer, proxy, or service mesh configuration.
In production, Gateway API helps with multi-team environments. Platform engineers can standardize TLS, listener ports, allowed namespaces, and shared infrastructure. App teams can publish or update service routes without editing a central Ingress file. Operators should monitor route attachment status, accepted conditions, listener conflicts, certificate readiness, and controller reconciliation errors. This shifts troubleshooting from annotation archaeology to explicit status fields.
The gotcha is that Gateway API does not magically remove governance. If allowedRoutes is too permissive, one team can still attach unexpected hostnames or paths. If it is too strict, teams see routes that never attach and traffic silently stays on the old path. Architects need namespace policy, hostname ownership, certificate automation, and clear dashboards showing which routes are accepted by which Gateway.
Code Example
# Install Gateway API CRDs for clusters that do not include them yet
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.3.0/standard-install.yaml
# Apply the shared production Gateway owned by the platform team
kubectl apply -f platform-gateway.yaml
# Apply an application route owned by the payments team
kubectl apply -f payments-route.yaml
# Verify whether the route was accepted by the Gateway controller
kubectl get httproute payments-api -n payments -o jsonpath='{.status.parents[*].conditions[*].type}'
# platform-gateway.yaml
apiVersion: gateway.networking.k8s.io/v1 # Uses the stable Gateway API
kind: Gateway # Declares a shared traffic entry point
metadata:
name: public-web # Platform-owned public web Gateway
namespace: platform-ingress # Keeps infrastructure config in a platform namespace
spec:
gatewayClassName: prod-nginx # Selects the installed Gateway controller implementation
listeners:
- name: https # Listener name used by routes and status
protocol: HTTPS # Accepts encrypted HTTP traffic
port: 443 # Standard public HTTPS port
hostname: '*.interviewcatalog.example' # Restricts accepted hostnames to the platform domain
allowedRoutes:
namespaces:
from: Selector # Allows only selected namespaces to attach routes
selector:
matchLabels:
expose-public: 'true' # Namespace opt-in controlled by platform policy
---
apiVersion: gateway.networking.k8s.io/v1 # Uses the stable route API
kind: HTTPRoute # Defines app-owned HTTP routing rules
metadata:
name: payments-api # Route for the payments service
namespace: payments # Owned by the payments application team
spec:
parentRefs:
- name: public-web # Attaches to the platform Gateway
namespace: platform-ingress # References the Gateway namespace explicitly
hostnames:
- payments.interviewcatalog.example # Hostname this team is allowed to serve
rules:
- backendRefs:
- name: payments-api # Kubernetes Service receiving traffic
port: 8080 # Service port for the API backendInterview Tip
A junior engineer typically answers that Gateway API is a newer Ingress, but for a senior/architect role, you need to show ownership and governance design. Explain GatewayClass, Gateway, and HTTPRoute as separate responsibility boundaries, then discuss allowedRoutes, hostname control, route status, and controller reconciliation. A mature answer also compares annotation-heavy Ingress operations with explicit Gateway API status and describes how platform teams can delegate routing safely without becoming a deployment bottleneck.
◈ Architecture Diagram
┌──────────┐
│GatewayCls│
└────┬─────┘
↓
┌──────────┐
│ Gateway │
└────┬─────┘
↓
┌──────────┐
│HTTPRoute │
└────┬─────┘
↓
┌──────────┐
│ Service │
└──────────┘💬 Comments
Quick Answer
Cilium loads eBPF programs into the Linux kernel to handle packet forwarding, service load balancing, network policy, and L7 observability without iptables rules or per-pod sidecar proxies. Architects must evaluate kernel version requirements, observability maturity via Hubble, CNI migration complexity, and the loss of fine-grained L7 control that a full sidecar proxy provides.
Detailed Answer
Think of a highway toll system. Traditional kube-proxy is like a toll booth where every car stops, gets checked, and is directed to its lane. EBPF with Cilium is like an electronic pass reader embedded in the road surface — the car never stops, the toll is processed at wire speed, and the road itself knows which lane to direct traffic into without a booth.
Cilium replaces the iptables-based kube-proxy and the user-space proxy model used by traditional service meshes. Instead of maintaining thousands of iptables rules that the kernel evaluates linearly, Cilium attaches eBPF programs to network hooks inside the kernel. These programs handle service IP translation, load balancing across endpoints, network policy enforcement, and even some L7 protocol parsing without packets ever leaving kernel space. This eliminates the context switches between kernel and user space that Envoy-based sidecars require for every connection.
Internally, Cilium uses several eBPF map types to store service endpoints, identity labels, policy rules, and connection tracking state. When a packet arrives, the eBPF program attached to the network interface or socket looks up the destination service, selects a backend Pod using consistent hashing or round-robin, rewrites headers, and forwards the packet — all within a single kernel function call chain. Hubble, the observability layer built on top of Cilium, taps into these eBPF data paths to provide flow logs, DNS visibility, and HTTP metrics without injecting any proxy.
At production scale, Cilium handles over 5,000 production deployments as of 2025, including platforms at Adobe, Bell Canada, and multiple hyperscalers. Teams should monitor eBPF program load errors, map memory usage, endpoint synchronization latency, dropped flow events in Hubble, and kernel version compatibility. Cilium requires Linux kernel 5.10 or later for full feature support, and some advanced features like bandwidth manager or BBR congestion control need even newer kernels.
The non-obvious gotcha is that Cilium does not fully replicate every L7 feature of Envoy-based meshes. While it handles mTLS via SPIFFE identities, basic HTTP routing, and L7 policy, complex traffic management like retries with budgets, circuit breaking with outlier detection, or gRPC-aware load balancing may still require a sidecar or gateway proxy. Architects should map their actual L7 requirements before declaring a full service mesh unnecessary, because removing sidecars and then re-adding them later is a painful migration.
Code Example
# Install Cilium with kube-proxy replacement enabled on a fresh cluster
helm install cilium cilium/cilium --version 1.16.4 \
--namespace kube-system \
--set kubeProxyReplacement=true \
--set k8sServiceHost=api.payments-cluster.internal \
--set k8sServicePort=6443 \
--set hubble.enabled=true \
--set hubble.relay.enabled=true \
--set hubble.ui.enabled=true
# Verify Cilium replaced kube-proxy and is handling service translation
kubectl -n kube-system exec ds/cilium -- cilium status --verbose | grep KubeProxyReplacement
# View real-time network flows for the payments namespace using Hubble
kubectl -n kube-system exec deploy/hubble-relay -- hubble observe --namespace payments --protocol http
# Check eBPF program load status on a specific node
kubectl -n kube-system exec ds/cilium -- cilium bpf lb list
# Apply an L7 network policy that restricts HTTP methods on the checkout API
apiVersion: cilium.io/v2 # Cilium-specific CRD for extended network policy
kind: CiliumNetworkPolicy # Extends Kubernetes NetworkPolicy with L7 rules
metadata:
name: checkout-api-l7-policy # Policy name describing its scope
namespace: payments # Applies to the payments namespace
spec:
endpointSelector:
matchLabels:
app: checkout-api # Targets the checkout API pods
ingress:
- fromEndpoints:
- matchLabels:
app: web-frontend # Allows traffic only from the frontend
toPorts:
- ports:
- port: "8080" # The checkout API listening port
protocol: TCP # HTTP runs over TCP
rules:
http:
- method: POST # Allows POST for creating orders
path: /api/v2/orders # Restricts to the orders endpoint
- method: GET # Allows GET for reading order status
path: /api/v2/orders/.* # Permits path parameters for order lookupsInterview Tip
A junior engineer typically answers that Cilium is a CNI plugin, but for a senior/architect role, you need to show a systems-level understanding of why eBPF changes the networking stack. Explain how eBPF programs attach to kernel hooks, why this eliminates iptables scaling problems, what Hubble provides for observability, and where Cilium stops short of a full L7 proxy mesh. A strong answer also covers kernel version requirements, the migration path from kube-proxy, and how to evaluate whether your L7 needs actually require sidecar proxies or whether Cilium's native L7 filtering suffices.
◈ Architecture Diagram
┌──────────┐ ┌──────────┐
│ Pod A │ │ Pod B │
└────┬─────┘ └────┬─────┘
│ │
↓ ↓
┌─────────────────────────────┐
│ eBPF (kernel) │
│ ┌────────┐ ┌────────────┐ │
│ │Svc LB │ │L7 Policy │ │
│ └────────┘ └────────────┘ │
│ ┌────────┐ ┌────────────┐ │
│ │ConnTrk │ │Hubble Tap │ │
│ └────────┘ └────────────┘ │
└─────────────────────────────┘💬 Comments
Quick Answer
Karmada provides a centralized control plane with its own API server, scheduler, and controllers that propagate Kubernetes resources to member clusters using PropagationPolicy for placement and OverridePolicy for cluster-specific customization. Architects must decide propagation scope, replica scheduling mode, failover behavior, and how overrides interact with GitOps pipelines.
Detailed Answer
Think of a franchise restaurant chain. The headquarters defines the menu, branding, and recipes, but each location adjusts portion sizes, local ingredients, and opening hours. Karmada works the same way for Kubernetes: you define workloads once in a central control plane, and propagation policies decide which clusters receive them while override policies customize each deployment for its target environment.
Karmada, a CNCF incubation project, addresses the operational complexity of managing workloads across multiple clusters in different regions, clouds, or environments. Its architecture mirrors a single Kubernetes cluster with a dedicated API server, etcd, scheduler, and controller manager, but instead of scheduling Pods to nodes, it schedules resources to member clusters. Users interact with the Karmada API server using standard kubectl commands and Kubernetes manifests.
Internally, PropagationPolicy and ClusterPropagationPolicy define which resources go where. The policy specifies resource selectors, target clusters by name or label, and a scheduling strategy — either Duplicated (run identical copies in every target) or Divided (split replicas across clusters by weight). The Karmada scheduler evaluates cluster health, available capacity, and spread constraints before binding resources. OverridePolicy and ClusterOverridePolicy then patch manifests for each target, adjusting image registries, resource limits, environment variables, or annotations per cluster without modifying the source template.
At production scale, architects must decide several things: whether to use push mode (Karmada pushes to clusters) or pull mode (agents in member clusters pull), how to handle failover when a member cluster becomes unhealthy, how to integrate with existing GitOps tools like ArgoCD or Flux without creating conflicting reconciliation loops, and how to manage RBAC across the Karmada control plane and member clusters. Monitoring should cover resource propagation status, binding conditions, cluster health checks, scheduler decisions, and override application success.
The non-obvious gotcha is that Karmada adds a layer of abstraction that can obscure debugging. When a Deployment fails in a member cluster, the error may appear in the member cluster's events but not bubble up cleanly to the Karmada control plane. Status aggregation across clusters is improving but not yet seamless, and teams that skip investing in cross-cluster observability will struggle with incident response. Architects should also plan for the Karmada control plane itself as a critical dependency — if it goes down, existing workloads continue running but new changes cannot propagate.
Code Example
# Register a member cluster named us-east-prod with the Karmada control plane
karmadactl join us-east-prod --kubeconfig=/etc/karmada/karmada-apiserver.config --cluster-kubeconfig=/etc/kube/us-east-prod.config
# Apply a Deployment to the Karmada API server (not directly to member clusters)
kubectl --kubeconfig=/etc/karmada/karmada-apiserver.config apply -f checkout-api-deploy.yaml
# Create a PropagationPolicy that spreads replicas across two regions
apiVersion: policy.karmada.io/v1alpha1 # Karmada policy API group
kind: PropagationPolicy # Controls where resources are placed
metadata:
name: checkout-api-spread # Names the propagation rule
namespace: payments # Scoped to the payments namespace
spec:
resourceSelectors:
- apiVersion: apps/v1 # Selects Deployment resources
kind: Deployment # Matches the checkout-api Deployment
name: checkout-api # Exact resource name to propagate
placement:
clusterAffinity:
clusterNames: [us-east-prod, eu-west-prod] # Target clusters by name
replicaScheduling:
replicaSchedulingType: Divided # Splits total replicas across clusters
weightPreference:
staticWeightList:
- targetCluster:
clusterNames: [us-east-prod] # Primary region gets more replicas
weight: 3 # Three-fifths of total replicas go to us-east
- targetCluster:
clusterNames: [eu-west-prod] # Secondary region gets fewer replicas
weight: 2 # Two-fifths of total replicas go to eu-west
---
apiVersion: policy.karmada.io/v1alpha1 # Karmada override API group
kind: OverridePolicy # Customizes resources per target cluster
metadata:
name: checkout-api-eu-registry # Override for EU image registry
namespace: payments # Same namespace as the resource
spec:
targetCluster:
clusterNames: [eu-west-prod] # Applies only to the EU cluster
overriders:
imageOverrider:
- component: Registry # Replaces only the registry portion of the image
operator: replace # Overwrites the default registry
value: eu-registry.company.com # EU-local container registryInterview Tip
A junior engineer typically answers that multi-cluster means running kubectl against different contexts, but for a senior/architect role, you need to show control-plane design and policy architecture. Explain Karmada's PropagationPolicy and OverridePolicy separation, the Duplicated versus Divided scheduling modes, push versus pull agent models, and how the Karmada control plane itself becomes a critical dependency. A mature answer also covers GitOps integration challenges, cross-cluster observability gaps, failover behavior when a member cluster goes unhealthy, and RBAC implications across control plane and member clusters.
◈ Architecture Diagram
┌──────────────────────────┐
│ Karmada Control Plane │
│ ┌────────┐ ┌───────────┐ │
│ │API Srv │ │ Scheduler │ │
│ └────────┘ └───────────┘ │
└─────┬──────────┬─────────┘
↓ ↓
┌──────────┐ ┌──────────┐
│us-east │ │eu-west │
│ Cluster │ │ Cluster │
└──────────┘ └──────────┘💬 Comments
Quick Answer
Architects tune etcd by sizing disks for low-latency IOPS, adjusting compaction and defragmentation schedules, monitoring database size and peer latency, and separating the events store. Sharding the main etcd or using virtual clusters becomes necessary when a single etcd instance approaches 8 GB or 30,000-40,000 objects and API server latency degrades.
Detailed Answer
Think of a library card catalog. When the library has a few thousand books, one cabinet handles lookups fine. But when the library grows to millions of books and hundreds of librarians are searching simultaneously, you either need a faster cabinet, multiple cabinets organized by subject, or a way to archive old cards. Etcd is that card catalog for Kubernetes — every resource definition, status update, and event is a card in the catalog. Etcd is the sole persistent store for Kubernetes cluster state. Every API server read and write flows through etcd, making its performance the ceiling for cluster responsiveness. For large clusters — those with tens of thousands of Pods, thousands of Services, or high churn from controllers and operators — etcd becomes the bottleneck before CPU, memory, or network do. The key metrics are fsync latency (which depends on disk IOPS), database size, number of keys, leader election frequency, and peer round-trip time between etcd members.
Internally, etcd uses a B-tree index with multi-version concurrency control, or MVCC, keeping every revision of every key until compacted. Compaction removes old revisions, and defragmentation reclaims disk space after compaction. Without regular compaction, the database grows unboundedly. Kubernetes runs automatic compaction every five minutes by default, but operators must also schedule defragmentation because compaction alone does not free physical disk space. On cloud providers, using provisioned IOPS SSD volumes (like gp3 with 6000+ IOPS on AWS) is critical because etcd performance degrades sharply when fsync latency exceeds 10 milliseconds.
At production scale, the first architectural decision is separating the events store. Kubernetes Events are high-volume, short-lived objects that create write pressure without carrying critical state. Running a dedicated etcd instance for Events reduces load on the main etcd cluster significantly. AWS EKS offers provisioned control plane tiers (XL, 2XL, 4XL) that scale etcd database limits up to 16 GB for clusters running AI and ML workloads with many custom resources. When even separated events and tuned compaction are insufficient, true etcd sharding — distributing different API groups to separate etcd clusters — or virtual clusters that maintain independent etcd instances per tenant become the next scaling lever.
The non-obvious gotcha is that etcd performance problems often manifest as API server timeouts or slow kubectl responses, and teams blame the API server rather than looking at etcd disk latency. A single slow etcd member in a three-node cluster can drag down the entire quorum because the leader waits for a majority of followers to acknowledge writes. Architects should alert on p99 fsync duration, database size approaching 8 GB, and any leader changes, because a leader election storm during high write load can cascade into control-plane unavailability.
Code Example
# Check etcd database size and key count on the leader member ETCDCTL_API=3 etcdctl endpoint status --endpoints=https://etcd-0.etcd.kube-system:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key \ --write-out=table # Monitor fsync latency histogram from Prometheus metrics curl -s https://etcd-0.etcd.kube-system:2379/metrics | grep etcd_disk_wal_fsync_duration_seconds # Trigger a manual defragmentation on a specific member during a maintenance window ETCDCTL_API=3 etcdctl defrag --endpoints=https://etcd-1.etcd.kube-system:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key # Configure API server to use a separate etcd instance for Event objects # In kube-apiserver manifest or startup flags: # --etcd-servers=https://etcd-main:2379 # Main etcd for all resources # --etcd-servers-overrides=/events#https://etcd-events:2379 # Separate etcd for Events # Check Kubernetes object counts by resource type to identify growth kubectl get --raw='/metrics' | grep apiserver_storage_objects | sort -t' ' -k2 -rn | head -20
Interview Tip
A junior engineer typically answers that etcd stores cluster state, but for a senior/architect role, you need to show operational depth. Explain how MVCC and compaction work, why fsync latency on the underlying disk is the primary performance constraint, how to separate the events store, and what happens during leader elections under load. A mature answer also discusses the 8 GB practical limit for single-instance etcd, when sharding across API groups becomes necessary, and why a slow follower in a three-node quorum can bring down the entire control plane. Bonus points for mentioning provisioned IOPS requirements and monitoring strategies.
◈ Architecture Diagram
┌──────────┐ │API Server│ └──┬───┬───┘ │ │ ↓ ↓ ┌─────┐ ┌─────────┐ │Main │ │Events │ │etcd │ │etcd │ │< 8GB│ │separate │ └─────┘ └─────────┘ │ ┌──┴──────────┐ │Compact+Defrag│ └──────────────┘
💬 Comments
Quick Answer
Istio ambient mesh replaces per-pod Envoy sidecars with two shared components: ztunnel, a per-node L4 proxy handling mTLS and basic routing, and optional waypoint proxies for L7 policy. Architects must evaluate the migration path for existing sidecar workloads, L7 feature parity, multi-cluster ambient support maturity, and the operational tradeoff of shared node-level proxies versus isolated per-pod proxies.
Detailed Answer
Think of an apartment building with two security options. The sidecar model gives every apartment its own security guard who checks visitors at the apartment door — effective but expensive. The ambient model puts a guard at the building entrance who checks IDs for everyone, and only apartments that need advanced screening get a shared floor-level inspector. You get security everywhere with far fewer guards.
Istio ambient mesh reached general availability with Istio 1.22 in late 2024 and has become production-stable through 2025 and into 2026. It fundamentally changes how the data plane is deployed. Traditional Istio injects an Envoy sidecar into every Pod, which adds memory overhead (typically 50-100 MB per Pod), increases startup latency, and creates operational complexity around sidecar injection, upgrade ordering, and resource accounting. Ambient mesh removes all of this by separating L4 and L7 concerns into shared infrastructure.
The architecture has two layers. Ztunnel is a lightweight Rust-based proxy that runs as a DaemonSet on every node. It handles all L4 concerns: mTLS encryption and identity using SPIFFE certificates, TCP-level authorization policy, and basic connection routing. Ztunnel performance has improved 75 percent over recent releases and adds negligible latency. For workloads that need L7 features — HTTP routing, retries, header-based authorization, traffic splitting — architects deploy waypoint proxies, which are shared Envoy instances scoped to a namespace or service account rather than injected per Pod.
In production migration, teams should start by enabling ambient mode on a namespace using the label istio.io/dataplane-mode=ambient. Existing sidecar workloads can coexist with ambient workloads during migration. The key evaluation points are: L7 feature gaps between sidecar and waypoint proxy configurations, whether multi-cluster ambient mesh is mature enough (alpha planned for Istio 1.27), how existing Istio AuthorizationPolicy and VirtualService resources translate, and whether shared ztunnel on a node creates a blast radius concern where a ztunnel crash affects all Pods on that node.
The non-obvious gotcha is that ambient mesh changes the failure domain. In sidecar mode, a proxy crash affects one Pod. In ambient mode, a ztunnel crash can disrupt networking for every Pod on that node. This makes ztunnel reliability, resource limits, and upgrade strategy (rolling DaemonSet updates) critical. Architects should also verify that their observability stack captures ztunnel metrics and waypoint proxy metrics in the same dashboards, because the telemetry surface shifts from per-pod to per-node and per-namespace.
Code Example
# Enable ambient mesh mode on the payments namespace
kubectl label namespace payments istio.io/dataplane-mode=ambient
# Verify ztunnel is running on every node in the mesh
kubectl get pods -n istio-system -l app=ztunnel -o wide
# Deploy a waypoint proxy for L7 policy in the payments namespace
istioctl waypoint apply --namespace payments --name payments-waypoint
# Verify the waypoint proxy is ready and accepting traffic
kubectl get gateway payments-waypoint -n payments
# Apply an L7 AuthorizationPolicy that requires the waypoint proxy
apiVersion: security.istio.io/v1 # Istio security API for authorization
kind: AuthorizationPolicy # Controls which requests are allowed
metadata:
name: checkout-api-auth # Policy name describing its scope
namespace: payments # Namespace where the waypoint proxy runs
spec:
targetRefs:
- kind: Service # Targets a specific Kubernetes Service
group: "" # Core API group
name: checkout-api # The service to protect
action: ALLOW # Permits matching requests
rules:
- from:
- source:
principals: ["cluster.local/ns/payments/sa/web-frontend"] # SPIFFE identity of the caller
to:
- operation:
methods: ["POST"] # Allows only POST requests
paths: ["/api/v2/orders"] # Restricts to the orders endpoint
# Check ztunnel connection metrics on a specific node
kubectl -n istio-system exec ds/ztunnel -- curl -s localhost:15020/metrics | grep ztunnel_tcp_connectionsInterview Tip
A junior engineer typically answers that Istio uses sidecar proxies for service mesh, but for a senior/architect role, you need to show awareness of the ambient mesh architecture shift and its production implications. Explain the two-layer model of ztunnel for L4 and waypoint proxies for L7, why this reduces resource overhead by removing per-pod Envoy sidecars, and how the failure domain changes from per-pod to per-node. A mature answer also covers the migration path from sidecar to ambient, coexistence during rollout, multi-cluster ambient maturity, and the observability changes when telemetry moves from sidecar metrics to ztunnel and waypoint metrics.
◈ Architecture Diagram
┌───── Node ─────────────────┐
│ ┌────────┐ ┌────────┐ │
│ │ Pod A │ │ Pod B │ │
│ │(no sidecar)(no sidecar) │
│ └───┬────┘ └───┬────┘ │
│ └─────┬─────┘ │
│ ┌─────┴─────┐ │
│ │ ztunnel │ (L4) │
│ │ mTLS+auth │ │
│ └─────┬─────┘ │
└───────────┼───────────────┘
↓
┌──────────┐
│ Waypoint │ (L7)
│ Proxy │
└──────────┘💬 Comments
Quick Answer
Topology spread constraints tell the scheduler to distribute Pods across failure domains defined by node labels such as zone or hostname, using maxSkew to control imbalance. When combined with cluster autoscaling, problems arise if a zone has zero nodes — the autoscaler may not know about the zone, causing the scheduler to leave Pods pending indefinitely.
Detailed Answer
Think of seating guests at a wedding reception. You want to spread friends evenly across tables so no table is overcrowded and no group is isolated. The wedding planner checks how many people are at each table and seats the next guest at the most empty one, but if a table does not exist yet (no physical table has been set up), the planner cannot seat anyone there even if the venue has room. Topology spread constraints in Kubernetes work the same way.
Kubernetes topology spread constraints are declared in the Pod spec under topologySpreadConstraints. Each constraint specifies a topologyKey (a node label like topology.kubernetes.io/zone or kubernetes.io/hostname), a maxSkew (the maximum allowed difference in Pod count between the most-populated and least-populated domain), a whenUnsatisfiable behavior (DoNotSchedule or ScheduleAnyway), and a labelSelector to identify which Pods count toward the spread calculation.
Internally, the scheduler evaluates topology spread during the Filter and Score phases. In the Filter phase, it eliminates nodes where placing the Pod would violate the maxSkew when whenUnsatisfiable is DoNotSchedule. In the Score phase, it ranks remaining nodes by how well they balance the distribution. The scheduler considers the topologyKey label on existing nodes to define domains — a domain only exists if at least one node carries that label value. It then counts matching Pods per domain and calculates whether the new Pod can land in each domain without exceeding maxSkew.
At production scale, the interaction with cluster autoscaling creates subtle failures. If a node pool in one availability zone scales to zero, that zone disappears from the scheduler's topology map. The scheduler only sees zones with active nodes, so it may consider a two-zone spread sufficient even when three zones are available. When maxSkew is 1 and whenUnsatisfiable is DoNotSchedule, the scheduler can leave Pods pending because it cannot place them in a zone that has no nodes, and the autoscaler may not create a node in the missing zone because it does not see pending Pods that specifically require it. This chicken-and-egg problem is one of the most common production issues with topology spread constraints.
The non-obvious gotcha is that topology spread constraints count all matching Pods, including ones that are terminating, not-ready, or failing. During a rolling update, old Pods being terminated still count toward the spread calculation, which can cause new Pods to be unschedulable until the old ones are fully removed. Architects should set minDomains to explicitly declare how many zones the spread should consider, use node affinity in combination with spread constraints to ensure the autoscaler knows about expected zones, and monitor for unschedulable Pods with topology spread violation events.
Code Example
# Apply a Deployment with zone and node spread constraints
apiVersion: apps/v1 # Stable Deployment API
kind: Deployment # Manages replicated Pods
metadata:
name: checkout-api # Production checkout service
namespace: payments # Team namespace
spec:
replicas: 6 # Six replicas to spread across three zones with two per zone
selector:
matchLabels:
app: checkout-api # Pod selector
template:
metadata:
labels:
app: checkout-api # Label used by spread constraint selector
spec:
topologySpreadConstraints:
- maxSkew: 1 # Allows at most one Pod difference between zones
topologyKey: topology.kubernetes.io/zone # Spreads across availability zones
whenUnsatisfiable: DoNotSchedule # Strictly enforces zone balance
labelSelector:
matchLabels:
app: checkout-api # Counts only checkout-api Pods
minDomains: 3 # Expects three zones even if some have zero nodes
- maxSkew: 1 # Allows at most one Pod difference between nodes within a zone
topologyKey: kubernetes.io/hostname # Spreads across individual nodes
whenUnsatisfiable: ScheduleAnyway # Prefers balance but allows imbalance
labelSelector:
matchLabels:
app: checkout-api # Counts only checkout-api Pods
containers:
- name: api # Application container
image: registry.company.com/checkout-api:3.7.2 # Versioned production image
resources:
requests:
cpu: 250m # Minimum CPU for scheduling
memory: 512Mi # Minimum memory for scheduling
# Check Pod distribution across zones
kubectl get pods -n payments -l app=checkout-api -o custom-columns='POD:.metadata.name,NODE:.spec.nodeName,ZONE:.metadata.labels.topology\.kubernetes\.io/zone'
# Identify Pods pending due to topology spread violations
kubectl get events -n payments --field-selector reason=FailedScheduling | grep topologyInterview Tip
A junior engineer typically answers that topology spread constraints distribute Pods across nodes, but for a senior/architect role, you need to show scheduler internals and failure mode awareness. Explain how the scheduler discovers topology domains from node labels, why maxSkew and whenUnsatisfiable interact differently in Filter versus Score phases, and what happens when a zone has zero nodes during autoscaling. A strong answer also covers minDomains as a mitigation, the impact of terminating Pods on spread calculations during rolling updates, and how to monitor for topology-related scheduling failures in production.
◈ Architecture Diagram
┌─── Zone A ──┐ ┌─── Zone B ──┐ ┌─── Zone C ──┐ │ ┌────┐┌────┐│ │ ┌────┐┌────┐│ │ ┌────┐┌────┐│ │ │Pod1││Pod2││ │ │Pod3││Pod4││ │ │Pod5││Pod6││ │ └────┘└────┘│ │ └────┘└────┘│ │ └────┘└────┘│ │ maxSkew=1 │ │ maxSkew=1 │ │ maxSkew=1 │ └─────────────┘ └─────────────┘ └─────────────┘
💬 Comments
Quick Answer
VPA and HPA conflict when both scale on the same metric because VPA resizes Pods while HPA changes Pod count, creating feedback loops. The safe pattern is VPA on memory and HPA on CPU or custom metrics, or VPA in recommendation-only mode. KEDA fits better for event-driven workloads that scale to zero or react to external queue depth rather than Pod-level CPU or memory.
Detailed Answer
Think of a restaurant kitchen during dinner rush. The horizontal approach is adding more cooks to handle more orders. The vertical approach is giving each cook a bigger stove and more counter space so they can cook faster. If you try both approaches based on the same signal — how backed up the order queue is — the kitchen oscillates between adding cooks and upgrading stoves in a confusing loop. You need different signals for each scaling axis.
The Horizontal Pod Autoscaler watches metrics like CPU use, memory use, or custom metrics, and adjusts the number of Pod replicas to meet a target value. The Vertical Pod Autoscaler observes actual resource consumption over time and recommends or applies changes to the resource requests and limits of individual Pods. When both use CPU as their metric, VPA may increase a Pod's CPU request, which changes the Pod's use percentage, which then triggers HPA to scale down replicas, which increases use again, creating an oscillation loop.
The recommended production pattern separates their concerns. Run VPA in recommendation mode (updateMode: Off) or target only memory, while HPA scales on CPU or custom metrics. Alternatively, use VPA in Auto mode for stateful workloads where horizontal scaling is impractical — databases, caches, or single-instance controllers — and reserve HPA for stateless services that benefit from replica scaling. Some teams use VPA recommendations to set initial resource requests in CI/CD pipelines rather than letting VPA mutate Pods at runtime, which avoids the Pod restart that VPA triggers when updating in-place.
At production scale, KEDA (Kubernetes Event-Driven Autoscaler) fills a gap that neither HPA nor VPA addresses well. KEDA scales based on external event sources — message queue depth in Kafka or SQS, pending items in a Redis stream, HTTP request rate from Prometheus, or custom metrics from any source with a KEDA scaler. Critically, KEDA can scale Deployments to zero replicas when there is no work, which standard HPA cannot do (HPA's minimum is one). This makes KEDA the right choice for batch processors, event consumers, and asynchronous workers where idle cost matters. KEDA works by deploying ScaledObject resources that create HPA objects under the hood, so it integrates with existing Kubernetes autoscaling infrastructure.
The non-obvious gotcha is that VPA in Auto mode restarts Pods when it changes resource requests, which can cause brief service disruption and interact badly with PodDisruptionBudget limits. Teams often discover this during peak traffic when VPA decides to right-size all replicas simultaneously. Architects should set VPA update policies with minReplicas and eviction requirements, and test VPA behavior during high-traffic scenarios before enabling Auto mode on critical services.
Code Example
# Deploy VPA in recommendation mode for a service — no automatic Pod restarts
apiVersion: autoscaling.k8s.io/v1 # VPA API group
kind: VerticalPodAutoscaler # Recommends or applies resource changes
metadata:
name: checkout-api-vpa # VPA for the checkout service
namespace: payments # Same namespace as the target
spec:
targetRef:
apiVersion: apps/v1 # References a Deployment
kind: Deployment # Target workload type
name: checkout-api # The Deployment to analyze
updatePolicy:
updateMode: "Off" # Generates recommendations without applying them
resourcePolicy:
containerPolicies:
- containerName: api # Target the main container
minAllowed:
cpu: 100m # Never recommend below 100m CPU
memory: 256Mi # Never recommend below 256Mi memory
maxAllowed:
cpu: 2 # Cap recommendations at 2 CPU cores
memory: 4Gi # Cap recommendations at 4Gi memory
# Deploy HPA scaling on CPU utilization (safe alongside VPA on memory)
apiVersion: autoscaling/v2 # HPA v2 API for custom metrics support
kind: HorizontalPodAutoscaler # Scales replica count
metadata:
name: checkout-api-hpa # HPA for the checkout service
namespace: payments # Same namespace
spec:
scaleTargetRef:
apiVersion: apps/v1 # References the same Deployment
kind: Deployment # Target workload type
name: checkout-api # The Deployment to scale
minReplicas: 3 # Never scale below three replicas
maxReplicas: 20 # Cap at twenty replicas
metrics:
- type: Resource # Uses built-in resource metrics
resource:
name: cpu # Scales on CPU utilization only
target:
type: Utilization # Target a percentage of requests
averageUtilization: 70 # Scale up when average CPU exceeds 70 percent
# Read VPA recommendations without applying them
kubectl get vpa checkout-api-vpa -n payments -o jsonpath='{.status.recommendation.containerRecommendations[*]}'Interview Tip
A junior engineer typically answers that HPA scales Pods horizontally and VPA scales vertically, but for a senior/architect role, you need to show conflict resolution and pattern selection. Explain why VPA and HPA cannot both use CPU, how to separate them by metric (VPA on memory, HPA on CPU or custom metrics), when VPA recommendation mode is safer than Auto mode, and when KEDA is the right choice over both. A mature answer also covers VPA Pod restart behavior and its interaction with PodDisruptionBudgets, KEDA's ability to scale to zero, and the pipeline pattern of using VPA recommendations to set resource requests in CI/CD rather than at runtime.
◈ Architecture Diagram
┌─────────────────────────────────┐ │ Scaling Decision │ ├───────────┬───────────┬─────────┤ │ HPA │ VPA │ KEDA │ │ ─ ─ ─ ─ │ ─ ─ ─ ─ │ ─ ─ ─ │ │ CPU/custom│ Memory │ Queue │ │ → replicas│ → sizing │ → 0..N │ │ stateless │ stateful │ events │ └───────────┴───────────┴─────────┘
💬 Comments
Quick Answer
WebAssembly workloads run on Kubernetes through containerd shims like runwasi that register WASM runtimes (Wasmtime, Spin, WasmEdge) as alternative container runtimes. WASM offers sub-millisecond cold starts and strong sandboxing, but lacks full POSIX support, mature networking, filesystem access, and the ecosystem tooling that OCI containers provide for general-purpose microservices.
Detailed Answer
Think of shipping containers at a port. Standard OCI containers are like the steel containers that fit every crane, truck, and ship — universal, well-understood, and can carry anything. WebAssembly modules are like pneumatic tube capsules that travel at incredible speed through dedicated tubes, but they only carry small, specific items and the tube network does not reach everywhere yet. WASM is not replacing the shipping container; it is adding a fast lane for workloads that fit.
WebAssembly, originally designed for browser execution, has expanded to server-side use through the WebAssembly System Interface (WASI), which was stabilized in late 2024. WASI provides a portable, capability-based interface for WASM modules to access host features like networking, file I/O, and HTTP. On Kubernetes, WASM workloads are scheduled using standard Pod specs but with a RuntimeClass that points to a containerd shim like runwasi. The shim loads the WASM module into a runtime like Wasmtime or Spin instead of starting a Linux container. From the scheduler's perspective, the Pod looks normal; from the node's perspective, the workload starts in under a millisecond and uses a fraction of the memory.
The integration architecture uses Kubernetes RuntimeClass to route Pods to the correct runtime. A node must have the WASM containerd shim installed alongside the standard runc shim. Spin, which joined the CNCF Sandbox, provides a higher-level developer experience where WASM components are defined as Spin applications and deployed as Kubernetes Deployments with the appropriate RuntimeClass annotation. At SUSECON 2025, Fermyon demonstrated sub-0.5ms cold starts for WASM functions on Kubernetes, compared to hundreds of milliseconds for traditional container cold starts.
In production, WASM is ready for specific workload profiles: computationally intensive serverless functions, plugin systems requiring sandboxed execution, edge deployments with extreme startup-time constraints, and short-lived event processors. However, WASM containers running inside Docker's containerd integration are actually 65-325ms slower than regular containers for startup because the WASM runtime initialization inside a container adds overhead. The sub-millisecond benefit only applies to native WASM runtimes, not WASM-inside-containers. General-purpose web applications and stateful microservices still face gaps in mature networking, persistent storage, debugging tools, and library ecosystem support.
The non-obvious gotcha is assuming WASM replaces containers today. The component model — allowing WASM modules to compose with each other — is still maturing. Most production WASM on Kubernetes in 2026 runs alongside containers, not instead of them. Architects should use WASM for high-density, short-lived, compute-bound functions and keep traditional containers for everything else, planning for coexistence rather than migration.
Code Example
# Create a RuntimeClass that uses the Spin WASM containerd shim
apiVersion: node.k8s.io/v1 # RuntimeClass API
kind: RuntimeClass # Declares an alternative container runtime
metadata:
name: wasmtime-spin # Name referenced by Pod specs
handler: spin # The containerd shim handler name
# Deploy a WASM-based request classifier as a Kubernetes Deployment
apiVersion: apps/v1 # Standard Deployment API
kind: Deployment # Manages replicated WASM workloads like any other Deployment
metadata:
name: fraud-classifier # ML inference function compiled to WASM
namespace: ml-platform # Team namespace
spec:
replicas: 10 # High replica count is practical because WASM Pods use minimal memory
selector:
matchLabels:
app: fraud-classifier # Pod selector label
template:
metadata:
labels:
app: fraud-classifier # Label for Service discovery
spec:
runtimeClassName: wasmtime-spin # Routes this Pod to the WASM runtime
containers:
- name: classifier # Container name (runs a WASM module, not a Linux process)
image: registry.company.com/fraud-classifier:1.2.0-wasm # OCI image containing the WASM binary
resources:
requests:
cpu: 50m # WASM modules are lightweight, need minimal CPU
memory: 32Mi # Typical WASM workload uses far less memory than a container
limits:
cpu: 100m # Prevents runaway computation
memory: 64Mi # Hard limit for the WASM sandbox
# Verify the WASM Pods are running with the correct RuntimeClass
kubectl get pods -n ml-platform -l app=fraud-classifier -o custom-columns='NAME:.metadata.name,RUNTIME:.spec.runtimeClassName,STATUS:.status.phase'
# Check containerd shim availability on a node
crictl info | grep -A5 spinInterview Tip
A junior engineer typically answers that WASM is a faster alternative to containers, but for a senior/architect role, you need to show nuanced production readiness assessment. Explain how RuntimeClass and containerd shims integrate WASM into the Kubernetes scheduling model, why sub-millisecond cold starts only apply to native WASM runtimes (not WASM-inside-containers), what WASI provides and what it still lacks, and which workload profiles genuinely benefit from WASM today. A mature answer also covers the coexistence strategy — running WASM and OCI containers on the same nodes — and why the WASM component model maturity determines when broader adoption becomes practical.
◈ Architecture Diagram
┌───── Node ──────────────────┐ │ ┌──────────┐ ┌────────────┐ │ │ │ OCI Pod │ │ WASM Pod │ │ │ │ (runc) │ │ (runwasi) │ │ │ └────┬─────┘ └─────┬──────┘ │ │ └──────┬──────┘ │ │ ┌──────┴──────┐ │ │ │ containerd │ │ │ │ ┌────┐┌────┐│ │ │ │ │runc││spin││ │ │ │ └────┘└────┘│ │ │ └─────────────┘ │ └─────────────────────────────┘
💬 Comments
Quick Answer
An Operator extends Kubernetes with a Custom Resource Definition and a controller that watches for changes to that resource, then reconciles the actual state toward the desired state in a loop. Unreliable operators result from non-idempotent reconciliation, missing status updates, unbounded requeue storms, lack of finalizers for cleanup, and not handling partial failures during multi-step workflows.
Detailed Answer
Think of a building superintendent who checks every apartment every morning. If a tenant requested a repair, the superintendent inspects the current state, compares it to the work order, does what is needed, and notes the result. If the superintendent is interrupted mid-repair, they pick up where they left off the next morning because the work order still exists. A Kubernetes Operator controller works the same way — it continuously reconciles the world toward what the Custom Resource says it should be.
The Operator pattern was introduced to encode human operational knowledge into software. A CRD (Custom Resource Definition) defines the schema for a new Kubernetes resource — for example, a PostgresCluster resource that describes a desired database cluster. The controller watches the API server for changes to these resources and runs a Reconcile function each time a resource is created, modified, deleted, or when a requeue timer fires. The Reconcile function reads the current resource state, inspects the actual cluster state (Pods, Services, PVCs, ConfigMaps), computes the difference, and takes action to close the gap.
Internally, the controller uses an informer to cache resource state locally and a work queue to process reconciliation events. When the informer detects a change, it enqueues the resource key. The controller dequeues it and calls Reconcile. If Reconcile returns an error or a requeue request, the controller puts the key back in the queue with exponential backoff. This level-triggered design means the controller does not need to track every individual event — it just needs to know the current desired state and current actual state each time Reconcile runs.
At production scale, the most common architectural mistakes are: writing non-repeatable without side effects (idempotent) reconciliation logic that creates duplicate resources on re-runs, failing to update the status subresource with meaningful conditions so users and other controllers cannot observe progress, creating requeue storms by always returning a short requeue interval regardless of whether work is needed, skipping finalizers so external resources (cloud load balancers, DNS entries, storage) are orphaned when the Custom Resource is deleted, and attempting complex multi-step workflows in a single Reconcile call without checkpointing progress in status.
The non-obvious gotcha is that Reconcile can be called at any time for any reason — not just when the Custom Resource changes. Node pressure, informer resyncs, and dependent resource changes all trigger reconciliation. If Reconcile has side effects that are not idempotent (like sending a notification or creating a cloud resource without checking if it already exists), the operator will misbehave under real-world conditions. Architects should design every operation inside Reconcile to be safely repeatable and should test operators with chaos engineering to ensure they recover from partial failures and controller restarts.
Code Example
# Operator CRD for a managed PostgreSQL cluster
apiVersion: apiextensions.k8s.io/v1 # CRD API group
kind: CustomResourceDefinition # Extends the Kubernetes API with a new resource
metadata:
name: postgresclusters.database.company.com # Fully qualified CRD name
spec:
group: database.company.com # API group for the custom resource
names:
kind: PostgresCluster # Resource kind used in YAML manifests
plural: postgresclusters # Plural form for kubectl and API paths
singular: postgrescluster # Singular form for display
shortNames: [pgc] # Shorthand for kubectl (kubectl get pgc)
scope: Namespaced # Each instance lives in a namespace
versions:
- name: v1 # API version
served: true # Accept requests for this version
storage: true # Store resources in this version
subresources:
status: {} # Enables the status subresource for condition updates
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
replicas:
type: integer # Number of PostgreSQL instances
version:
type: string # PostgreSQL major version (e.g., "16")
storage:
type: string # PVC size (e.g., "100Gi")
---
# Create a PostgreSQL cluster using the custom resource
apiVersion: database.company.com/v1 # Uses the CRD defined above
kind: PostgresCluster # Declares intent for a managed PostgreSQL cluster
metadata:
name: payments-db # Database for the payments service
namespace: payments # Team namespace
spec:
replicas: 3 # Three-member cluster for HA with one primary and two replicas
version: "16" # PostgreSQL 16 for latest features
storage: 100Gi # Each instance gets a 100Gi PVC
# Check the operator's reconciliation status and conditions
kubectl get pgc payments-db -n payments -o jsonpath='{.status.conditions[*]}'Interview Tip
A junior engineer typically answers that an Operator automates application management, but for a senior/architect role, you need to show reconciliation loop design and failure handling. Explain level-triggered versus edge-triggered design, why idempotency is non-negotiable in Reconcile, how the work queue and informer cache interact, and what happens when Reconcile is called during a partial failure state. A mature answer also discusses finalizer patterns for cleanup, status condition conventions for observability, exponential backoff for requeueing, and the testing strategies (like envtest and chaos engineering) needed to validate operator reliability under real production conditions.
◈ Architecture Diagram
┌──────────┐
│ CRD │
│ Spec │
└────┬─────┘
↓
┌──────────┐
│ Informer │
│ Cache │
└────┬─────┘
↓
┌──────────┐
│WorkQueue │
└────┬─────┘
↓
┌──────────┐
│Reconcile │→ Create/Update
│ Loop │→ Status
└────┬─────┘
↓
┌──────────┐
│ Requeue │
└──────────┘💬 Comments
Quick Answer
Karpenter provisions nodes directly from cloud provider APIs based on pending Pod requirements, selecting instance types dynamically without predefined node groups. Cluster Autoscaler adjusts the size of existing node groups. Karpenter is faster and more flexible for heterogeneous workloads, while Cluster Autoscaler is simpler for teams with well-defined node group templates and multi-cloud portability needs.
Detailed Answer
Think of hiring staff for a catering company. Cluster Autoscaler is like posting a job ad for a specific role — you have predefined job descriptions (node groups), and when you need more people, you hire from those templates. Karpenter is like a staffing agency that looks at the actual tasks on the board, finds a person with exactly the right skills and availability, and places them immediately. The agency is faster and more flexible, but you need to trust it with your hiring criteria.
Cluster Autoscaler has been the standard Kubernetes node scaling solution since early Kubernetes versions. It works by monitoring pending Pods that cannot be scheduled due to insufficient resources, then scaling up one of the configured node groups (Auto Scaling Groups on AWS, Managed Instance Groups on GCP, or VM Scale Sets on Azure). It also scales down by identifying underutilized nodes and draining them. The key limitation is that node groups must be pre-configured with specific instance types, and the autoscaler chooses among existing groups rather than selecting the optimal instance type for each workload.
Karpenter, originally created by AWS and now a CNCF project, takes a fundamentally different approach. Instead of managing node groups, Karpenter watches for unschedulable Pods and directly provisions compute from the cloud provider API, choosing the best instance type based on Pod resource requirements, node selectors, affinity rules, and topology spread constraints. NodePool resources define constraints like allowed instance families, availability zones, capacity types (on-demand or spot), and expiration policies. Karpenter evaluates all pending Pods together and can bin-pack them onto a single optimally-sized instance rather than scaling a node group one unit at a time.
At production scale, Karpenter typically provisions nodes in under 60 seconds compared to 2-5 minutes for Cluster Autoscaler, because it skips the Auto Scaling Group scaling process and calls the EC2 API directly. Karpenter also handles node disruption proactively through consolidation, which replaces underutilized nodes with cheaper or better-fitting ones, and expiration, which rotates nodes to pick up AMI updates. However, Karpenter is currently most mature on AWS, with Azure support in development and GCP support community-driven. Teams needing multi-cloud portability or running on GKE or AKS may still prefer Cluster Autoscaler.
The non-obvious gotcha is that Karpenter's flexibility requires careful constraint definition. Without proper NodePool limits on instance families, maximum Pods per node, or total cluster capacity, Karpenter can provision very large or very expensive instances. It can also create infrastructure drift if the team's Terraform or IaC does not account for Karpenter-managed nodes. Architects should set explicit NodePool constraints, integrate Karpenter's provisioned nodes into their monitoring and cost dashboards, and understand that Karpenter manages node lifecycle independently of any external node group definition.
Code Example
# Karpenter NodePool that provisions cost-optimized compute for general workloads
apiVersion: karpenter.sh/v1 # Karpenter API group
kind: NodePool # Defines provisioning constraints and behavior
metadata:
name: general-workloads # Pool name for general-purpose services
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type # Defines instance purchasing model
operator: In
values: [on-demand, spot] # Allows both on-demand and spot instances
- key: node.kubernetes.io/instance-type # Limits instance families
operator: In
values: [m6i.large, m6i.xlarge, m7i.large, m7i.xlarge, c6i.large, c6i.xlarge] # Curated list of right-sized instances
- key: topology.kubernetes.io/zone # Constrains to specific zones
operator: In
values: [us-east-1a, us-east-1b, us-east-1c] # All three availability zones
nodeClassRef:
group: karpenter.k8s.aws # AWS-specific node configuration
kind: EC2NodeClass # References the EC2 node template
name: general-al2023 # Node class with AL2023 AMI and security groups
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized # Replaces wasteful nodes automatically
consolidateAfter: 30s # Waits 30 seconds before consolidating
limits:
cpu: 200 # Maximum total CPU across all nodes in this pool
memory: 800Gi # Maximum total memory across all nodes in this pool
---
apiVersion: karpenter.k8s.aws/v1 # AWS-specific Karpenter API
kind: EC2NodeClass # Configures the EC2 instance template
metadata:
name: general-al2023 # Referenced by the NodePool above
spec:
amiSelectorTerms:
- alias: al2023@latest # Uses the latest Amazon Linux 2023 EKS-optimized AMI
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: payments-cluster # Discovers subnets by tag
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: payments-cluster # Discovers security groups by tag
# Check which instances Karpenter provisioned and why
kubectl get nodeclaims -o custom-columns='NAME:.metadata.name,TYPE:.status.instanceType,ZONE:.metadata.labels.topology\.kubernetes\.io/zone,CAPACITY:.metadata.labels.karpenter\.sh/capacity-type'Interview Tip
A junior engineer typically answers that Karpenter and Cluster Autoscaler both add nodes, but for a senior/architect role, you need to show architectural differences in the provisioning model. Explain that Cluster Autoscaler scales pre-defined node groups while Karpenter selects instance types dynamically from cloud APIs, why Karpenter is faster (no ASG intermediary), how consolidation and expiration replace the scale-down logic, and when Cluster Autoscaler is still the right choice (multi-cloud, GKE/AKS, or teams that want IaC-managed node groups). A strong answer also covers NodePool constraint design, cost control through instance family limits, and the integration challenges when Karpenter-managed nodes exist alongside Terraform-managed infrastructure.
◈ Architecture Diagram
┌──────────────────────────────────────┐ │ Cluster Autoscaler │ │ Pending → ASG Scale → Node Ready │ │ (2-5 min, fixed instance types) │ └──────────────────────────────────────┘ ┌──────────────────────────────────────┐ │ Karpenter │ │ Pending → EC2 API → Node Ready │ │ (<60s, dynamic instance selection) │ └──────────────────────────────────────┘
💬 Comments
Quick Answer
vcluster runs a lightweight Kubernetes control plane (API server, controller manager, and backing store) inside a namespace of a host cluster, giving each tenant their own API server, CRDs, RBAC, and resource naming while sharing the host cluster's compute nodes. It provides stronger isolation than namespaces but avoids the operational cost of fully separate clusters.
Detailed Answer
Think of a coworking space. Dedicated clusters are like each company renting its own building — maximum isolation but expensive. Namespaces are like shared open-plan desks — cheap but noisy, with limited privacy. Virtual clusters are like private offices inside the coworking building — each company gets its own locked room with its own reception desk, mailbox, and key card system, while sharing the building's elevators, HVAC, and parking garage. Vcluster creates a virtual Kubernetes cluster by running a minimal control plane (kube-apiserver, kube-controller-manager, and a backing store like SQLite, embedded etcd, or external etcd) as a StatefulSet inside a host cluster namespace. Tenants interact with the virtual cluster's API server using their own kubeconfig. When a tenant creates a Deployment, the virtual cluster's syncer component translates it into real resources on the host cluster — Pods run on host nodes, but they appear to live in the virtual cluster's namespace. From the tenant's perspective, they have full cluster-admin access, can install their own CRDs, create any namespace, and manage RBAC independently.
The isolation boundaries differ significantly from plain namespaces. With namespaces, tenants share the same API server, the same CRDs, the same admission policies, and the same RBAC system. One tenant's misconfigured webhook or CRD can affect all tenants. With vcluster, each tenant has independent RBAC, CRDs, admission policies, and namespace naming. The host cluster enforces compute isolation through resource quotas, network policies, and Pod security standards on the host namespace that backs the virtual cluster. The syncer translates virtual resources to host resources with name-mangling to prevent conflicts.
At production scale, virtual clusters are used for development environments, CI/CD isolation, multi-tenant SaaS platforms, and testing environments where each team needs cluster-admin access without the cost of dedicated clusters. The host cluster operator controls compute density, network isolation, storage classes, and node pool assignments. Monitoring should cover the virtual cluster control plane resource usage, syncer health, host namespace resource quotas, and any mismatch between virtual and host resource state.
The non-obvious gotcha is that vcluster does not provide kernel-level isolation. Virtual cluster Pods run on the same host nodes and share the same Linux kernel, container runtime, and network fabric as other tenants. A container escape or kernel vulnerability affects all tenants on that node. For hard multi-tenancy with untrusted workloads, architects still need dedicated node pools per tenant, Pod sandboxing (gVisor or Kata Containers), or fully separate clusters. Virtual clusters are best suited for soft multi-tenancy where tenants are trusted but need operational independence.
Code Example
# Create a virtual cluster for the payments team using the vcluster CLI
vcluster create payments-dev \
--namespace payments-vcluster \
--set controlPlane.distro=k8s \
--set controlPlane.backingStore.etcd.embedded.enabled=true \
--set sync.toHost.pods.enabled=true \
--set policies.resourceQuota.enabled=true \
--set policies.limitRange.enabled=true
# Connect to the virtual cluster and get cluster-admin access
vcluster connect payments-dev --namespace payments-vcluster
# Inside the virtual cluster: create a namespace and deploy freely
kubectl create namespace staging
kubectl -n staging create deployment checkout-api \
--image=registry.company.com/checkout-api:3.7.2 \
--replicas=3
# On the host cluster: verify the virtual cluster Pods run in the host namespace
kubectl get pods -n payments-vcluster -l vcluster.loft.sh/managed-by=payments-dev
# Check resource consumption of the virtual cluster control plane
kubectl top pods -n payments-vcluster -l app=vcluster
# Apply a NetworkPolicy on the host to isolate virtual cluster traffic
apiVersion: networking.k8s.io/v1 # Standard NetworkPolicy API
kind: NetworkPolicy # Isolates network traffic for the virtual cluster namespace
metadata:
name: isolate-vcluster # Prevents cross-tenant network access
namespace: payments-vcluster # Host namespace backing the virtual cluster
spec:
podSelector: {} # Applies to all Pods in this namespace
policyTypes: [Ingress, Egress] # Controls both directions
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: payments-vcluster # Allows traffic within the namespace
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: payments-vcluster # Restricts egress to same namespace
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system # Allows DNS resolution via CoreDNSInterview Tip
A junior engineer typically answers that namespaces provide multi-tenancy, but for a senior/architect role, you need to show isolation boundary analysis. Explain how vcluster gives each tenant an independent API server, RBAC, and CRD space while sharing host compute, how the syncer translates resources between virtual and host clusters, and why this is stronger than namespaces but weaker than dedicated clusters. A mature answer also compares isolation levels — namespace RBAC versus virtual cluster RBAC versus kernel-level isolation with gVisor or Kata — and explains when soft multi-tenancy with virtual clusters is sufficient versus when hard isolation requires dedicated node pools or separate clusters.
◈ Architecture Diagram
┌──── Host Cluster ───────────────┐ │ ┌──── vcluster NS ───────────┐ │ │ │ ┌──────────┐ ┌───────────┐ │ │ │ │ │API Server│ │ Syncer │ │ │ │ │ └──────────┘ └───────────┘ │ │ │ │ ┌────────┐ ┌────────┐ │ │ │ │ │Tenant │ │Tenant │ │ │ │ │ │Pod A │ │Pod B │ │ │ │ │ └────────┘ └────────┘ │ │ │ └────────────────────────────┘ │ │ ┌──── other NS ──────────────┐ │ │ │ (other tenants) │ │ │ └────────────────────────────┘ │ └─────────────────────────────────┘
💬 Comments
Quick Answer
Scheduler plugins hook into the scheduling framework's extension points (PreFilter, Filter, PreScore, Score, Reserve, Permit, PreBind, Bind) to add custom logic like gang scheduling, co-scheduling, or capacity reservation. Scheduling profiles allow running multiple schedulers with different plugin configurations. Risks include increased scheduling latency, unintended Pod starvation, and complex debugging when plugins interact.
Detailed Answer
Think of a wedding seating planner with very specific rules. The basic planner checks table capacity and guest preferences. But this wedding also requires that certain groups of guests must all be seated simultaneously (gang scheduling), some tables are reserved for VIPs until the last minute (capacity reservation), and guests from rival families must never share an aisle (anti-affinity). Standard rules cannot express all of this, so the planner adds specialized checkers at different stages of the seating process. Kubernetes scheduler plugins work exactly this way.
The Kubernetes scheduling framework replaced the old policy-based scheduler configuration with a plugin architecture. The scheduler processes each Pod through a pipeline of extension points: PreFilter (validate and preprocess), Filter (eliminate ineligible nodes), PostFilter (handle unschedulable Pods), PreScore (prepare scoring data), Score (rank eligible nodes), Reserve (tentatively claim resources), Permit (wait or approve), PreBind (prepare external resources), and Bind (commit the Pod to a node). Each extension point can have multiple plugins that run in order.
Scheduling profiles allow a single kube-scheduler binary to expose multiple scheduler personalities. Each profile has a name and its own set of enabled, disabled, and configured plugins. A Pod selects its scheduler by setting spec.schedulerName. This means architects can run a default profile for general workloads and a specialized profile for GPU workloads, batch jobs, or latency-sensitive services without deploying separate scheduler binaries. The scheduler-plugins project from Kubernetes SIGs provides production-grade plugins like Coscheduling (gang scheduling for batch workloads that need all Pods scheduled together), Capacity Scheduling (enforcing elastic quotas across namespaces), and Trimaran (scoring based on real-time node use from metrics server).
At production scale, custom scheduler plugins require careful testing because they affect every Pod placement decision. A slow PreFilter or Score plugin increases scheduling latency for all Pods using that profile. A buggy Filter plugin can make nodes ineligible when they should be available, causing Pods to remain pending. Plugin ordering matters because earlier plugins in the chain can mask or override later ones. Architects should measure scheduler latency percentiles (scheduling_duration_seconds), unschedulable Pod counts, and plugin-specific metrics before and after enabling custom plugins.
The non-obvious gotcha is debugging scheduling failures with custom plugins. When a Pod is unschedulable, the scheduler event says which extension point rejected it, but the interaction between multiple plugins can create emergent behavior that is hard to trace. For example, a topology spread constraint combined with a capacity reservation plugin can create scenarios where Pods are pending not because of resource shortage but because the combination of constraints has no feasible solution. Architects should use the scheduler's verbose logging, the scheduling-queue metrics, and dry-run scheduling tools to validate plugin interactions before production deployment.
Code Example
# KubeSchedulerConfiguration with two profiles: default and batch-coscheduling
apiVersion: kubescheduler.config.k8s.io/v1 # Scheduler configuration API
kind: KubeSchedulerConfiguration # Configures the kube-scheduler binary
profiles:
- schedulerName: default-scheduler # Default profile for general workloads
plugins:
score:
enabled:
- name: NodeResourcesFit # Scores nodes by resource availability
weight: 1 # Standard weight
- name: InterPodAffinity # Scores based on Pod affinity preferences
weight: 1 # Standard weight
- schedulerName: batch-scheduler # Specialized profile for ML training jobs
plugins:
queueSort:
enabled:
- name: Coscheduling # Sorts Pods so gang members are scheduled together
preFilter:
enabled:
- name: Coscheduling # Validates that all gang members exist
postFilter:
enabled:
- name: Coscheduling # Preempts to make room for complete gangs
permit:
enabled:
- name: Coscheduling # Holds Pods until all gang members are schedulable
reserve:
enabled:
- name: Coscheduling # Reserves resources for the complete gang
# A batch training job that uses gang scheduling via the batch-scheduler profile
apiVersion: batch/v1 # Standard Job API
kind: Job # Batch workload requiring all workers to start together
metadata:
name: fraud-model-training # Distributed training job
namespace: ml-platform # ML team namespace
labels:
pod-group.scheduling.sigs.k8s.io/name: fraud-training-gang # Gang scheduling group name
pod-group.scheduling.sigs.k8s.io/min-available: "4" # All four workers must be scheduled
spec:
parallelism: 4 # Four parallel training workers
completions: 4 # Job completes when all four finish
template:
metadata:
labels:
pod-group.scheduling.sigs.k8s.io/name: fraud-training-gang # Same gang group label
spec:
schedulerName: batch-scheduler # Uses the coscheduling profile
containers:
- name: trainer # Distributed training worker container
image: registry.company.com/fraud-trainer:4.3.1 # ML training image
resources:
requests:
cpu: 4 # Four CPU cores per worker
memory: 16Gi # 16GB memory per worker
# Monitor scheduler performance metrics for the batch profile
kubectl get --raw='/metrics' | grep scheduling_duration_seconds | grep batch-schedulerInterview Tip
A junior engineer typically answers that Kubernetes schedules Pods based on resource requests and affinity, but for a senior/architect role, you need to show scheduling framework extensibility. Explain the extension point pipeline (PreFilter through Bind), how scheduling profiles allow multiple scheduler personalities in one binary, and concrete use cases like gang scheduling with Coscheduling or real-time-aware scoring with Trimaran. A mature answer also covers the risks — scheduling latency from slow plugins, emergent constraint interactions, the importance of measuring scheduling_duration_seconds — and how to validate custom plugins with dry-run scheduling before production rollout.
◈ Architecture Diagram
┌──────────────────────────────────┐ │ Scheduling Pipeline │ │ │ │ PreFilter → Filter → PostFilter │ │ ↓ │ │ PreScore → Score → Reserve │ │ ↓ │ │ Permit → PreBind → Bind │ │ │ │ ┌──────────┐ ┌────────────────┐ │ │ │ Default │ │ Batch Profile │ │ │ │ Profile │ │ +Coscheduling │ │ │ └──────────┘ └────────────────┘ │ └──────────────────────────────────┘
💬 Comments
Quick Answer
Deploy EKS clusters in multiple AWS regions with Route 53 health-check-based DNS failover, replicate stateful data with cross-region RDS replicas or DynamoDB Global Tables, and use a GitOps tool like ArgoCD to keep workload configurations synchronized across all clusters.
Detailed Answer
Imagine a bank with two headquarters in different cities. If one building loses power, customers walk into the other branch and get the same service because both branches share the same customer records and follow identical procedures. Multi-region Kubernetes is exactly this — two or more EKS clusters running identical workloads with shared data, and a traffic director (Route 53) that automatically sends customers to whichever branch is open.
Multi-region EKS starts with provisioning identical clusters in at least two AWS regions — typically us-east-1 and us-west-2 for geographic diversity. Each cluster is created using Terraform with identical node group configurations, VPC layouts, and add-on versions. The clusters share the same container images from a central ECR registry (with cross-region replication enabled), the same Helm charts from a central chart museum, and the same ArgoCD ApplicationSets that deploy workloads to both clusters simultaneously. The key principle is that both clusters must be able to serve production traffic independently at all times — this means each cluster runs the full stack including payments-api, settlements-processor, and fraud-detector, not a partial subset.
The networking layer is where the magic happens. Route 53 is configured with health-check-based failover routing policies. Each cluster exposes health endpoints through an Application Load Balancer, and Route 53 health checks probe these endpoints every 10 seconds. When the primary region fails a health check three consecutive times, Route 53 automatically updates DNS to route traffic to the secondary region. For more sophisticated routing, you can use Route 53 weighted routing to split traffic 80/20 across regions during normal operations, which keeps the secondary cluster warm and validates it continuously handles real production traffic. AWS Global Accelerator can be layered on top for anycast IP addressing, reducing DNS propagation delays during failover from minutes to seconds.
Data replication is the hardest part. For the settlements-db (Amazon RDS PostgreSQL), you set up cross-region read replicas with asynchronous replication. During failover, you promote the replica to a primary — this involves a brief write outage (typically 1-2 minutes) and potential data loss of a few seconds of transactions. For banking workloads, this is mitigated by implementing idempotency keys on all payment transactions so that retries after failover do not result in duplicate transfers. DynamoDB Global Tables provide active-active multi-region writes with last-writer-wins conflict resolution, suitable for session state and caching layers. Secrets and certificates are synchronized using AWS Secrets Manager with cross-region replication, ensuring both clusters can access TLS certificates and database credentials.
In production at a bank, multi-region adds significant operational complexity. You need synchronized deployments — a version mismatch between regions during failover can cause data compatibility issues. ArgoCD ApplicationSets with a rolling sync strategy handle this by deploying to the secondary region first, running smoke tests, then deploying to the primary. You need regular failover drills — at least quarterly — where you intentionally fail over production traffic to the secondary region and verify all services function correctly. These drills must be coordinated with compliance officers and documented for audit trails. Monitoring must be region-aware, with Grafana dashboards showing per-region SLIs and cross-region replication lag.
The biggest gotcha is cost and the active-passive trap. Running a second EKS cluster that only handles failover traffic means you are paying double for compute that sits idle 99% of the time. Smart teams run active-active, where both regions serve traffic continuously, which amortizes cost and ensures the secondary region is always production-validated. Another gotcha is cross-region latency — if payments-api in us-east-1 needs to call fraud-detector in us-west-2, that is 60-80ms of additional latency per hop. Services must be fully self-contained within each region. Finally, Terraform state management across regions requires separate state files per region with a shared backend (S3 with DynamoDB locking), and you must test that destroying and recreating one region does not affect the other.
Code Example
# Terraform - EKS multi-region cluster provisioning
# modules/eks-cluster/main.tf
module "eks_primary" {
source = "terraform-aws-modules/eks/aws"
version = "~> 19.0"
cluster_name = "banking-platform-${var.region}"
cluster_version = "1.29"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets
# Managed node groups for payments workloads
eks_managed_node_groups = {
payments = {
instance_types = ["m6i.2xlarge"]
min_size = 3
max_size = 20
desired_size = 6
labels = {
workload = "payments"
tier = "critical"
}
}
}
}
# Route 53 health check and failover
resource "aws_route53_health_check" "payments_api_primary" {
fqdn = "payments-api-internal.us-east-1.banking.internal"
port = 443
type = "HTTPS"
resource_path = "/healthz"
failure_threshold = 3
request_interval = 10
tags = {
Name = "payments-api-primary-health"
Environment = "production"
}
}
resource "aws_route53_record" "payments_api" {
zone_id = var.route53_zone_id
name = "payments-api.banking.com"
type = "A"
failover_routing_policy {
type = "PRIMARY"
}
set_identifier = "primary"
health_check_id = aws_route53_health_check.payments_api_primary.id
alias {
name = module.alb_primary.dns_name
zone_id = module.alb_primary.zone_id
evaluate_target_health = true
}
}
---
# ArgoCD ApplicationSet for multi-region deployment
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: payments-api-multiregion
namespace: argocd
spec:
generators:
- list:
elements:
- cluster: banking-us-east-1
url: https://eks-primary.us-east-1.amazonaws.com
region: us-east-1
weight: "80"
- cluster: banking-us-west-2
url: https://eks-secondary.us-west-2.amazonaws.com
region: us-west-2
weight: "20"
strategy:
type: RollingSync
rollingSync:
steps:
- matchExpressions:
- key: region
operator: In
values: [us-west-2] # Deploy to secondary first
- matchExpressions:
- key: region
operator: In
values: [us-east-1] # Then primary after validation
template:
metadata:
name: payments-api-{{cluster}}
spec:
project: banking-production
source:
repoURL: https://github.com/bank/platform-manifests.git
targetRevision: main
path: apps/payments-api/overlays/{{region}}
destination:
server: "{{url}}"
namespace: banking-prodInterview Tip
A junior engineer typically describes multi-region as 'just deploy the same thing twice,' overlooking the critical challenges of data replication, DNS failover timing, and cross-region deployment synchronization. Demonstrate expertise by walking through the data layer first — explain RDS cross-region replicas vs DynamoDB Global Tables, idempotency keys for payment retries, and the RPO/RTO tradeoffs. Mention that you run quarterly failover drills coordinated with compliance, and that active-active routing keeps the secondary region warm rather than discovering it is broken during a real incident.
◈ Architecture Diagram
┌──────────────────────────────────────────────────────────────────────┐ │ Multi-Region EKS Architecture │ │ │ │ ┌──────────────────┐ │ │ │ Route 53 │ │ │ │ Failover DNS │ │ │ └────┬────────┬────┘ │ │ │ │ │ │ ┌──────────▼──┐ ┌──▼──────────┐ │ │ │ us-east-1 │ │ us-west-2 │ │ │ │ (PRIMARY) │ │ (SECONDARY) │ │ │ │ │ │ │ │ │ ┌───────────┤ EKS Cluster│ │ EKS Cluster ├───────────┐ │ │ │ │ │ │ │ │ │ │ │ ┌────────┴───────┐ │ │ ┌────────┴───────┐ │ │ │ │ │ payments-api │ │ │ │ payments-api │ │ │ │ │ │ settlements │ │ │ │ settlements │ │ │ │ │ │ fraud-detector│ │ │ │ fraud-detector│ │ │ │ │ └────────────────┘ │ │ └────────────────┘ │ │ │ │ │ │ │ │ │ │ │ │ ┌────────┴───────┐ │ │ ┌────────┴───────┐ │ │ │ │ │ RDS Primary │─────┼──┼───→│ RDS Replica │ │ │ │ │ │ settlements-db │ async│ │ │ (promote on │ │ │ │ │ └────────────────┘ repl│ │ │ failover) │ │ │ │ │ │ │ └────────────────┘ │ │ │ └─────────────────────────┘ └─────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ ArgoCD ApplicationSet → RollingSync (secondary first) │ │ │ └──────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────────┘
💬 Comments
Quick Answer
The query goes through the container's /etc/resolv.conf → CoreDNS (or kube-dns) → cluster service lookup or upstream DNS. Failures can occur at ndots expansion, CoreDNS pod availability, or upstream resolution.
Detailed Answer
1. Application makes DNS query for my-service.my-namespace.svc.cluster.local 2. Container's /etc/resolv.conf is configured by kubelet with nameserver <CoreDNS ClusterIP>, search <namespace>.svc.cluster.local svc.cluster.local cluster.local, and ndots:5 3. ndots behavior: If the query has fewer than 5 dots, the resolver appends each search domain and tries them ALL before trying the original name. api.example.com generates 4 queries before the actual one. 4. CoreDNS receives the query via the ClusterIP service (kube-dns), which load balances to CoreDNS pods 5. CoreDNS checks its plugins: kubernetes plugin for cluster-internal names, forward plugin for external names 6. For internal services: CoreDNS queries the Kubernetes API (via watch cache) to resolve Service → ClusterIP or Endpoints 7. Response returned through the chain back to the pod
- ndots:5 causes 5x DNS amplification for external domains — massive performance hit - CoreDNS pods overloaded: Too few replicas, leading to timeouts (default 5s) and retries - conntrack table full: High DNS QPS can exhaust conntrack entries on nodes, causing silent drops - Race condition (CVE-2017-15129): UDP source port reuse causing intermittent SERVFAIL on some kernel versions - CoreDNS cache TTL: Stale records after service updates if cache TTL is too aggressive
Use NodeLocal DNSCache (node-level DNS caching daemonset) to reduce CoreDNS load and avoid conntrack issues. Set ndots:2 for pods that primarily call external services. Monitor CoreDNS with coredns_dns_requests_total and coredns_dns_responses_total Prometheus metrics.
Code Example
# Check pod DNS configuration
kubectl exec -it <pod> -- cat /etc/resolv.conf
# Test DNS resolution with timing
kubectl exec -it <pod> -- nslookup -debug my-service.default.svc.cluster.local
# Check CoreDNS pods and logs
kubectl get pods -n kube-system -l k8s-app=kube-dns
kubectl logs -n kube-system -l k8s-app=kube-dns --tail=50
# Override ndots in pod spec
spec:
dnsConfig:
options:
- name: ndots
value: "2"Interview Tip
This question separates senior from staff engineers. The key insight is ndots:5 causing DNS amplification — most candidates miss this. Also mention conntrack exhaustion and NodeLocal DNSCache as the production solution.
💬 Comments
Quick Answer
Use a phased approach: replicate data stores first, gradually shift traffic via weighted DNS or service mesh, validate with shadow traffic, then cut over with rollback capability.
Detailed Answer
Phase 1: Foundation (Week 1-2) - Deploy identical infrastructure on target cluster using GitOps (ArgoCD) - Set up cross-cluster networking (VPC peering, transit gateway, or service mesh federation) - Establish data replication for stateful workloads (database replicas, PV snapshots)
Phase 2: Parallel Run (Week 3-4) - Deploy applications to target cluster alongside source - Route shadow/mirror traffic to target for validation (Istio traffic mirroring) - Compare responses between clusters for correctness - Monitor latency, error rates, resource usage on target
Phase 3: Gradual Traffic Shift (Week 5-6) - Use weighted DNS (Route53 weighted routing) or service mesh to shift traffic: 5% → 25% → 50% → 100% - At each step: validate SLIs, error budgets, latency percentiles - Stateful workload cut-over: promote read-replica to primary, update connection strings
Phase 4: Cleanup (Week 7) - Keep source cluster in standby for 1 week as rollback - Decommission source cluster after validation period
- Databases: Use logical replication (PostgreSQL) or change data capture (Debezium) for real-time sync - PersistentVolumes: CSI volume snapshots + cross-region restore, or Velero for backup/restore - StatefulSets: Cannot simply move — must re-create with same ordinal identity and reattach data - Kafka/MQ: Use MirrorMaker 2 for topic replication across clusters
Code Example
# Istio traffic mirroring for validation
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
spec:
hosts:
- my-service
http:
- route:
- destination:
host: my-service-source
weight: 95
- destination:
host: my-service-target
weight: 5
mirror:
host: my-service-target
mirrorPercentage:
value: 100.0Interview Tip
This is a staff+ system design question. Emphasize the phased approach with rollback at every stage. Mention specific tools (Velero, Istio mirroring, weighted DNS) and the distinction between stateless and stateful migration strategies.
💬 Comments
Quick Answer
Apply Non-Abstract Large System Design (NALSD) by quantifying the etcd write throughput, identifying whether the bottleneck is disk I/O (WAL fsync), network latency between etcd members, or excessive API server watch/list operations saturating etcd's MVCC layer.
Detailed Answer
Google's NALSD framework requires you to reason about large systems with concrete numbers. etcd is the Raft-based key-value store backing all Kubernetes state. At 5000 nodes, you're likely dealing with 100,000+ objects (pods, services, endpoints, configmaps). Each API server write becomes an etcd transaction, and etcd's performance is fundamentally bound by disk fsync latency for its Write-Ahead Log (WAL).
1. Quantify the load: etcd exposes etcd_disk_wal_fsync_duration_seconds and etcd_disk_backend_commit_duration_seconds. If WAL fsync p99 > 10ms, the disk subsystem is the bottleneck. At 5000 nodes, you should expect ~2000-5000 writes/second. 2. Check Raft consensus: etcd_server_leader_changes_seen_total increasing means leader instability. etcd_network_peer_round_trip_time_seconds reveals cross-member latency. Raft requires majority quorum for every write, so a slow member drags down the entire cluster. 3. Identify hot keys: Large list/watch operations on namespaces with thousands of pods generate excessive range queries. Use etcd_debugging_mvcc_keys_total and audit API server request logs for expensive LIST operations.
Move etcd to dedicated NVMe-backed instances (not shared with kubelet/container workloads). Implement API Priority and Fairness (APF) to throttle expensive LIST operations. Consider etcd defragmentation if the database has grown beyond 4GB due to historical revisions. For clusters this large, evaluate splitting etcd into separate clusters for events vs. core resources (the --etcd-servers-overrides flag).
Calculate: 5000 nodes x 30 pods avg = 150,000 pod objects. Each pod is ~2KB in etcd. Total data ~300MB, well within etcd limits. But the write rate during rolling updates (updating 10% of pods = 15,000 writes in minutes) can overwhelm disk I/O. Plan for sustained 5000 writes/sec with p99 fsync < 10ms, which requires dedicated NVMe with >10,000 IOPS.
Code Example
# Check etcd disk performance metrics
etcdctl endpoint status --write-out=table
etcdctl endpoint health --write-out=table
# Monitor WAL fsync latency via Prometheus
histogram_quantile(0.99, rate(etcd_disk_wal_fsync_duration_seconds_bucket[5m]))
# Check database size and fragmentation
etcdctl endpoint status --write-out=json | jq '.[] | {endpoint: .Endpoint, dbSize: .Status.dbSize, dbSizeInUse: .Status.dbSizeInUse}'
# Defragment etcd (run on each member, one at a time)
etcdctl defrag --endpoints=https://etcd-0:2379
# Split etcd for events (kube-apiserver flag)
# --etcd-servers-overrides=/events#https://etcd-events-0:2379,https://etcd-events-1:2379
# Check API server request latency to etcd
kubectl get --raw /metrics | grep etcd_request_durationInterview Tip
Google SRE interviews use NALSD to test whether you can reason about systems with concrete numbers. Never say 'it depends' without following up with actual calculations. Know etcd's performance characteristics: ~10,000 writes/sec on NVMe, WAL fsync is the critical path, and Raft consensus adds network RTT to every write.
💬 Comments
Quick Answer
iptables mode uses O(n) linear chain traversal per packet, IPVS uses O(1) hash table lookups, and eBPF (Cilium) bypasses iptables entirely for O(1) lookups with additional features like transparent encryption. At 10,000+ services, iptables becomes a significant bottleneck.
Detailed Answer
kube-proxy creates iptables rules for every Service/Endpoint combination. For a ClusterIP service with 3 endpoints, it creates ~8 rules (KUBE-SERVICES chain -> KUBE-SVC-xxx -> KUBE-SEP-xxx with probability-based load balancing). At 10,000 services with 3 endpoints each, you get ~240,000 iptables rules. Every packet traverses these chains linearly. Rule update time is O(n) because iptables does a full table replacement (iptables-restore). During updates, there's a brief period where connections can be dropped.
IPVS (IP Virtual Server) uses kernel-level hash tables for service-to-endpoint mapping. Lookup is O(1) regardless of service count. It supports multiple load balancing algorithms (round-robin, least-connection, source-hash). Connection tracking is built-in. At 10,000 services, IPVS adds negligible latency (<0.1ms) while iptables adds 1-5ms per connection. However, IPVS still uses iptables for SNAT/masquerade operations, so it doesn't fully eliminate iptables dependency.
Cilium attaches eBPF programs to network interfaces, bypassing both iptables and IPVS entirely. Service routing happens in the kernel at the socket/TC level before packets enter the Linux networking stack. Benefits include: true O(1) lookups, no conntrack dependency (optional), transparent encryption (WireGuard/IPsec), L7 policy enforcement, and observability via Hubble. At 10,000+ services, Cilium maintains consistent <0.1ms overhead.
- Small clusters (<200 services): iptables is fine, simple to debug - Medium clusters (200-5000 services): IPVS for performance without major architecture change - Large clusters (5000+ services) or need L7 visibility: Cilium/eBPF - Regulatory requirements for encryption in transit: Cilium with WireGuard
Code Example
# Check current kube-proxy mode kubectl get configmap kube-proxy -n kube-system -o yaml | grep mode # Switch to IPVS mode kubectl edit configmap kube-proxy -n kube-system # Set mode: "ipvs" and ipvs.scheduler: "rr" kubectl rollout restart daemonset/kube-proxy -n kube-system # Verify IPVS rules kubectl exec -it kube-proxy-xxxxx -n kube-system -- ipvsadm -Ln # Count iptables rules (detect bloat) kubectl exec -it kube-proxy-xxxxx -n kube-system -- iptables -L -n | wc -l # Install Cilium replacing kube-proxy helm install cilium cilium/cilium \ --namespace kube-system \ --set kubeProxyReplacement=true \ --set k8sServiceHost=<API_SERVER_IP> \ --set k8sServicePort=6443 \ --set hubble.enabled=true \ --set hubble.relay.enabled=true # Verify Cilium replaced kube-proxy cilium status kubectl exec -n kube-system ds/cilium -- cilium service list
Interview Tip
This is a networking deep-dive question common at Google and Meta. Know the Big-O complexity of each approach. The key insight is that iptables does linear traversal and full-table replacement, making it fundamentally unscalable. Mention specific latency numbers to show production experience.
💬 Comments
Quick Answer
Use a hierarchical RBAC model with ClusterRoles for common permissions, namespace-scoped RoleBindings for team isolation, aggregated ClusterRoles for platform services, and a time-limited break-glass system using impersonation or temporary RoleBindings managed by a custom operator.
Detailed Answer
The platform team needs cluster-wide access for infrastructure management. Create a platform-admin ClusterRole with permissions to manage nodes, namespaces, CRDs, webhook configurations, and storage classes. Bind this via ClusterRoleBinding to the platform team's IdP group.
Each team gets a dedicated namespace (or set of namespaces for dev/staging/prod). Create a standardized set of ClusterRoles using aggregation labels: team-developer (deploy, view pods, exec), team-lead (all developer perms + manage ResourceQuotas, NetworkPolicies), team-viewer (read-only). Bind these per-namespace using RoleBindings referencing IdP groups.
Teams that need to read ConfigMaps or Secrets from shared namespaces (e.g., platform-config) use ClusterRoles with resource name restrictions. For service-to-service communication, use NetworkPolicies rather than RBAC (RBAC controls API access, not data plane traffic).
Implement a custom operator that watches a CRD BreakGlassRequest. When an on-call engineer creates a request (approved via PagerDuty webhook), the operator creates a time-limited RoleBinding (using a finalizer with TTL) granting elevated access. All break-glass events are audit-logged and auto-expire after 4 hours.
Deploy OPA Gatekeeper or Kyverno to enforce RBAC boundaries: prevent teams from creating ClusterRoleBindings, restrict service account token mounting, enforce label-based namespace ownership. Use ValidatingAdmissionWebhooks to prevent privilege escalation (e.g., binding to cluster-admin).
Code Example
# Aggregated ClusterRole for team developers
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: team-developer
labels:
rbac.mycompany.io/aggregate-to-team-lead: "true"
rules:
- apiGroups: ["apps", "batch"]
resources: ["deployments", "statefulsets", "jobs", "cronjobs"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["pods", "pods/log", "pods/exec", "services", "configmaps"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch"]
# No create/update for secrets - managed via external-secrets
---
# Team-lead aggregates developer + management perms
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: team-lead
aggregationRule:
clusterRoleSelectors:
- matchLabels:
rbac.mycompany.io/aggregate-to-team-lead: "true"
rules: [] # Rules auto-populated by aggregation
---
# Namespace-scoped binding (created by automation per team)
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: team-payments-developers
namespace: payments
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: team-developer
subjects:
- kind: Group
name: "team-payments-devs" # From OIDC/IdP
apiGroup: rbac.authorization.k8s.io
---
# Break-glass CRD
apiVersion: breakglass.mycompany.io/v1
kind: BreakGlassRequest
metadata:
name: incident-12345
spec:
user: [email protected]
namespace: payments
role: team-lead
ttlMinutes: 240
justification: "P1 incident - payment processing failure"
pagerdutyIncident: "P-12345"Interview Tip
This question tests organizational design thinking, not just RBAC mechanics. Mention aggregated ClusterRoles (most candidates don't know about label-based aggregation), IdP integration via OIDC, and the break-glass pattern. The key insight is that RBAC alone is insufficient - you need admission controllers to prevent privilege escalation.
💬 Comments
Quick Answer
Titus uses a flat scheduling model without pods (one container per task), direct EC2 integration for resource isolation (ENI per container, EBS per container), and a centralized scheduler optimized for bin-packing and GPU workloads. Unlike K8s, Titus was designed for batch and streaming workloads from the start.
Detailed Answer
Titus uses a Fenzo-based scheduler (custom Mesos scheduler, later moved to their own) that operates on 'tasks' rather than pods. Each task is a single container with dedicated resources. There's no pod abstraction with sidecars - auxiliary functions are handled differently (network proxying via Titus Gateway, not sidecar injection). This simplifies scheduling but loses the co-location benefits of pod-based design.
Netflix's Titus allocates a dedicated ENI (Elastic Network Interface) to each container, giving every container its own IP address and security group. This is similar to AWS VPC CNI but was implemented before EKS existed. Each container gets dedicated EBS volumes rather than sharing node-level storage. This provides stronger isolation than Kubernetes' shared-node model.
Titus processes millions of container launches per week. Key optimizations: - Image pre-caching: Titus agents pre-pull popular images based on prediction models - Fast container startup: Custom container runtime optimized for <5s cold start - Opportunistic scheduling: Batch jobs run on spare capacity from service tier, preempted when capacity is needed - GPU scheduling: Native support for fractional GPU allocation (before K8s had device plugins)
You can replicate many Titus patterns in K8s: - Use Karpenter for intelligent instance selection (similar to Titus capacity management) - VPC CNI with pod-level security groups for network isolation - PriorityClasses for opportunistic batch scheduling - Device plugins for GPU sharing - Warm image caches via DaemonSets or Spegel for P2P distribution
Titus was built in 2015-2016 when Kubernetes was immature. Netflix needed deep AWS integration (IAM per container, VPC networking, EBS) that K8s didn't support. Today, with EKS Pod Identity, VPC CNI, and EBS CSI driver, many of these gaps are closed.
Code Example
# Titus-style pod isolation in Kubernetes
# Per-pod security group (EKS)
apiVersion: vpcresources.k8s.aws/v1beta1
kind: SecurityGroupPolicy
metadata:
name: per-pod-sg
spec:
podSelector:
matchLabels:
app: isolated-service
securityGroups:
groupIds:
- sg-0123456789abcdef0
---
# Titus-style opportunistic batch scheduling
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: batch-opportunistic
value: -1000 # Negative = preemptible
preemptionPolicy: Never # Don't preempt others, get preempted
---
apiVersion: batch/v1
kind: Job
metadata:
name: batch-analytics
spec:
template:
spec:
priorityClassName: batch-opportunistic
containers:
- name: analytics
resources:
requests:
cpu: "4"
memory: "16Gi"
nvidia.com/gpu: "1"
---
# Image pre-caching DaemonSet (Titus warm-cache equivalent)
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: image-warmer
spec:
selector:
matchLabels:
app: image-warmer
template:
spec:
initContainers:
- name: warm-app-image
image: my-registry/critical-app:latest
command: ["/bin/true"]
containers:
- name: pause
image: registry.k8s.io/pause:3.9Interview Tip
Netflix interviews ask about Titus to test whether you understand container orchestration beyond Kubernetes. The key insight is understanding WHY they built a custom solution (deep AWS integration, scale requirements, batch workload support) and being able to map those concepts to modern Kubernetes equivalents. This shows you understand the underlying problems, not just the tools.
💬 Comments
Quick Answer
Use hierarchical ResourceQuotas with LimitRanges for defaults, PriorityClasses for tiered preemption, and a custom admission webhook or Kyverno policies to enforce organizational quotas that span across namespaces. Implement a resource banking system where teams can borrow from a shared pool.
Detailed Answer
Each namespace gets a ResourceQuota defining hard limits on CPU, memory, pod count, and storage. For 200 namespaces on a cluster with 1000 cores, you might allocate 4 cores per namespace base quota (800 cores committed, 200 cores buffer). LimitRanges set default requests/limits per container so developers don't need to specify resources manually.
Kubernetes doesn't natively support org-level quotas (e.g., 'Team Payments gets 50 cores across their 5 namespaces'). Implement this via a custom controller or use Hierarchical Namespace Controller (HNC). The controller watches ResourceQuota usage across related namespaces and prevents new pod creation if the org-level quota is exceeded.
Allow critical services to burst beyond their quota by using PriorityClasses. Define tiers: critical (preempts everything), production (preempts batch), development (preempts nothing). When a critical service needs to burst, it can preempt lower-priority pods. This gives you overcommitment without overprovisioning.
Deploy VPA in recommendation mode to continuously suggest optimal resource requests based on actual usage. Feed VPA recommendations into a dashboard showing quota utilization efficiency. Most teams request 3-5x more resources than they actually use.
Export kube_resourcequota and kube_pod_container_resource_requests metrics to Prometheus. Build Grafana dashboards showing per-team utilization vs allocation. Implement a cost allocation system using kubecost or custom metrics to charge teams for reserved (not used) resources, incentivizing right-sizing.
Code Example
# Namespace ResourceQuota
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-quota
namespace: payments-prod
spec:
hard:
requests.cpu: "20"
requests.memory: "40Gi"
limits.cpu: "40"
limits.memory: "80Gi"
pods: "100"
persistentvolumeclaims: "20"
services.loadbalancers: "2"
---
# LimitRange for sensible defaults
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: payments-prod
spec:
limits:
- type: Container
default:
cpu: "500m"
memory: "512Mi"
defaultRequest:
cpu: "100m"
memory: "128Mi"
max:
cpu: "4"
memory: "8Gi"
min:
cpu: "50m"
memory: "64Mi"
- type: PersistentVolumeClaim
max:
storage: "100Gi"
min:
storage: "1Gi"
---
# Kyverno policy to enforce resource requests
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-resource-requests
spec:
validationFailureAction: Enforce
rules:
- name: check-resource-requests
match:
any:
- resources:
kinds: ["Pod"]
validate:
message: "All containers must have CPU and memory requests."
pattern:
spec:
containers:
- resources:
requests:
cpu: "?*"
memory: "?*"
---
# PromQL for quota utilization monitoring
# CPU utilization efficiency per namespace
# sum(rate(container_cpu_usage_seconds_total[5m])) by (namespace)
# /
# sum(kube_resourcequota{resource="requests.cpu",type="hard"}) by (namespace)
# * 100Interview Tip
Meta production engineering interviews focus on scale and efficiency. The key insight is that ResourceQuota alone is insufficient for organizational fairness - you need hierarchical enforcement. Mention the tension between resource efficiency (overcommitment) and reliability (guaranteed resources), and how PriorityClasses bridge this gap. Always bring up the cost/chargeback angle.
💬 Comments
Quick Answer
etcd uses Raft consensus requiring a majority (quorum) of nodes to agree on writes. Odd member counts (3, 5, 7) maximize fault tolerance without wasting a node—a 4-node cluster tolerates only 1 failure, same as 3. Losing quorum freezes the Kubernetes API; the data plane may keep running but cannot be managed.
Detailed Answer
etcd is the consistent, distributed key-value store holding all Kubernetes cluster state: Pod specs, Secrets, ConfigMaps, lease data, and RBAC rules. Every API server read and write for mutable state ultimately interacts with etcd. High availability deploys etcd as a Raft cluster where one leader handles writes and replicates log entries to followers.
Raft requires a majority of members to acknowledge a write before it commits. For N members, quorum is floor(N/2)+1. A 3-member cluster tolerates 1 failure (2 of 3 = quorum). A 5-member cluster tolerates 2 failures. Critically, a 4-member cluster still requires 3 for quorum—also tolerating only 1 failure—so the fourth node adds latency and operational cost without improving resilience. This is why etcd clusters always use odd counts.
When quorum is lost—imagine 2 of 3 etcd members permanently fail—the remaining member cannot elect a leader or accept writes. kubectl commands fail. Controllers stop reconciling. Existing Pods on worker nodes continue running (kubelet operates from local cache), but no scheduling, scaling, or Service updates occur. This split-brain protection is preferable to serving stale or divergent cluster state.
Recovery options include etcd snapshot restore to a fresh cluster, disaster recovery procedures documented per distribution (kubeadm, RKE, EKS manages etcd), and preventive monitoring of etcd latency, db size, and leader elections. Regular automated snapshots to object storage are non-negotiable for self-managed clusters.
Analogy: etcd quorum is a panel of judges requiring majority agreement before publishing a verdict. With three judges, one may be absent and rulings continue. If only one judge remains, court adjourns—no new rulings until quorum is restored—even though convicted prisoners (running Pods) remain in their cells.
Architect-level discussions cover stacked vs external etcd topology, TLS peer encryption, defragmentation schedules, and performance tuning (SSD, network latency under 10ms between members).
Monitor etcd db size and set auto-compaction retention to prevent unbounded growth. Leader election metrics (etcd_server_leader_changes_seen_total) spiking indicate network instability. For managed Kubernetes, understand what the provider backs up and test restore into an isolated environment quarterly—assuming the control plane is someone else's problem until it isn't is a common organizational failure mode.
Code Example
# Check etcd cluster health (self-managed / stacked etcd) etcdctl endpoint health \ --endpoints=https://127.0.0.1:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key # Member list and quorum status etcdctl member list -w table # Snapshot backup (run on healthy leader, cron every 15 min) etcdctl snapshot save /backup/etcd-$(date +%F-%H%M).db \ --endpoints=https://127.0.0.1:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key # Verify snapshot integrity etcdctl snapshot status /backup/etcd-snapshot.db -w table # Monitor from Kubernetes (if etcd metrics exposed) kubectl get --raw /metrics | grep etcd_server_has_leader
Interview Tip
This question separates senior architects from implementers. Lead with odd-number rationale and the 4-node trap. Explain that losing quorum is a control-plane freeze, not an automatic data-plane outage—then clarify the operational blindness that follows. Discuss stacked etcd (kubeadm default, colocated with API server) vs external etcd (OpenShift, larger clusters). Mention managed Kubernetes (EKS, GKE) hides etcd but DR remains your concern for application state. Know snapshot restore high-level steps without pretending every distro is identical. Touch defragmentation when db size grows and auto-compaction retention. Interviewers value calm disaster narratives over panic—describe runbooks, RTO/RPO targets, and regular restore drills.
◈ Architecture Diagram
3-member etcd Raft cluster
+--------+ +--------+ +--------+
| etcd-0 | | etcd-1 | | etcd-2 |
| LEADER | |follower| |follower|
+---+----+ +---+----+ +---+----+
| | |
+----- Raft replication --+
Quorum = 2/3
Tolerates 1 failure OK
2 failures = NO quorum -> API frozen💬 Comments
Quick Answer
Back up existing CRs, check CRD compatibility between versions, dry-run the Helm upgrade, then upgrade the controller. Verify existing workloads continue reconciling without restarts.
Detailed Answer
Upgrading an operator in production is risky because operators manage custom resources representing running workloads. A new version may change CRD schemas or reconciliation logic.
Step 1: Check release notes for breaking CRD changes between versions. Step 2: Back up all existing custom resources: kubectl get sparkapplications -A -o yaml > backup.yaml Step 3: Dry-run the upgrade: helm upgrade --dry-run to preview changes. Step 4: Upgrade the controller deployment. The new controller picks up existing CRs and reconciles. Step 5: Watch controller logs for errors. Verify existing pods are not restarted.
For multi-version jumps (e.g., Spark Operator 3.1 to 3.5), check webhook configs, RBAC changes, and deprecated CRD fields. Always test on a staging namespace first.
Code Example
kubectl get sparkapplications -n data-pipelines -o yaml > spark-backup.yaml helm upgrade spark-operator spark-operator/spark-operator --version 3.5.0 --namespace spark-operator --dry-run helm upgrade spark-operator spark-operator/spark-operator --version 3.5.0 --namespace spark-operator kubectl logs -n spark-operator deployment/spark-operator -f
Interview Tip
Show you think about CRD backward compatibility, backup, dry-run, and staged rollout. Mention that operators manage state through CRDs and a bad upgrade can orphan workloads.
💬 Comments
Quick Answer
iptables-based CNIs evaluate NetworkPolicies as sequential chain rules, so packet-processing cost grows roughly linearly with rule count and every node must hold rules for every policy that could apply to it. Cilium replaces that with eBPF programs attached at the socket/XDP layer that do near-constant-time hash lookups, so policy evaluation stays fast as the policy count grows, at the cost of needing a newer kernel and more careful upgrade/rollback discipline.
Detailed Answer
Traditional CNIs (kube-router, Calico in iptables mode) implement NetworkPolicy by generating iptables/ipset rules and chains. The kernel walks these chains per-packet. With a handful of policies this is fine; with thousands of policies and large clusters, three things degrade: (1) iptables-restore time during scale-out — rule programming becomes a control-plane bottleneck during mass pod churn, (2) per-packet latency grows because chain traversal is not O(1), and (3) debugging is hard because the effective rule set is the product of many overlapping chains.
eBPF-based CNIs like Cilium attach programs directly to the kernel's networking hooks (TC, XDP, socket layer) and represent policy as hash maps and identity-based lookups (Cilium assigns each pod a 'security identity' derived from labels, not just an IP). Policy evaluation becomes a map lookup keyed by identity rather than chain traversal, so it scales much better with policy count and connection rate. eBPF also enables features iptables can't do efficiently: kube-proxy replacement (no more iptables/IPVS for service routing), transparent encryption, and L7-aware policies without a sidecar.
The tradeoffs: eBPF CNIs require a relatively modern kernel (5.x+ recommended for full feature set), upgrades touch live kernel-loaded programs so you need to validate kernel/Cilium version compatibility carefully, and observability tooling (Hubble for Cilium) is a new piece of operational surface to learn. For very large multi-tenant clusters with heavy NetworkPolicy usage and high pod churn, the eBPF model is generally the better fit; for smaller or more conservative environments, the simplicity and maturity of iptables-based CNIs may outweigh the scalability ceiling.
Code Example
# Check current CNI and kube-proxy mode kubectl get pods -n kube-system -o wide | grep -E 'cilium|calico|kube-proxy' # Cilium: inspect eBPF policy maps for a given endpoint cilium endpoint list cilium bpf policy get <endpoint-id> # Hubble: observe live policy decisions (allow/deny) per flow hubble observe --namespace checkout --verdict DENIED # Compare: count of iptables rules under a legacy CNI (the thing eBPF avoids) iptables -t filter -L KUBE-NETWORK-POLICY -n | wc -l
Interview Tip
Anchor the answer on the actual mechanism difference: chain traversal (O(n) in rule count) vs. identity-based hash map lookup (near O(1)). Mention kube-proxy replacement and Hubble observability as concrete operational wins, and be honest about the kernel-version and upgrade-risk tradeoffs — interviewers at this level want to know you'd evaluate the migration cost, not just declare eBPF strictly better.
💬 Comments
Quick Answer
Every write to the Kubernetes API server (create/update/delete) must be committed by etcd's Raft leader to a majority quorum of etcd members before it's acknowledged, so write latency is bounded by the slowest member needed to reach quorum plus fsync-to-disk time on the leader. Under load, debug by checking etcd's `disk_wal_fsync_duration_seconds`, leader election counts, and the API server's request latency metrics broken out by verb.
Detailed Answer
Kubernetes treats etcd as the single source of truth, and every mutating API request goes through Raft consensus: the etcd leader appends the entry to its write-ahead log, replicates it to followers, and only acknowledges the write once a majority (quorum) of members have persisted it to disk (fsync, not just memory — this is what makes etcd durable across crashes). This means write latency has a hard floor set by disk fsync latency on however many nodes are needed for quorum, and any added network latency between etcd members directly inflates write latency.
1. Disk-bound latency: etcd is extremely fsync-sensitive. If etcd shares a disk with other I/O-heavy workloads, or runs on non-SSD storage, fsync latency spikes and every write in the cluster slows down. Diagnose with etcd_disk_wal_fsync_duration_seconds — Google/etcd's own guidance treats >10ms p99 as a red flag for production etcd. 2. Quorum loss / leader churn: if network partitions or resource starvation cause repeated leader elections (etcd_server_leader_changes_seen_total incrementing), every election pauses writes cluster-wide for the duration of the election, which can be a second or more under contention. 3. API server overload independent of etcd: large List/Watch requests (e.g., a controller doing an unbounded LIST of all Pods cluster-wide) can starve the API server's request queue even when etcd itself is healthy — distinguish this by comparing apiserver_request_duration_seconds against etcd's own latency metrics; if etcd is fast but the API server is slow, the bottleneck is API server-side (admission webhooks, conversion, or queue depth), not etcd. 4. Large key/value churn: high rates of resource updates (e.g., frequent Endpoints/EndpointSlice updates from a churny Service) inflate etcd's DB size and revision history, which slows compaction and can eventually trigger 'etcdserver: mvcc: database space exceeded' errors if compaction/defrag isn't running regularly.
The debugging path: check etcd's own metrics first (fsync latency, leader changes) to rule in/out etcd as the bottleneck, then check API server request latency by verb and resource to see if it's etcd-bound or API-server-bound, then check what's generating the write/read volume (a misbehaving controller is the most common real-world cause).
Code Example
# etcd: check fsync latency (the dominant cost of consensus writes) curl -s http://localhost:2379/metrics | grep etcd_disk_wal_fsync_duration_seconds # etcd: leader churn indicates instability pausing writes curl -s http://localhost:2379/metrics | grep etcd_server_leader_changes_seen_total # API server: write latency by verb/resource, to localize the bottleneck curl -s http://localhost:6443/metrics | grep apiserver_request_duration_seconds_bucket | grep 'verb="PUT"' # Find controllers doing expensive unbounded LISTs kubectl get --raw /metrics | grep apiserver_request_total | sort -t'=' -k2 -n -r | head # etcd db size / compaction health ETCDCTL_API=3 etcdctl endpoint status --write-out=table
Interview Tip
Strong answers separate three distinct bottlenecks that get conflated: etcd disk fsync latency, etcd leader churn, and API-server-side queueing from a misbehaving controller. Naming the actual metrics (`etcd_disk_wal_fsync_duration_seconds`, `etcd_server_leader_changes_seen_total`) and explaining why etcd's durability guarantee (quorum fsync) is the latency floor shows you understand consensus isn't just a buzzword here — it's the mechanism setting your write SLO.
💬 Comments
Quick Answer
Kubelet API requests can now be authorized against specific node subresources such as `nodes/metrics`, `nodes/stats`, and `nodes/pods` instead of broadly granting `nodes/proxy`. This lets monitoring agents scrape what they need without receiving a much wider kubelet API permission.
Detailed Answer
Before fine-grained kubelet authorization, many monitoring agents received nodes/proxy access because that was the practical way to reach kubelet endpoints. That permission is broad and can expose more kubelet API surface than a metrics scraper needs.
In Kubernetes v1.36, fine-grained kubelet authorization is GA and always active. The kubelet first checks a specific subresource for common paths, then falls back to nodes/proxy for compatibility. This creates a migration path: existing charts continue to work, while new or hardened deployments can move to scoped permissions.
A production migration should inventory ClusterRoles that grant nodes/proxy, identify why each workload needs kubelet access, and replace broad rules with subresources such as nodes/metrics and nodes/stats where possible. Then use admission policy or policy-as-code to flag new broad grants. Watch for mixed-version clusters and third-party Helm charts that have not yet adopted the new RBAC model.
Code Example
apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: monitoring-agent rules: - apiGroups: [""] resources: ["nodes/metrics", "nodes/stats"] verbs: ["get"] # Prefer this scoped access over granting nodes/proxy to a metrics-only agent.
Interview Tip
Explain the old `nodes/proxy` pattern, why it was risky, and how you would migrate Helm charts safely without breaking observability during a cluster upgrade.
◈ Architecture Diagram
Agent ↓ get nodes/metrics ↓ no broad proxy Kubelet metrics only
💬 Comments
Quick Answer
Three things: CRD schemas must declare list-merge semantics (x-kubernetes-list-type / merge keys) or SSA treats their lists atomically and replaces the whole list; imperative writes (patch/update/subresource) still overwrite SSA-owned fields without raising a conflict; and Helm 4 uses SSA by default for new releases but existing releases need an explicit server-side migration.
Detailed Answer
SSA merges structurally using the OpenAPI schema, so behavior depends on how types describe their lists. Built-in types are annotated, but a CRD without x-kubernetes-list-type: map/set and merge keys is treated as atomic — an apply replaces the entire list instead of merging elements, which can wipe entries other managers added. Second, SSA only governs the apply path; a plain kubectl patch, an update, or a subresource write still overwrites an SSA-owned field and does not surface an SSA conflict, so mixing imperative edits can still bite you. Third, plan the Helm story: Helm 4 adopts server-side apply for new releases, but releases created under the old model must be migrated explicitly rather than switching automatically. Validate these before flipping SSA on cluster-wide.
Code Example
# CRD list that merges by key under SSA (not atomic) # openAPIV3Schema: # properties: # spec: # properties: # rules: # x-kubernetes-list-type: map # x-kubernetes-list-map-keys: ["name"]
Interview Tip
The CRD list-type point is the one most people miss — leading with it signals real production experience with SSA and custom resources.
💬 Comments
Quick Answer
Adopt SSA when multiple controllers or tools manage the same objects and you need the API server to track ownership and surface contested fields — for example Git plus an HPA plus Helm all touching one Deployment. For simple, single-writer objects, classic apply is perfectly fine; SSA is about coordinating many writers safely.
Detailed Answer
The decision is about how many writers share an object. If one manifest is the only thing that ever changes an object, client-side apply is simple and adequate. But as soon as several actors co-manage it — GitOps reconciling the template, an HPA owning replicas, admission webhooks defaulting fields, operators writing status — you want per-field ownership and loud conflicts, which is exactly what SSA provides. Practically, teams standardize on stable field-manager names, remove autoscaled/defaulted fields from Git so the right owner holds them, prefer apply over imperative edits to keep managedFields clean, and plan the Helm 4 migration. The payoff is that shared objects stop silently reverting and every change is attributable.
Code Example
# Rule of thumb # single writer -> classic apply is fine # many writers/object -> server-side apply (ownership + conflicts)
Interview Tip
Answer with the decision axis (number of writers per object), not a blanket "always use SSA" — architects reason about trade-offs.
💬 Comments