← Back to posts

GCP ACE: Beyond Deployment — DaemonSet, StatefulSet, Job, and CronJob

Every GKE article so far has used a Deployment — the right default for stateless, scalable apps. But the exam tests three other workload controllers, each solving a problem a Deployment can’t.

DaemonSet — exactly one Pod per node

A Deployment’s Pods land wherever the scheduler decides, and you control the count with replicas. A DaemonSet is different: it guarantees one Pod on every eligible node, with no replica count to set. When a node joins the cluster, the DaemonSet’s controller schedules a Pod on it automatically; when a node leaves, that Pod is garbage-collected with it.

DaemonSetDeployment
PlacementOne Pod per eligible nodeN replicas, wherever the scheduler picks
Replica countNone — derived from node countspec.replicas
Scales withThe cluster’s node countA number you (or an HPA) set
Typical useNode-local agentsStateless, horizontally scalable apps

Typical use cases: a logging agent (Fluentd, Fluent Bit) that reads each node’s log files, a monitoring agent (Prometheus node-exporter) that reports per-node metrics, or a CSI storage plugin that needs to run on every node to mount volumes.

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentd-elasticsearch
  namespace: kube-system
spec:
  selector:
    matchLabels:
      name: fluentd-elasticsearch
  template:
    metadata:
      labels:
        name: fluentd-elasticsearch
    spec:
      containers:
      - name: fluentd-elasticsearch
        image: quay.io/fluentd_elasticsearch/fluentd:v2.5.2
        resources:
          limits:
            memory: 200Mi
          requests:
            cpu: 100m
            memory: 200Mi
      terminationGracePeriodSeconds: 30

spec.selector.matchLabels must match the labels in spec.template.metadata.labels — the API server rejects the manifest otherwise, same rule as a Deployment.

StatefulSet — stable identity for stateful apps

A Deployment’s Pods are interchangeable: if web-7f8c9 dies, web-2k9x1 takes its place and nothing cares. That’s wrong for a database or anything that needs to keep its name, its network address, and its own disk across restarts. That’s what a StatefulSet guarantees.

Naming and ordering: Pods are named <statefulset-name>-<ordinal>, starting at 0 — a StatefulSet called web with 3 replicas creates web-0, web-1, web-2. By default (podManagementPolicy: OrderedReady), they’re created in order, each waiting until the previous one is Running and Ready; they’re removed in reverse order on scale-down.

Stable network identity: a StatefulSet must reference a headless Service (clusterIP: None) via spec.serviceName. Each Pod then gets a predictable DNS name: <pod-name>.<service-name>.<namespace>.svc.cluster.local — for example web-0.nginx.default.svc.cluster.local. That name stays the same even after the Pod is rescheduled to a different node.

Stable storage: volumeClaimTemplates creates one PersistentVolumeClaim per Pod, named <template-name>-<pod-name> (e.g. www-web-0). When a Pod is rescheduled, it reattaches to that same PVC — its data follows it. By default, PVCs are not deleted when the StatefulSet is scaled down or removed; you have to clean them up yourself (or use persistentVolumeClaimRetentionPolicy on newer clusters).

apiVersion: v1
kind: Service
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  clusterIP: None        # headless: enables per-Pod DNS records
  selector:
    app: nginx
  ports:
  - name: web
    port: 80
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"    # must match the headless Service above
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: registry.k8s.io/nginx-slim:0.24
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:   # creates www-web-0, www-web-1, www-web-2
  - metadata:
      name: www
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 1Gi

Job — run to completion

A Deployment keeps its Pods running forever; a Job runs Pods until a target number succeed, then stops.

FieldDefaultMeaning
completions1Successful Pod completions required before the Job is done
parallelism1Max Pods running at the same time
backoffLimit6Pod failures tolerated before the Job is marked failed (exponential backoff, capped at 6 minutes between retries)

The Pod template’s restartPolicy must be Never or OnFailure — Always isn’t valid inside a Job.

apiVersion: batch/v1
kind: Job
metadata:
  name: pi
spec:
  completions: 5
  parallelism: 2
  backoffLimit: 4
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: pi
        image: perl:5.34.0
        command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]

This runs until 5 Pods succeed, at most 2 at a time, tolerating up to 4 failures.

CronJob — Jobs on a schedule

A CronJob creates Jobs on a repeating schedule, same syntax as Unix cron:

┌───────── minute (0–59)
│ ┌─────── hour (0–23)
│ │ ┌───── day of month (1–31)
│ │ │ ┌─── month (1–12)
│ │ │ │ ┌─ day of week (0–6, Sunday = 0)
* * * * *

concurrencyPolicy decides what happens if a new run is due while the previous Job is still going:

ValueBehavior
Allow (default)Runs the new Job alongside the still-running one
ForbidSkips the new run entirely
ReplaceCancels the running Job, starts the new one
apiVersion: batch/v1
kind: CronJob
metadata:
  name: hello
spec:
  schedule: "0 2 * * *"      # daily at 02:00
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
          - name: hello
            image: busybox:1.28
            command: ["/bin/sh", "-c", "date; echo Hello"]

Exam Takeaways

Sources

↗ LinkedIn