Skip to main content
RunBook Academy

LinuxLXXV · Immutable vs Mutable InfrastructureState

State in an immutable fleet - what survives replacement and what does not

Advanced⏱ ~17 minfindutilslsof

What you'll learn

  • Audit a running host for state that would be lost on replacement
  • Place each class of state deliberately
  • Run a stateful service on disposable compute
  • Recognise the state losses that surface long after the deploy

Prerequisites

Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-11

Not yet marked complete on this device.

Immutable infrastructure has exactly one hard requirement, and every difficulty with it descends from that requirement: replacing an instance discards everything written to its root disk since launch.

For a stateless web server that is a feature. For a real production system it is a design constraint that has to be applied to every path the machine writes to - and most teams discover the paths they missed weeks later, when the loss finally surfaces.

Audit the machine first

Before making a class of host disposable, find out what it is actually writing. Two commands go a long way.

# Files on the root filesystem modified since the machine booted,
# excluding the paths you expect to churn
find / -xdev -type f -newermt "$(uptime --since)" \
  -not -path '/proc/*' -not -path '/sys/*' -not -path '/run/*' \
  -not -path '/var/log/*' -not -path '/var/lib/systemd/*' 2>/dev/null
Read-only / Safe
$ find /var -xdev -type f -mmin -60 2>/dev/null | head
/var/log/kern.log
/var/log/wtmp
/var/lib/myapp/outbox/00042.msg
/var/lib/myapp/outbox/00043.msg
/var/spool/postfix/deferred/A/AB12C4

Illustrative output

The first two lines are expected. The last three are the ones that matter: an application spool and a mail queue, both on the root disk, both containing work that has been accepted from somebody and not yet delivered.

The second command is who has files open:

sudo lsof -w /var/lib /srv /opt 2>/dev/null | awk '$4 ~ /[0-9]+[uw]/ {print $1, $NF}' | sort -u

lsof restricted to writable descriptors tells you which processes are writing where. Between the two you get an inventory of state, which is the thing you need before you can decide anything.

Where each class of state belongs

StateBelongsIf left on the root disk
Database files, uploadsPersistent volume or managed serviceLost at replacement
LogsShipped to a collector as writtenThe logs explaining the incident die with the instance
CachesLocal - they are rebuildableCold start after every replacement, which is a capacity issue
SessionsExternal storeUsers logged out on every deploy
Queues and spoolsA real queue service, or a volumeAccepted work silently dropped
Metrics buffersShort flush interval, accept small lossMinutes of gaps at every replacement
Machine identityGenerated at first bootSee below

Logs. journald on a disposable instance is a buffer, not storage. Being honest about that is better than pretending:

# /etc/systemd/journald.conf on an instance that ships its logs
# Storage=volatile
# RuntimeMaxUse=200M

Storage=volatile keeps the journal in /run only. It makes the instance faster and the loss explicit, and it forces the question “is the shipper actually working” to be answered before you need the logs rather than after. The cost is that if the shipper fails, the logs are gone entirely - so pair it with an alert on the shipper, not merely on the application.

Caches. Genuinely rebuildable state can stay local, but “rebuildable” is not “free”. A replaced instance with a cold cache serves slower and pushes more load to the tier behind it. During a rolling replacement of a whole fleet, that is a sustained capacity event, and it is the usual explanation for a deploy that looked fine in staging and caused a latency excursion in production. Replace in smaller batches, and let each batch warm before the next - which is what the checkpoint settings on a rolling refresh are for.

Machine identity. A replaced instance has new SSH host keys, so every operator gets a host key warning and eventually learns to type yes without reading it. That is a real security regression caused by the deployment model. SSH certificates fix it properly - clients trust a CA rather than individual host keys - and that is covered in linux-ssh-certificates-and-bastions.

Stateful services on disposable compute

The pattern is not “immutable does not apply to databases”. It is replace the compute, keep the data: the instance is disposable, the volume attached to it is not.

The mechanics:

  1. Data lives on a block volume whose lifecycle is independent of the instance - explicitly not delete-on-termination.
  2. Replacing the instance means detach, terminate, launch from the new image, attach, mount by UUID, start the service.
  3. The image contains the database software and its configuration. The volume contains the data. Neither contains the other.

The mount must be by UUID and must tolerate a missing volume at boot, or the replacement instance will fail to boot rather than fail to mount - a distinction that matters at three in the morning:

UUID=$(sudo blkid -s UUID -o value /dev/disk/by-id/DEVICE_ID)
echo "UUID=$UUID /var/lib/pgsql xfs defaults,nofail,x-systemd.device-timeout=30 0 0" \
  | sudo tee -a /etc/fstab

nofail keeps the boot going; x-systemd.device-timeout stops systemd waiting the full default timeout for a device that is not coming.

For a replicated database, the replacement is a failover, not a deploy:

  1. Launch a new instance from the new image as a replica.
  2. Let it replicate until it is caught up. This is dominated by the data volume, not by the image, and can take hours.
  3. Promote it, redirect clients, retire the old primary.

Everything an image pipeline gives you still applies - the software is baked and tested, the replacement is a launch rather than an in-place upgrade, and rollback is the previous image. What changes is the timing: you cannot roll a database fleet the way you roll a web tier, and treating them the same way is how a routine deploy becomes an outage.

In-flight work

Replacement also discards work that is in progress, which is state with a very short lifetime:

  • Requests being served. Deregister from the load balancer, wait for the connection-draining period, then stop. A replacement that skips the drain returns errors to real users for every deploy, at a rate low enough that nobody attributes it to the deploy.
  • Long-running jobs. A worker that has claimed a job and is eight minutes into it needs to either finish or return the job to the queue. TimeoutStopSec must be long enough for the first, and the job protocol must support the second.
  • Buffered telemetry. A metrics agent with a 60-second flush interval loses up to 60 seconds of data per replacement. Usually acceptable, occasionally not - and worth knowing rather than discovering during an incident review.

Knowledge check

Knowledge check · 4 questions

  1. Q1. An application writes accepted webhooks to /var/lib/myapp/outbox/ and a worker drains it in seconds. The directory is empty every time anyone looks. What is the risk under an immutable deployment model?

  2. Q2. Which of these are safe to leave on the root disk of a disposable instance? Select all that apply.

  3. Q3. Attaching a data volume to the replacement instance before the old instance is confirmed terminated risks unrecoverable filesystem corruption.

  4. Q4. You are about to make a class of hosts disposable. How do you find out what state they currently hold?

Passing score: 75%. Answers are checked in this browser.