Experimenting Locally With Terraform, and Re-Running on File Changes

Most Terraform mistakes are cheap to find on a laptop and expensive to find in production. A typo in a for expression, a module input with the wrong type, or a plan that wants to replace a database: all of them show up before anything is created, if you have a fast way to try the change.
This post shows a local loop for Terraform: what you can run without any cloud account, how to keep experiments out of shared state, how to emulate AWS APIs, and how to test modules with mock providers. The last part answers a question that comes up once the loop is fast: can Terraform watch a folder and run again when files change?
TLDR
- Expressions, modules and outputs can be tested with no cloud account:
terraform console, the built-interraform_dataresource, and thelocalandrandomproviders. - Put a
backend_override.tfwith a local backend in your experiment folder, soinitnever touches the shared state. Do not commit it. terraform testwithmock_provider(Terraform 1.7 and later) runs module tests without credentials.- For AWS APIs on your machine, use an emulator such as AWS Emulator, Moto or LocalStack (which needs an auth token since March 2026).
- Terraform has no watch mode. Use file hashes so the next plan sees changed files,
watchexecorentrto re-runplanlocally, and CI to apply.
Prerequisites
- Terraform 1.7 or later. The examples ran on Terraform 1.15.8.
- Docker, if you want to run an AWS emulator.
- No cloud credentials for most of the post. Never point local experiments at production credentials.
How Do I Experiment With Terraform Locally?
Start with the parts of Terraform that need no provider at all, then add emulators or sandbox accounts only when you need real API behavior.
Test Expressions in terraform console
terraform console evaluates expressions against your configuration, including variables, locals and anything already in the state. It is the fastest way to check a for expression or a function:
Pipe an expression in when you want the result in a script: echo 'local.subnets' | terraform console. From Terraform 1.16, terraform console -scope=module.network evaluates expressions inside a child module. The console playground in our guide has more examples.
Build With Resources That Need No Account
terraform_data is built into Terraform and creates nothing outside the state. Combined with the local and random providers, it lets you test modules, for_each, moved blocks and outputs end to end:
module "network" {
source = "./modules/network"
for_each = toset(["staging", "production"])
name = each.key
}
resource "terraform_data" "server" {
for_each = module.network
input = "server in ${each.value.vpc_id}"
}
When the module uses a cloud provider, run the plan only. A plan of new resources often works with fake credentials and the provider's skip_* settings, because nothing exists to read yet.
Keep Experiments Out of Shared State
If the folder you experiment in has an S3 or HCP Terraform backend, a careless init and apply writes to the shared state. Add an override file instead of editing the backend block:
# backend_override.tf (in .gitignore)
terraform {
backend "local" {
path = "local.tfstate"
}
}
Terraform merges files that end in _override.tf over the normal configuration, so terraform init -reconfigure now uses a local state file. On Terraform 1.15.8, this replaced an S3 backend block without any other change. Add *_override.tf and *.tfstate* to .gitignore, and delete the local state when you start over.
Workspaces are another option in the same folder (terraform workspace new play-01), but they still use the shared backend and the same credentials. Use them for short-lived copies, not as protection for production.
Save Plans and Read Them as JSON
terraform plan -out=tfplan
terraform show -json tfplan | jq -r '.resource_changes[] | "\(.change.actions | join(",")) \(.address)"'
The jq filter prints one line per resource, such as delete,create aws_instance.web, which makes replacements easy to spot. How to Save Terraform Plan and Apply Output to a File covers plan files in detail.
Test Modules With Mock Providers
terraform test runs .tftest.hcl files. With mock_provider, the provider returns generated values and never calls an API, so the test needs no credentials:
# tests/network.tftest.hcl
mock_provider "aws" {}
variables {
name = "test"
vpc_cidr = "10.99.0.0/16"
}
run "subnets_use_the_vpc_range" {
command = plan
assert {
condition = alltrue([for s in aws_subnet.public : startswith(s.cidr_block, "10.99.")])
error_message = "Every subnet must be inside the VPC range."
}
}
Mocks show whether your logic is right. They do not show whether AWS accepts the request, so keep a few tests against a real sandbox account.
Emulate AWS APIs on Your Machine
For tests that need real API calls without an AWS account, point the AWS provider at a local emulator. There are three common choices in late 2026:
- AWS Emulator, released by AWS in November 2026. Our hands-on test found that it passed our workflow checks but needs care with some provider versions.
- Moto in server mode, open source and free.
- LocalStack. Since March 2026 the
localstack/localstackimage needs an auth token, and the free plan is for non-commercial use.
The provider configuration is the same idea for all of them. This one targets LocalStack's default port:
# provider_override.tf, for local runs only
provider "aws" {
region = "us-east-1"
access_key = "test"
secret_key = "test"
s3_use_path_style = true
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
endpoints {
s3 = "http://localhost:4566"
sts = "http://localhost:4566"
dynamodb = "http://localhost:4566"
sqs = "http://localhost:4566"
}
}
Older examples use s3_force_path_style. The AWS provider replaced it with s3_use_path_style, so the old name fails on current versions. Pin the emulator version and the provider version together, and check the emulator's docs for the services you use.
Keep the Loop Fast
terraform fmt -recursive && terraform validate && tflint
Run this before every commit. validate needs terraform init, but init -backend=false is enough when you only want to validate. When you do need real cloud behavior, use a sandbox account with its own state bucket and a role that cannot reach production.
Can Terraform Watch a Directory for Changes?
No. Terraform has no watch mode, and it never runs on its own. Every change goes through plan and apply when you, or a pipeline, start them. What you can do is:
- Make sure the next plan notices when a file changes.
- Re-run the plan automatically while you edit, on your machine.
- Apply on commit in CI, after review.
Make File Changes Show Up in the Plan
file(), templatefile() and fileset() read the disk at every plan, so a changed file changes the arguments that use it. When a resource has no argument that contains the file, add a hash:
data "archive_file" "lambda" {
type = "zip"
source_dir = "${path.module}/src"
output_path = "${path.module}/build/lambda.zip"
}
resource "aws_lambda_function" "processor" {
function_name = "data-processor"
role = aws_iam_role.lambda.arn
runtime = "python3.13"
handler = "main.handler"
filename = data.archive_file.lambda.output_path
source_code_hash = data.archive_file.lambda.output_base64sha256
}
To replace a resource when any file in a folder changes, hash the folder into a terraform_data resource and point replace_triggered_by at it:
resource "terraform_data" "app_source" {
triggers_replace = sha256(join("", [
for f in sort(fileset("${path.module}/app", "**/*.py")) : filesha256("${path.module}/app/${f}")
]))
}
resource "aws_instance" "worker" {
# ...
lifecycle {
replace_triggered_by = [terraform_data.app_source]
}
}
We tested the pattern on Terraform 1.15.8 with a second terraform_data resource named deploy in place of the instance. After one Python file changed, the plan showed both actions:
# terraform_data.app_source must be replaced
# terraform_data.deploy will be replaced due to changes in replace_triggered_by
Plan: 2 to add, 0 to change, 2 to destroy.
With the configuration above, the second line reads aws_instance.worker will be replaced due to changes in replace_triggered_by.
replace_triggered_by accepts only managed resources, which is why the hash lives in terraform_data and not in a local value.
Re-Run the Plan While You Edit
A file watcher can run terraform plan every time you save:
# watchexec: re-run the plan when a .tf, .tfvars or .py file changes
watchexec --exts tf,tfvars,py -- terraform plan -input=false -compact-warnings
# entr: watches only the files it gets on stdin; -c clears the screen first
find . \( -name '*.tf' -o -name '*.tfvars' \) -not -path './.terraform/*' | entr -c terraform plan -input=false
entr does not see files that are created after it starts, so restart it when you add a file. Every plan refreshes state and calls the provider APIs, and it takes the state lock. Keep this for local state or a sandbox. Do not add --restart to a command that applies: if a file changes during an apply, the watcher stops Terraform in the middle, and you can end up with partly applied changes and a stale lock. Do not run an auto-apply watcher against anything that matters.
Apply on Commit Instead
For shared environments, the trigger should be a reviewed commit, not a saved file. In GitHub Actions, a paths filter starts the pipeline only when Terraform or its data files change:
on:
push:
branches: [main]
paths:
- 'infra/**'
- 'config/*.json'
pull_request:
paths:
- 'infra/**'
- 'config/*.json'
HCP Terraform does the same with VCS-driven workspaces: each push to the connected branch starts a plan, and you can limit runs to changes in chosen paths. Terraform Best Practices has a full plan-and-apply workflow.
When the Data Changes More Often Than the Infrastructure
If you want a watch mode because a configuration file changes many times a day, the data probably does not belong in Terraform. Let Terraform create the place where the data lives, such as an SSM parameter or an S3 bucket, and let the application or a small deploy script update the data. Then a data change is not an infrastructure change, and nobody has to run apply for it.
Related Resources
- Terraform terminal simulator to practice the CLI in the browser.
- Terraform Puzzle to put resources and dependencies in order.
- Your first configuration, no cloud account needed in our Terraform guide.
- Test your code for more
terraform testpatterns. - Deploy a DigitalOcean Droplet with Terraform, a beginner exercise with a real provider.
Try it hands-on
Run the commands from this article in the browser. Nothing to install.
Terraform Basics Simulator
Learn Terraform basics in an interactive browser lab. Read and edit the HCL config, run init, validate, plan, apply, and destroy, and watch state react to your edits. A free, hands-on way to learn infrastructure as code with no AWS account.
Terraform State Puzzle
Fix broken Terraform state without destroying production. Eight puzzles with real Terraform output: a rename that would replace a database, an import that forces replacement, a stale lock, drift, count index shifts and more. Choose between moved, import and removed blocks and the state commands.
We earn commissions when you shop through the links below.
Bitrise
Managed runners for GitHub Actions
Bitrise Build Hub runs your GitHub Actions jobs on M4 Macs and AMD Linux machines. Change runs-on and keep your workflows, secrets and logs on GitHub.
Atomsized
AWS platform engineering and GitOps
Design and automation for reliable AWS and Kubernetes platforms, safer delivery workflows, and preview and UAT environments your engineers can understand and own.
DigitalOcean
Cloud infrastructure for developers
Simple, reliable cloud computing designed for developers
DevDojo
Developer community & tools
Join a community of developers sharing knowledge and tools
SMTPfast
Developer-first email API
Send transactional and marketing email through a clean REST API. Detailed logs, webhooks, and embeddable signup forms in one dashboard.
QuizAPI
Developer-first quiz platform
Build, generate, and embed quizzes with a powerful REST API. AI-powered question generation and live multiplayer.
Want to support DevOps Daily and reach thousands of developers?
Become a SponsorTags
Found an issue?
Related Posts
Also worth your time on this topic
How to Run Terraform From a Different Directory and in Scripts
Use terraform -chdir to run init, plan and apply against another folder, run Terraform without interactive prompts, limit a run to one resource with -target, and destroy everything except one resource without losing it.
CI/CD Pipeline Setup Checklist
Step-by-step checklist for a production-ready CI/CD pipeline: source control, builds, tests, security scans, deploy gates, secrets, and rollback paths.
1-2 hours
Complete CI/CD Pipeline with GitHub Actions
Hands-on lab: build a production CI/CD pipeline with GitHub Actions, including tests, security scanning, container builds, and automated deployment.
90 minutes