Run a Chaos Engineering Lab with Chaos Mesh, k6, and Grafana

Deploy Chaos Mesh, run pod-kill and network-delay experiments during live k6 load tests, and measure the SLO impact on Grafana.

Related Concepts & TermsGrafana

Domains & Technologies

Domains
CHAOS-ENGINEERINGSRE
Technologies
KUBERNETESGRAFANA

Blueprint Walkthrough

Architecture Overview & Problem Statement
Architecture Overview

Netflix famously runs Chaos Monkey in production — a program that randomly kills production services to confirm the system can survive failures. The logic: if you never deliberately break things, you never know how they behave when they break on their own — and they will, at the worst possible time.

Chaos engineering is the discipline of deliberately injecting failure into systems in a controlled way, to discover weaknesses before they become real incidents. This project builds a complete chaos engineering practice on Kubernetes using Chaos Mesh, with k6 generating realistic load and Grafana showing the real-time impact — the same practice an SRE team at Hotstar would run before a high-traffic event like an IPL final.

COQ
[ Define steady state ]
(SLOs: p99 latency < 200ms, error rate < 0.5%)
|
v
[ Form hypothesis ]
("Service maintains SLO when 1 of 3 pods is killed")
|
v
[ Inject failure with Chaos Mesh ]
(Kill a pod while k6 sends continuous traffic)
|
v
[ Observe on Grafana ]
(Did latency spike? Did errors occur? Did it recover?)
|
v
[ Document findings ]
(Hypothesis confirmed or refuted - what did we learn?)
|
v
[ Fix the weaknesses found ]
(Add PDB, increase replicas, add timeouts)
Tip

You will run this same five-step cycle — hypothesis, inject, observe, document, fix — three separate times against three different failure types. The cycle itself is the actual skill being practiced, not any one experiment.

Problem Solved

Most teams only discover their system's real failure behavior during an actual incident — at 2am, under real user impact, with no controlled way to isolate what broke. By then it's too late to learn calmly; the priority is restoring service, not understanding the system.

Chaos engineering solves this by moving failure discovery into a controlled, scheduled, low-stakes setting. You define a steady state hypothesis — a measurable statement of what "normal" looks like, backed by real metrics, not a guess. You then inject a specific failure and observe whether the steady state holds. If your hypothesis is refuted, you found a genuine weakness on your own terms, with time to fix it, instead of during a customer-facing outage during peak order volume at a company like Swiggy.

Security

Never run chaos experiments in production before validating the same experiment in staging. This project builds the practice safely on a local cluster — treat that discipline as non-negotiable before ever pointing these tools at a live environment.


Milestone 1: Deploy the Target Application
Concept

Chaos engineering requires something with redundancy to test against. A single-replica deployment can't teach you anything useful — killing its only pod is simply an outage, not an experiment. Three replicas is the practical minimum for a meaningful pod-kill test.

Steps
Bash
kubectl create namespace chaos-demo
cat > target-app.yaml << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-api
namespace: chaos-demo
spec:
replicas: 3
selector:
matchLabels:
app: orders-api
template:
metadata:
labels:
app: orders-api
spec:
containers:
- name: api
image: nginx:alpine
ports:
- containerPort: 80
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "200m"
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 3
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: orders-api
namespace: chaos-demo
spec:
selector:
app: orders-api
ports:
- port: 80
targetPort: 80
EOF
kubectl apply -f target-app.yaml
kubectl get pods -n chaos-demo
TEXT
NAME READY STATUS
orders-api-7d9f8c6b-2kmpq 1/1 Running
orders-api-7d9f8c6b-5xnpl 1/1 Running
orders-api-7d9f8c6b-9rvbf 1/1 Running
Common Mistake

Running pod-kill experiments against a single-replica deployment isn't a chaos experiment — it's just an outage. Chaos engineering requires redundancy to actually test against.


Milestone 2: Install Chaos Mesh
Concept

