Introduction
When you deploy a server, can you change it later — or do you throw it away and build a new one? That’s the core question behind mutable vs immutable infrastructure.
Mutable: You deploy a server and modify it over time (patches, config changes, updates). Immutable: You deploy a server and never change it. Need an update? Build a new one, replace the old one.
Most modern infrastructure leans immutable, but both approaches have their place. Here’s when to use each.
Quick Comparison
| Mutable | Immutable | |
|---|---|---|
| Changes | Modify servers in place | Replace servers entirely |
| Config drift | Likely over time | Impossible by design |
| Rollback | Complex (undo changes) | Simple (deploy previous version) |
| Tools | Ansible, Chef, Puppet, SSH | Terraform, Docker, Packer, Kubernetes |
| Deploy speed | Fast (small changes) | Fast (swap instances) |
| State | Servers are unique | Servers are disposable |
| Scaling | Copy server + apply scripts | Launch from image |
| Best for | Legacy systems, stateful servers | Microservices, cloud-native apps |
Mutable Infrastructure
In mutable infrastructure, servers are updated in place after deployment. You SSH in, apply patches, change configurations, restart services.
How It Works
Day 1: Deploy Ubuntu 22.04 server
Day 30: apt update && apt upgrade (security patches)
Day 60: Change nginx config, restart nginx
Day 90: Install monitoring agent
Day 120: Update application to v2.0
The server accumulates changes over time. Each server becomes unique based on its history.
Real-World Example: Ansible
Ansible is the most popular mutable infrastructure tool. It connects to existing servers and applies changes:
# ansible playbook — modify servers in place
- hosts: webservers
tasks:
- name: Update packages
apt:
upgrade: dist
update_cache: yes
- name: Install nginx
apt:
name: nginx
state: present
- name: Deploy app config
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
The Configuration Drift Problem
The biggest risk with mutable infrastructure is configuration drift — servers that should be identical gradually become different:
Server A: Ubuntu 22.04, nginx 1.22, Node 18, monitoring agent v2
Server B: Ubuntu 22.04, nginx 1.24, Node 20, monitoring agent v3
Server C: Ubuntu 22.04, nginx 1.22, Node 18, no monitoring agent (someone forgot)
All three servers were supposed to be identical. Over months of manual patches and hotfixes, they diverged. Now a deployment that works on Server A fails on Server B. This is the “snowflake server” problem.
When Mutable Makes Sense
- Stateful servers: Database servers that can’t be easily replaced
- Legacy applications: Apps that weren’t designed for containerization
- Quick hotfixes: Emergency patches that need to go out immediately
- Small teams: When the overhead of building images isn’t justified
- Compliance: When you need to prove exactly what changed and when
Immutable Infrastructure
In immutable infrastructure, servers are never modified after deployment. Every change produces a new server image, and the old server is destroyed.
How It Works
v1.0: Build image → Deploy 3 instances → Running
v1.1: Build NEW image → Deploy 3 NEW instances → Destroy old instances
v1.2: Build NEW image → Deploy 3 NEW instances → Destroy old instances
No SSH, no patching, no config changes. If something needs to change, you build a new version from scratch.
Real-World Example: Terraform + Packer
Step 1: Build a machine image with Packer:
# packer template
source "amazon-ebs" "web" {
ami_name = "web-app-{{timestamp}}"
instance_type = "t3.micro"
region = "us-east-1"
source_ami = "ami-0c55b159cbfafe1f0"
}
build {
sources = ["source.amazon-ebs.web"]
provisioner "shell" {
inline = [
"sudo apt-get update",
"sudo apt-get install -y nginx nodejs",
"sudo systemctl enable nginx"
]
}
provisioner "file" {
source = "app/"
destination = "/var/www/app"
}
}
Step 2: Deploy with Terraform:
# terraform — deploy the immutable image
resource "aws_instance" "web" {
count = 3
ami = "ami-NEW-VERSION" # New AMI from Packer
instance_type = "t3.micro"
tags = {
Name = "web-${count.index}"
Version = "1.1"
}
}
Every deployment uses a fresh image. All servers are identical by construction.
Real-World Example: Docker + Kubernetes
Containers are the most common form of immutable infrastructure:
# Dockerfile — immutable by design
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
# kubernetes deployment — replace pods, never patch them
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 3
template:
spec:
containers:
- name: web
image: myapp:1.1 # New version = new image
ports:
- containerPort: 3000
Why Immutable Wins for Most Use Cases
- No configuration drift: Every instance is built from the same image
- Easy rollback: Deploy the previous image version
- Reproducible: Same image works in dev, staging, and production
- Testable: Test the image before deploying to production
- Secure: No SSH access needed, smaller attack surface
- Auditable: Git history shows exactly what changed between versions
When Immutable Makes Sense
- Stateless web services: API servers, web frontends
- Microservices: Each service is its own image
- Auto-scaling: Launch identical instances from an AMI
- CI/CD pipelines: Build → Test → Deploy image
- Cloud-native applications: Designed for containers/Kubernetes
Hybrid Approach: Immutable Compute, Mutable Data
Most real-world architectures use both:
┌─────────────────────────────────────────────┐
│ IMMUTABLE (replace, never modify) │
│ • Web servers (Docker/AMI) │
│ • API servers │
│ • Worker processes │
│ • Lambda functions │
└─────────────────────────────────────────────┘
↕
┌─────────────────────────────────────────────┐
│ MUTABLE (update in place) │
│ • Databases (RDS, PostgreSQL) │
│ • File storage (S3, EFS) │
│ • Message queues (SQS, RabbitMQ) │
│ • Caches (Redis, ElastiCache) │
└─────────────────────────────────────────────┘
Compute is immutable (easy to replace). Data is mutable (needs to persist). This is the standard pattern in modern cloud architecture.
Tool Mapping
| Tool | Approach | What It Does |
|---|---|---|
| Terraform | Immutable | Provisions infrastructure (VMs, networks, services) |
| Packer | Immutable | Builds machine images (AMIs, Docker images) |
| Docker | Immutable | Packages applications as container images |
| Kubernetes | Immutable | Orchestrates container deployments |
| Ansible | Mutable | Configures existing servers |
| Chef | Mutable | Configures existing servers |
| Puppet | Mutable | Configures existing servers |
Terraform + Packer + Docker = fully immutable infrastructure pipeline.
Migration Path: Mutable → Immutable
If you’re currently using mutable infrastructure:
- Start with containers: Dockerize one application
- Build CI/CD pipeline: Automate image builds
- Use Terraform: Manage infrastructure as code
- Adopt blue-green deploys: Run old and new versions simultaneously
- Remove SSH access: Once everything is automated, disable manual access
- Scale: Apply the pattern to more services
Hands-On Courses
Learn by doing with interactive courses on CopyPasteLearn:
Conclusion
Immutable infrastructure is the modern standard for cloud-native applications: build an image, test it, deploy it, never modify it. Mutable infrastructure still works for stateful systems and legacy applications. Most production environments use both — immutable compute (containers, AMIs) with mutable data (databases, storage). Start immutable with Terraform and Docker, use Ansible for the things that genuinely need in-place changes.




