Skip to content

Commit 9b5c36f

Browse files
n0rq1jburns24
andauthored
Updated the StatefulSet second exercise (#781)
* Updated the StatefulSet second exercise * docs(kubernetes): enhance StatefulSets exercise to highlight Deployment limitations for stateful apps --------- Co-authored-by: Joshua Burns <joshua.burns@liatrio.com>
1 parent 29c7987 commit 9b5c36f

File tree

10 files changed

+316
-43
lines changed

10 files changed

+316
-43
lines changed

docs/6-software-development-practices/6.4-pairprogramming.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ docs/6-software-development-practices/6.4-pairprogramming.md:
44
estReadingMinutes: 20
55
exercises:
66
-
7-
name: Pair Programing
7+
name: Pair Programming
88
description: Using 'Live Share' or some equivillant try pair programming a 'Hello World' app in the language of your choice
99
estMinutes: 30
1010
technologies:
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
---
2+
docs/9-kubernetes-container-orchestration/9.2.1-stateful-sets.md:
3+
category: Container Orchestration
4+
estReadingMinutes: 10
5+
exercises:
6+
-
7+
name: Stateful Sets
8+
description: Create a simple StatefulSet in Kubernetes, understand the lifecycle of StatefulSets.
9+
estMinutes: 120
10+
technologies:
11+
- Kubernetes
12+
- StatefulSets
13+
---
14+
15+
# 9.2.1 Stateful Sets
16+
17+
StatefulSets are a type of workload controller in Kubernetes specifically designed to manage stateful applications—applications that require persistent storage and stable identities for their Pods. Unlike Deployments, where Pods are interchangeable and ephemeral, StatefulSets provide “sticky” identities and guarantees that each Pod maintains a unique, stable network identity and persistent storage across rescheduling, scaling, and updates.
18+
19+
This stickiness is crucial for stateful workloads such as databases, message queues, and distributed systems, where each instance must maintain its state consistently and be reachable by other Pods in a predictable manner.
20+
21+
For more information, see the [Kubernetes documentation](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/).
22+
23+
## Exercise 1 - Stateful Sets
24+
25+
1. Destroy and recreate a new cluster.
26+
`unset KUBECONFIG; k3d cluster delete <cluster-name>; k3d cluster create <cluster-name>`.
27+
28+
2. Copy the contents of `examples/ch9/volumes/random-num-pod.yaml` into a new file called `examples/ch9/statefulsets/random-num-statefulset.yaml` and modify it to use a `StatefulSet` resource instead of a `Pod` resource. Set the number of replicas to 3.
29+
30+
> To prevent the container from dying and restarting, add a sleep command to the container:
31+
`args: ["shuf -i 0-100 -n 1 >> /opt/number.out; sleep 10000"]`.
32+
33+
?> [VolumeClaimTemplates](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#volume-claim-templates) give each statefulset replica an automatically managed, per-pod persistent volume that sticks to the Pod’s ordinal, avoids manual PVC creation, and prevents unsafe volume sharing.
34+
35+
3. Apply the `StatefulSet` to the cluster.
36+
37+
4. Exec into each pod and view the contents of `/opt/number.out` or use the command:
38+
```shell
39+
kubectl get pods -l app=<statefulset-label> -o name | xargs -I {} sh -c 'echo {}; kubectl exec {} -- cat /opt/number.out'
40+
```
41+
42+
5. Delete one of the pods from the `StatefulSet`, run the command again and observe the output.
43+
44+
## Exercise 2 - More Stateful Sets
45+
46+
Navigate to `examples/ch9/statefulsets/counterapp/`, there is a Dockerfile and a directory `src` which contains a simple web application that displays a counter and the name of the pod it's running on.
47+
48+
1. Destroy and recreate a new cluster.
49+
`unset KUBECONFIG; k3d cluster delete <cluster-name>; k3d cluster create <cluster-name>`.
50+
51+
2. Create a `StatefulSet` for the counterapp.
52+
53+
3. Create a `Service` for the `StatefulSet`.
54+
55+
?> `StatefulSets` are a bit unique and require a `Headless Service` to function properly. This is because `StatefulSets` require stable network identities for their pods, and a `Headless Service` provides this by not load balancing traffic across pods, but instead routing traffic to specific pods based on their stable network identity. For more information: [https://kubernetes.io/docs/concepts/services-networking/service/#headless-services](https://kubernetes.io/docs/concepts/services-networking/service/#headless-services)
56+
57+
4. Apply both the `StatefulSet` and `Service` to the cluster.
58+
59+
5. Port forward each pod, delete one of the pods and observe each instance of the counter app in your browser.
60+
61+
?> Notice that deleting the pod may take a while to actually terminate. Take note on why you think this is the case.
62+
63+
6. Clean up the cluster by deleting the `StatefulSet`, `Service`, and any dangling PV/PVCs.
64+
65+
7. Create an analogous `Deployment` with 3 replicas and a dynamically provisioned `PersistentVolumeClaim`.
66+
67+
a. Apply the `Deployment` and `PVC` to the cluster.
68+
69+
b. Port forward to each pod and observe the counter app in your browser. What happens to the counter value across different pods?
70+
71+
c. Delete one of the pods and observe:
72+
- How quickly does the pod terminate compared to the StatefulSet?
73+
- What is the new pod's name?
74+
- What happened to the counter value?
75+
76+
d. Scale the Deployment to 5 replicas, then back down to 2. Which pods were removed? How does this compare to StatefulSet scaling behavior?
77+
78+
8. Based on your observations, research how you might achieve per-replica persistent storage using only Deployments. What trade-offs or additional complexity would be required?
79+
80+
?> Consider the differences in pod naming, storage behavior, scaling order, and data continuity. Why might these differences matter for stateful applications?
81+
82+
## Deliverables
83+
84+
- Look into other workload controllers (Deployments, ReplicaSets, DaemonSets, etc.), what are the differences between them? Why might you use one over the other?
85+
- What are some downsides to using `StatefulSets`?

docs/README.md

Lines changed: 54 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -801,7 +801,7 @@ docs/6-software-development-practices/6.4-pairprogramming.md:
801801
category: Agile Development
802802
estReadingMinutes: 20
803803
exercises:
804-
- name: Pair Programing
804+
- name: Pair Programming
805805
description: >-
806806
Using 'Live Share' or some equivillant try pair programming a 'Hello
807807
World' app in the language of your choice
@@ -1103,6 +1103,47 @@ docs/8-infrastructure-configuration-management/8.1.3-terraform-modules.md:
11031103
technologies:
11041104
- Terraform
11051105
- AWS S3
1106+
docs/8-infrastructure-configuration-management/8.2-ansible.md:
1107+
category: Infrastructure as Code
1108+
estReadingMinutes: 15
1109+
exercises:
1110+
- name: Vagrant and Ansible
1111+
description: >-
1112+
Provision a virtual machine and install a GitHub self-hosted runner
1113+
using Ansible as a provisioner in Vagrant.
1114+
estMinutes: 300
1115+
technologies:
1116+
- Ansible
1117+
- Vagrant
1118+
- GitHub self-hosted runner
1119+
- name: Idempotency
1120+
description: >-
1121+
Provision a virtual machine and install a GitHub self-hosted runner
1122+
using Ansible as a provisioner in Vagrant while maintaining idempotency.
1123+
estMinutes: 300
1124+
technologies:
1125+
- Ansible
1126+
- Vagrant
1127+
- GitHub self-hosted runner
1128+
- name: Ansible and AWS EC2
1129+
description: >-
1130+
Provision an AWS EC2 instance and install a GitHub self-hosted runner
1131+
using Ansible.
1132+
estMinutes: 300
1133+
technologies:
1134+
- Ansible
1135+
- AWS EC2
1136+
- GitHub self-hosted runner
1137+
- name: Terraform and Ansible
1138+
description: >-
1139+
Provision an EC2 instance using Terraform and install a GitHub
1140+
self-hosted runner with Ansible.
1141+
estMinutes: 360
1142+
technologies:
1143+
- Terraform
1144+
- Ansible
1145+
- AWS EC2
1146+
- GitHub self-hosted runner
11061147
docs/8-infrastructure-configuration-management/8.3-terraform-providers.md:
11071148
category: Infrastructure as Code
11081149
estReadingMinutes: 20
@@ -1147,47 +1188,6 @@ docs/8-infrastructure-configuration-management/8.3-terraform-providers.md:
11471188
technologies:
11481189
- Terraform
11491190
- Go
1150-
docs/8-infrastructure-configuration-management/8.2-ansible.md:
1151-
category: Infrastructure as Code
1152-
estReadingMinutes: 15
1153-
exercises:
1154-
- name: Vagrant and Ansible
1155-
description: >-
1156-
Provision a virtual machine and install a GitHub self-hosted runner
1157-
using Ansible as a provisioner in Vagrant.
1158-
estMinutes: 300
1159-
technologies:
1160-
- Ansible
1161-
- Vagrant
1162-
- GitHub self-hosted runner
1163-
- name: Idempotency
1164-
description: >-
1165-
Provision a virtual machine and install a GitHub self-hosted runner
1166-
using Ansible as a provisioner in Vagrant while maintaining idempotency.
1167-
estMinutes: 300
1168-
technologies:
1169-
- Ansible
1170-
- Vagrant
1171-
- GitHub self-hosted runner
1172-
- name: Ansible and AWS EC2
1173-
description: >-
1174-
Provision an AWS EC2 instance and install a GitHub self-hosted runner
1175-
using Ansible.
1176-
estMinutes: 300
1177-
technologies:
1178-
- Ansible
1179-
- AWS EC2
1180-
- GitHub self-hosted runner
1181-
- name: Terraform and Ansible
1182-
description: >-
1183-
Provision an EC2 instance using Terraform and install a GitHub
1184-
self-hosted runner with Ansible.
1185-
estMinutes: 360
1186-
technologies:
1187-
- Terraform
1188-
- Ansible
1189-
- AWS EC2
1190-
- GitHub self-hosted runner
11911191
docs/9-kubernetes-container-orchestration/9.1-kubectl-ref.md:
11921192
category: Container Orchestration
11931193
estReadingMinutes: 120
@@ -1212,6 +1212,18 @@ docs/9-kubernetes-container-orchestration/9.2-volumes.md:
12121212
technologies:
12131213
- Kubernetes
12141214
- Jenkins
1215+
docs/9-kubernetes-container-orchestration/9.2.1-stateful-sets.md:
1216+
category: Container Orchestration
1217+
estReadingMinutes: 10
1218+
exercises:
1219+
- name: Stateful Sets
1220+
description: >-
1221+
Create a simple StatefulSet in Kubernetes, understand the lifecycle of
1222+
StatefulSets.
1223+
estMinutes: 120
1224+
technologies:
1225+
- Kubernetes
1226+
- StatefulSets
12151227
docs/9-kubernetes-container-orchestration/9.3-probes.md:
12161228
category: Container Orchestration
12171229
estReadingMinutes: 10

docs/_sidebar.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@
140140
- [9.1.4 - Cluster Management](9-kubernetes-container-orchestration/9.1.4-cluster-management.md)
141141
- [9.1.5 - Kubectl Settings and Usage](9-kubernetes-container-orchestration/9.1.5-kubectl-settings-and-usage.md)
142142
- [9.2 - Volumes](9-kubernetes-container-orchestration/9.2-volumes.md)
143+
- [9.2.1 - Stateful Sets](9-kubernetes-container-orchestration/9.2.1-stateful-sets.md)
143144
- [9.3 - Probes](9-kubernetes-container-orchestration/9.3-probes.md)
144145
- [9.4 - RBAC](9-kubernetes-container-orchestration/9.4-rbac.md)
145146
- [9.5 - HPAs](9-kubernetes-container-orchestration/9.5-hpas.md)
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
FROM node:18-alpine
2+
3+
WORKDIR /app
4+
5+
COPY package*.json ./
6+
7+
RUN npm install --omit=dev
8+
9+
COPY server.js ./
10+
COPY src/ ./src/
11+
12+
RUN mkdir -p /data && chown -R node:node /data
13+
14+
USER node
15+
16+
EXPOSE 3000
17+
18+
CMD ["npm", "start"]
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "counterapp",
3+
"version": "1.0.0",
4+
"private": true,
5+
"description": "StatefulSet demo counter app with persistent per-pod state",
6+
"main": "server.js",
7+
"scripts": {
8+
"start": "node server.js"
9+
},
10+
"dependencies": {
11+
"express": "^4.18.2"
12+
}
13+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
const express = require('express');
2+
const fs = require('fs');
3+
const path = require('path');
4+
const os = require('os');
5+
6+
const app = express();
7+
const PORT = process.env.PORT || 3000;
8+
const DATA_DIR = process.env.DATA_DIR || '/data';
9+
const DATA_FILE = path.join(DATA_DIR, 'counter.json');
10+
const POD_NAME = process.env.POD_NAME || os.hostname();
11+
12+
app.use(express.json());
13+
app.use(express.static(path.join(__dirname, 'src')));
14+
app.get('/', (_req, res) => {
15+
res.sendFile(path.join(__dirname, 'src', 'index.html'));
16+
});
17+
18+
function readCounter() {
19+
try {
20+
if (!fs.existsSync(DATA_DIR)) {
21+
fs.mkdirSync(DATA_DIR, { recursive: true });
22+
}
23+
if (!fs.existsSync(DATA_FILE)) {
24+
fs.writeFileSync(DATA_FILE, JSON.stringify({ count: 0 }), 'utf8');
25+
return 0;
26+
}
27+
const raw = fs.readFileSync(DATA_FILE, 'utf8');
28+
const obj = JSON.parse(raw);
29+
return typeof obj.count === 'number' ? obj.count : 0;
30+
} catch (e) {
31+
return 0;
32+
}
33+
}
34+
35+
function writeCounter(value) {
36+
try {
37+
fs.writeFileSync(DATA_FILE, JSON.stringify({ count: value }), 'utf8');
38+
return true;
39+
} catch (e) {
40+
return false;
41+
}
42+
}
43+
44+
app.get('/healthz', (_req, res) => {
45+
res.status(200).send('ok');
46+
});
47+
48+
app.get('/api/state', (_req, res) => {
49+
const count = readCounter();
50+
res.json({ count, podName: POD_NAME });
51+
});
52+
53+
app.post('/api/increment', (_req, res) => {
54+
let count = readCounter();
55+
count += 1;
56+
if (!writeCounter(count)) {
57+
return res.status(500).json({ error: 'failed_to_persist' });
58+
}
59+
res.json({ count, podName: POD_NAME });
60+
});
61+
62+
app.listen(PORT, () => {
63+
console.log(`server listening on ${PORT} as ${POD_NAME}`);
64+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
async function fetchState() {
2+
const res = await fetch('/api/state');
3+
if (!res.ok) return;
4+
const data = await res.json();
5+
document.getElementById('count').textContent = data.count;
6+
document.getElementById('podName').textContent = `pod: ${data.podName}`;
7+
}
8+
9+
async function increment() {
10+
const res = await fetch('/api/increment', { method: 'POST' });
11+
if (!res.ok) return;
12+
const data = await res.json();
13+
document.getElementById('count').textContent = data.count;
14+
}
15+
16+
window.addEventListener('DOMContentLoaded', () => {
17+
document.getElementById('incrementBtn').addEventListener('click', increment);
18+
fetchState();
19+
});
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<title>Counter</title>
7+
<link rel="stylesheet" href="/style.css" />
8+
</head>
9+
<body>
10+
<div class="container">
11+
<div class="card">
12+
<div class="header">
13+
<h1>Counter</h1>
14+
</div>
15+
<div class="status">
16+
<div class="pill" id="podName">pod: …</div>
17+
</div>
18+
<div class="counter">
19+
<div class="count" id="count">0</div>
20+
<button id="incrementBtn" class="btn">Increment</button>
21+
</div>
22+
</div>
23+
</div>
24+
<script src="/app.js"></script>
25+
</body>
26+
</html>

0 commit comments

Comments
 (0)