Chaos Mesh is a CNCF project providing Kubernetes-native chaos engineering. Instead of writing ad-hoc scripts to kill processes, you apply YAML resources — PodChaos, NetworkChaos, StressChaos — that Chaos Mesh translates into real, time-bounded failures.

Steps
Bash
helm repo add chaos-mesh https://charts.chaos-mesh.org
helm repo update
kubectl create namespace chaos-mesh
helm install chaos-mesh chaos-mesh/chaos-mesh \
--namespace chaos-mesh \
--set chaosDaemon.runtime=containerd \
--set chaosDaemon.socketPath=/run/containerd/containerd.sock \
--wait
kubectl get pods -n chaos-mesh
TEXT
NAME READY STATUS
chaos-controller-manager-abc123-xyz 3/3 Running
chaos-daemon-2hkqp 1/1 Running
chaos-daemon-5xnpl 1/1 Running
chaos-daemon-9rvbf 1/1 Running
chaos-dashboard-7d9f8c6b-klmnp 1/1 Running

Access the dashboard:

Bash
kubectl port-forward -n chaos-mesh \
svc/chaos-dashboard 2333:2333 &

Open http://localhost:2333 in your browser.

Tip

The Chaos Mesh dashboard lets you build and launch experiments visually, which is a fast way to explore what's possible — but writing the YAML directly, as we do in this project, is what you'll actually check into Git for repeatable experiments.


Milestone 3: Install k6 and Write the Load Test
Concept

k6 generates realistic HTTP traffic during the experiment so you're measuring the failure's effect on real simulated load, not a theoretical availability number. The thresholds block in the script below encodes your SLOs directly — k6 will report pass/fail against them automatically.

Steps
Bash
## macOS
brew install k6
## Linux
sudo gpg -k
sudo gpg --no-default-keyring \
--keyring /usr/share/keyrings/k6-archive-keyring.gpg \
--keyserver hkp://keyserver.ubuntu.com:80 \
--recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] \
https://dl.k6.io/deb stable main" | \
sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6
Bash
cat > load-test.js << 'EOF'
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate, Trend } from 'k6/metrics';
const errorRate = new Rate('errors');
const p99Latency = new Trend('p99_latency');
export const options = {
stages: [
{ duration: '30s', target: 20 },
{ duration: '5m', target: 20 },
{ duration: '30s', target: 0 },
],
thresholds: {
'errors': ['rate<0.01'],
'http_req_duration': ['p(95)<200'],
},
};
const SERVICE_URL = 'http://localhost:8090';
export default function () {
const response = http.get(`${SERVICE_URL}/`);
const success = check(response, {
'status is 200': (r) => r.status === 200,
'response time < 200ms': (r) => r.timings.duration < 200,
});
errorRate.add(!success);
p99Latency.add(response.timings.duration);
sleep(0.5);
}
EOF
Bash
kubectl port-forward -n chaos-demo svc/orders-api 8090:80 &
k6 run load-test.js

Let it run to steady state before injecting any chaos — this baseline run is what every experiment result gets compared against.

Tip

Keep this port-forward and k6 run active in a separate terminal tab throughout Milestones 4-6 — every chaos experiment needs continuous traffic running alongside it to be measurable at all.


Milestone 4: Experiment 1 — Pod Kill
Concept

Hypothesis: When one of three orders-api pods is killed, the remaining two pods absorb the traffic without the error rate exceeding 1% or p99 latency exceeding 200ms.

Writing the hypothesis before running the experiment matters — it forces you to commit to a measurable prediction instead of retroactively deciding what the result "meant."

