Terraform Plan File: resource_changes, resource_drift, and Machine-Readable Output

2 min readCloud Infrastructure

terraform plan -out=plan.tfplan saves a binary plan file that terraform apply consumes without re-planning. Converting it to JSON (terraform show -json) exposes resource_changes, resource_drift, prior_state, and validation checks — useful for CI/CD policy gates, audit logging, and understanding why Terraform wants to make a change.

infrastructure-as-codeterraform

Saving and consuming plan files

# Save plan to binary file — guarantees apply uses exactly this plan
terraform plan -out=plan.tfplan

# Apply the saved plan (no re-plan, no prompts)
terraform apply plan.tfplan

# Convert to JSON for programmatic inspection
terraform show -json plan.tfplan > plan.json

Saving the plan as a binary file decouples the plan and apply steps. In CI/CD, plan in one job and apply in another — the apply job uses the exact plan reviewed in the PR without re-running plan (which could produce different results if infrastructure changed between jobs).

resource_drift shows infrastructure changes made outside Terraform — it's not in resource_changes

ConceptTerraform

resource_drift lists resources that were changed directly (console, CLI, API) rather than through Terraform. These changes aren't part of resource_changes (which only shows what Terraform will do). Drift is detected during plan by comparing the prior state against the current actual infrastructure. Terraform will overwrite drifted values when you apply — unless you use lifecycle { ignore_changes }.

Prerequisites

  • Terraform state
  • terraform plan basics

Key Points

  • resource_drift: changes that happened outside Terraform since the last apply.
  • resource_changes: changes Terraform will make during the next apply.
  • Drift is shown in terraform plan output with a ~ drift symbol.
  • errored: true means the plan failed — resource_changes may be incomplete.

JSON plan structure

{
  "format_version": "1.2",
  "terraform_version": "1.7.0",
  "variables": {},
  "planned_values": {
    "outputs": {},
    "root_module": {
      "resources": [],
      "child_modules": []
    }
  },
  "resource_drift": [],
  "resource_changes": [],
  "output_changes": {},
  "prior_state": {},
  "configuration": {},
  "relevant_attributes": [],
  "checks": [],
  "timestamp": "2025-02-21T05:09:55Z",
  "errored": false
}

1. format_version

Purpose: Specifies the schema version of the Terraform plan file format.
Example:

"format_version": "1.2",
  • A value like "1.2" indicates compatibility with Terraform's JSON schema version 1.2.

2. terraform_version

Purpose: The version of Terraform CLI used to generate the plan.
Example:

"terraform_version": "1.7.0"
  • "1.7.0" means the plan was created with Terraform v1.7.0.

3. variables

Purpose: Lists input variables and their values provided for the plan.
Example:

"variables": {
  "region": {
    "value": "us-east-1"
  },
  "instance_type": {
    "value": "t3.micro"
  }
}
  • If a variable region is set to "us-east-1", it appears here as "region": { "value": "us-east-1" }.

4. planned_values

Purpose: Describes the projected state of resources and outputs after applying changes.
Example:

"planned_values": {
  "root_module": {
    "resources": [
      {
        "address": "aws_instance.web",
        "type": "aws_instance",
        "values": {
          "ami": "ami-0c55b159cbfafe1f0",
          "instance_type": "t3.micro"
        }
      }
    ]
  }
}

Structure:

  • root_module.resources: Resources expected to exist post-apply (e.g., AWS instances, S3 buckets).
  • outputs: Output values like IP addresses or resource IDs.

5. resource_drift

Purpose: Detects infrastructure changes made outside Terraform (configuration drift).
Example:

"resource_drift": [
  {
    "address": "aws_instance.web",
    "change": {
      "before": { "tags": { "Name": "WebServer" } },
      "after": { "tags": { "Name": "OldWebServer" } },
      "actions": ["update"]
    }
  }
]
  • If an EC2 instance’s tags are manually modified, it shows the before and after states of the tags.

6. resource_changes

Purpose: Lists all changes Terraform will execute to reach the desired state.
Example:

"resource_changes": [
  {
    "address": "aws_s3_bucket.data",
    "change": {
      "actions": ["create"],
      "before": null,
      "after": { "bucket": "my-data-bucket" }
    }
  }
]

Key Details:

  • address: Resource identifier (e.g., aws_s3_bucket.data).
  • actions: Operations like create, update, or delete.
  • before/after: Previous and new configurations of the resource.

7. output_changes

Purpose: Tracks modifications to output values.
Example:

"output_changes": {
  "instance_ip": {
    "change": {
      "actions": ["create"],
      "before": null,
      "after": "192.168.1.1"
    }
  }
}
  • An output instance_ip changing from null to "192.168.1.1" is recorded here.

8. prior_state

Purpose: Represents the infrastructure state before this plan (mirrors terraform.tfstate).
Example:

"prior_state": {
  "values": {
    "root_module": {
      "resources": [
        {
          "address": "aws_instance.web",
          "type": "aws_instance",
          "values": { "ami": "ami-0c55b159cbfafe1f0" }
        }
      ]
    }
  }
}
  • Includes details like existing resources, their IDs, and attributes.

9. configuration

Purpose: Contains the full Terraform configuration (resources, providers, variables).
Example:

"configuration": {
  "provider_config": {
    "aws": {
      "name": "aws",
      "expressions": { "region": { "constant_value": "us-east-1" } }
    }
  },
  "root_module": {
    "resources": [
      {
        "type": "aws_instance",
        "name": "web"
      }
    ]
  }
}
  • Provider blocks (e.g., AWS region) and resource definitions (e.g., aws_instance.web).

10. relevant_attributes

Purpose: Attributes that triggered changes (e.g., forced resource updates).
Example:

"relevant_attributes": [
  {
    "resource": "aws_instance.web",
    "attribute": "ami"
  }
]
  • A changed ami value causing an EC2 instance replacement is listed here.

11. checks

Purpose: Results of custom validation checks (preconditions/postconditions).
Example:

"checks": [
  {
    "address": "check.s3_encryption",
    "status": "pass",
    "error_message": null
  }
]
  • A check enforcing S3 bucket encryption appears as "status": "pass" or "fail".

12. timestamp

Purpose: Timestamp (UTC) when the plan was generated.
Example:

  • "2025-02-21T05:09:55Z" indicates the exact creation time.

13. errored

Purpose: Indicates if the plan failed due to errors (e.g., invalid syntax).
Example:

  • true means Terraform encountered issues during planning.