In a nutshell
Picking an infrastructure-as-code tool feels like it should have a single winner, the way people argue about the “best” programming language. It doesn’t — and the fastest way to see why is to stop asking “which tool is best?” and start asking “which vehicle is right for this trip?” Nobody thinks a motorbike is objectively better than a cargo van; a motorbike wins the solo commute and loses the house move. Building infrastructure works the same way. Terraform and Pulumi are for building the ground — creating cloud resources like networks, databases, and clusters. Ansible is for outfitting what sits on that ground — installing software and configuring the insides of servers. Terragrunt isn’t a vehicle at all; it’s closer to a trailer hitch that lets one Terraform “vehicle” tow the same setup across dozens of near-identical trips without repacking each time. Ask “best” and you get a religious war; ask “which trip” and the choices sort themselves.
The single distinction that unlocks everything below: these tools split along what they touch (provisioning cloud resources vs configuring the inside of a machine) and how they describe the work (declarative “here’s the end state I want” vs procedural “here are the steps, in order”). Get those two axes straight and you’ll never again confuse a tool that builds the road with one that paints the lines on it. And the professional answer, for almost any team past a certain size, is a small, deliberate combination of tools — each doing the one job it’s genuinely good at — not a single tool stretched over jobs it’s bad at.
Level: Intermediate · Time: ~34 min
Before this, it helps to know: what infrastructure as code is and why teams adopt it (see IaC core concepts: state, drift, idempotency); the rough idea of a cloud resource (a VM, a network, a managed database); and that a “pipeline” runs your code automatically on a change. No deep Terraform, Ansible, or Pulumi experience is assumed — this lesson is the map you read before committing to any one of them.
After this lesson you’ll be able to:
- Place any IaC tool on the two axes that actually decide the choice — provision-vs-configure and declarative-vs-procedural — instead of arguing by preference.
- Explain the four state models (explicit state file, service-managed state, stateless convergence, and control-plane reconciliation) and why each changes how drift and locking behave.
- Decide when Terragrunt earns its keep and when reaching for it is premature.
- Tell the honest OpenTofu-vs-Terraform licensing story (the BSL relicense and the MPL fork) and know when the fork is the right call.
- Combine tools deliberately — Terraform/OpenTofu provisions, Ansible configures, Terragrunt keeps many environments DRY — and defend the split in a design review.
A national EdTech provider runs Moodle for 1.4 million students across a dozen state education boards, and the platform team has a problem that has nothing to do with Moodle and everything to do with how they build the ground it stands on. They have four engineers, forty environments (each board gets isolated dev/staging/prod, plus shared platform tooling), two clouds because two boards mandated data residency on a domestic provider, and an audit obligation: every exam-season scale-up has to be reproducible, reviewed, and traceable back to a commit, because the year the autoscaling was hand-edited in the console during a results-day surge, a misconfigured security group left a grades database reachable for nine hours. The mandate from the new head of platform is blunt: everything that touches infrastructure goes through code, through review, through a pipeline — no exceptions, no console. The question that has stalled them for a month is not whether to use infrastructure as code. It is which tool, because someone proposed Terraform, someone else swears by Pulumi, the senior SRE wants Ansible for the VM fleet, and a blog post convinced a junior that Terragrunt fixes everything. This article is the decision framework that ends that argument.
The trap here is treating “IaC tool” as a single choice with one winner. It is not. These four tools occupy different points on two axes — what they describe (provisioning cloud resources vs configuring what runs inside them) and how they describe it (declarative desired-state vs imperative steps) — and the mature answer for a team like this is usually a small, deliberate combination, not a monoculture. The skill is knowing which tool owns which job, and refusing to use a tool outside the job it is good at.
The two axes that actually decide this
Before naming tools, name the distinctions, because every real disagreement traces back to one of them.
Declarative vs imperative. A declarative tool takes a description of the desired end state and figures out the diff to get there — you say “this VNet, these three subnets, this database tier,” and the tool computes what to create, change, or destroy. An imperative tool runs steps in order — “install this package, write this file, restart this service.” Declarative is reproducible and converges to the same state regardless of where you started; imperative is procedural and depends on order. Provisioning cloud resources is naturally declarative. Configuring the inside of a server — packages, files, services, OS tuning — is often more naturally imperative.
State model. A declarative tool needs to know what it already created so it can compute the next diff. Terraform and Pulumi keep an explicit state file — a recorded inventory of managed resources mapped to real cloud IDs. That state is powerful (it enables plan, drift detection, dependency graphs) and dangerous (it can drift from reality, it must be stored securely because it contains resource metadata and sometimes secrets, and concurrent writes corrupt it without locking). Ansible is largely stateless — it inspects the target at run time, decides what is out of compliance, and fixes it, holding no persistent record between runs. Stateless is simpler and harder to corrupt; stateful is what gives you a true plan-before-apply and drift detection.
Hold those two axes in your head and the four tools sort themselves cleanly.
The four tools, by the job each one owns
Terraform — declarative cloud provisioning in HCL. This is the default substrate for standing up cloud resources: networks, managed databases, Kubernetes clusters, load balancers, IAM. You write HCL (HashiCorp Configuration Language) describing desired state; Terraform builds a dependency graph, shows you a plan (the exact diff it intends to apply), and on apply reconciles reality to the config, recording everything in state. Its decisive advantage is the provider ecosystem — thousands of providers covering every major cloud and SaaS, all driven by the same workflow. For the EdTech team, Terraform is what creates the per-board VPCs/VNets, the managed Postgres for Moodle, the object storage for course content, and the Kubernetes clusters — identically, on both clouds, from reviewed code.
Terragrunt — a thin DRY wrapper around Terraform for many environments. Terragrunt is not a separate IaC engine; it is a wrapper that calls Terraform and solves the specific pain of running the same Terraform across many environments without copy-paste. Forty environments in plain Terraform means forty near-identical sets of backend config, provider config, and variable wiring — and the day you need to change the state-bucket naming convention, you edit it forty times. Terragrunt lets you define that boilerplate once and generate it per environment, keep each environment’s inputs in a small file, and run a change across a whole tree of environments. You only reach for Terragrunt once the environment count makes Terraform’s own repetition genuinely painful — which is exactly this team’s situation, and would be over-engineering for a shop with three environments.
Ansible — agentless, procedural configuration management. Ansible owns the inside of machines and the day-2 procedural work Terraform is bad at: install and patch packages, lay down config files, manage services, run an ordered upgrade, orchestrate a rolling restart. It is agentless (it connects over SSH/WinRM — nothing to pre-install on targets), describes work as ordered playbooks (YAML), and is built to be idempotent (re-running converges rather than duplicating), but the model is fundamentally procedural and largely stateless. For the EdTech fleet, Ansible is what hardens the Moodle application VMs to the CIS benchmark, installs the PHP/Moodle stack and cron jobs on any board still on VMs rather than containers, applies emergency OS patches across the fleet when a CVE lands, and configures the virtual appliances — the third-party WAF and load-balancer appliances that ship as VM images and expose no Terraform provider, so they have to be driven over SSH/API by playbooks.
Pulumi — declarative provisioning in real programming languages. Pulumi covers the same job as Terraform — declarative cloud provisioning with an explicit state model — but you write it in TypeScript, Python, Go, or C# instead of HCL. The payoff is real programming-language power: loops, conditionals, functions, classes, unit tests, and your IDE’s autocomplete and type-checking, plus the ability to build genuine reusable abstractions (a typed MoodleEnvironment component you instantiate forty times). The cost is that you now carry general-purpose-language complexity into your infrastructure, your reviewers must read code rather than declarative config, and the talent pool that knows your IaC shrinks to people who know both the cloud and that language. Pulumi wins when your team is software-engineering-heavy and your infrastructure has genuinely complex logic that HCL expresses awkwardly; it is a harder sell when ops engineers, not application developers, own the platform.
Architecture overview
The reference shape for the EdTech team is a layered pipeline where each tool does the one job it is best at, and a single source of identity, secrets, security scanning, and ITSM wraps all of them. Follow the control flow from a commit to a running, configured environment.
- An engineer opens a pull request against the infrastructure monorepo. Authentication to every system in this pipeline — the Git host, the CI runners, the cloud accounts — is brokered through Okta as the workforce IdP, federated to Entra ID where Azure resources need a first-class token, so there is one identity plane and no tool holds its own user database.
- The PR triggers GitHub Actions (and, for the boards still on the team’s older self-hosted estate, a Jenkins pipeline that mirrors the same stages). The pipeline authenticates to the clouds via OIDC federation — short-lived tokens, no stored cloud keys — and pulls any unavoidable secrets (a virtual-appliance admin credential, a third-party API token) from HashiCorp Vault with dynamic, short-TTL leases rather than baking them into the repo or the CI config.
- Provisioning layer — Terraform, orchestrated by Terragrunt. Terragrunt runs Terraform across the affected environments, generating each environment’s backend and provider config from a single shared definition and feeding per-board inputs. Terraform produces a
planfor every environment, which is posted to the PR for human review. State for all forty environments lives in a locked remote backend (one state object per environment, isolated so a bad apply to one board cannot touch another). - Policy and security gates. Before any
apply, Wiz Code scans the Terraform/Pulumi in the PR for misconfigurations — a public storage bucket, an over-broad security group, an unencrypted database — and blocks the merge if it finds the class of mistake that caused the nine-hour exposure. Post-deploy, Wiz continuously scans the live cloud posture so configuration drift or a console hot-fix that bypassed the pipeline surfaces as an alert, not as next year’s incident. - Configuration layer — Ansible. Once Terraform has created the VMs and appliances, Ansible playbooks (triggered by the same pipeline, using Terraform’s output as a dynamic inventory) configure the inside: harden the OS, install the Moodle/PHP stack on VM-based boards, configure the virtual WAF appliances, lay down cron and backup jobs. This is the imperative, day-2 work that Terraform deliberately does not do.
- Runtime and operations. On the running fleet, CrowdStrike Falcon provides runtime threat detection on every node and appliance VM, feeding the SOC. Dynatrace (with Datadog on the boards that standardized on it before the consolidation) instruments the platform end to end — and, crucially, watches the pipeline itself, so a Terraform apply that regresses latency or a drift-remediation run shows up on a dashboard. Any failed apply, blocked policy gate, or detected drift auto-raises a ServiceNow change/incident record, giving audit the documented trail the mandate requires. At the edge, Akamai terminates TLS and provides WAF/DDoS protection for results-day surges — itself provisioned as code through its Terraform provider, so even the CDN config lives in the same reviewed pipeline.
The shape’s discipline is the point: Terraform/Pulumi provision, Ansible configures, Terragrunt keeps the forty-way repetition DRY, and one identity/secrets/security/ITSM spine governs all of it. No tool reaches outside its job.
The decision table
This is the artifact that ends the team’s argument. Map the job to the tool, not the tool to a preference.
| If the job is… | Reach for | Because | Not because the others “can’t” |
|---|---|---|---|
| Standing up cloud resources (network, DB, K8s, IAM) declaratively | Terraform | Largest provider ecosystem; plan/state/graph; team-standard HCL |
Pulumi does this too — pick by language preference |
| The same Terraform across 10+ environments without copy-paste | Terragrunt | DRY backend/provider generation; run a change across a whole env tree | Plain Terraform works for a handful of envs; this is for many |
| Configuring inside servers — packages, files, services, OS hardening | Ansible | Agentless, idempotent, procedural day-2 work; drives appliances over SSH/API | Terraform can run remote-exec, but it is the wrong tool for config mgmt |
| Provisioning with complex logic, in an app team’s own language | Pulumi | Real languages: loops, types, tests, true reusable components | HCL handles most cases; choose Pulumi for genuine code complexity + skills |
| Driving a virtual appliance with no first-class provider | Ansible | SSH/WinRM/API automation; no provider required | Terraform needs a provider; appliances often don’t have one |
| An auditable, repeatable exam-season scale-up | Terraform (+ Terragrunt) | Declarative desired state, reviewed plan, state-tracked, drift-detectable |
Ansible is stateless — weaker fit for “what exactly is provisioned” |
The horizontal split (provision vs configure) is Terraform/Pulumi above the line, Ansible below it — these compose, they do not compete. The vertical choice (Terraform vs Pulumi) is one-or-the-other on language and team, and Terragrunt only enters once Terraform’s own repetition hurts.
Where teams get this wrong
Using Terraform to configure servers. Terraform has remote-exec and local-exec provisioners, and it is tempting to install packages with them. Do not. They run once at create time, are not idempotent, are invisible to plan, and turn your state into a lie about what is actually installed. Provisioning and configuration are different jobs; remote-exec is an escape hatch, not a config-management strategy. Hand the inside of the box to Ansible.
Using Ansible to provision cloud resources. Ansible has cloud modules and can create a VPC. But it is stateless, so it has no real plan, no dependency graph, and no clean answer to “what does this manage and what would change” — exactly the questions an audit asks. For the declarative “here is the desired set of cloud resources” job, a state-backed tool is the right model.
Reaching for Terragrunt on day one. Terragrunt earns its keep when repetition across environments is genuinely painful. A team with three environments that adopts it early pays the cost — another tool, another layer of indirection, a steeper onboarding — for benefits they don’t yet have. Start with plain Terraform; adopt Terragrunt when the forty-way copy-paste is the actual pain.
Choosing Pulumi because “real code is nicer,” then handing it to ops. Pulumi is excellent if your team writes that language daily and your infra logic is genuinely complex. If your platform is owned by ops engineers who do not write TypeScript, you have traded HCL — which they can read — for code that shrinks your reviewer pool and your bus factor. The honest question is “who maintains this at 2 a.m. on results day,” not “which is more elegant.”
Multi-environment DRY: a worked comparison
The forty-environment problem is where the choice becomes concrete. In plain Terraform, each environment needs its own backend and provider config, repeated:
# environments/board-ka/prod/backend.tf — duplicated, with edits, ×40
terraform {
backend "s3" {
bucket = "edtech-tfstate-board-ka-prod"
key = "moodle/terraform.tfstate"
region = "ap-south-1"
}
}
With Terragrunt, the backend is generated from one root definition and each environment shrinks to its inputs:
# root.hcl — written ONCE
remote_state {
backend = "s3"
generate = { path = "backend.tf", if_exists = "overwrite" }
config = {
bucket = "edtech-tfstate-${local.board}-${local.env}"
key = "${path_relative_to_include()}/terraform.tfstate"
region = local.region
}
}
# environments/board-ka/prod/terragrunt.hcl — tiny, per environment
include "root" { path = find_in_parent_folders() }
inputs = { board = "board-ka", env = "prod", moodle_db_tier = "db.r6g.xlarge" }
Change the state-bucket convention once in root.hcl and it applies to all forty. With Pulumi, the same DRY goal is met with a typed component and a loop — no wrapper tool, but you are now maintaining a software project:
# one reusable component, instantiated per board
class MoodleEnv(pulumi.ComponentResource):
def __init__(self, board: str, env: str, db_tier: str): ...
for board in BOARDS: # forty instances from real code
MoodleEnv(board, "prod", db_tier=TIERS[board])
Three valid ways to kill the same duplication. Terragrunt wins when the team is HCL-native and wants the smallest new concept. Pulumi wins when they would rather express it in a language they already test and refactor.
Drift, security, and operating reality
Drift detection is where the state model pays off — and where Ansible’s statelessness shows its edge differently. A scheduled terraform plan (or pulumi preview) against an environment reports any difference between code and reality: the console hot-fix someone applied during a surge shows up as a diff to be reverted or codified. Wiz backstops this from outside the pipeline by scanning live cloud posture continuously, so even drift the plan misses — or a resource created entirely outside IaC — gets caught. Ansible, by contrast, doesn’t report drift so much as erase it: re-running the playbook simply re-converges the box to the playbook’s definition, which is its own kind of guarantee for the inside of a server.
Security wraps every tool identically. Wiz Code scans the Terraform/Pulumi in the PR before merge, catching the public bucket or open security group as code — the shift-left control that would have prevented the original exposure. The blast-radius discipline of one state object per environment means a bad apply is contained to one board. Secrets never live in code: Vault issues short-lived, dynamic credentials, and the pipeline authenticates to clouds via OIDC, so there are no long-lived keys in the repo or CI — a direct answer to the standing rule that leaked credentials must never be re-committed.
Cost is mostly about avoiding the wrong tool’s overhead. Terragrunt and Ansible are open-source and free; cost shows up as operational burden — Terragrunt is another concept to learn, Pulumi pulls language-runtime and dependency management into your infra repo, and HCP Terraform / Pulumi Cloud / Ansible Automation Platform are paid SaaS tiers you adopt only if you need managed state, RBAC, and policy-as-a-service rather than running runners and a state backend yourself. For a four-person team, the dominant cost is cognitive: every extra tool is onboarding time and a thing that breaks at 2 a.m.
Explicit tradeoffs and the honest recommendation
The combined stack’s cost is real. Running Terraform + Terragrunt + Ansible means three tools, three mental models, and a pipeline that orchestrates all of them — more than a one-tool shop carries. State is a liability as well as an asset: it must be stored securely, locked against concurrent writes, and reconciled when it drifts. The provision/configure split means an environment is only “done” after both layers run, so your pipeline and your runbooks have to treat them as one logical unit. And Terragrunt, for all its DRY power, is another layer between you and Terraform — when something breaks, you debug through the wrapper.
The alternatives, and when each genuinely wins. If you provision only cloud resources and never touch the inside of a VM (a pure-serverless, pure-managed-services shop), you may need just Terraform or just Pulumi — no Ansible, no Terragrunt. If you have a handful of environments, skip Terragrunt; plain Terraform’s repetition is tolerable and the wrapper is overkill. If your platform is owned by application engineers who live in TypeScript or Python and your infra has real branching logic, Pulumi alone (with its own config component instead of Terragrunt) is a coherent, single-language stack. If you mostly manage a fleet of long-lived VMs and appliances and provision little, Ansible-heavy with a thin Terraform base is right. And if you want managed state, policy-as-code, and RBAC out of the box rather than assembling them, the paid platforms (HCP Terraform, Pulumi Cloud, Ansible Automation Platform) buy that — at a per-seat price a four-person team should weigh hard.
For the EdTech team specifically, the framework lands here: Terraform as the provisioning substrate on both clouds, Terragrunt to keep the forty environments DRY (they have earned it), Ansible for OS hardening, the Moodle stack on VM-based boards, and the virtual appliances with no provider, and Pulumi held in reserve — not adopted, because the platform is ops-owned and HCL is what the team reads, but the right escape hatch the day a piece of infra genuinely needs real-language logic. All of it rides one spine: Okta/Entra identity, Vault secrets, GitHub Actions/Jenkins with Argo CD for the GitOps delivery of the Kubernetes workloads themselves, Wiz/Wiz Code for shift-left and posture, CrowdStrike Falcon at runtime, Dynatrace/Datadog for observability, ServiceNow for the audit trail, and Akamai at the edge. The win is not a favorite tool. It is that next results-day’s scale-up is a reviewed pull request with a readable plan, applied by a pipeline, scanned before it merges, and traceable to a commit — and the security group that was hand-edited last year is now a line of code that Wiz would refuse to let through.
Going deeper
The two axes get a team to a decision. The next layer — the one that separates someone who picked a tool from someone who can defend the pick in an architecture review — is understanding the wider field these four tools live in, the four different ways they remember what they have done, and the honest weakness each one carries. This section is that layer.
The wider field: five declarative provisioners, one configurator, one wrapper
The four tools in the title are the ones teams argue about, but the same two axes place every tool in the ecosystem, and naming the wider field stops the argument restarting every time a new name comes up. On the declarative, desired-state provisioning side sit five relatives: Terraform and its fork OpenTofu (HCL, explicit state file), Pulumi (general-purpose languages, explicit state), AWS CloudFormation (AWS-only, YAML/JSON, AWS holds the state for you), and Crossplane (Kubernetes-native, no state file — the cluster’s control plane reconciles continuously). On the procedural, configuration-management side sits Ansible (ordered YAML playbooks, agentless, largely stateless), with Chef, Puppet, and SaltStack as its cousins. And Terragrunt sits on neither axis, because it is not an engine — it is a DRY wrapper that runs Terraform or OpenTofu across many environments. Around the edges, Packer bakes golden machine images and the CDKs (AWS CDK for CloudFormation, CDKTF for Terraform) let you generate declarative provisioning from real code. Learn to ask “where on the two axes does this sit,” and every new tool name becomes a placement exercise instead of a fresh debate.
Four state models, not two
The intro drew one line — stateful (Terraform, Pulumi) versus stateless (Ansible). Zoom in and there are really four distinct models for how a tool remembers what it manages, and the model dictates how drift, locking, and blast radius behave.
| State model | Tools | Where “what I manage” lives | Consequence |
|---|---|---|---|
| Explicit state file | Terraform, OpenTofu, Pulumi | A state object you store and lock (remote backend / Pulumi Cloud) | True plan/preview and drift detection — but the file must be secured, locked, and reconciled, and it can be lost or corrupted |
| Service-managed state | CloudFormation | AWS keeps it server-side, per stack | Nothing to host or lock yourself — but you are inside one cloud, and you inspect via change sets/drift detection, not a file |
| Stateless convergence | Ansible, Chef, Puppet | Nowhere persistent — inspected on the target at run time | Nothing to corrupt; re-running re-converges the box — but no standing answer to “what exactly does this manage and what would change” |
| Control-plane reconciliation | Crossplane | The Kubernetes API, reconciled continuously by controllers | Desired state is enforced in a loop, not on a run — but you must operate Kubernetes as your infrastructure control plane |
The lesson for the EdTech team: “stateful vs stateless” is a useful first cut, but the honest answer to “what tracks reality” is four answers, and the audit question — “what exactly is provisioned and what would change” — is answered cleanly by the first two models, differently by the fourth, and only obliquely by the third.
The language axis, and why “it’s just YAML” tells you almost nothing
People reach for language as a tie-breaker — “we like Python,” “we hate HCL” — but language is the third axis, and it is easy to misread. Three families:
- Domain-specific (HCL): Terraform, OpenTofu, and Terragrunt. Purpose-built for describing infrastructure; small surface, readable by ops and developers alike, but it hits ceilings on genuinely complex logic.
- General-purpose: Pulumi (TypeScript, JavaScript, Python, Go, C#, Java — and even a YAML mode) and the CDKs (CDKTF, AWS CDK). Full loops, types, unit tests, and IDE support — at the cost of pulling a language runtime and its dependency management into your infra repo.
- YAML: Ansible, CloudFormation, Crossplane, and Pulumi’s YAML mode.
Here is the trap: “it’s YAML” tells you nothing about the model. CloudFormation and Crossplane are declarative YAML — you describe the end state and the engine reconciles it. Ansible is procedural YAML — you describe ordered steps. Same file format, opposite mental model. Compare a declarative Crossplane claim (desired state) with an Ansible play (ordered steps):
# Crossplane — DECLARATIVE YAML: "I want this Postgres; reconcile it for me"
apiVersion: database.example.org/v1alpha1
kind: PostgreSQLInstance
metadata:
name: moodle-board-ka
spec:
parameters:
storageGB: 100
compositionRef:
name: production-postgres
# Ansible — PROCEDURAL YAML: "do these steps, in this order"
- name: Configure Moodle app node
hosts: moodle_vms
become: true
tasks:
- name: Install the PHP and web stack
ansible.builtin.package:
name: "{{ item }}"
state: present
loop: [php, php-pgsql, nginx]
- name: Restart nginx after config changes
ansible.builtin.service:
name: nginx
state: restarted
Both are YAML; only one describes an end state. Choose language for who maintains the code — HCL keeps the reviewer pool wide, a general-purpose language narrows it to people who know both the cloud and that language — and never let “it’s YAML, so it must be simple” make the decision for you.
Provisioning vs configuration vs orchestration
Three verbs that beginners blur into one, and the blur is behind most wrong-tool choices:
- Provisioning — create and manage the resources: networks, databases, clusters, load balancers, IAM. Declarative provisioners own this (Terraform, OpenTofu, Pulumi, CloudFormation, Crossplane).
- Configuration — manage the inside of a resource that already exists: packages, files, services, OS hardening. Configuration management owns this (Ansible, Chef, Puppet).
- Orchestration — coordinate an ordered, multi-step process across systems: run this, wait, then that, with rollback. Ansible playbooks orchestrate procedures; Terragrunt orchestrates Terraform across a dependency graph of environments; and the CI/CD pipeline orchestrates all the tools end to end.
Watch the overloaded word: “orchestration” also means container orchestration (Kubernetes) — scheduling and healing running containers — which is a different sense entirely. When someone says “we need orchestration,” pin down which one they mean before you reach for a tool.
OpenTofu vs Terraform: the license fork, told straight
This is the one corner of the landscape where the “facts” people repeat are often out of date, so here is the timeline plainly:
- Through mid-2023, Terraform shipped under the MPL 2.0 — an OSI-approved, genuinely open-source license.
- In August 2023, HashiCorp relicensed Terraform (and Vault, Consul, Nomad, and others) to the Business Source License (BSL) 1.1. BSL is source-available, not open source: you may read and use the code, but not to build a product that competes commercially with HashiCorp. Each release automatically converts to MPL 2.0 four years after it ships.
- In response, the community forked the last MPL-licensed Terraform (1.5.x) as OpenTF, quickly renamed it OpenTofu, and donated it to the Linux Foundation. OpenTofu remains MPL 2.0 — open source and vendor-neutral.
- OpenTofu 1.6 was the first generally-available release, in January 2024. The two have diverged since: OpenTofu shipped features Terraform does not have — most notably client-side state encryption — while tracking the bulk of Terraform’s language surface.
- HashiCorp is now an IBM company (the acquisition closed in 2025), which sharpens the governance question for some buyers.
The practical read: OpenTofu is a near-drop-in replacement up to the fork point — the same HCL, the same init/plan/apply workflow, terraform swapped for tofu. Choose it when your organization needs a truly open-source license, wants a Linux Foundation-governed tool, or wants an OpenTofu-only feature like state encryption. For most day-to-day authoring the two are interchangeable; the decision is about licensing posture and governance, not syntax. Do not tell a beginner “OpenTofu is a different tool you’d have to relearn” — it is Terraform’s fork, and the muscle memory transfers.
Combining tools without them fighting
The mature stack is a combination, and combinations have their own rules:
- Provision, then configure. Terraform/OpenTofu (or Pulumi) creates the VM or appliance; Ansible configures its insides, taking the provisioner’s outputs as a dynamic inventory. This is the backbone of the EdTech shape above.
- Wrap for DRY. Terragrunt runs Terraform/OpenTofu and generates the repeated backend/provider blocks across many environments. It does not replace Terraform; it drives it.
- Bake instead of configure. Packer builds a golden image up front so there is little left to configure at boot — immutable infrastructure as an alternative to heavy run-time Ansible.
- Bridge engines. Pulumi can consume Terraform providers and read Terraform state; CDKTF synthesizes to Terraform. You are rarely fully locked in.
- The one hard rule: one resource, one owner. The fastest way to make two good tools fight is to let both manage the same resource — Terraform creates a security group and an Ansible task also edits it, so each run undoes the other and drift ping-pongs forever. Draw a clean boundary: this resource is Terraform’s, that file inside it is Ansible’s, and never the twain overlap.
Migration paths
You will inherit infrastructure built the “wrong” way, or change your mind. The common moves, and the mechanism each uses:
| From → To | How | Watch out for |
|---|---|---|
| Console/manual → Terraform | import blocks (1.5+) with -generate-config-out to generate config from live resources |
Generated config needs hand-cleanup; import maps one resource at a time |
| Terraform → OpenTofu | tofu init against existing state; near drop-in from 1.5.x |
Features either side added after the fork may not port cleanly |
| CloudFormation → Terraform | Import resources (or generate config), retire the stack gradually | Two tools must not manage the same resource during the cutover |
| Terraform → Pulumi | pulumi import, plus pulumi convert --from terraform for a starting translation |
You are adopting a general-purpose language and its tooling |
| Plain Terraform → Terragrunt | Incremental: wrap existing modules, move backend/provider into generated blocks | No need to rewrite resources; it is additive |
Note the recurring hazard: during any cutover, two tools must never manage the same resource at once — the same one-owner rule that governs combining them.
The capability matrix
The artifact to keep. Read a row as “when the job is this, here is the tool’s shape and its honest weakness.”
| Tool | Primary job | Model | State model | Language | Scope | Shines when | Honest weakness |
|---|---|---|---|---|---|---|---|
| Terraform | Provision | Declarative | Explicit state file | HCL | Multi-cloud (providers) | You want the biggest ecosystem and a team-standard DSL | BSL license; HCL ceilings on complex logic; state to secure |
| OpenTofu | Provision | Declarative | Explicit state file (+ client-side encryption) | HCL | Multi-cloud (providers) | You need a truly open-source, LF-governed Terraform | Smaller mindshare; slow divergence from Terraform over time |
| Pulumi | Provision | Declarative | Explicit state (Pulumi Cloud or self-managed) | TS/JS/Python/Go/C#/Java/YAML | Multi-cloud | Your team writes that language and infra logic is genuinely complex | General-purpose-language complexity; narrower talent pool |
| CloudFormation | Provision | Declarative | Service-managed (AWS) | YAML/JSON (or CDK) | AWS only | You are all-in on AWS and want no state to host | AWS-only; verbose; occasionally trails provider coverage |
| Crossplane | Provision | Declarative | Control-plane reconciliation (K8s API) | Kubernetes YAML (XRD/Composition/function-*) |
Multi-cloud (providers) | You offer self-service infra APIs on Kubernetes | You must run and operate Kubernetes as a control plane |
| Ansible | Configure (+ light provision) | Procedural | Stateless convergence | YAML playbooks | Any SSH/WinRM target + cloud modules | You configure OS/appliances or run ordered procedures | No real plan/graph; weak fit for declarative provisioning at scale |
| Terragrunt | Wrap Terraform (DRY) | Delegates to Terraform | Uses Terraform’s state | HCL (thin) | Whatever Terraform covers | You run the same Terraform across many environments | Another layer to learn and debug through; overkill for few envs |
The matrix is the decision table’s evidence: the horizontal split (provision above the line, configure below) is why Terraform/OpenTofu/Pulumi/CloudFormation/Crossplane and Ansible compose rather than compete, and the “honest weakness” column is the one beginners skip and seniors read first.
Practice challenges
Each scenario gives you a situation; decide which tool (or combination) fits before opening the solution. There is a defensible answer and a one-line why — the goal is to reason from the job, not from a favorite.
1. (Beginner) The greenfield three-env startup. A four-person startup needs to stand up a VPC, a managed Postgres, and an EKS cluster on AWS, all reviewed through pull requests. A blog post told them to adopt Terragrunt on day one. What do you recommend?
<details><summary>Show answer</summary>
Terraform (or OpenTofu) — and not Terragrunt yet. Standard declarative cloud provisioning is exactly Terraform’s job. Terragrunt earns its keep only once the same Terraform is copy-pasted across many near-identical environments; with three environments the wrapper is a concept and a layer of indirection paid for without the benefit. Why: match the tool to the pain you actually have, not the pain a blog post has.
</details>
2. (Beginner) 200 servers, one CVE. A CVE drops and you must patch OpenSSL, then enforce a CIS benchmark, across 200 already-running long-lived Linux VMs. Someone suggests writing a Terraform module. Which tool?
<details><summary>Show answer</summary>
Ansible. This is configuration management of the inside of existing machines — packages, files, services — which is procedural day-2 work. Terraform provisions resources; it has no good, idempotent, plan-visible way to patch and harden an OS fleet. Ansible connects agentlessly over SSH and converges each box to the desired config. Why: patching the inside of a server is configuration, not provisioning.
</details>
3. (Intermediate) The thirty-fold copy-paste. A platform team runs 30 near-identical environments across two clouds and just realized they edit the same backend and provider configuration in 30 places every time the naming convention changes. They are HCL-native and don’t want to learn a programming language. Which tool solves this?
<details><summary>Show answer</summary>
Terragrunt. This is the exact pain it exists for: define backend/provider boilerplate once and generate it per environment, keep each environment down to a few inputs, and run a change across the whole tree. Because the team is HCL-native, Terragrunt is the smallest new concept. (Pulumi’s typed-component-in-a-loop would also kill the duplication, but it asks them to adopt a general-purpose language they explicitly don’t want.) Why: many-environment DRY on an HCL-native team → the wrapper, not a language switch.
</details>
4. (Intermediate) The developer-owned platform. An application team lives in TypeScript, wants unit tests over their infrastructure, typed reusable components, and real branching logic that HCL expresses awkwardly. They own the platform themselves. Which tool?
<details><summary>Show answer</summary>
Pulumi. Same declarative, state-backed provisioning model as Terraform, but authored in a general-purpose language — so loops, types, unit tests, and genuine reusable components come naturally, and the team already writes and tests that language daily. The usual objection (it shrinks the reviewer pool to people who know the cloud and the language) doesn’t bite here because the owners are those people. Why: a software-engineering-heavy team with genuinely complex logic is exactly Pulumi’s sweet spot.
</details>
5. (Advanced) Uneasy about the license. Your org standardized on Terraform but is uncomfortable relying on a BSL-licensed tool for critical infrastructure, wants to stay on a genuinely open-source license, and would like client-side state encryption. Migration disruption must be minimal. What’s the move?
<details><summary>Show answer</summary>
OpenTofu. It is the MPL-2.0, Linux Foundation-governed fork of Terraform 1.5.x — genuinely open source, near-drop-in (terraform → tofu, same HCL and workflow, tofu init against existing state), and it ships client-side state encryption, which upstream Terraform does not. Why: the concern is licensing/governance, not syntax, and the fork answers exactly that with minimal relearning.
</details>
6. (Advanced) Self-service infra by kubectl. A platform team wants application developers to request their own databases and buckets by applying a small manifest with kubectl, have the cluster provision and then continuously reconcile them, and avoid babysitting an external state file entirely. They already run Kubernetes. Which tool, and how does its model differ from Terraform’s?
<details><summary>Show answer</summary>
Crossplane. It turns the Kubernetes cluster into a control plane: developers apply a claim, and controllers provision the real cloud resource and reconcile it forever, with desired and observed state living in the Kubernetes API rather than a Terraform-style state file you host and lock. The difference from Terraform is run-based reconciliation (a diff when you run plan/apply) versus continuous reconciliation (a loop that never stops). Why: self-service APIs plus continuous reconciliation on existing Kubernetes is the control-plane model, not the state-file model — at the cost of operating Kubernetes as your infrastructure control plane.
</details>
Common beginner mistakes
These are misconceptions — wrong mental models — distinct from the tool-role anti-patterns in “Where teams get this wrong” above. Fix the model and the right tool choice follows.
“Ansible replaces Terraform (or Terraform replaces Ansible).” The most common one. They are not competitors on the same axis — Terraform provisions the resources, Ansible configures what runs inside them. Asking which “wins” is like asking whether a crane or an electrician builds a house; you need both, doing different jobs. The right model: provision with a declarative tool, configure with Ansible, and let them compose.
“Terragrunt is a different language / a competitor to Terraform.” Terragrunt is neither a language nor an engine. It is a thin wrapper that runs Terraform (or OpenTofu) for you, written in the same HCL, solving one problem — repetition across many environments. If you know Terraform, you already know most of Terragrunt. The right model: Terragrunt drives Terraform; it does not replace it.
“Pulumi is just Terraform with Python.” It shares Terraform’s model (declarative, state-backed provisioning), but swapping HCL for a general-purpose language is not cosmetic — it changes who can maintain your infrastructure, brings a language runtime and dependency management into the repo, and turns config review into code review. Powerful when your team writes that language daily; a liability when it doesn’t. The right model: same model, different blast radius on your team.
“OpenTofu is a whole new tool I’d have to relearn.” OpenTofu is a fork of Terraform 1.5.x — same HCL, same init/plan/apply, terraform renamed tofu. The differences are licensing (MPL vs BSL), governance (Linux Foundation), and a few added features. The muscle memory transfers almost entirely. The right model: it’s Terraform’s open-source sibling, not a rewrite.
“Declarative means I never think about order or dependencies.” Declarative means you describe the desired end state and let the engine compute the order — but it computes that order from the references you write (and explicit depends_on). If you never model the relationship (the subnet references the VNet), the engine can’t infer it. The right model: you declare what and you declare relationships; the tool derives the sequence.
“State is just a cache I can delete if it gets weird.” For Terraform, OpenTofu, and Pulumi the state file is the source of truth mapping your code to real cloud resource IDs. Delete it and the tool forgets it manages those resources — the next apply tries to recreate them, orphaning the originals. State is to be backed up, locked, and recovered, never casually deleted. The right model: state is an asset and a liability, not a throwaway cache. (For CloudFormation the service holds this for you; for Crossplane the Kubernetes API does; for Ansible there’s nothing to delete — different models, same underlying truth that something has to track reality.)
Glossary
Infrastructure as Code (IaC) — managing infrastructure by writing and version-controlling machine-readable definitions instead of clicking in a console, so every change is reviewable, repeatable, and traceable to a commit.
Declarative — you describe the desired end state and the tool computes the steps to reach it (Terraform, Pulumi, CloudFormation, Crossplane).
Imperative / procedural — you specify the steps, in order, and the tool runs them (Ansible playbooks).
Desired state — the end configuration you declare; a declarative engine’s job is to make reality match it.
Convergence — repeatedly applying a definition until reality matches it; a stateless tool like Ansible converges the target on every run.
Idempotency — running the same operation twice leaves the same result as running it once; re-applying doesn’t duplicate or damage anything.
State / state file — a recorded inventory mapping your IaC definitions to real resource IDs (Terraform, OpenTofu, Pulumi). It enables plan and drift detection, and must be stored securely and locked.
Drift — divergence between what your code says and what actually exists in the cloud, typically from a manual “hot-fix” applied outside the pipeline.
Plan / preview — the diff a declarative tool shows before applying: exactly what it will create, change, or destroy (terraform plan, pulumi preview).
Provider — a plugin that teaches a provisioner how to talk to one platform’s API (AWS, Azure, a SaaS); the breadth of the provider ecosystem is Terraform’s decisive advantage.
HCL (HashiCorp Configuration Language) — the domain-specific language Terraform, OpenTofu, and Terragrunt are written in.
Module — a reusable, parameterized package of IaC you instantiate many times.
Provisioning — creating and managing cloud resources (networks, databases, clusters).
Configuration management — managing the inside of an existing machine: packages, files, services, OS hardening (Ansible, Chef, Puppet).
Orchestration — coordinating an ordered, multi-step process across systems; distinct from container orchestration (Kubernetes scheduling running containers).
Agentless — needing nothing pre-installed on the target; Ansible connects over SSH/WinRM.
Playbook — an Ansible file of ordered tasks, written in YAML.
Dynamic inventory — a target list generated at run time (for example from Terraform’s outputs) rather than hand-maintained, so Ansible configures exactly what was just provisioned.
Terragrunt — a thin wrapper that runs Terraform/OpenTofu and generates repeated backend/provider config across many environments to keep them DRY. Not a separate engine.
DRY (Don’t Repeat Yourself) — the principle of defining shared configuration once; the specific pain Terragrunt addresses across many environments.
Control plane (Crossplane) — a system that continuously reconciles desired against observed state; Crossplane turns a Kubernetes cluster into one for cloud infrastructure, with no external state file.
Composition / XRD (Crossplane) — a CompositeResourceDefinition declares a self-service API; a Composition (increasingly built from function-* pipelines) maps it to the real managed resources.
BSL (Business Source License) — the source-available (not open-source) license HashiCorp moved Terraform to in 2023; each release converts to MPL four years later.
MPL 2.0 (Mozilla Public License) — the OSI-approved open-source license Terraform used before 2023 and that OpenTofu retains.
OpenTofu — the MPL-licensed, Linux Foundation-governed fork of Terraform 1.5.x; near-drop-in, with some independent features such as client-side state encryption.
Automation API (Pulumi) — Pulumi’s programmatic interface for driving deployments from your own code instead of the CLI.
StackSet (CloudFormation) — CloudFormation’s mechanism for deploying a stack across many AWS accounts and regions at once.
Blast radius — how much can break from one change or mistake; isolating one state object per environment keeps a bad apply contained to a single environment.
OIDC federation — short-lived, keyless authentication from a pipeline to a cloud, so no long-lived credentials sit in the repo or CI.
Remote backend — where Terraform/OpenTofu stores state centrally (for example in object storage) with locking, so a team shares one authoritative state safely.