terraform plan is the safety net of Terraform. Before making any infrastructure changes, it calculates exactly what would happen and shows you a precise diff, without actually doing anything.
terraform planThink of it as asking Terraform: "If I run apply right now, what changes will happen?" The answer is printed as an execution plan: a line-by-line breakdown of every resource that will be created, updated, replaced, or deleted.
Running plan is always safe. It has zero side effects. No API calls are made to create or destroy resources. You can run it as many times as you like.
The plan output uses symbols to communicate each action:
| Symbol | Meaning | Risk |
|---|---|---|
+ | Resource will be created | New infrastructure |
- | Resource will be destroyed | Permanent deletion |
~ | Resource will be updated in-place | Change without recreation |
-/+ | Resource will be destroyed then recreated | Downtime risk! |
<= | Data source will be read | No infrastructure change |
Terraform will perform the following actions:
# aws_instance.web will be created
+ resource "aws_instance" "web" {
+ ami = "ami-0c55b159cbfafe1f0"
+ instance_type = "t3.micro"
+ id = (known after apply)
+ public_ip = (known after apply)
+ vpc_security_group_ids = (known after apply)
}
# aws_s3_bucket.assets will be updated in-place
~ resource "aws_s3_bucket" "assets" {
~ tags = {
+ "Environment" = "production"
"Name" = "my-assets"
}
}
# aws_instance.old will be destroyed
- resource "aws_instance" "old" {
- ami = "ami-0abcdef1234567890" -> null
- instance_type = "t2.micro" -> null
- id = "i-0abc12345def67890" -> null
}
Plan: 1 to add, 1 to change, 1 to destroy.
The final line is the summary. Read it every time before approving.
The -/+ symbol (destroy then create) is the one to pay careful attention to:
# aws_instance.web must be replaced
-/+ resource "aws_instance" "web" {
~ id = "i-0abc123" -> (known after apply) # forces replacement
~ ami = "ami-0old" -> "ami-0new" # forces replacement
}
Some resource attributes, like ami on an EC2 instance, cannot be changed in-place. Changing them forces Terraform to delete the old resource and create a new one. This means downtime.
Rule: Whenever you see
-/+in your plan, pause and think about the impact. Can your application handle a restart? Is there a safer way to do this migration?
You can save the execution plan to a file:
terraform plan -out=tfplanThis is the recommended practice for production deployments. When you later run terraform apply tfplan, Terraform applies exactly what was planned, nothing more, nothing less. No interactive approval prompt is shown.
This is the standard CI/CD pattern:
terraform plan -out=tfplan and posts the output for reviewterraform apply tfplan, guaranteed to match what was reviewed# In CI/CD
terraform plan -input=false -out=tfplan
terraform apply -input=false tfplanThe plan file is binary. Do not commit it to Git, it may contain secrets. In CI/CD, pass it between pipeline stages as an artifact.
The -target flag limits the plan to only the specified resource (and its dependencies):
# Plan only the S3 bucket
terraform plan -target=aws_s3_bucket.assets
# Plan only a specific module
terraform plan -target=module.vpc-target is useful for debugging or making emergency changes to a single resource without touching the rest of the infrastructure.
Warning: Avoid
-targetin routine operations. It can leave your state out of sync with reality. Use it only for emergencies or debugging.
Drift happens when someone manually changes infrastructure (e.g., via the AWS console) without updating the Terraform config. When you run plan, Terraform compares your configuration against the real infrastructure (via the state file) and shows the drift:
# aws_security_group.web has been modified manually
~ resource "aws_security_group" "web" {
~ ingress = [
+ {
+ cidr_blocks = ["203.0.113.5/32"]
+ from_port = 22
+ to_port = 22
+ protocol = "tcp"
},
]
}
Plan: 0 to add, 1 to change, 0 to destroy.
This plan shows that someone opened port 22 manually. Running apply will remove that manual change and bring infrastructure back to the declared state. This is exactly the value of IaC, configuration is the source of truth.
In automated pipelines, use these flags:
terraform plan \
-input=false \ # Prevent interactive prompts
-no-color \ # Disable ANSI colors for cleaner logs
-out=tfplan # Save plan for later applyA minimal GitLab CI example:
plan:
stage: plan
script:
- terraform init -input=false
- terraform plan -input=false -no-color -out=tfplan
artifacts:
paths:
- tfplanError: No configuration files
Error: No configuration files
â Plan requires configuration to be present.
You are running plan in the wrong directory. cd to the directory containing your .tf files.
Error: Invalid reference
Error: Reference to undeclared resource
â on main.tf line 14
â A managed resource "aws_vpc" "main" has not been declared.
You referenced a resource (aws_vpc.main) that does not exist in your config. Check your resource names.
<MCQExercise exercise={{ id: 'tf-plan-q1', title: 'Question 1: Plan Symbols', description: 'You run terraform plan and see this in the output: "-/+ resource aws_instance.web (forces replacement)". What does this mean?', options: [ { id: 'a', text: 'The instance will be updated in-place with no downtime' }, { id: 'b', text: 'The instance will be destroyed and a new one created, potential downtime' }, { id: 'c', text: 'The instance name will be changed but nothing else' }, { id: 'd', text: 'Terraform is unsure what will happen and needs more input' }, ], correctId: 'b', explanation: 'The -/+ symbol means destroy then recreate. Terraform cannot update the attribute in-place (for example, changing the AMI on an EC2 instance). It must delete the existing resource and create a new one. This causes downtime for the resource. Always pause when you see -/+ and consider if there is a safer approach (e.g., blue/green deployment).', wrongExplanations: { a: 'In-place updates are shown with ~. The -/+ symbol specifically means the resource will be replaced (destroyed and recreated).', c: 'The -/+ symbol is about resource replacement, not renaming. Resource names in Terraform are just logical identifiers.', d: 'Terraform is certain about -/+ replacements. The "forces replacement" annotation tells you exactly which attribute change is causing it.', }, }} />
<MCQExercise exercise={{ id: 'tf-plan-q2', title: 'Question 2: terraform plan -out', description: 'Your team uses terraform plan -out=tfplan in CI, then terraform apply tfplan in a separate stage. Why is this the recommended production pattern?', options: [ { id: 'a', text: 'It makes apply run faster by caching the plan' }, { id: 'b', text: 'It guarantees apply makes exactly the changes that were reviewed, no surprises' }, { id: 'c', text: 'It is required by Terraform for apply to work in CI' }, { id: 'd', text: 'It prevents Terraform from making API calls during plan' }, ], correctId: 'b', explanation: 'Saving a plan with -out and then applying the saved plan guarantees the apply step executes exactly what was reviewed and approved. Without a saved plan, running terraform apply recalculates a fresh plan at apply time, which could differ if someone else made changes in the meantime. The -out pattern is the gold standard for safe production deployments.', wrongExplanations: { a: 'Speed is not the purpose. The plan file is used for correctness guarantees, not performance.', c: 'apply works in CI without -out, but it is not the recommended practice for production.', d: 'terraform plan always makes read API calls (to describe current infrastructure state). The -out flag does not change this.', }, }} />
<MCQExercise exercise={{ id: 'tf-plan-q3', title: 'Question 3: Drift Detection', description: 'A colleague manually added an ingress rule to a security group in the AWS console. You run terraform plan. What will happen?', options: [ { id: 'a', text: 'Terraform will not detect the change since it was done outside Terraform' }, { id: 'b', text: 'Terraform will show the manual change and plan to remove it on next apply' }, { id: 'c', text: 'Terraform will import the manual change into its configuration automatically' }, { id: 'd', text: 'Terraform will lock the security group to prevent further manual changes' }, ], correctId: 'b', explanation: 'terraform plan compares your declared configuration against the real current state of infrastructure. A manual change creates drift, a difference between what Terraform expects and what actually exists. Terraform will detect this drift and plan to revert the resource back to what is declared in your .tf files. This is one of the most powerful features of IaC: the configuration is always the source of truth.', wrongExplanations: { a: 'Terraform actively queries the cloud API during plan to get the real state. It will detect manual changes.', c: 'Terraform does not auto-import. Importing requires the explicit terraform import command.', d: 'Terraform has no ability to lock cloud resources against manual changes.', }, }} />
It means replace: Terraform will destroy the existing resource and create a new one, because the property you changed cannot be modified in place. On a stateless resource that is harmless. On a database, an EBS volume or anything holding data, it is data loss delivered by an apparently routine change. Terraform states the reason with a "forces replacement" annotation next to the offending attribute. Read every plan for this before applying to anything stateful.
Usually drift. Something was changed outside Terraform, in a console or by another tool, and Terraform intends to bring it back in line. It can also be a provider upgrade that computes a default differently, a value that genuinely changes on every run such as a timestamp, or a resource whose API returns attributes in a different order. Read which attribute is changing: drift from a console edit is obvious, whereas a perpetual diff on the same attribute every run usually means a provider quirk that needs ignore_changes.
terraform plan -out=tfplan writes the plan to a file, and terraform apply tfplan applies exactly that plan without recalculating. This matters in CI and for anything reviewed by a human, because it guarantees what was approved is what gets applied, without it, the state or the real infrastructure could change between the plan and the apply. The caveat is that a saved plan contains sensitive values in plain text, so treat it as a secret and do not archive it carelessly.
Sparingly, and as a recovery tool rather than a habit. -target limits the plan to specific resources, which is genuinely useful when untangling a partially failed apply or working around a dependency problem. It is not a normal workflow: Terraform warns about it because operating on a subset means the rest of your configuration is not validated against reality, and it is easy to leave infrastructure in a state that does not match the code. If you reach for it routinely, the configuration probably wants splitting up.
Run terraform plan -input=false -out=tfplan on pull requests and post the output as a comment so the change is reviewable, then apply the saved plan file on merge. Use -detailed-exitcode, which returns 0 for no changes, 2 for changes present and 1 for an error, that lets a pipeline distinguish "nothing to do" from "changes await approval" and act accordingly. Never let apply run automatically from a feature branch against shared infrastructure.
Use terraform apply safely: review the plan, apply a saved plan file, tune parallelism, use -auto-approve in CI/CD and recover from common apply errors.
Run terraform destroy safely: read the destroy plan, preview it first, remove single resources with -target, and keep code tidy with fmt and validate.
See how data flows through Terraform: input variables, locals and outputs, where each belongs, how variable precedence works and how to handle secrets safely.