CloudFormation Versus Terraform: Two Models of the Same Job
If you work on AWS, the incumbent alternative is CloudFormation, and comparing the two is a fixture of infrastructure interviews. The comparison is really about three design decisions: where state lives, what the language can reach, and how changes are reviewed.
CloudFormation keeps its state server-side: each stack is an object in your AWS account, and templates describe desired stack content. There is no state file to secure, share, or corrupt — and no terraform import exercise after someone creates resources by hand, because AWS itself is the source of truth. Drift detection exists but is a batch operation you invoke, with coverage that lags Terraform's per-plan refresh; CloudFormation change sets also give you a preview before execution, which covers the same "see it before it runs" ground that terraform plan provides.
Terraform keeps state where you put it: a local file by default, a remote backend like S3 with locking in production. That choice is a burden — you must secure, version, and lock it — and simultaneously Terraform's superpower, because the same engine applies to any provider in the registry. One configuration can create an AWS VPC, a Cloudflare DNS record, a Datadog monitor, and a GitHub team, with real references flowing between them:
resource "aws_route53_record" "api" {
# AWS
name = "api.${var.zone_name}"
}
resource "github_team" "platform" {
# GitHub, same run, same state
name = "platform"
}
CloudFormation cannot reach outside AWS except through custom resources and macros — heavier machinery with more moving parts. If you are all-in on AWS, CloudFormation's native coupling (and CDK, which compiles TypeScript/Python/Go into CloudFormation templates) is a legitimate advantage: no extra state to protect, service teams ship new resource support on AWS's own schedule, and StackSets handle multi-account fan-out as a first-class feature.
Terraform's edge is portability and review ergonomics: HCL diffs read cleanly in a pull request, providers span clouds and SaaS, and the plan/apply separation you have already practiced carries to every platform identically. The provider model deserves one precise sentence because it explains everything else: a provider is a separately versioned plugin binary implementing a resource schema against one platform's API, published to registry.terraform.io as <namespace>/<name> (for example hashicorp/aws) and pinned per directory. That indirection is what makes "add Datadog to this deployment" a required_providers entry and a few resource blocks instead of a platform migration.
CloudFormation's counter-punches are worth stating for a balanced interview answer: change sets preview executions before they run; drift detection is a first-class API operation on a stack; stack imports bring existing resources under management; and tools like cfn-lint and CloudFormation Guard add policy checking over templates. StackSets fan a template across dozens of accounts as a native feature — the multi-account story Terraform teams assemble from separate states and CI wiring. One more fact for interviews: HashiCorp relicensed Terraform to BUSL 1.1 after 1.5.x, and OpenTofu now exists as an MPL-2.0-compatible alternative — CloudFormation, being a managed AWS service, raises no such governance question, which matters in risk-averse enterprises. The honest summary: CloudFormation is the better default for a homogeneous, all-AWS shop; Terraform wins the moment a second cloud or a SaaS dependency enters the architecture.
Pulumi: When HCL Is the Wrong Amount of Language
Pulumi makes one big bet: infrastructure should be written in a real programming language — TypeScript, Python, Go, C#, or Java — instead of a configuration language. Compare the same idea in both:
# HCL: a for expression and a dynamic block when you outgrow simple cases
resource "aws_security_group" "web" {
name = "web-sg"
dynamic "ingress" {
for_each = var.ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = ["10.0.0.0/8"]
}
}
}
// TypeScript: the language you already know, with its loops and modules
const sg = new aws.ec2.SecurityGroup("web-sg", {});
for (const port of ports) {
sg.createIngressRule(`port-${port}`, {
fromPort: port, toPort: port, protocol: "tcp", cidrBlocks: ["10.0.0.0/8"],
});
}
The gains are real. Loops, conditionals, and error handling are the language's, not workarounds around a DSL; abstractions are packages and functions rather than modules stapled to string templating; and you can write unit tests against infrastructure logic before any cloud exists — something structurally awkward in HCL. Shared libraries let you express "a service" once — repository, queue, dashboard, alarm wired together — and instantiate it fifty times from application teams who never see the wiring. Teams already fluent in TypeScript often review Pulumi code faster than HCL precisely because it is not a second language to learn.
The costs are equally real, and interviewers probe them. A general-purpose language invites the general-purpose sins: unbounded cleverness, abstraction layered before the third use, and review burden for reviewers who must now evaluate programming style, not just declared state. Execution requires a runtime and credentials with a longer trust chain than terraform plan's static evaluation. And HCL's constraints are also its strengths: a configuration language is legible to non-programmers — a security reviewer can read an ingress block — and its inability to execute arbitrary logic is a feature when the audience is auditors.
Pulumi's execution model is otherwise deliberately familiar: it plans previews against state (stored in Pulumi Cloud, or self-hosted backends like S3), refreshes, and destroys. State is organized into stacks — one per environment — so the dev/staging/production split you would build with separate Terraform directories or workspaces exists as a first-class concept, with stack-level configuration values replacing tfvars files. Secrets are handled too: stack outputs marked secret are encrypted with a service-managed key before they touch state.
The testing story is where Pulumi's language bet pays off most visibly. Infrastructure logic can be unit-tested with ordinary test runners and mocked provider libraries — assert that a module produces the right VPC CIDR layout without ever contacting a cloud — and policy-as-code engines (Pulumi's CrossGuard, built on Open Policy Agent) evaluate previews the same way Sentinel or OPA gate Terraform plans in HCP pipelines. So the transferable skills — state, preview, drift, locking, policy gates — carry over intact; only the authoring language changes. The decision question is not "which is more powerful" — Pulumi, being Turing-complete, is strictly more expressive — but "who reads this code, and what do they need to trust it?" Teams whose infrastructure logic grows genuinely algorithmic reach for Pulumi; teams whose infrastructure is a few hundred declarative lines usually do not need it.
Terraform vs Ansible vs Chef: Provisioning Is Not Configuration
The most common category error in tool discussions is treating Terraform and Ansible as competitors. They solve different halves of the same problem, and the professional answer to "Terraform or Ansible?" is "which half are we talking about?"
Terraform provisions resources: it calls provider APIs to create servers, networks, load balancers, databases, IAM objects, and DNS records. Its verbs are create/read/update/delete against infrastructure APIs, and it knows nothing about what runs inside a machine. Ansible, Chef, and Puppet configure running systems: they SSH in (or run an agent), install packages, drop config files, start services, and converge drift between the desired and actual contents of a host. Their verbs are file, package, service.
The pipeline view makes the boundary concrete:
Terraform → creates the EC2 instance, security group, RDS endpoint
Ansible → installs nginx, deploys app config, enables the service
App deploy → your CI/CD ships the actual application artifact
Each tool is idempotent, but idempotence is where the similarity ends. Terraform's unit of management is the resource record in state; Ansible's is the task list applied to a live host. If a package install fails at 2 a.m., Ansible reports the failed task on that host; if an instance disappears, Terraform's next plan recreates it. Asking one tool to do the other's job produces the familiar anti-patterns: user-data scripts that smuggle thousands of lines of shell into instance boot (untestable, silent when they fail), or configuration-management drift databases that re-implement resource tracking badly.
The trend worth naming in interviews: immutable infrastructure has been shrinking configuration management's territory for a decade. Bake the OS, runtime, and application shell into a machine image (Packer, or an image builder), then let Terraform replace instances when the image version changes. At that extreme, Ansible shrinks to bootstrap glue or disappears entirely — every difference between environments lives in the image recipe plus the Terraform configuration, both version-controlled. Plenty of teams still run both, and that is often the right answer: Terraform for the platform, a thin Ansible layer for host-level nuance that does not justify a new image build.
Watch the failure signatures, because they tell you which tool you are actually missing. When Terraform owns too much host detail — cloud-init user_data scripts hundreds of lines long, for_each over package lists — plans slow down, diffs become unreadable, and a typo in a package name triggers a full instance replacement. When Ansible owns too much infrastructure — dynamic inventory scripts that create security groups — you lose the plan preview and the state-backed audit trail, and deletes become folklore. The healthy boundary has a clear smell test: if the change is about an object that appears in a cloud console, it is Terraform's; if it is about a file, package, or process on a running host, it is configuration management's or the image's.
Decision rule to state plainly when asked: choose by team skills and target platforms, then by pipeline position — not by whichever tool's feature page is longest. All-AWS homogeneous shop with platform engineers who know the SDK: CloudFormation or CDK is defensible. Multi-cloud and SaaS dependencies in one deployment: Terraform. Infrastructure logic that has become genuinely algorithmic, with strong unit-test culture: consider Pulumi. The provisioning/configuration split stays constant regardless.
Quiz
1. Your organization is AWS-only, and a reviewer asks why Terraform is preferred over CloudFormation. Which answer is the honest technical justification?
2. A security team needs to review infrastructure changes but has no HCL or TypeScript experience. What is a legitimate argument for keeping HCL (or CloudFormation) rather than moving to Pulumi?
3. A team writes a 400-line bash script in EC2 user_data to install packages and configure nginx after Terraform creates the instance. What is the primary problem with this pattern?
4. When is Pulumi the most defensible choice over Terraform/HCL?
Flashcards
Question
How do Terraform and CloudFormation differ in where infrastructure state lives?
Click to reveal answer
Answer
CloudFormation's state is the stack object in your AWS account — server-side, no file to secure, drift detection as a batch operation. Terraform stores state where configured (local file, or S3 backend with locking), which costs management effort but enables one state spanning any provider, and gives per-plan refresh-driven drift visibility.
Question
What does Pulumi change relative to Terraform, and what stays the same?
Click to reveal answer
Answer
It replaces HCL with general-purpose languages (TypeScript, Python, Go, C#, Java), unlocking real abstraction, loops, and unit tests at the cost of review complexity and a runtime trust chain. The operating model is unchanged: plan/preview against state, refresh, apply, destroy, with locking.
Question
Provisioning vs configuration management: which tool is which, and what is the boundary?
Click to reveal answer
Answer
Terraform provisions (API calls creating servers, networks, DNS, IAM); Ansible/Chef/Puppet configure running hosts (packages, files, services, convergence). The pipeline places Terraform first, host configuration second; immutable images increasingly absorb the second role.
Question
Why is baking machine images (e.g. Packer) considered a rival to configuration management?
Click to reveal answer
Answer
Images move host configuration into version-controlled recipes built before deployment; Terraform then replaces instances to roll changes. Every environment difference lives in image recipe plus configuration, eliminating in-place drift — at the cost of longer builds and image versioning discipline.
Question
What is the pragmatic decision rule for choosing an IaC tool?
Click to reveal answer
Answer
Follow team skills and target platforms and pipeline position: all-AWS → CloudFormation/CDK is defensible; multi-cloud or SaaS dependencies → Terraform; algorithmic infrastructure logic with test culture → Pulumi; provisioning → Terraform regardless, with configuration management or images for hosts. Feature-checklist length is not a criterion.
Revision Notes
Key Takeaways
- 1. State location is the deepest design split: CloudFormation's server-side stacks remove state management but bind you to AWS; Terraform's external state costs effort and buys provider reach
- 2. Pulumi changes the authoring language, not the operating model: previews against state, refreshes, locking — its real trade is expressiveness against auditability by non-programmers
- 3. Provisioning and configuration management are different jobs in the pipeline; putting host configuration in user_data hides failures and testing, which is why immutable images plus thin bootstrap won the argument
- 4. All tools are idempotent; that shared property does not make them interchangeable — units of management differ (resource records vs task lists)
- 5. Tool choice follows team skills, target platforms, and pipeline position; interviewers reward the criterion, and punish feature-checklist recitation
Interview Tips
- • "Terraform vs CloudFormation?" — anchor on state and reach: CFN's state is the AWS stack (no file to secure, but AWS-bound), Terraform's external state enables multi-cloud and SaaS in one run; then state the honest default: CFN for all-AWS shops, Terraform when a second provider exists
- • "Terraform vs Ansible?" — reframe the question: provisioning vs configuration management; draw the pipeline (Terraform creates, Ansible/images configure) and note both are idempotent with different units of management
- • "What would make you adopt Pulumi?" — algorithmic infrastructure logic, heavy abstraction reuse, and a typed-language unit-testing culture; the counterweight is reviewer accessibility and governance
- • "How do you handle a new provider (e.g. Datadog) in one deployment?" — Terraform's registry provider model: add the required_providers entry, reference data/resources across providers in one state and one plan; contrast the CloudFormation custom-resource path
Cheat Sheet
IaC Tool Comparison
| Dimension | Terraform | CloudFormation | Pulumi | Ansible/Chef |
|---|---|---|---|---|
| Language | HCL (DSL) | JSON/YAML (CDK: TS/Py/Go) | TS, Py, Go, C#, Java | YAML/DSL, Ruby |
| State | Local or remote backend, you manage it | Server-side stack | Pulumi Cloud or self-hosted | None (live convergence) |
| Reach | Multi-cloud + SaaS providers | AWS-native (+custom resources) | Multi-cloud | Hosts, network devices |
| Primary job | Provision resources | Provision AWS resources | Provision resources | Configure running hosts |
| Review ergonomics | Plan diffs in PR | Change sets | Program diffs + unit tests | Playbook runs |
| Key risk | State security/locking | Lock-in to AWS | Language complexity creep | Drift, invisible failures |
Position in a pipeline: Terraform (provision) → config management or immutable image (host) → CI/CD (app).
Signals to pick: second cloud or SaaS dependency appears → Terraform; homogeneous AWS + platform team on SDK → CloudFormation/CDK; infrastructure logic algorithmic + typed-language test culture → Pulumi; host-level convergence between image builds → thin Ansible layer.
Licensing note: Terraform post-1.5.x is BUSL 1.1 (restricts competing hosted offerings); OpenTofu is the MPL-2.0 compatible fork. CloudFormation is a managed AWS service; Pulumi is Apache 2.0.