What the symbols in a Terraform plan mean
Every resource in terraform plan output starts with a comment that says what will happen to it, followed by its resource block with a symbol in front. The same symbols then mark each attribute inside the block. OpenTofu prints the same symbols and wording.
Paste a plan and see it explained
The symbol in front of a resource
| Symbol | The comment above it | What happens |
|---|---|---|
+ | will be created | A new object is created. |
- | will be destroyed | The object is destroyed. A second comment often says why, such as (because aws_sqs_queue.jobs is not in configuration). |
~ | will be updated in-place | The existing object is changed. It keeps its ID. |
-/+ | must be replaced | The old object is destroyed, then a new one is created. There's a gap in between, and anything stored in the old one is gone unless you move it. More on replacement. |
+/- | must be replaced | The new object is created first, then the old one is destroyed. You get this order from create_before_destroy. |
<= | will be read during apply | A data source can't be read until something else in the plan exists, so it's read during apply instead of now. |
| has moved to | Only the address changes, usually because of a moved block. When the object also changes, you see ~ with # (moved from …) under the comment. |
| will be imported | An existing object is brought under management by an import block. Nothing is created. |
| will no longer be managed | A removed block takes it out of the state. The real object is left alone. |
Inside a resource block
| You see | It means |
|---|---|
~ instance_type = "t3.small" -> "t3.medium" | The value changes from the left to the right. |
+ name = "invented-jobs-dlq" | A value that wasn't set before. |
- name = "id" -> null | The value is removed. On a destroyed resource every line ends like this. |
(known after apply) | The provider works the value out while applying, so the plan can't show it. On a replacement, IDs, ARNs and addresses all show this because the object is new. A whole nested block can show it too: ~ metadata_options (known after apply). |
# forces replacement | This attribute is why the resource is replaced. Revert it and the replacement usually goes away. |
(sensitive value) | The value is marked sensitive, by a variable or by the provider, so the text hides it. The saved plan file still holds it in cleartext. |
jsonencode( | The attribute is a JSON string, such as an IAM policy, and Terraform compares it by structure. Only the parts that change carry a symbol. |
# (15 unchanged attributes hidden) | Lines that don't change are left out to keep the plan short. # (2 unchanged blocks hidden) and # (2 unchanged elements hidden) do the same for blocks, and for items in lists and maps. |
<<-EOT | A multi-line string, such as a script, shown between <<-EOT and EOT. |
A real example
From a plan made with Terraform 1.9.8 and the AWS provider, trimmed. The names are invented. Changing a DynamoDB table's hash_key can't be done in place, so the table is rebuilt, old one first:
# module.data.aws_dynamodb_table.sessions must be replaced
-/+ resource "aws_dynamodb_table" "sessions" {
~ arn = "arn:aws:dynamodb:…:invented-sessions" -> (known after apply)
~ hash_key = "id" -> "session_id" # forces replacement
~ id = "invented-sessions" -> (known after apply)
name = "invented-sessions"
# (1 unchanged attribute hidden)
- attribute {
- name = "id" -> null
- type = "S" -> null
}
+ attribute {
+ name = "session_id"
+ type = "S"
}
~ point_in_time_recovery (known after apply)
}
The table holds data, and -/+ means it's destroyed before the new one exists. That's the line in this plan to stop at.
The summary line
Plan: 5 to add, 6 to change, 3 to destroy.
A replacement counts twice: once in "to add" and once in "to destroy". This line came from a plan with 3 creates, 6 updates, 1 destroy and 2 replacements. So "3 to destroy" doesn't mean three objects disappear for good. It means three old objects go, and two of them come back as new ones.
Planreader separates them for you, ranks whatever looks like it holds data first, and checks its count against this line. Paste a plan to try it. It runs in your browser and the page can't send your plan anywhere.