Lesson 07 Deployments & StatefulSets
Theme:

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