Steps
Bash
cat > experiment-pod-kill.yaml << 'EOF'
apiVersion: chaos-mesh.org/v1alpha1
kind: PodChaos
metadata:
name: pod-kill-experiment
namespace: chaos-demo
spec:
action: pod-kill
selector:
namespaces:
- chaos-demo
labelSelectors:
app: orders-api
mode: one
duration: "2m"
EOF
kubectl apply -f experiment-pod-kill.yaml
Bash
kubectl get pods -n chaos-demo --watch
TEXT
NAME READY STATUS
orders-api-7d9f-9rvbf 1/1 Running
orders-api-7d9f-9rvbf 0/1 Terminating
orders-api-7d9f-9rvbf 0/1 Pending
orders-api-7d9f-9rvbf 0/1 Running
orders-api-7d9f-9rvbf 1/1 Running

Watch the k6 output during the experiment for error spikes or latency increases during the restart window.

Bash
kubectl delete -f experiment-pod-kill.yaml
Common Mistake

Not watching k6's live output during the experiment window and only checking the final summary. The interesting data is the transient spike during the 10-30 second restart window — the final summary can average it away and hide exactly what you're trying to observe.


Milestone 5: Experiment 2 — Network Delay
Concept

Hypothesis: When 300ms of network delay is injected on outbound traffic from the orders-api pods, users will observe latency increases but the service will not return errors.

This experiment simulates a slow downstream dependency — the kind of thing that happens when a payment gateway or a third-party API (a real scenario for a company like Razorpay) degrades without actually going down.

Steps
Bash
cat > experiment-network-delay.yaml << 'EOF'
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: network-delay-experiment
namespace: chaos-demo
spec:
action: delay
selector:
namespaces:
- chaos-demo
labelSelectors:
app: orders-api
mode: all
direction: to
delay:
latency: "300ms"
jitter: "50ms"
correlation: "25"
duration: "3m"
EOF
kubectl apply -f experiment-network-delay.yaml

Watch k6's p99 latency metric during this run — the question isn't whether it rises, but whether it stays under your SLO threshold.

Bash
kubectl delete -f experiment-network-delay.yaml
Tip

Try adjusting latency to 800ms and re-running this same experiment. Finding the exact delay value where your SLO actually breaches gives you a concrete number for capacity planning conversations, instead of a vague sense that "slow downstreams are bad."


Milestone 6: Experiment 3 — CPU Stress
Concept

Hypothesis: When CPU is throttled to 50% of the container limit across all pods, the service will maintain its error rate SLO but may breach the latency SLO.

This experiment tests a different failure mode entirely — not a dependency slowing down, but the pods themselves running under resource pressure, which is common during traffic spikes like a Zerodha market-open surge.

Steps
Bash
cat > experiment-cpu-stress.yaml << 'EOF'
apiVersion: chaos-mesh.org/v1alpha1
kind: StressChaos
metadata:
name: cpu-stress-experiment
namespace: chaos-demo
spec:
selector:
namespaces:
- chaos-demo
labelSelectors:
app: orders-api
mode: all
stressors:
cpu:
workers: 2
load: 50
duration: "2m"
EOF
kubectl apply -f experiment-cpu-stress.yaml

The experiment cleans itself up automatically after 2 minutes.

Common Mistake

Not setting an explicit duration on any chaos experiment. Without it, an experiment like this one persists until someone manually deletes it — a forgotten Friday-afternoon experiment can quietly degrade a system all weekend. Every experiment YAML in this project sets one deliberately.


Milestone 7: Write a Chaos Engineering Report
Concept

Documenting results is as important as running the experiment — an experiment without a written finding gets forgotten by the next incident, and the whole point of chaos engineering is to retain what you learn.

