Planreader

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

SymbolThe comment above itWhat happens
+will be createdA new object is created.
-will be destroyedThe object is destroyed. A second comment often says why, such as (because aws_sqs_queue.jobs is not in configuration).
~will be updated in-placeThe existing object is changed. It keeps its ID.
-/+must be replacedThe 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 replacedThe new object is created first, then the old one is destroyed. You get this order from create_before_destroy.
<=will be read during applyA data source can't be read until something else in the plan exists, so it's read during apply instead of now.
 has moved toOnly the address changes, usually because of a moved block. When the object also changes, you see ~ with # (moved from …) under the comment.
 will be importedAn existing object is brought under management by an import block. Nothing is created.
 will no longer be managedA removed block takes it out of the state. The real object is left alone.

Inside a resource block

You seeIt 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" -> nullThe 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 replacementThis 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.
<<-EOTA 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.