Skip to main content

Declarative vs Imperative Infrastructure

What This Concept Is

Two different ways of describing a piece of infrastructure.

Imperative: a sequence of steps. "Run aws ec2 run-instances ..., then aws ec2 create-tags ..., then SSH in and install nginx." The code describes how to reach the state.

Declarative: a description of the desired end state. "There is one t3.small EC2 instance tagged role=web running in subnet subnet-abc." A tool diffs this description against reality and decides the steps.

Terraform, Pulumi, CDK, CloudFormation, and Kubernetes manifests are declarative. Shell scripts, most Ansible playbooks without a state file, and anything you type into a cloud console are imperative.

The shift is not cosmetic. It changes what "safe change" means, what a review looks like, and what rerunning the code does.

Why It Matters Here

Most infrastructure failures come from imperative drift: someone ran a script once, the system is in some state, and nobody can tell whether it matches what the script would do if run again. The declarative model eliminates that ambiguity by making the desired state the source of truth.

You want declarative IaC because:

  • you get a diff (plan) before every change
  • re-applying is safe by design, not by discipline
  • two engineers reading the same config see the same system
  • code review maps to infrastructure review without translation

Concrete Example

Same requirement, two styles.

Imperative (bash):

#!/usr/bin/env bash
aws ec2 run-instances --image-id ami-0abc --instance-type t3.small \
--subnet-id subnet-abc --tag-specifications \
'ResourceType=instance,Tags=[{Key=role,Value=web}]'

Running this twice creates two instances. Running it after the instance was terminated creates one. Running it after someone edited tags in the console gives you an untagged clone alongside the edited one. The script does not know.

Declarative (Terraform):

resource "aws_instance" "web" {
ami = "ami-0abc"
instance_type = "t3.small"
subnet_id = "subnet-abc"
tags = {
role = "web"
}
}

Running terraform apply twice is a no-op the second time. If someone edited tags in the console, the next plan will propose reverting them. The description is the source of truth; reality is forced to match.

Common Confusion / Misconception

"Bash in a git repo is IaC." No. Version control is necessary but not sufficient. A script in git is still imperative -- it describes steps, not desired state. "IaC" is specifically about declarative descriptions backed by a planner.

"Ansible is declarative." Partly. Modules like apt and file are idempotent, which gives local declarative behavior, but the playbook order is imperative, and Ansible (by default) does not track state between runs. This is why Cluster 5 separates configuration management from IaC.

"Declarative means slow." No. The planner is fast; the slow part is cloud API calls, which dominate either style.

How To Use It

Before writing a line of provisioning code, ask:

  1. Is there a tool for this that takes a desired state and produces a plan? If yes, use it.
  2. What will running this code twice in a row do? If the answer is "I don't know," it is not declarative.
  3. Where does the "source of truth" about the end state live? If the answer is "in my head" or "in the cloud console," stop.

Check Yourself

  1. Why is a terraform apply on an already-correct system a no-op, while bash deploy.sh on the same system is not?
  2. If two engineers run the same declarative config at the same time, what still has to exist to keep them safe?
  3. Give one infrastructure task where imperative scripting is still the right answer.

Mini Drill or Application

Take a shell script that provisions something (any script you have written, or apt-get install). In 10 minutes, rewrite it as either:

  • a declarative Terraform stanza describing the end state, or
  • a short paragraph explaining why this particular task is genuinely imperative (one-shot migration, ad-hoc debugging, etc.)

The goal is not to Terraform-ify everything. It is to notice when you are defaulting to scripts because you know bash, not because the task is imperative.

See also (external)


Source Backbone

Infrastructure-as-code details are tool-specific, but these local books provide the operational backbone for shell, Git, and change discipline.