SRE Labs (Advanced Track) — Kubernetes War Room: Checkout API Pod Stuck in Pending State
Structured educational resource covering sre labs (advanced track) — kubernetes war room: checkout api pod stuck in pending state.
Prefer to reason through this failure class as a guided, decision-by-decision walkthrough first? Work the matching Incident Replay:
Live Team-Based Debugging — Pod Anti-Affinity, Node Taints/Tolerations & a Conflicting NodeSelector
2. Table of Contents
- Executive Summary
- Table of Contents
- Detailed Structured Notes
- 3.1 Infrastructure Overview
- 3.2 Application Overview
- 3.3 The Incident — Problem Statement
- 3.4 Documentation Format (Incident Summary Structure)
- 3.5 Instructor’s Solo Investigation Pass
- 3.6 Pre-Hands-On Hypotheses from Participants
- 3.7 Live Team Debugging — Full Walkthrough
- 3.8 Root Cause — Final Synthesis
- 3.9 Required vs. Preferred Affinity — The Core Concept
- 3.10 Tooling Notes (EKS Access Entries, K8s Lens)
- 3.11 Session Wrap-Up & Continuation Plan
- Key Concepts Table
- Architecture & Workflow Analysis
- Commands & Configurations
- Tools & Technologies
- Real-World Production Usage
- Interview Preparation (Beginner / Intermediate / Advanced)
- Exam & Certification Notes
- Cheat Sheet
- Gaps & Assumptions
3. Detailed Structured Notes
3.1 Infrastructure Overview
| Attribute | Detail |
|---|---|
| Cluster name | sre-labs-ha-lab |
| Kubernetes version | 1.29 (documentation referenced 1.28, but that version had gone out of AWS EKS support by the time of the live build, so the instructor upgraded to 1.29 for consistency — explicitly noted as not being version-dependent for the lesson itself) |
| Node groups | Two: primary (on-demand; min 2 / max 3 / desired 2) and batch (Spot; min/max/desired 1) |
| Add-ons | CoreDNS, kube-proxy, VPC CNI, metrics-server |
| Networking | Dedicated VPC (eksctl-infra-lab), 3 public + 3 private subnets, EKS-managed security groups |
| CI/CD | GitHub Actions pipeline deploying to the EKS cluster |
A real, unplanned issue encountered while building this environment (not the designed lesson, but worth noting): the instructor hit actual ENI (Elastic Network Interface) exhaustion in the ap-south-1 region while setting this cluster up — new pods couldn’t get IPs assigned because available ENIs were exhausted, requiring the instructor to manually delete existing unused ENIs before pods could be scheduled with networking. This was explicitly flagged as an infrastructure-build artifact, separate from the actual designed root cause of the exercise.
3.2 Application Overview
- Namespace:
payments - Deployment:
checkout-api— framed as the business-critical microservice handling the payment gateway on an e-commerce platform’s checkout page. - Replicas: 3
- Container image: an Alpine-based nginx image (used as a stand-in application for the exercise)
- Resource requests/limits: requests ~50m CPU / 64Mi RAM; limits ~200m CPU / 256Mi RAM
- Probes: liveness and readiness probes configured
- Service exposure: exposed externally via an AWS Application Load Balancer (ALB); confirmed reachable via
curlagainst the ALB’s DNS name at the start of the session - Downstream dependency (per the incident documentation, architectural context only): a “risk calculation engine” — a fraud-intelligence microservice — plus logging/monitoring as further downstream consumers.
3.3 The Incident — Problem Statement
- Trigger: during a routine deployment (framed as deploying a hotfix), the team observed that of the 3
checkout-apireplicas, 2 entered Running state successfully, and 1 remained stuck in Pending — reportedly for as long as 8 hours in the scenario’s narrative. - No obvious errors anywhere: no container crashes, no CrashLoopBackOff, no failing liveness/readiness probes, no node-level issues — nodes, CNI, and kube-proxy all appeared healthy from every standard health check.
- Business impact escalation path (as explained): with only 2 of 3 replicas actually serving traffic, load concentrates on the remaining 2 — first manifesting as increased latency, and if unresolved, eventually leading to 503 Service Unavailable errors for end users as the remaining pods become overwhelmed.
- Deployment pipeline blocked: because the rollout couldn’t complete (the new pod never became Ready), the CI/CD pipeline was effectively stuck — meaning no further hotfixes or changes could be deployed until this was resolved, compounding the incident’s severity.
- Declared severity: Severity 1 (highest), based on three red flags: (1) the critical service is in a partially degraded state, (2) the deployment pipeline itself is blocked, and (3) there’s no immediately visible error to point to.
- Stated time pressure: framed as needing resolution within roughly 5–10 minutes in a real e-commerce production scenario, given how quickly latency degradation can cascade into customer-visible failures under real traffic.
3.4 Documentation Format (Incident Summary Structure)
Consistent with the documentation pattern established in earlier war-room sessions in this series, the incident was pre-documented with:
- Incident summary — what happened, in plain language.
- Architecture diagram — showing the request path: client → ALB → NodePort → pod → application, with the fraud-detection engine and logging/monitoring as downstream dependencies.
- Environmental context — cluster name, namespace, workload details, region/subnet layout, networking components, add-ons — all matching the live infrastructure walkthrough in Section 3.1–3.2.
- Blast radius — explicitly separating current impact (internal/pipeline-level) from potential/escalating impact (direct customer-facing failure, financial and reputational loss) if unresolved.
- Upstream / midstream / downstream flow — a deliberate, explicit breakdown of how a request enters the system (upstream: client → ALB → NodePort, with ALB performing health checks against target groups), what happens inside the cluster (midstream: kube-proxy resolving the pod IP via iptables-based routing, e.g. round-robin-style algorithms, then forwarding to the pod/application), and how the response leaves the system (downstream: application → kube-proxy → ALB target group → external client).
- Explicit pedagogical point made about this upstream/downstream framing: understanding exactly how data moves through an infrastructure — in detail, end to end — is presented as a prerequisite for effective troubleshooting; without that mental model, it’s much harder to know where in the chain a gap might exist.
3.5 Instructor’s Solo Investigation Pass
Before handing off to the team, the instructor ran through an initial diagnostic pass live, to model the process:
kubectl get pods -n payments -o wide— confirmed 2 pods Running, 1 pod Pending with no IP and no node assigned at all (a specific, important detail — a pod with no node assignment hasn’t even been scheduled yet, distinct from a pod that’s scheduled but failing to start).kubectl describe pod <pending-pod>— reviewed configuration (image, resource requests/limits, probes) but at this stage, no immediately obvious error jumped out in the description.- Checked node status — all nodes reported Ready, with healthy-looking resource utilization (CPU/memory both “green”).
- Checked CNI status/logs — reported healthy via the console UI, though the instructor noted being unable to successfully
kubectl exec/grep directly into CNI pod logs during the live session (a minor live friction point, not a finding). - Checked kube-proxy logs (last ~20 lines) — showed routine “sync loop” activity and periodic warnings, explicitly characterized as normal background chatter, not errors — iptables rules were confirmed in sync.
- Checked namespace-level events (
kubectl get events -n payments, sorted by timestamp) — surfaced some scheduling-related warning content, setting up the eventual pivot toward the actual root cause area. - Reviewed the full deployment manifest (YAML) directly.
Instructor’s mid-investigation synthesis, before handing off: since the application layer showed no errors, the working hypothesis narrowed toward the CNI, kube-proxy, or ENI/networking layer as the likely culprit — explicitly not yet landing on the actual scheduling-constraint root cause at this point, modeling honest, in-progress reasoning rather than jumping straight to the answer.
3.6 Pre-Hands-On Hypotheses from Participants
Before the team took over the keyboard, several participants offered hypotheses based on the symptoms described so far — worth preserving since multiple contributors converged on parts of the actual answer independently:
- Scheduler health — check whether the kube-scheduler component itself is running (acknowledged as rarely an issue on EKS specifically, but still a valid first check).
- Taints, tolerations, and affinity/anti-affinity — explicitly named early by a participant as a likely area, based on the “everything looks healthy but nothing schedules” pattern.
- VPC CNI version compatibility — a participant raised the possibility that the VPC CNI add-on had been updated to a version incompatible with the cluster’s EKS version, which can also silently block pod scheduling/networking.
- Priority classes / preemption policies — raised as another possible scheduling-layer cause.
- A cordoned node — a participant specifically hypothesized that a node might have been marked unschedulable (
cordon), which wouldn’t affect already-running pods on that node but would block new pods from landing there — this hypothesis turned out to be directly relevant to the actual root cause.
3.7 Live Team Debugging — Full Walkthrough
The session then handed control to the group, with one participant (“Sai”) volunteering to drive hands-on while the rest of the group collaboratively reasoned through next steps in real time — preserved here in reasonable detail because the process, including its false starts, is the pedagogically valuable part.
Step 1 — Gaining cluster access:
- The driving participant used the EKS “Access Entries” feature (IAM-based cluster access management) to self-grant cluster access, rather than the older
aws-authConfigMap method. - A brief, useful side-discussion occurred here: participants explicitly contrasted the two access-management approaches — the
aws-authConfigMap is the older, more manual method for mapping IAM identities to Kubernetes RBAC permissions; EKS Access Entries is confirmed as the newer, AWS-recommended, more manageable approach (UI/IAM-native), and was used here specifically because it was faster to set up live.
Step 2 — Reading the actual scheduling failure reason:
kubectl describe pod <pending-pod>on the actual pending pod (not just a general describe) revealed the key events:- “One node didn’t match anti-affinity rules."
- "One node has untolerated taint.”
- This immediately confirmed the affinity/taint hypothesis from Section 3.6.
Step 3 — Checking node state:
kubectl get nodesshowed one node inSchedulingDisabledstate (i.e., cordoned) and the other two nodes carrying taints.
Step 4 — Reviewing the deployment YAML for affinity/toleration configuration:
- Found a pod anti-affinity rule using
requiredDuringSchedulingIgnoredDuringExecution, keyed ontopology.kubernetes.io/hostname(later corrected during live editing — see below) — meaning: no two pods from this deployment may run on the same node, and this rule is a hard requirement, not a preference. - Critically: no matching
tolerationswere present in the deployment spec for the taints found on the nodes. - Root cause hypothesis formed at this point: the taints were very likely added to the nodes after the original 3 pods had already been scheduled and were running — meaning existing pods kept running undisturbed (taints don’t evict already-running pods by default), but this specific rollout’s new pod, needing a fresh scheduling decision, had nowhere valid to go: the cordoned node was unschedulable outright, and the other nodes’ taints blocked placement without a matching toleration.
Step 5 — First fix attempt: loosen anti-affinity from required to preferred (live YAML editing, with genuine friction):
- The team attempted to edit the deployment’s affinity block from
requiredDuringSchedulingIgnoredDuringExecutiontopreferredDuringSchedulingIgnoredDuringExecutionlive viakubectl edit. - This produced a realistic sequence of YAML syntax errors that the group worked through together in real time: a missing/misplaced
podAffinityTermwrapper, incorrect indentation levels, a missing requiredweightfield (mandatory specifically for thepreferredvariant, since it expresses relative priority among multiple preferred rules — the API rejected the edit with “invalid value: 0, must be in range 1–100” until a weight was explicitly added), and a stray/misplacedlabelSelectorpositioning issue. - After several corrected attempts, the edit was successfully saved.
Step 6 — Confirming the fix wasn’t yet complete:
- Even after correcting the affinity block to “preferred,”
kubectl get eventsstill showed a scheduling failure — because the taint/toleration mismatch was still unresolved (loosening affinity alone didn’t address the second contributing cause). - The team also reasoned through resource availability as a possible contributing factor at this point (i.e., is there actually enough CPU/RAM capacity on the remaining schedulable node), using
kubectl top nodes— concluded resources were not the limiting factor.
Step 7 — Uncordoning a node, and an important intermediate observation:
- The team uncordoned the previously-
SchedulingDisablednode. - Important nuance surfaced: this node had no taint (distinct from the other two nodes, which did have taints) — meaning once uncordoned, since it required no toleration, all three pods ended up scheduling onto this single node — technically “fixing” the Pending state, but defeating the actual purpose of the anti-affinity rule (spreading replicas across nodes for resiliency) and not actually validating whether the toleration fix would work on the tainted nodes.
- This was explicitly recognized by the team as an incomplete/unsatisfying fix, prompting further investigation and correction.
Step 8 — Adding tolerations to the deployment:
- The team added a
tolerationsblock to the deployment spec, matching it to the taint present on thebatchnode group’s node (referenced as a “workload=batch”-style taint, matching the earlier-establishedbatchnode group’s Spot-instance role). - More YAML indentation troubleshooting occurred here as well (the tolerations block initially needed repositioning relative to the container spec).
Step 9 — Discovering and resolving a third contributing misconfiguration — a conflicting nodeSelector:
- Even after the affinity and toleration fixes, a participant noticed the deployment also contained an explicit
nodeSelectorset to a value likeprimary— a hard requirement that the pod land specifically on a node carrying that exact label/name. - This was identified as a genuinely separate, additional misconfiguration: a
nodeSelector, pod affinity/anti-affinity rules, and toleration/taint compatibility are all evaluated together with AND logic — every single one must be independently satisfiable for the scheduler to place the pod. AnodeSelectorpointing toward a node that doesn’t actually align with the other constraints (or that doesn’t exist/match anything valid, as was effectively the case here) makes the entire combination unsatisfiable, regardless of how correctly the other rules are configured. - The team commented out (and later confirmed removal of) the conflicting
nodeSelector, since hardcoding a specific node name/label is generally poor practice in an autoscaling environment in the first place (node names/instances can change, especially with Spot or autoscaled capacity).
Step 10 — Final validation and rolling restart:
- With all three contributing issues addressed (affinity loosened to preferred for immediate resolution, tolerations added, conflicting nodeSelector removed), the team performed a rolling restart of the deployment (
kubectl rollout restart deployment/checkout-api -n payments) to force the still-outdated running pods (which had been scheduled under the old, broken configuration) to be replaced with pods reflecting the corrected spec. - Confirmed via
kubectl get podsandkubectl describe deploymentthat all 3 replicas were now Running successfully.
3.8 Root Cause — Final Synthesis
The team explicitly enumerated the final, combined root cause as three separate, simultaneously-contributing misconfigurations, not a single cause:
- Required (hard) pod anti-affinity (
requiredDuringSchedulingIgnoredDuringExecution, keyed on node hostname) demanding strict one-pod-per-node placement, in an environment where there weren’t actually enough distinct, untainted, schedulable nodes available to satisfy that strict requirement for all 3 replicas simultaneously. - Node taints added to the cluster’s nodes without corresponding
tolerationsever being added to the deployment spec — very likely added to the nodes after the original pods were already running (a timing-dependent trap: existing pods were unaffected, but the next fresh scheduling attempt was blocked). - A conflicting/incorrect
nodeSelectorhardcoded to a specific node label/name, which — combined with the other two constraints — made the overall combination of requirements effectively impossible to satisfy.
Discussion of the “correct” permanent fix vs. the fast interim fix: the team explicitly debated two different classes of resolution:
- Fast/interim: keep anti-affinity as
preferredrather thanrequired— unblocks scheduling immediately, but weakens the original high-availability guarantee (the scheduler may now colocate replicas on the same node under resource pressure, rather than being forced to spread them). - Slower/more correct, preserving original intent: add a genuinely new, correctly-configured node to the cluster (with matching taint/label setup) so that
requiredanti-affinity can actually be satisfied by 3 truly distinct nodes — preserving the original resiliency guarantee, at the cost of additional infrastructure and time to provision. - A related point raised: the cluster did not have a cluster autoscaler configured, meaning capacity issues like this require manual node-group scaling rather than automatic remediation — flagged as a separate, standing gap in the cluster’s resiliency posture, beyond the specific incident being debugged.
3.9 Required vs. Preferred Affinity — The Core Concept
Explained clearly mid-session by a participant, confirmed by the instructor, and worth preserving as the single most important conceptual takeaway from this entire incident:
preferredDuringSchedulingIgnoredDuringExecution(“preferred”): a soft constraint. The scheduler will try to satisfy the rule, but if it can’t (e.g., no node satisfies the preference), it will still schedule the pod anyway on the best available node rather than leaving it Pending.requiredDuringSchedulingIgnoredDuringExecution(“required”): a hard constraint. If no node can satisfy the rule, the pod remains Pending indefinitely — the scheduler will not compromise.- The
...IgnoredDuringExecutionsuffix (common to both variants) means the rule is only evaluated at scheduling time — if conditions change after a pod is already running (e.g., a node’s labels change, or a taint is added later, as happened in this exact incident), already-running pods are not affected/evicted because of it. This is precisely the mechanism that explains why the original 3 pods were fine, but a fresh rollout hit the wall.
3.10 Tooling Notes (EKS Access Entries, K8s Lens)
- EKS Access Entries (used live in this session): the current, AWS-recommended, IAM-native way to grant Kubernetes cluster access, superseding the older
aws-authConfigMap approach. Explicitly discussed and contrasted live, with the group agreeing Access Entries is the better, more modern practice going forward. - K8s Lens (referenced by the instructor as a recommended tool, in response to a participant question about GUI-based troubleshooting options): a graphical, multi-cluster Kubernetes management interface — supports connecting to and visually browsing multiple clusters (EKS, GKE, AKS, etc.) via kubeconfig, offering a more approachable visualization layer than raw CLI output for exploring workloads, nodes, and cluster state. Recommended especially for participants earlier in their Kubernetes learning curve, or when working across many clusters where a unified visual interface adds real value.
3.11 Session Wrap-Up & Continuation Plan
- The team completed roughly 60–70% of the full incident/exercise within this session; the instructor explicitly offered the group a choice to extend by up to an hour or continue the following day — the group chose to continue the next day.
- Explicit acknowledgment that engagement was uneven: the instructor directly noted that only a small number of participants (three, by name) were actively interactive/hands-on during the live debugging, and checked in with the broader group about whether they were able to follow along — one participant candidly noted limited direct Kubernetes experience made real-time participation difficult without more foundational familiarity first.
- A parallel/independent GCP-based version of the same outage scenario was confirmed as already built by the instructor, for participants wanting to explore and practice the same class of problem independently ahead of the next session.
- Planned for the next session: complete the remaining portion of this incident and its assignment component, cover RCA-writing practice specifically (how to write an effective RCA, with examples), finish the security-hardening track, and begin the ~700-microservice-scale project call — total planned session length roughly 2.5–3 hours.
- Explicit note on Advanced Track difficulty: this specific incident was framed as Foundations Track-level (basic to intermediate); the corresponding Advanced Track version of this class of problem is explicitly framed as intermediate to advanced, and participants were encouraged to make sure they genuinely understood this session’s concepts (not just the specific commands run) before attempting it.
4. Key Concepts Table
| Concept | Explanation | Example | Why It Matters |
|---|---|---|---|
| Pod stuck Pending with no IP/node assigned | A pod that hasn’t even been scheduled yet (distinct from a pod that’s scheduled but failing health checks) | The checkout-api pod had no IP and no node in kubectl get pods -o wide | Immediately narrows investigation toward scheduling-layer causes (affinity, taints, resources) rather than runtime/application issues |
kubectl describe pod events as the primary diagnostic | The Events section of a pod’s description directly states the scheduler’s reason for failing to place it | ”One node didn’t match anti-affinity rules,” “one node has untolerated taint” | Often the single fastest path to the actual root cause for scheduling-related incidents — check this early |
| Required vs. preferred affinity/anti-affinity | Required = hard constraint, pod stays Pending if unsatisfiable; preferred = soft constraint, scheduler does its best but still places the pod | Changing requiredDuringScheduling... to preferredDuringScheduling... unblocked scheduling | The single most important conceptual distinction in this entire incident |
...IgnoredDuringExecution | Affinity/anti-affinity and toleration rules are evaluated at scheduling time only — changes afterward don’t retroactively evict already-running pods | Taints added to nodes after the original pods were scheduled didn’t evict them, but blocked the next new pod | Explains the exact timing-dependent trap this incident was built around |
| Taints and tolerations as an AND condition alongside affinity/nodeSelector | All scheduling constraints (nodeSelector, affinity/anti-affinity, toleration-vs-taint) must be simultaneously satisfiable — not evaluated independently or with OR logic | A correct anti-affinity fix alone wasn’t enough while a conflicting nodeSelector remained | A single misaligned constraint can make an otherwise-correct configuration unsatisfiable as a whole |
| Taint added after pods are already scheduled | Taints don’t affect already-running pods (unless using NoExecute with eviction); they only block future scheduling attempts without a matching toleration | Original 3 pods were fine; a fresh rollout’s new pod couldn’t schedule | A subtle, realistic root-cause pattern — “it worked before, why not now” incidents often trace back to exactly this |
| Uncordoning without fixing tolerations = incomplete fix | Removing a cordon can “resolve” a Pending pod by concentrating all replicas onto the one untainted node, without addressing the underlying toleration gap | All 3 pods landed on a single node after uncordon, defeating the anti-affinity rule’s purpose | A fix that resolves the symptom without addressing the underlying cause can silently reintroduce the original risk (loss of spread/resiliency) |
| Fast/interim fix vs. permanent fix trade-off | Loosening a constraint (required→preferred) unblocks quickly but weakens the original guarantee; adding real capacity preserves intent but takes longer | Debated live: keep “preferred” vs. add a new correctly-tainted node | A realistic illustration of incident-response trade-offs between speed and preserving original architectural intent |
| No cluster autoscaler present | Without an autoscaler, capacity-related scheduling problems require manual node-group scaling rather than automatic remediation | Flagged as a standing gap during the incident, separate from the specific root cause | A good example of an incident surfacing an unrelated, pre-existing architectural gap worth addressing separately |
EKS Access Entries vs. aws-auth ConfigMap | Two different mechanisms for granting IAM identities access to an EKS cluster’s Kubernetes API | Access Entries used live for faster, IAM-native access grants | Reflects current EKS best practice — Access Entries is the newer, AWS-recommended approach |
5. Architecture & Workflow Analysis
5.1 Request Flow (Upstream → Midstream → Downstream)
UPSTREAM:
End Customer
|
v
ALB (external endpoint, health-checks target groups)
|
v
NodePort (on a healthy worker node)
MIDSTREAM (inside the cluster):
|
v
kube-proxy (iptables-based routing, e.g. round-robin-style
algorithm to select a healthy pod IP)
|
v
Pod (checkout-api container, listening on port 80)
|
v
Application generates response
DOWNSTREAM:
|
v
kube-proxy -> Node -> ALB target group
|
v
End Customer receives response
5.2 Scheduling Failure Decision Tree (As Actually Diagnosed)
Pod stuck Pending, no IP, no node assigned
|
v
kubectl describe pod -> check Events section
|
v
Events show: "anti-affinity rule not matched" +
"untolerated taint"
|
v
kubectl get nodes -> one node SchedulingDisabled (cordoned)
other nodes carry TAINTS
|
v
Review deployment YAML:
|
-----------------------------------------------
| | |
REQUIRED anti- NO tolerations for nodeSelector
affinity (hard existing node taints pointing to a
constraint, conflicting/
one-pod-per- non-matching
node, strict) node label
| | |
-----------------------------------------------
|
v
ALL THREE must be resolved together
(AND logic -- fixing only one is insufficient)
5.3 Fix Sequence (As Actually Executed, Including False Starts)
1. Attempt: required -> preferred anti-affinity
(multiple YAML syntax errors fixed live: missing weight,
misplaced podAffinityTerm, indentation issues)
|
v
2. Re-check events: STILL failing
(toleration gap not yet addressed)
|
v
3. Check node resources (kubectl top nodes) -> NOT the bottleneck
|
v
4. Uncordon the disabled node
-> ALL 3 pods land on this ONE node (it has no taint)
-> "works" but defeats anti-affinity's purpose
|
v
5. Add tolerations matching the tainted nodes' taint
|
v
6. Discover conflicting nodeSelector ("primary")
-> remove/comment out
|
v
7. Rolling restart deployment
|
v
8. Verify: all 3 replicas Running, correctly distributed
|
v
9. DISCUSS permanent fix:
- Keep "preferred" (fast, weaker HA guarantee), OR
- Add a new correctly-tainted node + revert to "required"
(slower, preserves original HA intent)
5.4 Constraint Combination Logic (AND, Not OR)
Pod Scheduling Decision
|
v
MUST satisfy ALL of:
|
-------------------------------------------
| | |
nodeSelector Affinity/Anti-Affinity Toleration matches
match rules satisfied any relevant Taint
| | |
-------------------------------------------
|
v
If ANY ONE fails -> pod stays Pending
(assuming required/hard constraints;
a "preferred" constraint alone won't
block scheduling, but nodeSelector and
toleration/taint compatibility ARE
effectively hard requirements)
6. Commands & Configurations
| Command / Config | Purpose | Explanation |
|---|---|---|
kubectl get pods -n payments -o wide | List pods with node/IP assignment detail | First command run; revealed the pending pod had no IP and no node assigned |
kubectl describe pod <pod-name> -n payments | Full pod detail including scheduling Events | The single most important diagnostic command in this incident — directly surfaced the anti-affinity and taint failure reasons |
kubectl get nodes | List node status | Revealed one node in SchedulingDisabled (cordoned) state |
kubectl describe node <node-name> | Full node detail including taints | Used to confirm exact taints present on each node |
kubectl get events -n payments --sort-by=.lastTimestamp | Namespace-scoped events, chronologically sorted | Used repeatedly throughout the investigation to check whether a fix had actually resolved the scheduling failure |
kubectl top nodes | Check current CPU/memory utilization per node | Used to rule out resource exhaustion as a contributing factor |
kubectl edit deployment checkout-api -n payments | Live-edit the deployment spec | Used to iteratively fix the anti-affinity block, add tolerations, and remove the conflicting nodeSelector |
podAntiAffinity — requiredDuringSchedulingIgnoredDuringExecution vs. preferredDuringSchedulingIgnoredDuringExecution | Kubernetes affinity API fields controlling hard vs. soft scheduling constraints | The central YAML construct debugged throughout this session |
topologyKey: kubernetes.io/hostname | Specifies the node-grouping granularity for anti-affinity evaluation | Ensures “one pod per node” is evaluated at the individual-node level (as opposed to e.g. per-zone) |
weight field (required specifically for preferred anti-affinity terms) | Expresses relative priority when multiple preferred rules exist (valid range 1–100) | The API rejected the edit with a “must be in range 1–100” error until this was explicitly added — a real, live-encountered validation requirement |
tolerations block in the pod spec | Allows a pod to be scheduled onto a node carrying a matching taint | Added to match the existing node taints, resolving the second contributing cause |
nodeSelector field in the pod spec | Hard requirement that a pod land on a node with matching labels | Identified as a third, conflicting misconfiguration; removed as part of the fix |
kubectl rollout restart deployment/checkout-api -n payments | Force a rolling restart of all pods under a deployment | Used to apply the corrected configuration to all replicas, replacing pods that had been running under the broken original config |
| EKS Access Entries (Console/IAM) | Grant IAM identities Kubernetes cluster access — the modern, AWS-recommended method | Used live to quickly self-grant cluster access, contrasted against the older aws-auth ConfigMap approach |
7. Tools & Technologies
EKS Access Entries
- Purpose: IAM-native mechanism for granting Kubernetes RBAC access to an EKS cluster.
- When to use it: As the current, AWS-recommended replacement for manually editing the
aws-authConfigMap — simpler to manage, especially for granting/revoking access quickly. - Contrast: the older
aws-authConfigMap approach requires directly editing a Kubernetes-native ConfigMap to map IAM ARNs to RBAC roles/groups — more manual and error-prone.
K8s Lens
- Purpose: A graphical, multi-cluster Kubernetes management and visualization tool.
- When to use it: As a more approachable alternative/complement to pure CLI-based troubleshooting, especially useful when managing multiple clusters (EKS, GKE, AKS) from one interface, or for engineers still building CLI fluency.
kubectl (core diagnostic commands used throughout)
- Purpose: The standard Kubernetes CLI, used here for essentially the entire live investigation.
- When to use it: For direct, precise inspection and modification of cluster state —
describe,get events,top,edit, androllout restartwere the specific subcommands central to this session’s resolution.
8. Real-World Production Usage
- This session is a genuinely authentic, unpolished demonstration of real Kubernetes incident response, including the actual messiness (YAML syntax errors, iterative correction, false starts like the “uncordon alone” partial fix) that characterizes real live debugging far more accurately than a scripted, error-free walkthrough would — this authenticity is itself valuable, since it models what to actually expect when debugging live under time pressure with a team.
- The “pod stuck Pending with a completely healthy-looking cluster” pattern is extremely common in real production Kubernetes environments, and the diagnostic sequence demonstrated here (check pod events first, then nodes, then the full scheduling-constraint picture: affinity, taints/tolerations, nodeSelector, resources) is a directly reusable, general-purpose troubleshooting sequence for this entire class of incident.
- The “taint added after pods were already running” root-cause pattern is a realistic, easily-overlooked operational trap — teams that add node taints for legitimate reasons (e.g., dedicating nodes to specific workload types) without auditing whether all relevant deployments have matching tolerations can create exactly this kind of delayed, non-obvious incident that only manifests on the next deployment or scale-up event, not immediately.
- The explicit trade-off discussion between “loosen the constraint” and “add real capacity to preserve the original guarantee” reflects a mature, realistic incident-response judgment call that real SREs/platform engineers make constantly — fast mitigation versus fully preserving original architectural intent is a genuine tension, not a simple “right answer,” and this session models discussing that trade-off explicitly rather than picking one silently.
- Using EKS Access Entries over the older
aws-authConfigMap reflects current, real-world EKS best practice — this is directly relevant, up-to-date operational knowledge for anyone managing real EKS clusters today. - The three-way combined root cause (anti-affinity + taints/tolerations + nodeSelector) is a realistic illustration of why Kubernetes incidents are so often multi-factor rather than single-cause — and why declaring victory after fixing just the first identified issue (as the team’s own mid-session “uncordon” partial-fix moment demonstrates) can produce an incomplete, symptom-masking resolution rather than a true fix.
9. Interview Preparation
Beginner Questions
Q1: What does it mean when a Kubernetes pod is stuck in Pending state with no node or IP assigned?
A: It means the Kubernetes scheduler has not yet been able to place the pod onto any node at all — this is different from a pod that’s been scheduled but is failing to start (e.g., due to an image pull error or crash). A pod with no node assignment indicates a scheduling-level problem: the scheduler couldn’t find any node satisfying all of the pod’s placement constraints (resource requests, node affinity/anti-affinity, taints/tolerations, node selectors).
Q2: What’s the difference between requiredDuringSchedulingIgnoredDuringExecution and preferredDuringSchedulingIgnoredDuringExecution in Kubernetes affinity rules?
A: required is a hard constraint — if no node satisfies it, the pod remains Pending indefinitely; the scheduler will not compromise. preferred is a soft constraint — the scheduler tries to satisfy it, but will still place the pod on the best available node even if the preference can’t be met, rather than leaving it unscheduled.
Q3: What’s the first command you’d run to understand why a specific pod is stuck in Pending state?
A: kubectl describe pod <pod-name> — specifically, review its Events section, which directly states the scheduler’s reason(s) for failing to place the pod (e.g., “node(s) didn’t match pod affinity/anti-affinity rules,” “node(s) had untolerated taint,” or resource-related messages).
Intermediate Questions
Q4: A node has a new taint added to it, but the pods that were already running on that node before the taint was added are still running fine. Why doesn’t the taint affect them?
A: Taints (with the default NoSchedule effect, as opposed to NoExecute) only affect future scheduling decisions — they prevent new pods without a matching toleration from being scheduled onto that node, but they don’t retroactively evict pods that are already running there. This is exactly why a cluster can “look fine” after a taint is added — until the next deployment, rollout, or scale-up event triggers a fresh scheduling decision that then gets blocked.
Q5: You’ve fixed a pod’s anti-affinity rule from required to preferred, but the pod is still stuck in Pending. What would you check next?
A: Loosening the anti-affinity constraint only addresses that one specific requirement — if the pod is still failing to schedule, other independent constraints are likely still unsatisfied. Check kubectl get events again for the current failure reason, and specifically review the pod/deployment spec for taints/tolerations mismatches and any nodeSelector that might be pointing to a node that doesn’t actually exist or doesn’t satisfy the other constraints — all of these are evaluated together (AND logic), so fixing one doesn’t guarantee the pod becomes schedulable if others remain unresolved.
Q6: Why might simply uncordoning a previously-cordoned node “fix” a stuck pod, but not actually be a correct or complete fix? A: Uncordoning makes a node schedulable again, which can allow a Pending pod to finally be placed — but if the underlying cause was a combination of issues (e.g., taints on other nodes with no matching tolerations, as in this session), the newly-uncordoned node may become the only viable target, causing all replicas to concentrate there rather than being properly spread according to the deployment’s original anti-affinity intent. This “resolves” the immediate symptom (pod no longer Pending) without addressing the underlying misconfiguration (missing tolerations), and undermines the resiliency goal the anti-affinity rule was meant to enforce in the first place.
Advanced Questions
Q7: Design a systematic diagnostic sequence for a pod stuck in Pending state in an EKS cluster where all standard component health checks (nodes, CNI, kube-proxy) report healthy.
A: Start with kubectl describe pod and read the Events section carefully — this almost always directly names the specific scheduling constraint that’s failing, rather than requiring guesswork. Cross-reference with kubectl get nodes to check for cordoned (SchedulingDisabled) nodes and kubectl describe node to check for taints. Review the full pod/deployment spec for nodeSelector, nodeAffinity, podAffinity/podAntiAffinity, and tolerations — and critically, evaluate all of these together as a combined AND condition rather than checking each in isolation, since a single misaligned constraint can make an otherwise-correct configuration unsatisfiable. Check resource requests against actual available node capacity (kubectl top nodes) to rule out simple resource exhaustion. If the cluster recently had any node group, taint, or labeling changes, specifically investigate whether those changes were made after existing pods were already scheduled — a common, easily-overlooked timing-dependent root cause pattern, since ...IgnoredDuringExecution semantics mean already-running pods aren’t retroactively affected by such changes.
Q8: A team wants to enforce strict one-pod-per-node placement for a critical, high-availability service using required pod anti-affinity, but their cluster doesn’t reliably have enough distinct, correctly-configured nodes available to satisfy this at all times (e.g., during a temporary node group scaling event). What are the trade-offs of different approaches to this tension?
A: Using required anti-affinity preserves the strongest guarantee — the scheduler will never colocate replicas on the same node, ensuring true node-level fault isolation — but at the cost of pods potentially being stuck Pending (and therefore under-replicated/under capacity) whenever sufficient distinct nodes aren’t available, which is itself an availability risk during exactly the kind of event (node loss, scaling, maintenance) that high availability is meant to protect against. Using preferred anti-affinity instead guarantees pods will always be scheduled (assuming other resources are available), trading away the strict node-spread guarantee for scheduling reliability — meaning under resource pressure, the scheduler might place two replicas on the same node, reducing the fault-isolation benefit exactly when it might matter most. A more robust approach than choosing one or the other outright is ensuring genuinely sufficient, correctly-configured node capacity is reliably available (e.g., via a properly-configured cluster autoscaler with node groups that match the deployment’s actual constraints) so that required anti-affinity’s guarantee can be honored without regularly hitting scheduling failures — treating capacity planning and autoscaling configuration as the actual fix, rather than permanently weakening the availability guarantee to work around insufficient capacity.
Q9: How would you prevent the specific class of incident demonstrated in this session (a taint added to nodes without corresponding tolerations being added to existing deployments) from recurring in a real production environment? A: Establish a change-management practice where any node-group-level change that introduces or modifies taints is explicitly cross-checked against all deployments that might need to schedule onto affected nodes — ideally as an automated check (e.g., a CI/CD or admission-control step that validates a proposed taint change against currently-deployed workloads’ tolerations before it’s applied) rather than a purely manual review, since this exact class of issue is easy to miss precisely because it doesn’t manifest immediately (existing pods keep running fine) and only surfaces on the next fresh scheduling event. Additionally, maintaining clear documentation of the purpose of each node group’s taints (e.g., “this node group is Spot-only, tainted for batch workloads, requires this specific toleration”) makes it much easier for engineers making unrelated deployment changes to recognize when their workload needs a corresponding toleration update. Regular, proactive audits comparing node taints against deployment tolerations across the cluster (rather than waiting for an incident to reveal a mismatch) would catch this class of drift before it causes a production incident.
10. Exam & Certification Notes
(Highly relevant to CKA (Certified Kubernetes Administrator) and CKAD (Certified Kubernetes Application Developer) certifications — this entire incident maps almost directly onto core CKA/CKAD scheduling-domain exam content.)
- Pod affinity/anti-affinity syntax and semantics: Know the exact structure of
requiredDuringSchedulingIgnoredDuringExecutionvs.preferredDuringSchedulingIgnoredDuringExecution, including that thepreferredvariant requires aweightfield (1–100) and wraps its rule in apodAffinityTerm, whilerequireddoes not use a weight — a frequently tested syntax distinction, and one this session’s live YAML-editing friction illustrates concretely. topologyKey: Understand that this field determines the granularity at which affinity/anti-affinity is evaluated (e.g., per-node viakubernetes.io/hostname, or per-zone via a zone label) — a commonly tested concept for controlling the scope of a spread constraint.- Taints and tolerations: Know the three taint effects —
NoSchedule(blocks new scheduling, doesn’t evict existing pods),PreferNoSchedule(soft version of NoSchedule), andNoExecute(blocks new scheduling AND evicts existing pods lacking a matching toleration, optionally after a grace period viatolerationSeconds) — this session specifically involvedNoSchedule-style behavior (existing pods unaffected), a key distinction fromNoExecute. nodeSelectorvs.nodeAffinity: Know thatnodeSelectoris the older, simpler, exact-match-only mechanism, whilenodeAffinitysupports more expressive matching (operators likeIn,NotIn,Exists) and both required/preferred variants — both were present in this incident’s deployment, and both are commonly tested.kubectl rollout restart: Know this command’s purpose — forcing a deployment’s pods to be recreated (picking up any spec changes) without requiring an actual image/config version bump — directly relevant to applying corrected scheduling configuration to already-running pods, as done in this session.- EKS Access Entries vs.
aws-authConfigMap: Relevant to AWS-specific EKS certification content (not core CKA/CKAD, which are cloud-agnostic) — know Access Entries as the current AWS-recommended IAM-to-RBAC mapping mechanism.
11. Cheat Sheet
Pod Stuck Pending — Diagnostic Order:
kubectl describe pod <name>→ read Events section firstkubectl get nodes→ check forSchedulingDisabled(cordoned) nodeskubectl describe node <name>→ check taints- Review deployment YAML →
nodeSelector,nodeAffinity/podAntiAffinity,tolerations kubectl top nodes→ rule out resource exhaustion
Required vs. Preferred (memorize):
| Behavior if unsatisfiable | |
|---|---|
required... | Pod stays Pending indefinitely |
preferred... | Pod schedules anyway, best-effort |
Constraint Combination Rule: nodeSelector + affinity/anti-affinity + toleration-vs-taint = ALL must pass (AND logic). Fixing one doesn’t help if another is still broken.
Taint Timing Trap: Taints added to a node after pods are already running there do NOT evict those pods (with NoSchedule) — they only block future scheduling attempts without a matching toleration. “It was working before” ≠ “nothing changed.”
Common YAML Gotchas (from this session’s live debugging):
preferredanti-affinity requires aweightfield (1–100)podAffinityTermmust correctly wrap the match criteria- Indentation errors are the most common source of
kubectl editrejections
Fix Trade-off:
required→preferred= fast, but weakens the HA guarantee- Add a real, correctly-tainted node = slower, but preserves original intent
Rolling Restart to Apply a Fix:
kubectl rollout restart deployment/<name> -n <namespace>
EKS Cluster Access — Modern Method: EKS Access Entries (IAM-native) > aws-auth ConfigMap (older, more manual)
12. Gaps & Assumptions
- This session ends with the incident only ~60-70% complete per the instructor’s own estimate — the remaining ~30-40% (further assignment components, and presumably a deeper RCA-writing exercise) was explicitly deferred to a follow-up session not captured in this transcript. Treat the resolution documented here as the actual, confirmed fix for the pod-scheduling incident itself, but not necessarily the full scope of the planned exercise.
- Exact YAML content is reconstructed from the live dialogue, not from directly-viewed file content — since this is a text transcript of a live screen-share session, the precise final YAML (indentation, exact field ordering, exact taint key/value/effect strings) is described based on what participants said aloud while editing, not extracted from an actual file. The concepts and sequence of fixes are accurately preserved; treat exact YAML syntax details as illustrative/representative rather than a verbatim copy of the final working manifest.
- The exact taint key/value used (referenced informally as something like “workload=batch”) was not stated with full precision in the transcript — presented here as the best available reconstruction from context (matching the
batchnode group’s role), not a confirmed exact string. - Only a subset of participants were actively hands-on during this session, as explicitly acknowledged by the instructor — this document reflects the full technical content of the exercise, but it’s worth noting that real-time engagement was uneven across the full cohort, per the instructor’s own direct check-in.
- The parallel GCP-based version of this same scenario was mentioned as already built but not demonstrated within this transcript — referenced here only as something confirmed to exist for independent practice, not as content covered live in this session.
- This document consolidates a long, genuinely iterative live-debugging session (including real syntax errors and corrections) — content has been reorganized topically for clarity (grouping the debugging steps into a coherent sequence) rather than presented in the exact raw chronological back-and-forth, while preserving the substance and sequence of what was actually tried, in what order, consistent with the approach used for prior packages in this series.
Active Objective: Triage Phase
[Triage Step] What is the primary operational procedure to complete the triage phase of the "SRE Labs (Advanced Track) — Kubernetes War Room: Checkout API Pod Stuck in Pending State" incident?