Create a Terraform plan
The core Terraform workflow consists of three main steps after you have written your Terraform configuration:
- Initialize prepares your workspace so Terraform can apply your configuration.
- Plan allows you to preview the changes Terraform will make before you apply them.
- Apply makes the changes defined by your plan to create, update, or destroy resources.
When you provision infrastructure, Terraform creates an execution plan before it
applies any changes. Terraform creates the plan by comparing your Terraform
configuration to the state of your infrastructure. The execution plan consists
of a set of changes that create, update, or destroy resources. You can use the
terraform plan
command to compare your configuration to your resource's state,
review changes before you apply them, or to refresh your workspace's state.
Terraform plan supports automation workflows in CI/CD pipelines by guaranteeing
that the infrastructure changes Terraform applies match the ones you or your
team approve, even if the deploy process completes across different machines or
at different times.
In this tutorial, you will review how Terraform generates an execution plan,
what the plan contains, and the role of the terraform plan
command in your
Terraform workflow. To do so, you will create and apply a saved Terraform plan,
review its contents, and analyze how a plan reflects changes to your
configuration. You will also learn how to target specific resources when you
create a Terraform plan.
Prerequisites
You can complete this tutorial using the same workflow with either Terraform Community Edition or HCP Terraform. HCP Terraform is a platform that you can use to manage and execute your Terraform projects. It includes features like remote state and execution, structured plan output, workspace resource summaries, and more.
Select the HCP Terraform tab to complete this tutorial using HCP Terraform.
This tutorial assumes that you are familiar with the Terraform workflow. If you are new to Terraform, complete the Get Started tutorials first.
In order to complete this tutorial, you will need the following:
- Terraform v1.6+ installed locally.
- An AWS account with local credentials configured for use with Terraform.
- The jq command line utility.
Clone the example repository
In your terminal, clone the learn-terraform-plan
repository.
$ git clone https://github.com/hashicorp-education/learn-terraform-plan
Navigate to the cloned repository.
$ cd learn-terraform-plan
Review configuration
The example configuration in this repository creates an EC2 instance through
resources and local and public modules. The modules/aws-ec2-instance
subdirectory contains the local module used to create the instance.
$ tree.├── LICENSE├── README.md├── main.tf├── modules│ └── aws-ec2-instance│ ├── main.tf│ └── variables.tf├── terraform.tf└── variables.tf
Terraform uses the provider versions specified in the terraform.tf
file.
terraform.tf
terraform { required_version = "~> 1.6" required_providers { aws = { source = "hashicorp/aws" version = "5.7.0" } random = { source = "hashicorp/random" version = "3.5.1" } }## ...}
Open the top-level main.tf
file. This configuration uses the aws
provider to
create an EC2 instance using an Ubuntu AMI.
main.tf
provider "aws" { region = var.region}provider "random" {}data "aws_ami" "ubuntu" { most_recent = true filter { name = "name" values = ["ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-*"] } filter { name = "virtualization-type" values = ["hvm"] } owners = ["099720109477"] # Canonical}resource "random_pet" "instance" { length = 2}module "ec2-instance" { source = "./modules/aws-ec2-instance" ami_id = data.aws_ami.ubuntu.id instance_name = random_pet.instance.id}
This configuration uses the random_pet
resource to generate a name for your
instance. The module.ec2-instance
block uses the local aws-ec2-instance
module to define your instance.
The configuration also passes the random pet name to the hello
module, which
will generate outputs with the random pet name.
main.tf
module "hello" { source = "joatmon08/hello/random" version = "6.0.0" hellos = { hello = random_pet.dog.id second_hello = "World" } some_key = "secret"}
Initialize your configuration
In order to generate your execution plan, Terraform needs to install the providers and modules referenced by your configuration. Then, it will reference them to create your plan.
Initialize the Terraform configuration with terraform init
.
$ terraform initInitializing the backend...Initializing modules...- ec2-instance in modules/aws-ec2-instanceDownloading registry.terraform.io/joatmon08/hello/random 6.0.0 for hello...- hello in .terraform/modules/helloInitializing provider plugins...- Reusing previous version of hashicorp/random from the dependency lock file- Reusing previous version of hashicorp/aws from the dependency lock file- Installing hashicorp/aws v5.7.0...- Installed hashicorp/aws v5.7.0 (signed by HashiCorp)- Installing hashicorp/random v3.5.1...- Installed hashicorp/random v3.5.1 (signed by HashiCorp)Terraform has been successfully initialized!You may now begin working with Terraform. Try running "terraform plan" to seeany changes that are required for your infrastructure. All Terraform commandsshould now work.If you ever set or change modules or backend configuration for Terraform,rerun this command to reinitialize your working directory. If you forget, othercommands will detect it and remind you to do so if necessary.
Create a plan
There are three commands that tell Terraform to generate an execution plan:
The
terraform plan
command creates a plan consisting of a set of changes that will make your resources match your configuration. This lets you preview the actions Terraform would take to modify your infrastructure before applying them. Terraform plan does not make any changes to your resources, you must apply a plan for Terraform to make changes.You can also save a plan with the
-out
flag. Later, you can apply the saved plan, and Terraform will only perform the changes listed in the plan. In an automated Terraform pipeline, applying a saved plan file ensures that Terraform only makes the changes you expect, even if your pipeline runs across multiple machines at different times.The
terraform apply
command applies a Terraform plan. If you do not pass a saved plan, then Terraform will a create a plan and prompt you for approval before applying the plan.The
terraform destroy
command creates an execution plan to delete all of the resources managed by your workspace.
Generate a saved plan with the -out
flag. You will review and apply this plan
later in this tutorial.
$ terraform plan -out "tfplan"data.aws_ami.ubuntu: Reading...data.aws_ami.ubuntu: Read complete after 0s [id=ami-055744c75048d8296]Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + createTerraform will perform the following actions: # random_pet.instance will be created + resource "random_pet" "instance" { + id = (known after apply) + length = 2 + separator = "-" } # module.ec2-instance.aws_instance.main will be created + resource "aws_instance" "main" {## ...Plan: 4 to add, 0 to change, 0 to destroy.───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────Saved the plan to: tfplanTo perform exactly these actions, run the following command to apply: terraform apply "tfplan"
Terraform created a plan and saved it in the tfplan
file.
Since you have not yet applied this configuration, Terraform plans to create all of the resources defined in it.
When you create a plan, Terraform checks your workspace for an existing state file. Since you have not yet applied this configuration, your workspace's state is empty, and Terraform plans to create all of the resources defined in your configuration.
You can apply the saved plan file to execute these changes, but the contents of
the plan are not in a human-readable format. Use the terraform show
command to
print out the saved plan.
$ terraform show "tfplan"Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + createTerraform will perform the following actions: # random_pet.instance will be created + resource "random_pet" "instance" { + id = (known after apply) + length = 2 + separator = "-" }## ... # module.hello.random_pet.server will be created + resource "random_pet" "server" { + id = (known after apply) + keepers = { + "hello" = (known after apply) + "secret_key" = "secret" } + length = 2 + separator = "-" }Plan: 4 to add, 0 to change, 0 to destroy.
This is the same output that Terraform printed when you created your saved plan. You can review this plan output with your team before you apply the saved plan to ensure that the changes are the ones you expect.
Terraform can also report the contents of the saved plan as JSON. This is often useful when using Terraform in automated pipelines, as you can use code to inspect the plan.
Convert the saved plan into JSON, pass it to jq
to format it, and save the
output into a new file.
$ terraform show -json "tfplan" | jq > tfplan.json
Warning
Terraform plan files can contain sensitive data. Never commit a plan file to version control, whether as a binary or in JSON format.
Review the plan
In this section, you will review the data Terraform captures about your
resources in a plan file. Use the jq
command to query the JSON formatted
version of your plan.
Terraform records the version of Terraform used to generate the plan, and the version of the plan file format. This will ensure that you use the same version to apply these changes when you use the saved plan.
$ jq '.terraform_version, .format_version' tfplan.json"1.6.0""1.2"
Review plan configuration
The .configuration
JSON object is a snapshot of your configuration at the time of
the terraform plan
.
This configuration snapshot captures the versions of the providers recorded in
your .terraform.lock.hcl
file, ensuring that you use the same provider
versions that generated the plan to apply it. Note that the configuration
accounts for both the provider version used by the root module and child
modules.
$ jq '.configuration.provider_config' tfplan.json{ "aws": { "name": "aws", "full_name": "registry.terraform.io/hashicorp/aws", "version_constraint": "5.7.0", "expressions": { "region": { "references": [ "var.region" ] } } }, "random": { "name": "random", "full_name": "registry.terraform.io/hashicorp/random", "version_constraint": "3.5.1" }}
The configuration
section further organizes your resources defined in your top
level root_module
.
$ jq '.configuration.root_module.resources' tfplan.json[ { "address": "random_pet.instance", "mode": "managed", "type": "random_pet", "name": "instance", "provider_config_key": "random", "expressions": { "length": { "constant_value": 2 } }, "schema_version": 0 },## ...
The module_calls
section contains the details of the modules used, their input
variables and outputs, and the resources to create.
$ jq '.configuration.root_module.module_calls' tfplan.json{ "ec2-instance": { "source": "./modules/aws-ec2-instance", "expressions": { "ami_id": { "references": [ "data.aws_ami.ubuntu.id", "data.aws_ami.ubuntu" ] }, "instance_name": { "references": [ "random_pet.instance.id", "random_pet.instance" ] } },## ...
The configuration
object also records any references to other resources in a
resource's written configuration, which helps Terraform determine the correct
order of operations when it applies your plan.
$ jq '.configuration.root_module.module_calls.hello.expressions.hellos.references' tfplan.json[ "random_pet.instance.id", "random_pet.instance"]
Review planned resource changes
Review the planned resources changes to the aws_instance
resource from the
ec2-instance
local module.
The representation includes:
- The
action
field captures the action taken for this resource, in this casecreate
. - The
before
field captures the resource state prior to the run. In this case, the value isnull
because the resource does not yet exist. - The
after
field captures the state to define for the resource. - The
after_unknown
field captures the list of values that will be computed or determined through the operation and sets them totrue
. - The
before_sensitive
andafter_sensitive
fields capture a list of any values markedsensitive
. Terraform will use these lists to determine which output values to redact when you apply your configuration.
$ jq '.resource_changes[] | select( .address == "module.ec2-instance.aws_instance.main")' tfplan.json{ "address": "module.ec2-instance.aws_instance.main", "module_address": "module.ec2-instance", "mode": "managed", "type": "aws_instance", "name": "main", "provider_name": "registry.terraform.io/hashicorp/aws", "change": { "actions": [ "create" ], "before": null, "after": { "ami": "ami-055744c75048d8296", "credit_specification": [], "get_password_data": false, "hibernation": null, "instance_type": "t2.micro", "launch_template": [], "source_dest_check": true, "timeouts": null, "user_data_replace_on_change": false, "volume_tags": null }, "after_unknown": { "arn": true, "associate_public_ip_address": true, "availability_zone": true,## ... } }}
Review planned values
The planned_values
object is a report of the differences between the "before"
and "after" values of your resources, showing you the planned outcome for a run
that would use this plan file.
In this example, the module.ec2-instance.aws_instance
resource includes the
address that you will use to reference the resource in your Terraform
configuration, the provider name, and the values of all of the attributes as one
object. This format resolves the differences between the prior and expected
state in one object to demonstrate the planned outcomes for the configuration,
which is easier to use for any downstream consumers of the plan data. For
example, the Terraform Sentinel CLI tests policies against the planned outcomes
recorded here. The cost estimation feature in HCP Terraform also relies on the
planned_values
data to determine changes to your infrastructure spend.
$ jq '.planned_values' tfplan.json{ "root_module": { "resources": [ { "address": "random_pet.instance",## ... } ], "child_modules": [ { "resources": [ { "address": "module.ec2-instance.aws_instance.main", "mode": "managed", "type": "aws_instance", "name": "main", "provider_name": "registry.terraform.io/hashicorp/aws", "schema_version": 1, "values": { "ami": "ami-055744c75048d8296", "credit_specification": [], "get_password_data": false,## ...
Apply a saved plan
In your terminal, apply your saved plan.
Note
When you apply a saved plan file, Terraform will not prompt you for approval and instead immediately execute the changes. This workflow is primarily used in automation.
$ terraform apply "tfplan"random_pet.instance: Creating...random_pet.instance: Creation complete after 0s [id=apt-zebra]module.hello.random_pet.number_2: Creating...module.hello.random_pet.server: Creating...module.hello.random_pet.number_2: Creation complete after 0s [id=sweet-kid]module.hello.random_pet.server: Creation complete after 0s [id=more-swan]module.ec2-instance.aws_instance.main: Creating...module.ec2-instance.aws_instance.main: Still creating... [10s elapsed]module.ec2-instance.aws_instance.main: Still creating... [20s elapsed]module.ec2-instance.aws_instance.main: Still creating... [30s elapsed]module.ec2-instance.aws_instance.main: Creation complete after 32s [id=i-04107c0289b72e9c1]Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
Terraform applied your changes according to the saved plan.
Modify configuration
Input variables let you easily update configuration values without having to edit your configuration files.
Open the variables.tf
file in the top-level configuration directory. Add the
configuration below to define a new input variable to use for the hello
module.
variables.tf
variable "secret_key" { type = string sensitive = true description = "Secret key for hello module"}
Then, create a terraform.tfvars
file, and set the new secret_key
input
variable value.
terraform.tfvars
secret_key = "TOPSECRET"
Warning
Never commit .tfvars
files to version control.
Finally, update the hello
module configuration in main.tf
to reference the
new input variable.
main.tf
module "hello" { source = "joatmon08/hello/random" version = "6.0.0" hellos = { hello = random_pet.instance.id second_hello = "World" } some_key = var.secret_key}
Create a new plan
Create a new Terraform plan and save it as tfplan-input-var
.
$ terraform plan -out "tfplan-input-var"random_pet.instance: Refreshing state... [id=apt-zebra]module.hello.random_pet.number_2: Refreshing state... [id=sweet-kid]module.hello.random_pet.server: Refreshing state... [id=more-swan]data.aws_ami.ubuntu: Reading...data.aws_ami.ubuntu: Read complete after 0s [id=ami-055744c75048d8296]module.ec2-instance.aws_instance.main: Refreshing state... [id=i-04107c0289b72e9c1]Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:-/+ destroy and then create replacementTerraform will perform the following actions: # module.hello.random_pet.server must be replaced-/+ resource "random_pet" "server" { ~ id = "more-swan" -> (known after apply) ~ keepers = { # forces replacement # Warning: this attribute value will be marked as sensitive and will not # display in UI output after applying this change. ~ "secret_key" = (sensitive value) # (1 unchanged element hidden) } # (2 unchanged attributes hidden) }Plan: 1 to add, 0 to change, 1 to destroy.───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────Saved the plan to: tfplan-input-varTo perform exactly these actions, run the following command to apply: terraform apply "tfplan-input-var"
Convert the new plan file into a machine-readable JSON format.
$ terraform show -json tfplan-input-var | jq > tfplan-input-var.json
Review new plan
When you created this plan, Terraform determined that the working directory already contains a state file, and used that state to plan the resource changes.
Since Terraform created this plan with existing resources and using input variables, your plan file has some new fields.
Review plan input variables
Now that you have defined input variables, Terraform captures them in the plan file as well.
$ jq '.variables' tfplan-input-var.json{ "project_name": { "value": "terraform-plan" }, "region": { "value": "us-east-1" }, "secret_key": { "value": "TOPSECRET" }}
Warning
Although you marked the input variable as sensitive
, Terraform still stores
the value in plaintext in the plan file. Since Terraform plan files can
contain sensitive information, you should keep them secure and never commit
them to version control.
Unlike input variables, Terraform does not record the values of any environment variables used for your configuration in your plan files. Using environment variables is one of the recommended ways to pass sensitive values, such as provider credentials, to Terraform.
Review plan prior_state
When you created this plan, Terraform determined that the working directory
already contains a state file, and used that state to plan the resource changes.
Unlike the first run's plan file, this file now contains a prior_state
object,
which captures the state file exactly as it was prior to the plan action.
$ jq '.prior_state' tfplan-input-var.json{ "format_version": "1.0", "terraform_version": "1.6.0", "values": { "root_module": { "resources": [ { "address": "data.aws_ami.ubuntu", "mode": "data", "type": "aws_ami", "name": "ubuntu", "provider_name": "registry.terraform.io/hashicorp/aws", "schema_version": 0,## ... } ] } }}
Review plan resource changes
Now that your state file tracks resources, Terraform will take the existing
state into consideration when it creates an execution plan. For example, the
module.hello.random_pet.server
object now contains data in both the before
and after
fields, representing the prior and desired configurations
respectively.
$ jq '.resource_changes[] | select( .address == "module.hello.random_pet.server")' tfplan-input-var.json{ "address": "module.hello.random_pet.server", "module_address": "module.hello", "mode": "managed", "type": "random_pet", "name": "server", "provider_name": "registry.terraform.io/hashicorp/random", "change": { "actions": [ "delete", "create" ], "before": { "id": "tidy-crawdad", ## ... "after": { "keepers": { "hello": "fun-possum", "secret_key": "TOPSECRET" },## ... "action_reason": "replace_because_cannot_update"}
Notice that the actions
list is now set to ["delete","create"]
and that the
action_reason
is "replace_because_cannot_update"
- the change to the
secret_key
for the resource is destructive, so Terraform must both delete and
create this resource. Terraform determines whether it can update a resource in
place or must recreate it based on which provider attributes you changed.
Once you have created resources in the working directory, Terraform uses the prior state, the data returned by a refresh operation, and the written configuration to determine the changes to make. Terraform supports additional flags that you can use to modify how it constructs an execution plan. For example, you can create a plan that only refreshes the state file without modifying resource configuration, or target only specific resources for either update or replacement.
Clean up infrastructure
Now that you have completed this tutorial, destroy the resources created before moving on. Create and apply a destroy plan.
$ terraform plan -destroy -out "tfplan-destroy"## ...Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: - destroyTerraform will perform the following actions: # random_pet.instance will be destroyed - resource "random_pet" "instance" {## ...Plan: 0 to add, 0 to change, 4 to destroy.───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────Saved the plan to: tfplan-destroyTo perform exactly these actions, run the following command to apply: terraform apply "tfplan-destroy"
When you use the -destroy
flag, Terraform creates a plan to destroy all of the
resources in the configuration. Apply the plan to destroy your resources.
$ terraform apply "tfplan-destroy"## ...Terraform used the selected providers to generate the following execution plan.Resource actions are indicated with the following symbols: - destroyTerraform will perform the following actions: # random_pet.instance will be destroyed - resource "random_pet" "instance" {## ...Plan: 0 to add, 0 to change, 4 to destroy.Do you really want to destroy all resources? Terraform will destroy all your managed infrastructure, as shown above. There is no undo. Only 'yes' will be accepted to confirm. Enter a value: yes## ...Destroy complete! Resources: 4 destroyed.
The terraform destroy
command is a shortcut that creates a destroy plan and
then waits for you to approve it. Saving a destroy plan allows you to review it
before applying, just like a regular saved plan.
Next steps
In this tutorial, you reviewed how Terraform constructs an execution plan and
uses saved plans. You also explored the relationship of the terraform plan
and
terraform apply
commands.
Check out the following tutorials to try out some of the available plan modes and options:
Learn how to use a refresh-only plan to update your state file with the actual configuration of your resources.
Learn how to use the
-replace
flag to recreate resources.Learn more about using resource targeting to scope the resources affected by Terraform operations.
Review the JSON output format documentation for more detail on the contents and format of a plan file.