11  Reading a Terraform plan

Before changes are applied to a cloud environment, Terraform produces a plan — a description of exactly what it intends to do. Reading this plan is one of the most important review skills you can develop. It tells you what will actually happen, not just what the code says.

This chapter explains how to read and interpret a terraform plan output.

11.1 What a plan is

When you run terraform plan, Terraform:

  1. Reads your configuration files
  2. Queries the current state (what it believes exists in your cloud environment)
  3. Optionally refreshes state by querying the cloud provider APIs
  4. Computes the difference between desired state (your code) and current state
  5. Outputs the changes it would make if you ran terraform apply

The plan does not make any changes. It only tells you what would happen.

11.2 How to read the symbols

Terraform uses symbols to indicate the type of change for each resource:

Symbol Meaning
+ Create — a new resource will be added
- Destroy — an existing resource will be removed
~ Update in place — an existing resource will be modified without recreation
-/+ Replace — the resource will be destroyed and recreated
<= Read — a data source will be read from the provider

The most important to watch for are - (destroy) and -/+ (replace), because these can cause service disruption or data loss.

11.3 A simple example

Terraform will perform the following actions:

  # google_storage_bucket.pipeline_outputs will be created
  + resource "google_storage_bucket" "pipeline_outputs" {
      + id                          = (known after apply)
      + location                    = "EUROPE-WEST2"
      + name                        = "my-project-pipeline-outputs"
      + project                     = "my-project-12345"
      + uniform_bucket_level_access = true

      + lifecycle_rule {
          + action {
              + type = "Delete"
            }
          + condition {
              + age = 90
            }
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

This plan creates one resource and destroys nothing. The + on every line shows that all of these are new attributes on a new resource. (known after apply) means the value will be assigned by the cloud provider when the resource is created — this is normal for things like generated IDs and self-links.

11.4 An update in place

  # google_storage_bucket.pipeline_outputs will be updated in-place
  ~ resource "google_storage_bucket" "pipeline_outputs" {
        id                          = "my-project-pipeline-outputs"
      ~ labels                      = {
          + "env"  = "sandbox"
            # (1 unchanged element hidden)
        }
        name                        = "my-project-pipeline-outputs"
        # (6 unchanged attributes hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

The ~ on the resource and the + on the label show that a new label is being added to an existing bucket. Unchanged attributes are hidden by default for readability. The bucket itself will not be deleted or recreated.

11.5 A replacement — the most important case

  # google_sql_database_instance.main must be replaced
-/+ resource "google_sql_database_instance" "main" {
      ~ connection_name             = "my-project:europe-west2:main" -> (known after apply)
      - database_version            = "POSTGRES_15" -> null # forces replacement
      + database_version            = "POSTGRES_16"
        name                        = "main"
        # (10 unchanged attributes hidden)
    }

Plan: 1 to add, 0 to change, 1 to destroy.

The -/+ symbol means the database instance will be destroyed and recreated. This is because database_version is an immutable attribute — you cannot change it on a running Cloud SQL instance; Terraform must create a new one.

The comment # forces replacement tells you which attribute triggered the replacement.

A replacement of a database, storage bucket, or any other stateful resource can cause data loss. Data stored in the resource may not survive the destroy-and-recreate cycle unless you have taken explicit steps to preserve it (such as a database backup or a bucket migration).

Before approving a plan that shows a -/+ on a stateful resource, make sure you understand why the replacement is happening and what will happen to the data.

11.6 The summary line

The last line of a plan always shows a summary:

Plan: 2 to add, 1 to change, 0 to destroy.

Read this first. If it says anything other than 0 to destroy and you did not expect any destruction, that is your first signal to read the plan carefully.

11.7 Dangerous patterns to look for

Unexpected resource replacement

-/+ resource "google_compute_instance" "worker" {

Ask: why is this being replaced? Is it expected? What happens to the data or workload on this instance during the replacement?

Destruction of a resource with no replacement

  # google_storage_bucket.archive will be destroyed
  - resource "google_storage_bucket" "archive" {

Ask: is this bucket expected to be removed? If it contains data, where does that data go? Is prevent_destroy = true missing from this resource?

A large number of resources affected

Plan: 47 to add, 12 to change, 8 to destroy.

A plan with many changes is harder to review. Ask the author whether this is expected, and consider whether the change can be broken into smaller, safer steps.

IAM changes

  # google_project_iam_binding.data_access will be updated in-place
  ~ resource "google_project_iam_binding" "data_access" {
      ~ members = [
          - "serviceAccount:old-service@my-project.iam.gserviceaccount.com",
          + "serviceAccount:new-service@my-project.iam.gserviceaccount.com",
        ]
        role    = "roles/bigquery.dataViewer"
    }

An IAM change that removes a member (-) means someone or something will lose access. Make sure this is intentional.

google_project_iam_binding replacing all members

  ~ resource "google_project_iam_binding" "editors" {
      ~ members = [
          - "serviceAccount:service-a@my-project.iam.gserviceaccount.com",
          - "serviceAccount:service-b@my-project.iam.gserviceaccount.com",
          + "serviceAccount:service-c@my-project.iam.gserviceaccount.com",
        ]

This is an authoritative binding. It will remove service-a and service-b from the role entirely. If those service accounts are still running workloads, those workloads will break. Check whether this is intentional.

11.8 Attaching the plan to a PR

For any non-sandbox environment, the plan output should be attached to or linked from the pull request. The author should run:

terraform plan -out=tfplan
terraform show -no-color tfplan > plan.txt

and attach plan.txt, or paste the relevant sections into the PR description.

Some CI pipelines post the plan output as a PR comment automatically using tools like Atlantis or Terraform Cloud. If your team uses one of these, the plan will appear in the PR without the author needing to do anything extra.