Debugging Kubernetes and Terraform
Debugging Kubernetes and Writing Terraform Modules
Kubernetes triage (first 60 seconds):
Bashkubectl get pods -A --field-selector=status.phase!=Running kubectl get events -A --sort-by='.lastTimestamp' | tail -30 kubectl top nodes kubectl describe pod <pod> -n <namespace> # check Events section first
Terraform module skeleton:
modules/<name>/
├── main.tf # resources
├── variables.tf # inputs with type + description + validation
├── outputs.tf # outputs with description
├── versions.tf # required_providers, required_version
└── README.md # usage example
Progress:
- Identify blast radius (one pod? one node? cluster-wide?)
- Check pod status and events
- Check logs (current + previous container)
- Check resource pressure (node/pod)
- Check networking (DNS, Service, NetworkPolicy)
- Check control plane health if cluster-wide
- Form hypothesis, verify, fix, confirm resolution
Step detail
-
Scope the problem.
kubectl get pods -A -o wide— is it one pod, one node, one namespace, or everywhere? Cluster-wide issues point to control plane, DNS, CNI, or a shared dependency. -
Pod status diagnosis — match symptom to cause:
CrashLoopBackOff→ checkkubectl logs <pod> --previous; usually app crash, bad config, or failed healthcheckImagePullBackOff→ bad tag, missing registry auth, or network egress blockedPending→kubectl describe podEvents; usually unschedulable (resource requests, taints, affinity, PVC not bound)OOMKilled(checkkubectl describe podlast state reason) → raise memory limits or fix leakRunningbut not ready → failing readiness probe; check probe config vs actual startup time
-
Logs. Always check both current and
--previouslogs. Use-fto tail live,--since=10mto scope noise. -
Resource pressure.
kubectl top nodes/kubectl top pods. Compare againstrequests/limits. Node pressure causes evictions — checkkubectl describe nodeforConditions(MemoryPressure, DiskPressure). -
Networking.
- DNS:
kubectl run -it --rm debug --image=busybox --restart=Never -- nslookup <service> - Service routing: confirm
Endpoints/EndpointSlicehas pod IPs (kubectl get endpoints <svc>) — empty means selector mismatch - NetworkPolicy: check for policies blocking traffic with
kubectl get networkpolicy -A
- DNS:
-
Control plane (cluster-wide symptoms only):
kubectl get componentstatuses(if available), check etcd health, API server logs, and nodekubeletstatus viakubectl get nodesconditions. -
Fix and verify. Apply minimal fix, re-check the original symptom, and check for regressions elsewhere (e.g., HPA reacting to the change).
Progress:
- Define interface (variables/outputs) before resources
- Write resources with sane defaults, no hardcoded values
- Add validation blocks to variables
- Pin provider versions
- Write outputs consumers actually need
- Add README with usage example
- Run
terraform validate+tflint+terraform planagainst example
Step detail
-
Design the interface first. Decide what the module takes as input and returns as output before writing resources — this is the module's contract.
-
Variables: every variable gets
type,description, and sensibledefault(or no default if required). Usevalidationblocks for constraints instead of relying on provider-side errors:HCLvariable "environment" { type = string description = "Deployment environment" validation { condition = contains(["dev", "staging", "prod"], var.environment) error_message = "environment must be dev, staging, or prod." } } -
No hardcoded values — region, account ID, naming prefixes all come from variables or data sources, never literals.
-
Pin versions in
versions.tf:HCLterraform { required_version = ">= 1.5.0" required_providers { aws = { source = "hashicorp/aws" version = "~> 5.0" } } } -
Outputs expose IDs/ARNs/endpoints consumers need to wire into other modules — not everything the resource has.
-
Tag/label everything created resources for cost tracking and ownership (
var.tagsmerged with module-level defaults). -
Test with a minimal
examples/basicdirectory that calls the module — this doubles as living documentation and a smoke test viaterraform plan.
Example 1:
Input: Pods stuck in Pending with event "0/3 nodes are available: 3 Insufficient cpu."
Output: Node capacity exhausted. Either scale the node pool, reduce pod CPU requests, or add a cluster-autoscaler. Verify with kubectl describe nodes | grep -A5 "Allocated resources".
Example 2:
Input: A Terraform module for an S3 bucket hardcodes bucket = "my-app-prod-logs".
Output: Replace with bucket = "${var.name_prefix}-${var.environment}-logs", add variable "name_prefix" and variable "environment" with validation, and expose output "bucket_arn" for downstream IAM policies.
Example 3:
Input: Service has no traffic reaching pods despite pods being Running and Ready.
Output: kubectl get endpoints <svc> returns empty — Service selector labels don't match pod labels. Fix selector or pod labels to match; confirm endpoints populate.
- Reproduce the minimal failing case before changing cluster-wide config.
- Prefer
kubectl describeoverget -o yamlfor diagnosis — Events are the fastest signal. - In Terraform, keep modules single-purpose (one module = one logical resource group); compose via root modules, don't build mega-modules.
- Always run
terraform planand read the full diff before apply, especially for destroy/recreate operations. - Use
for_eachovercountfor resource collections to avoid index-shift diffs. - Store Terraform state remotely with locking (S3+DynamoDB, Terraform Cloud, etc.) — never local state in shared environments.
- Debugging: don't restart pods before capturing logs — you lose the crash evidence (
--previousonly works if the container already restarted and kept history). - Debugging: don't assume DNS is fine because one lookup worked — test from the actual failing pod's namespace/context, not a fresh debug pod with different NetworkPolicy exposure.
- Terraform: don't put secrets in
variables.tfdefaults or commit.tfvarswith real credentials — use a secrets manager + data source. - Terraform: avoid
countwith conditional logic across resources that depend on each other's indices — it causes brittle, hard-to-read diffs. - Terraform: don't skip
required_version/provider pinning — "works on my machine" state drift is a top cause of broken applies.