Chaos Engineering

Deliberately injecting failure into a system to find weaknesses before an incident does — turning "we think it's resilient" into "we tested it, and here's what broke." This walks through the principles, the tooling (Litmus, Chaos Mesh, AWS FIS), and what a real game day looks like end to end.

0/0 checks

1. Principles

Principle Description
Steady state Define normal behavior (p99 latency, error rate, RPS)
Hypothesis "If X fails, the system stays healthy"
Blast radius Limit scope — start small, expand gradually
Run in prod Staging findings differ; prod is ground truth
Learn from failures Post-mortem every experiment, fix weaknesses

Chaos engineering is not breaking things randomly — it's controlled experiments to build confidence.

Chaos engineering means randomly turning things off to see what breaks. True or false?


2. Tools

Tool Target Notes
Chaos Monkey EC2 instances Netflix original, terminates random instances
Litmus Chaos Kubernetes CNCF project, CRD-driven, huge experiment library
Chaos Mesh Kubernetes CNCF, GUI + CRDs, fine-grained network faults
k6 HTTP load + chaos Combine load test with failure scenarios
AWS FIS AWS resources Fault Injection Simulator, native AWS service

Chaos Monkey is the original chaos engineering tool — can you point it at a Kubernetes Deployment?


3. Litmus CRDs

ChaosEngine — runs an experiment on a target

apiVersion: litmuschaos.io/v1alpha1
kind: ChaosEngine
metadata:
  name: pod-kill-engine
  namespace: default
spec:
  appinfo:
    appns: default
    applabel: "app=payment"
    appkind: deployment
  engineState: active
  chaosServiceAccount: litmus-admin
  experiments:
  - name: pod-delete
    spec:
      components:
        env:
        - name: TOTAL_CHAOS_DURATION
          value: "60"          # seconds
        - name: CHAOS_INTERVAL
          value: "10"
        - name: FORCE
          value: "false"

Litmus Execution Flow

flowchart TD
    eng["ChaosEngine created"]
    runner["Chaos Runner Pod<br/>(spawned by operator)"]
    probe["Pre-Chaos Probe<br/>(steady state check)"]
    inject["Chaos Experiment Pod<br/>(injects fault)"]
    monitor["Monitor Metrics<br/>(during chaos)"]
    revert["Revert / Cleanup"]
    postprobe["Post-Chaos Probe<br/>(verify recovery)"]
    result["ChaosResult CR<br/>(Pass / Fail)"]

    eng --> runner
    runner --> probe
    probe -->|pass| inject
    inject --> monitor
    monitor --> revert
    revert --> postprobe
    postprobe --> result
    probe -->|fail| result

Step through a single run:

1. ChaosEngine created. You apply the CR above; the Litmus operator picks it up.
2. Chaos Runner Pod spawned. The operator spawns a runner pod to orchestrate this experiment run.
3. Pre-Chaos Probe. Steady state gets checked before anything is injected. If it fails here, the run jumps straight to ChaosResult marked Fail — the experiment pod never starts and no fault gets injected at all.
4. Chaos Experiment Pod injects fault. Only reached if the pre-chaos probe passed.
5. Monitor Metrics. Metrics are watched for the duration of the chaos window.
6. Revert / Cleanup. The injected fault is reverted.
7. Post-Chaos Probe. Recovery gets verified.
8. ChaosResult CR. Pass or Fail is recorded for the run.

In the Litmus execution flow, what happens if the pre-chaos probe fails?


4. Experiments

Experiment What it tests
Pod kill Pod restarts, K8s self-healing
Network partition Service mesh fallback, timeouts
CPU hog Throttling, resource limits
Memory hog OOMKilled handling, limits
Node drain Pod disruption budgets, rescheduling
Disk fill Ephemeral storage limits, log rotation
Network latency Circuit breakers, timeout configs
DNS failure Service discovery fallback

Pod kill and node drain both remove running pods. What's the difference in what each one actually tests?


5. Game Days

Structured chaos sessions:

  1. Define scope: which service, which experiment, what blast radius
  2. Set steady state: agree on SLIs to watch (e.g., error rate < 1%)
  3. Hypothesis: "payment service continues serving after one pod kill"
  4. Run experiment: start small (1 replica), observe
  5. Rollback plan: know how to stop the experiment (kubectl delete chaosengine)
  6. Post-mortem: document findings, create follow-up tickets

Step through a run:

1. Define scope. Which service, which experiment, what blast radius.
2. Set steady state. Agree on the SLIs to watch (e.g., error rate < 1%).
3. Hypothesis. "Payment service continues serving after one pod kill."
4. Run experiment. Start small (1 replica), observe.
5. Rollback plan. Know how to stop the experiment (kubectl delete chaosengine) — decided before the experiment starts, not improvised mid-run.
6. Post-mortem. Document findings, create follow-up tickets.

Why does a game day need a rollback plan defined up front, if the experiment is supposed to be safe?


6. Observability During Chaos

Watch these signals during any experiment:

# Error rates
sum(rate(http_requests_total{status=~"5.."}[1m])) / sum(rate(http_requests_total[1m]))

# Latency p99
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[1m]))

# Pod restarts
kube_pod_container_status_restarts_total

# CPU throttling
container_cpu_cfs_throttled_seconds_total

# HPA scaling events
kubectl get events --field-selector reason=SuccessfulRescale

Chaos + load test together: run k6 traffic while injecting faults to see real user impact.

Why run k6 load traffic at the same time as the fault, instead of just watching metrics on an otherwise idle system?


7. Failure Injection Patterns

Blast Radius Diagram

graph TD
    subgraph Outer["Full cluster blast radius"]
        subgraph Mid["Single namespace"]
            subgraph Inner["Single deployment"]
                subgraph Smallest["Single pod"]
                    pod["Start here"]
                end
                dep["Scale to deployment"]
            end
            ns["Then namespace-wide"]
        end
        cluster["Finally full cluster"]
    end

Expand the blast radius one stage at a time — don't jump straight to cluster-wide:

1. Single pod. Start here. Smallest possible blast radius.
2. Scale to deployment. Once the single-pod result is understood, widen to the whole deployment.
3. Then namespace-wide. Widen further to every deployment in the namespace.
4. Finally full cluster. Only once the narrower blast radii are understood does the experiment expand to the full cluster.

Patterns

Pattern Implementation
Latency injection tc netem delay 200ms on pod network interface
Error rate injection Return 500 for X% of requests (Envoy fault injection)
Dependency kill Kill downstream service, verify circuit breaker opens
Packet loss tc netem loss 20% to test retries
DNS failure Block CoreDNS or return NXDOMAIN

Envoy fault injection (Istio)

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: payment-fault
spec:
  hosts: [payment]
  http:
  - fault:
      delay:
        percentage:
          value: 10.0        # 10% of requests
        fixedDelay: 500ms
      abort:
        percentage:
          value: 5.0         # 5% return 500
        httpStatus: 500
    route:
    - destination:
        host: payment

In the Envoy fault injection example, what's the difference between the delay fault and the abort fault?