How to Run Terraform From a Different Directory and in Scripts

Terraform works on the folder you run it in, and by default it asks before it changes anything. That is right for a person at a terminal. In scripts, Makefiles and CI pipelines you want the opposite: run against a folder you name, never wait for input, and change only what you meant to change.
This post covers the CLI options for that: -chdir to work on another folder, the flags that turn off prompts, -target to limit a run to one resource, and a safe way to destroy every resource except one.
TLDR
| Question | Short answer |
|---|---|
How do I run init in another folder? |
terraform -chdir=environments/prod init. -chdir goes before the subcommand |
Where do -out and -var-file paths point? |
They are relative to the -chdir folder |
| How do I skip the "Enter a value: yes" prompt? | terraform apply tfplan for a saved plan, or -auto-approve. Add -input=false in scripts |
| How do I apply only one resource? | terraform apply -target=aws_instance.web, for recovery only, then a full plan |
| How do I destroy everything except one resource? | Replace its block with a removed block (destroy = false), apply, then terraform destroy |
Prerequisites
- Terraform 0.14 or later for
-chdir, and 1.7 or later forremovedblocks. - The outputs in this post come from Terraform 1.15.8 with the built-in
terraform_dataresource.
How Do I Run Terraform Init From a Different Directory?
Use the global -chdir option. It changes the working folder before the subcommand runs, and your shell stays where it is:
terraform -chdir=environments/prod init
terraform -chdir=environments/prod plan -out=tfplan
terraform -chdir=environments/prod apply tfplan
-chdir must come before the subcommand. After it, the subcommand rejects it:
$ terraform init -chdir=environments/prod
Error: Failed to parse command-line flags
flag provided but not defined: -chdir
The old form with the folder as the last argument was removed. On current versions, terraform init environments/prod fails with Too many command line arguments. Did you mean to use -chdir?.
Paths Are Relative to the New Folder
Every relative path on the command line is resolved from the -chdir folder. That includes -out, -var-file, -backend-config and plan file names:
# writes environments/prod/tfplan
terraform -chdir=environments/prod plan -out=tfplan
# reads backends/prod.hcl from the folder you are in
terraform -chdir=environments/prod init -backend-config="$PWD/backends/prod.hcl"
# the same with a path relative to the target folder
terraform -chdir=environments/prod init -backend-config=../../backends/prod.hcl
Inside the configuration, path.module and path.root point at the target folder, and local module sources such as ../../modules/vpc resolve from it. The exception is path.cwd: it still returns the folder you started in, which is one more reason not to use it. Where path.module points shows the values side by side.
The .terraform folder is created inside the target folder. If you set TF_DATA_DIR to move it, set the same value for every command that uses that folder, because a later plan without it will not find the providers that init installed.
Run Several Folders From One Script
A loop with -chdir is enough for a handful of root folders:
#!/usr/bin/env bash
set -euo pipefail
for dir in environments/dev/network environments/dev/app; do
echo "== $dir"
terraform -chdir="$dir" init -input=false
terraform -chdir="$dir" validate
terraform -chdir="$dir" plan -input=false -out=tfplan
done
cd in a subshell does the same and is fine too: (cd "$dir" && terraform plan). The parentheses keep the cd from affecting the rest of the script. A Makefile target such as plan-%: with terraform -chdir=environments/$* plan gives you make plan-prod.
In GitHub Actions, a matrix runs each folder as its own job:
jobs:
plan:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
dir: [environments/dev/app, environments/staging/app, environments/prod/app]
steps:
- uses: actions/checkout@v7
- uses: hashicorp/setup-terraform@v4
with:
terraform_version: 1.17.2
- run: terraform -chdir=${{ matrix.dir }} init -input=false
- run: terraform -chdir=${{ matrix.dir }} plan -input=false -no-color -out=tfplan
Each job also needs credentials for its environment, ideally a separate role per environment.
How Do I Run Terraform Without the Interactive Prompt?
There are two prompts, and each has its own switch:
- The approval prompt (
Enter a value:before apply or destroy). Apply a saved plan,terraform apply tfplan, which never asks. Or pass-auto-approvetoapplyordestroy. - Questions for missing variables. Pass
-input=false, or setTF_INPUT=0. Terraform then stops with an error instead of waiting for someone to type a value.
The safe pattern for automation saves the plan and applies exactly that plan:
export TF_IN_AUTOMATION=1 # shorter output, no "run terraform apply next" hints
terraform init -input=false
terraform plan -input=false -out=tfplan
# review or approve the plan here
terraform apply -input=false tfplan
terraform apply tfplan refuses to run if the state changed after the plan was made, so you never apply a plan that is out of date. terraform apply -auto-approve without a plan file makes a new plan and applies it at once, so nobody sees what it will do. Keep -auto-approve for throwaway environments and for destroy in test pipelines.
With TF_INPUT=0 and a required variable without a value, Terraform stops at once:
Error: No value for required variable
on main.tf line 1:
1: variable "name" {
Give variable values with -var-file, TF_VAR_<name> environment variables, or an *.auto.tfvars file. Add -lock-timeout=5m when two pipelines can run against the same state, so the second one waits for the lock instead of failing. How to Save Terraform Plan and Apply Output to a File shows how to keep the plan and the logs as artifacts.
How Do I Run Terraform Only for a Specific Resource?
Pass -target with the resource address to plan or apply:
terraform plan -target=aws_instance.web -out=tfplan
terraform apply tfplan
# for_each and count instances need the key, in quotes
terraform apply -target='aws_instance.servers["web"]'
# a whole module
terraform apply -target=module.network
Terraform includes the resources that the target depends on, so a target on an instance also creates its security group if it does not exist yet. With destroy, -target also destroys everything that depends on the target. Here is the start of a targeted apply where terraform_data.app depends on terraform_data.net:
# terraform_data.app will be created
# terraform_data.net will be created
Plan: 2 to add, 0 to change, 0 to destroy.
...
Warning: Resource targeting is in effect
You are creating a plan with the -target option, which means that the result
of this plan may not represent all of the changes requested by the current
configuration.
The -target option is not for routine use, and is provided only for
exceptional situations such as recovering from errors or mistakes, or when
Terraform specifically suggests to use it as part of an error message.
Take the warning seriously. A targeted run skips every other change in the configuration, so the state and the code drift apart until someone runs a full plan. Use -target to recover, for example when one broken resource blocks the whole apply, and run a full terraform plan afterwards to see what is still pending. If you need -target every week, the configuration is too big: split it into smaller root folders with their own state.
Two related flags are often what you want instead:
-replace=aws_instance.webreplaces one resource that is in a bad state, as part of a normal full plan. It is the replacement for the deprecatedterraform taintcommand.-refresh-onlyupdates the state from the real objects without changing them. Who Owns the State File explains when that helps.
How Do I Delete All Resources Except One?
Tell Terraform to forget the resource you want to keep, then destroy the rest. On Terraform 1.7 and later, do the first step with a removed block, so it is part of the code and shows up in review:
# The block for terraform_data.db is deleted from the code and replaced with:
removed {
from = terraform_data.db
lifecycle {
destroy = false
}
}
terraform apply # forgets terraform_data.db, changes nothing else
terraform destroy # destroys every resource that is still in the state
The plan for the first step says what will happen:
# terraform_data.db will no longer be managed by Terraform, but will not be destroyed
# (destroy = false is set in the configuration)
Plan: 0 to add, 0 to change, 0 to destroy.
Warning: Some objects will no longer be managed by Terraform
After that, terraform destroy showed Plan: 0 to add, 0 to change, 2 to destroy. and the kept object was not part of it. To manage the kept object again, in this configuration or another one, write its resource block and an import block with its ID.
Before you run the destroy, check the dependencies:
- The kept resource may depend on resources you destroy. A database in a subnet that you destroy either blocks the destroy (AWS refuses to delete a subnet that is in use) or loses its network. Keep those resources too, or move the database first.
prevent_destroydoes not help here. It makes the wholeterraform destroyfail withInstance cannot be destroyed. It does not skip the resource.terraform state rmdoes the same as theremovedblock in one command, but nothing records it in Git. Use it only when you cannot change the code.- Terraform 1.16 and later also accept
destroy = falsein a resource's ownlifecycleblock. Terraform then removes that resource from the state instead of destroying it. Read your version's documentation before you rely on it, and check the plan.
If only a few resources must go, the reverse is simpler: terraform destroy -target=... for each of them, followed by removing their blocks from the code. The state management chapter of our Terraform guide covers removed blocks, and Terraform State: Remove, Move and Migrate Resources shows moving resources between state files.
Related Resources
- Terraform terminal simulator to practice
init,plan,applyandstatecommands. - Targeting and forcing replacement in our Terraform guide.
- Plan in pull requests, apply after review, a full CI workflow.
- How to Organize a Terraform Repository for the folder layout that
-chdirruns against.
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 SponsorFound an issue?
Related Posts
Also worth your time on this topic
Gate Your Terraform Plans: Rules Decide, the Model Explains
A pull request says "3 to add, 1 to change, 1 to destroy" and everyone approves it. This is a GitHub Action that reads the plan JSON, fails the job on selected changes that risk data loss or public exposure, and uses DigitalOcean inference only to write the comment. Measured against twenty labelled plans, including the four it misses.
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
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