Lab Guide
Lesson 07 — Hands-on: Deployment, StatefulSet & PDB templates
Prerequisites
Helm 3 installed, access to a Kubernetes cluster (minikube, kind, or a cloud cluster), kubectl configured and working.
Exercise 1 — Production-Ready Deployment
Create a Helm chart with a Deployment that includes resource limits, all three probe types, a rolling update strategy, and environment variables from multiple sources.
Step 1 — Scaffold the chart
helm create webapp
cd webapp
Step 2 — Edit values.yaml
Set the following values:
replicaCount: 3
image:
repository: nginx
pullPolicy: IfNotPresent
tag: "" # falls back to Chart.AppVersion
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "250m"
memory: "256Mi"
probes:
startup:
path: /
failureThreshold: 30
periodSeconds: 10
liveness:
path: /
initialDelaySeconds: 0
periodSeconds: 10
readiness:
path: /
periodSeconds: 5
strategy:
type: RollingUpdate
maxSurge: 1
maxUnavailable: 0
env:
APP_ENV: production
initContainers:
enabled: true
Step 3 — Edit templates/deployment.yaml
Add the strategy block, all three probes, an init container, and environment variables:
spec:
replicas: {{ .Values.replicaCount }}
strategy:
type: {{ .Values.strategy.type }}
{{- if eq .Values.strategy.type "RollingUpdate" }}
rollingUpdate:
maxSurge: {{ .Values.strategy.maxSurge }}
maxUnavailable: {{ .Values.strategy.maxUnavailable }}
{{- end }}
selector:
matchLabels:
{{- include "webapp.selectorLabels" . | nindent 6 }}
template:
spec:
{{- if .Values.initContainers.enabled }}
initContainers:
- name: init-check
image: busybox:1.36
command: ['sh', '-c', 'echo "pre-flight check complete"']
{{- end }}
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
ports:
- name: http
containerPort: 80
env:
- name: APP_ENV
value: {{ .Values.env.APP_ENV | quote }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
startupProbe:
httpGet:
path: {{ .Values.probes.startup.path }}
port: http
failureThreshold: {{ .Values.probes.startup.failureThreshold }}
periodSeconds: {{ .Values.probes.startup.periodSeconds }}
livenessProbe:
httpGet:
path: {{ .Values.probes.liveness.path }}
port: http
initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
periodSeconds: {{ .Values.probes.liveness.periodSeconds }}
readinessProbe:
httpGet:
path: {{ .Values.probes.readiness.path }}
port: http
periodSeconds: {{ .Values.probes.readiness.periodSeconds }}
Step 4 — Validate and install
# Dry-run to check rendered YAML
helm template webapp . | less
# Install into cluster
helm install webapp . --namespace lab07 --create-namespace
# Verify
kubectl -n lab07 get deploy,pods
kubectl -n lab07 describe deploy webapp
Checkpoint
You should see 3 pods running, each with the init container completed, all three probes configured, and the rolling update strategy applied. Run kubectl -n lab07 get events to confirm no probe failures.
Exercise 2 — StatefulSet with Persistent Storage
Add a StatefulSet template to the chart for a Redis-like workload with persistent volumes.
Step 1 — Add values to values.yaml
statefulset:
enabled: true
replicas: 3
storageClass: standard
storageSize: 1Gi
podManagementPolicy: OrderedReady
Step 2 — Create templates/statefulset.yaml
{{- if .Values.statefulset.enabled }}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ include "webapp.fullname" . }}-sts
labels:
{{- include "webapp.labels" . | nindent 4 }}
spec:
serviceName: {{ include "webapp.fullname" . }}-headless
replicas: {{ .Values.statefulset.replicas }}
podManagementPolicy: {{ .Values.statefulset.podManagementPolicy }}
selector:
matchLabels:
{{- include "webapp.selectorLabels" . | nindent 6 }}
component: stateful
template:
metadata:
labels:
{{- include "webapp.selectorLabels" . | nindent 8 }}
component: stateful
spec:
containers:
- name: {{ .Chart.Name }}
image: "redis:7-alpine"
ports:
- name: redis
containerPort: 6379
volumeMounts:
- name: data
mountPath: /data
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: {{ .Values.statefulset.storageClass | quote }}
resources:
requests:
storage: {{ .Values.statefulset.storageSize }}
{{- end }}
Step 3 — Create the headless Service
Create templates/headless-service.yaml:
{{- if .Values.statefulset.enabled }}
apiVersion: v1
kind: Service
metadata:
name: {{ include "webapp.fullname" . }}-headless
labels:
{{- include "webapp.labels" . | nindent 4 }}
spec:
type: ClusterIP
clusterIP: None
ports:
- port: 6379
targetPort: redis
name: redis
selector:
{{- include "webapp.selectorLabels" . | nindent 4 }}
component: stateful
{{- end }}
Step 4 — Upgrade and verify
helm upgrade webapp . --namespace lab07
# Watch pods come up one at a time
kubectl -n lab07 get pods -w
# Check PVCs were created
kubectl -n lab07 get pvc
# Verify stable DNS names
kubectl -n lab07 run -it --rm dns-test --image=busybox:1.36 -- nslookup webapp-sts-0.webapp-headless.lab07.svc.cluster.local
Checkpoint
You should see 3 StatefulSet pods (webapp-sts-0, webapp-sts-1, webapp-sts-2) and 3 matching PVCs. DNS resolution should return the pod IP for each hostname.
Exercise 3 — PodDisruptionBudget & Topology Spread
Add a PDB and topology spread constraints to protect availability.
Step 1 — Add values
pdb:
enabled: true
maxUnavailable: 1
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
Step 2 — Create templates/pdb.yaml
{{- if .Values.pdb.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "webapp.fullname" . }}
labels:
{{- include "webapp.labels" . | nindent 4 }}
spec:
{{- if .Values.pdb.minAvailable }}
minAvailable: {{ .Values.pdb.minAvailable }}
{{- else }}
maxUnavailable: {{ .Values.pdb.maxUnavailable | default 1 }}
{{- end }}
selector:
matchLabels:
{{- include "webapp.selectorLabels" . | nindent 6 }}
{{- end }}
Step 3 — Add topology constraints to the Deployment template
Inside spec.template.spec:
{{- if .Values.topologySpreadConstraints }}
topologySpreadConstraints:
{{- range .Values.topologySpreadConstraints }}
- maxSkew: {{ .maxSkew }}
topologyKey: {{ .topologyKey }}
whenUnsatisfiable: {{ .whenUnsatisfiable }}
labelSelector:
matchLabels:
{{- include "webapp.selectorLabels" $ | nindent 14 }}
{{- end }}
{{- end }}
Step 4 — Upgrade and validate
helm upgrade webapp . --namespace lab07
# Check PDB status
kubectl -n lab07 get pdb
kubectl -n lab07 describe pdb webapp
# Verify topology constraints in rendered spec
kubectl -n lab07 get deploy webapp -o yaml | grep -A 6 topologySpreadConstraints
Checkpoint
The PDB should show ALLOWED DISRUPTIONS: 1 (with 3 replicas and maxUnavailable 1). Topology spread constraints should be visible in the Deployment spec.
Clean Up
helm uninstall webapp --namespace lab07
# StatefulSet PVCs are retained - delete manually if desired:
kubectl -n lab07 delete pvc --all
kubectl delete namespace lab07