Terraform Plan File: resource_changes, resource_drift, and Machine-Readable Output
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.
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
ConceptTerraformresource_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
regionis 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
beforeandafterstates 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 likecreate,update, ordelete.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_ipchanging fromnullto"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
amivalue 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:
truemeans Terraform encountered issues during planning.