Steps
Bash
cat > chaos-report.md << 'EOF'
## Chaos Engineering Report - Orders API
Date: 2026-08-14
## Steady State
* Request rate: 20 virtual users, ~40 req/sec
* Baseline p99 latency: 45ms
* Baseline error rate: 0%
* SLO targets: error rate < 1%, p99 < 200ms
---
## Experiment 1: Pod Kill
Hypothesis: Killing one of three pods will not cause SLO breach.
What happened: Error rate spiked to 2.3% for 12 seconds during pod
restart. p99 latency spiked to 380ms for 8 seconds. Recovered fully
after 22 seconds.
Result: REFUTED - error rate breached 1% during recovery.
Root cause: No Pod Disruption Budget. No readiness gate holding
traffic during pod startup - the new pod received traffic before
it was ready.
Fix: Add a PDB with minAvailable: 2. Tune readinessProbe timing.
---
## Experiment 2: Network Delay (300ms)
Hypothesis: 300ms delay causes latency increase but no errors.
What happened: p99 latency rose to 380ms, breaching the 200ms SLO.
Error rate stayed at 0%.
Result: PARTIALLY CONFIRMED - no errors, but latency SLO breached.
Finding: No timeout propagation. A slow downstream call delays the
entire request with no client-side timeout configured.
Fix: Add request timeouts. Reconsider whether the latency SLO
reflects realistic downstream call chains.
---
## Experiment 3: CPU Stress
Hypothesis: 50% CPU throttle breaches latency but not errors.
What happened: p99 latency rose to 150ms - within SLO. Error rate
stayed at 0%. Fully recovered within 5 seconds of experiment ending.
Result: CONFIRMED - both SLOs held under CPU stress.
Finding: Current CPU limits are appropriately sized for this load.
---
## Action Items
| Finding | Fix | Priority |
| :--- | :--- | :--- |
| No PDB, pod kill causes errors | Add PDB minAvailable: 2 | HIGH |
| No readiness gate | Tune readinessProbe | HIGH |
| Latency SLO breached on delay | Add timeouts, review SLO | MEDIUM |
EOF
Tip

Store chaos-report.md in the same Git repo as your Kubernetes manifests. Over time, a folder of these reports becomes a searchable history of every weakness your team has already found and fixed — genuinely useful during onboarding or post-incident reviews.


Milestone 8: Fix the Weakness Found
Concept

A Pod Disruption Budget tells Kubernetes the minimum number of pods that must remain available during any voluntary disruption (like a node drain or, in this case, Chaos Mesh's pod-kill action). It's the direct fix for the weakness Experiment 1 uncovered.

Steps
Bash
cat > pod-disruption-budget.yaml << 'EOF'
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: orders-api-pdb
namespace: chaos-demo
spec:
minAvailable: 2
selector:
matchLabels:
app: orders-api
EOF
kubectl apply -f pod-disruption-budget.yaml

Re-run Experiment 1 (Milestone 4) with the PDB in place. The pod kill should now cause noticeably less disruption, since Kubernetes will enforce the minimum available pod count during any voluntary disruption.

Tip

A PDB only protects against voluntary disruptions like this one — it does nothing against an involuntary failure like a node hardware crash. Don't mistake "passed the pod-kill experiment" for "fully resilient to all pod loss."


Validation & Testing
Verification Steps
Bash
# 1. Confirm the PDB is active
kubectl get pdb -n chaos-demo
# Expected: orders-api-pdb with MIN AVAILABLE 2
# 2. Re-run the pod-kill experiment with k6 running
kubectl apply -f experiment-pod-kill.yaml
# Watch k6 output - error rate spike should be smaller than the first run
# 3. Confirm no chaos experiments are left running
kubectl get podchaos,networkchaos,stresschaos -n chaos-demo
# Expected: empty, or only experiments you're intentionally running
# 4. Confirm cleanup after each experiment
kubectl delete -f experiment-pod-kill.yaml
Tip

Compare the k6 threshold pass/fail output (not just eyeballing the numbers) between the first and second pod-kill runs — k6 reports whether your defined thresholds passed or failed for that run, giving you an objective before/after comparison.

Common Mistakes Recap
Mistake Why It Breaks Fix
No observability before chaos Can't tell if the experiment did anything Install Prometheus/Grafana first
Testing in prod before staging How chaos engineering gets banned Always validate in staging first
No duration set Experiment persists indefinitely Always set an explicit duration
Single-replica pod-kill test It's an outage, not an experiment Require redundancy before testing
Writing the hypothesis after the fact Defeats the purpose of the exercise Hypothesis first, always