How to Use Terragrunt: The Beginner's Guide (First Install, First DRY Config, the Remote-State Lesson, and the Plan-Before-Apply Rule)
CodexFuse Team · 2026-11-07T00:32:46.887Z · 20 min read min read
How to Use Terragrunt?
Learning how to use terragrunt takes seven moves: install Terragrunt and OpenTofu, write your first terragrunt.hcl, point it at a module, set up remote state, move shared settings into a root config, wire dependencies between units, and run plan before every apply. Terragrunt is a thin wrapper around OpenTofu and Terraform that keeps infrastructure code DRY, manages remote state, and runs commands across many modules in the right order. This terragrunt tutorial follows the official docs for the current release, v1.2.1, with every fact checked as of November 2026.
Terragrunt wraps an engine, so it helps to know the engine first. The OpenTofu beginner guide on CodexFuse covers the open-source Terraform fork that most Terragrunt examples use today, including installs, state files, and the plan and apply loop. Terragrunt does not replace OpenTofu or Terraform; it orchestrates them and extends what they do.
This guide sits in the infrastructure-as-code cluster opened by the OpenTofu guide, and it opens the Terraform-wrapper lane inside that cluster. Terragrunt is the wrapper lane: the DRY layer that sits on top of the engine. You will build one small stack as you read: a VPC unit and an EC2 unit, sharing one root config, with state in S3.
- Install OpenTofu and Terragrunt v1.2.1.
- Write your first terragrunt.hcl with a local module.
- Add a remote_state block for S3 state and DynamoDB locking.
- Move shared settings into a root.hcl and include it.
- Wire the EC2 unit to the VPC unit with a dependency block.
- Run plan across the whole stack with run --all.
- Apply only after you have read the plan.
What Is Terragrunt?
What is terragrunt, in one sentence? Terragrunt is a flexible orchestration tool that lets infrastructure as code written in OpenTofu or Terraform scale, in the project's own words.
It is a single Go binary that shells out to the tofu or terraform binary for every real plan and apply. You keep writing ordinary Terraform modules, and Terragrunt adds the layers that get painful at scale: DRY configuration, remote state, dependencies, and multi-module runs.
The project started on 23 May 2016, and it is maintained by Gruntwork under the MIT license. As of this run it has 9,915 GitHub stars and 1,250 forks, with 211 open issues.
The repository is 3,820 days old, which is 10.5 years, and it works out to 7.93 stars per fork with only 2.13 open issues per 100 stars. The last push to the repository landed on 6 November 2026, so the project is actively maintained.
Terragrunt reached v1.0.0 on 30 March 2026, which is 222 days before this guide, and the current release is v1.2.1, published on 23 October 2026, 15 days before this guide. The 207 days between v1.0.0 and v1.2.1 brought the 1.x compatibility promise: CLI commands and HCL configuration valid in 1.0 stay valid through 1.x. CodexFuse offers no Terragrunt tool; this guide teaches the official install, and nothing in it depends on a CodexFuse product.
What Do You Need Before You Install Terragrunt?
You need three things before the install: an engine, a cloud account, and a Git repository. The engine is OpenTofu or Terraform, installed first, because Terragrunt shells out to it. A cloud account gives the remote state somewhere to live, and the examples here use AWS.
The Git beginner guide on CodexFuse covers the repository workflow your infrastructure code lives in, from first commit to branching.
Keep the machine simple: any Linux or macOS machine with a shell works, and Windows works through the Chocolatey package. You also need an AWS account with permissions to create an S3 bucket and a DynamoDB table, or a teammate who can create them for you. If you have no cloud account yet, the quick-start example in the next section runs with local state, so you can learn the commands first and add remote state after.
| Item | Why you need it | How to check |
|---|---|---|
| OpenTofu or Terraform | The engine Terragrunt wraps | tofu version or terraform version |
| Terragrunt v1.2.1 | The wrapper itself | terragrunt --version |
| AWS account | Remote state and locking | aws sts get-caller-identity |
| Git | Versioning for your infrastructure repo | git --version |
| A text editor | Editing HCL files | Any editor works |
Step 1: Install Terragrunt
A terragrunt install starts with the engine, then adds the wrapper. Install OpenTofu first, confirm it runs, and only then install Terragrunt, because Terragrunt finds the engine binary on your PATH and calls it for every command. The official install page pins the current release in its own convenience script, and this guide follows that page for v1.2.1.
curl -sSfL --proto '=https' --tlsv1.2 https://terragrunt.com/install | bash # Add this line to ~/.bashrc: export PATH="$HOME/.terragrunt/bin:$PATH" terragrunt --version
The quick install script puts the binary in ~/.terragrunt/bin and prints the PATH line to add to your shell config. Use ~/.zshrc for Zsh, or fish_add_path ~/.terragrunt/bin on Fish, then open a new shell and confirm the install with terragrunt --version. The script is the fastest route on Linux and macOS; the releases page is the route to pick when you want to verify checksums or pin an exact archive.
| Platform | Command | Note |
|---|---|---|
| Linux or macOS, quick install | curl -sSfL --proto '=https' --tlsv1.2 https://terragrunt.com/install | bash | Official script, installs to ~/.terragrunt/bin |
| macOS, Homebrew | brew install terragrunt | Third-party package, check the version |
| Windows, Chocolatey | choco install terragrunt | Third-party package, check the version |
| Arch Linux, Pacman | pacman -S terragrunt | Third-party package, check the version |
| mise | mise install terragrunt v1.2.1 | Pins the version in your project |
| asdf | asdf plugin add terragrunt, then asdf install terragrunt v1.2.1 | Pins the version in your project |
Package managers are third-party packages, and the official docs warn they may lag the latest release, so check your version against the releases page after installing one. Tenv is not listed on the official install page, so treat it as an unverified route for Terragrunt even though other tools list it. Check the releases page before you install, because v1.2.1 is 15 days old and a newer patch release can ship at any time.
Tab completion is one command away once the binary is on your PATH: terragrunt --install-autocomplete, then restart your shell. That is the whole install: one binary, no background service, nothing to run as a daemon.
Step 2: Your First terragrunt.hcl
Your first terragrunt.hcl can be almost empty, and that is the point. Run the quick-start in a fresh directory, and Terragrunt creates the file, runs init for you when needed, and applies the module. This auto-init behavior is why you no longer run tofu init or terraform init by hand before an apply.
resource "local_file" "file" {
content = "Hello, World!"
filename = "${path.module}/hi.txt"
}
touch foo/terragrunt.hcl cd foo terragrunt apply -auto-approve
The local_file example writes hi.txt into the module directory, and it proves the loop works before any cloud resource exists. Terragrunt runs init automatically when a command needs it, so the first apply just works. The .terragrunt-cache directory appears beside your files on every run, and it belongs in .gitignore next to the .terraform directory.
Point the terraform block at a shared module and the same file becomes a reusable unit. The source can be a local path or a registry module, and inputs pass values into it. This terragrunt for beginners pattern is the whole job: one small file per unit, one shared module, inputs that change per environment.
Step 3: Remote State, the Right Way
A terragrunt remote state block tells Terragrunt where to store backend state, and it generates the backend.tf file for you. The example below stores state in an S3 bucket named my-tofu-state in us-east-1, with the state file named tofu.tfstate and a DynamoDB table named my-lock-table for locking. The generate block writes backend.tf with if_exists set to overwrite_terragrunt, so Terragrunt owns that file.
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
config = {
bucket = "my-tofu-state"
key = "tofu.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "my-lock-table"
}
}
# Terragrunt generates this backend.tf for you:
terraform {
backend "s3" {
bucket = "my-tofu-state"
dynamodb_table = "my-lock-table"
encrypt = true
key = "tofu.tfstate"
region = "us-east-1"
}
}
The bucket name must be globally unique, so use your own name or add a date to it. The --backend-bootstrap flag lets the apply run create the bucket and the lock table when they do not exist yet, and you can also create them beforehand with terragrunt backend bootstrap. Either way, you never create the backend resources by hand and never run init yourself.
Two units sharing one state key is the classic beginner trap. Give every unit the same key and the second unit's plan proposes destroying the first unit's resources, because both units read and write the same state file. The fix is one function call in the root config, and the next section shows it.
| Field | Example value | What it does |
|---|---|---|
| backend | s3 | The backend type Terragrunt generates |
| bucket | my-tofu-state | The S3 bucket holding state |
| key | tofu.tfstate | The state file name inside the bucket |
| region | us-east-1 | The AWS region of the bucket |
| encrypt | true | Encryption of the state at rest |
| dynamodb_table | my-lock-table | The table used for state locking |
| generate.path | backend.tf | The file Terragrunt writes |
| generate.if_exists | overwrite_terragrunt | Terragrunt owns the generated file |
The servers Terragrunt provisions often run on a VPS, and the VPS hosting guide on CodexFuse covers picking and securing that server before your first apply. State lives in S3 either way; the guide keeps the two concerns separate, which is the point of the wrapper.
Step 4: The DRY Lesson
Terragrunt dry terraform is the whole reason the tool exists. Copying the same provider block, backend block, and inputs into every environment is the repetition Terragrunt removes, and the fix is one shared file at the root of your repo. That file is called root.hcl by convention, and every unit pulls it in with an include block.
# root.hcl
locals {
region_hcl = find_in_parent_folders("region.hcl")
region = read_terragrunt_config(local.region_hcl).locals.region
}
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
config = {
bucket = "my-tofu-state"
key = "${path_relative_to_include()}/tofu.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "my-lock-table"
}
}
# live/dev/vpc/terragrunt.hcl
include "root" {
path = find_in_parent_folders("root.hcl")
}
terraform {
source = "tfr:///terraform-aws-modules/vpc/aws?version=5.16.0"
}
inputs = {
name = "my-vpc"
cidr = "10.0.0.0/16"
}
The include block pulls the parent root.hcl into the unit, and the root label is a convention, not a requirement. The key line is the one that builds each unit's state path from its directory relative to root.hcl, so the VPC unit stores state at vpc/tofu.tfstate and the EC2 unit at ec2/tofu.tfstate. That one function call is what keeps two units from sharing a state key.
Locals carry values like the region out of small files, and read_terragrunt_config loads them from a region.hcl that sits beside root.hcl. Change the region in one file and every unit picks it up, which is the DRY lesson in one line. The generate block for the provider works the same way: write it once in root.hcl and every unit gets a provider.tf before it runs.
| File | Lives where | What it holds |
|---|---|---|
| root.hcl | Repo root | remote_state, generate, and shared locals |
| region.hcl | Repo root | The region local every unit reads |
| live/dev/vpc/terragrunt.hcl | The VPC unit directory | include, module source, and VPC inputs |
| live/dev/ec2/terragrunt.hcl | The EC2 unit directory | include, module source, EC2 inputs, and the dependency block |
| .gitignore | Repo root | .terragrunt-cache and .terraform entries |
A terragrunt modules layout like this one scales by adding directories, not by editing files. Add a database unit by copying the EC2 unit's file, changing the source and inputs, and running plan. Nothing in root.hcl changes, and nothing in the VPC unit changes.
Step 5: Dependencies, Modules, and run --all
Terragrunt modules are ordinary Terraform modules, and a unit is one directory with one terragrunt.hcl that points at one module. The dependency block is how units share outputs: the EC2 unit declares that it depends on the VPC unit, and Terragrunt passes the VPC outputs in as inputs. The directed acyclic graph decides the order, so applies run dependencies first and destroys run them last.
dependency "vpc" {
config_path = "../vpc"
mock_outputs = {
private_subnets = ["mock-subnet"]
}
mock_outputs_allowed_terraform_commands = ["plan"]
}
inputs = {
name = "single-instance"
instance_type = "t2.micro"
monitoring = true
subnet_id = dependency.vpc.outputs.private_subnets[0]
}
Mock outputs exist for one situation: planning a unit whose dependency has not been applied yet. Without them, run --all plan fails while resolving the dependency, because the VPC outputs do not exist yet. Tell Terragrunt to use the mock values only during plan, and the real outputs take over on apply.
# region.hcl
locals {
region = "us-east-1"
}
# Terragrunt generates this provider.tf from root.hcl:
provider "aws" {
region = "us-east-1"
}
The provider file is generated before every run, so the region you set in region.hcl flows into every unit without a copy in each one. That is the same DRY lesson applied to providers: one source of truth, many consumers. The generated files carry a Generated by Terragrunt comment, so you always know which files are yours and which ones the tool wrote.
Running run --all from a directory limits the run to the units below it, and your working directory sets the blast radius. A run from the region directory touches only that region's units, while a run from the repo root touches everything. That is the safety valve for big stacks: scope the run, then widen it.
Step 6: Plan Before You Apply
Plan before you apply is the rule that saves beginners from themselves. Run plan across the whole stack, read every line of the output, and only then run apply. The plan shows what Terragrunt will create, change, and destroy, and a destroy in the plan is the line that deserves a second look.
terragrunt run --all plan terragrunt run --all apply terragrunt run --all destroy terragrunt run --all -- plan -destroy terragrunt run --all --filter-affected -- plan terragrunt run --backend-bootstrap -- apply -auto-approve
The --filter flag is a small query language for selecting units by name, path, attribute, or dependency relationship, and --filter-affected picks up only the units a Git change touched. The plan -destroy form previews a destroy without running it, which is the safe way to check a teardown. Never apply a plan you have not read, because apply is the step that spends money and creates real resources.
Auto-approve exists for CI pipelines, not for learning. Type the approval by hand while you are new, and let the pipeline take over once the plan output is boring. That habit is what keeps a typo in an input from becoming a bill.
Terragrunt vs Terraform: What the Wrapper Adds
A terragrunt vs terraform comparison starts with what does not change: Terragrunt never plans or applies anything itself. It shells out to the tofu or terraform binary for every real operation, so the engine still owns the plan, the apply, and the state format. What Terragrunt adds is the orchestration around those operations, and the official FAQ lists it plainly.
| Capability | Plain Terraform or OpenTofu | With Terragrunt |
|---|---|---|
| Backend config | Hand-written backend block per root module | One remote_state block in root.hcl, backend.tf generated per unit |
| Multi-environment repetition | Copy provider, backend, and inputs per environment | include, locals, and read_terragrunt_config from one root |
| Cross-module outputs | terraform_remote_state data source with typed keys | dependency blocks, outputs passed as inputs, DAG ordering |
| Multi-module runs | One directory at a time | run --all plan, apply, and destroy, parallel where the DAG allows |
| Hooks and retries | Shell wrappers | before_hook, after_hook, error_hook, and the errors retry block |
| When plain is enough | One or two root modules with no dependencies | The FAQ says plain OpenTofu or Terraform alone might be fine for you |
The Terraform beginner guide on CodexFuse covers the engine on its own, from first install to first apply, if you want the engine's mental model before the wrapper. Learn the engine first when you are new to both, because Terragrunt's value only shows once you have felt the repetition it removes. The wrapper and the engine stay separate in your head: the engine builds, the wrapper organizes.
OpenTofu 1.8 added early evaluation of variables and locals in backend configuration, which removes one of the oldest reasons to wrap, according to an October 2026 comparison. Dependency ordering and run --all remain the durable reasons to keep the wrapper. Choose the wrapper for the orchestration, not for the backend trick.
The 1.x Changes That Break Old Tutorials
Old tutorials are the biggest trap in this field, because Terragrunt 1.0 redrew the command line. The freshest page-one guides still show the old syntax: the Spacelift tutorial was updated about 185 days before this guide, the Scalr guide about 44 days before it, and both still show run-all in places. The Academeez lesson is about 734 days old and the CloudThat guide about 764 days old, so both predate the 1.x redesign entirely.
| Old 0.x | Current 1.x | Notes |
|---|---|---|
| terragrunt run-all apply | terragrunt run --all apply | run-all is a hard error in 1.x |
| terragrunt run-all plan | terragrunt run --all plan | Same mechanical change |
| terragrunt run-all destroy | terragrunt run --all destroy | Destroy order is the reverse of apply order |
| terragrunt run-all hclvalidate | Removed in 0.90 | Removed 393 days before this guide |
| terragrunt.hcl in every unit | root.hcl plus include | The current recommended layout |
| terraform.tfvars with a terragrunt block | terragrunt.hcl | The 0.12 era syntax in old README mirrors |
Run-all is gone in Terragrunt 1.x, and the old command is a hard error, not a warning. The migration is mechanical: insert run --all in place of run-all and keep the rest of the command. That one change is the difference between a tutorial that works and one that fails on the first line.
Two more 1.x facts belong in every new setup. Terragrunt now creates a .terragrunt-cache directory for every run, so add it to .gitignore beside .terraform.
Releases from v1.1.0 are immutable and attested, so a release tag can be verified with the GitHub CLI and cannot be replaced after publishing. The docs moved too: the current home is docs.terragrunt.com, and the old gruntwork.io host serves stale copies.
The engine block is experimental, and it stays behind the TG_EXPERIMENTAL_ENGINE flag. Install tofu and Terragrunt just uses it, with no extra config for the common case, and leave explicit engine pinning alone until the docs call it stable.
Seven Mistakes Beginners Make
Most Terragrunt failures come from seven mistakes, and all seven are avoidable. The table below is the short version; the sections above are the long version. Read the plan, scope the run, and keep the root config small.
| Mistake | What happens | Fix |
|---|---|---|
| Copying run-all from an old tutorial | Hard error on the first command | Use run --all |
| Sharing one state key across units | The second plan proposes destroying the first unit's resources | Use path_relative_to_include in the key |
| Skipping remote state | State lives on one laptop and the team cannot share it | Add the remote_state block before the first real apply |
| Applying without reading the plan | A typo becomes real infrastructure and a real bill | Read every plan, then apply |
| Running run --all from the repo root on day one | The blast radius is the whole stack | Run from the smallest directory that holds the units you mean |
| Forgetting .terragrunt-cache in .gitignore | Cache directories get committed to the repo | Ignore .terragrunt-cache and .terraform |
| Installing from a stale package manager copy | Old commands and old syntax | Check the version against the releases page |
The example repos are the best next step after this guide. The GitHub beginner guide on CodexFuse covers cloning and working with repos, and the terragrunt-infrastructure-live-example repo shows a full live layout to study. No testbed outputs were executed for this guide, so run the commands yourself and watch the output before trusting any of it.
from datetime import date
ref = date(2026, 11, 7)
def days(d):
return (ref - d).days
print("v1.2.1 age days:", days(date(2026, 10, 23)))
print("v1.0.0 age days:", days(date(2026, 3, 30)))
print("v1.0.0 to v1.2.1 days:", (date(2026, 10, 23) - date(2026, 3, 30)).days)
print("repo age days:", days(date(2016, 5, 23)))
print("repo age years:", round(days(date(2016, 5, 23)) / 365.25, 1))
print("stars per fork:", round(9915 / 1250, 2))
print("open issues per 100 stars:", round(211 / 9915 * 100, 2))
print("v0.90.0 age days:", days(date(2025, 10, 10)))
print("spacelift age days:", days(date(2026, 5, 6)))
print("scalr age days:", days(date(2026, 9, 24)))
print("academeez age days:", days(date(2024, 11, 3)))
print("cloudthat age days:", days(date(2024, 10, 4)))
# v1.2.1 age days: 15
# v1.0.0 age days: 222
# v1.0.0 to v1.2.1 days: 207
# repo age days: 3820
# repo age years: 10.5
# stars per fork: 7.93
# open issues per 100 stars: 2.13
# v0.90.0 age days: 393
# spacelift age days: 185
# scalr age days: 44
# academeez age days: 734
# cloudthat age days: 764
Sources
The Terragrunt docs at docs.terragrunt.com supplied the install steps, the quick start, the overview examples, and the FAQ, all fetched on 7 November 2026. The GitHub API supplied the release and repository figures for gruntwork-io/terragrunt on the same day. The Spacelift tutorial, the Scalr guide, the Academeez lesson, the CloudThat guide, and the Medium tutorial were the page-one results for the primary keyword, and the dev.to comparison supplied the run-all hard-error test on 1.1.6 while the ops0 comparison supplied the OpenTofu 1.8 note.
Every number in this guide comes from one of those sources or from a Python calculation shown in the computed block above. No search volumes are quoted, because none were published for this keyword. The release figures are a 7 November 2026 snapshot, and the releases page is the place to check for a newer version.
The page-one field for the primary keyword was thin: Go-package mirrors of README versions from the 0.12 era, one undated Medium tutorial, and guides updated 44 to 764 days before this guide. That thin field is why this guide pins every command to the current 1.2.1 release.
Frequently Asked Questions
Is Terragrunt a replacement for Terraform?
No, Terragrunt is a thin wrapper that orchestrates Terraform or OpenTofu, and the engine still runs every plan and apply. You install the engine first, and Terragrunt calls it. If you have one or two root modules and no dependencies between them, the official FAQ says plain OpenTofu or Terraform alone might be fine for you.
Does Terragrunt work with OpenTofu?
Yes. Terragrunt detects the tofu binary automatically, and the official docs use OpenTofu in most examples. Explicit engine pinning is still experimental and sits behind a flag, so install tofu and let Terragrunt find it.
What happened to run-all?
Terragrunt 1.0 removed run-all, so the old command is now a hard error. The replacement is run --all, as in terragrunt run --all apply, and the migration is mechanical. Old tutorials that still show run-all predate the 1.x redesign.
Where does Terragrunt store state?
In the backend your remote_state block names, usually an S3 bucket with a DynamoDB table for locking. Terragrunt generates the backend.tf file from the block, and each unit gets its own state key from path_relative_to_include. Local state works for learning, and remote state is the step before any real apply.
Do I need Terragrunt for a small project?
Probably not. A single environment with a handful of modules is simpler in plain Terraform or OpenTofu, and the official FAQ says so. Terragrunt earns its place when environments multiply, modules share outputs, and the same backend block gets copied for the tenth time.




