Infrastructure as Code (IaC) is a key attribute of enabling best practices in any organization practicing a DevOps culture. The ability to build up and tear down infrastructure via code has many benefits, like reducing cost, increasing deployment speed, and lowering risk through consistency. There is, however, a dark side to IaC. If not used cautiously and carefully, it is way too easy to cause harm to an environment.
Terraform plan is a Terraform CLI command that previews the changes that will be made to the infrastructure based on the current code configuration. It generates and displays an execution plan, detailing which actions will be taken on which resources, allowing for a review before actual application.
This step is crucial for understanding the potential impact of changes and ensuring that they align with intentions, thereby preventing unintended modifications. With terraform plan
, you will always have a summary at the end, in which you observe the number of resources that will be created/modified/destroyed.
Note: New versions of Terraform are placed under the BUSL license, but everything created before version 1.5.x stays open-source. OpenTofu is an open-source version of Terraform that expands on Terraform’s existing concepts and offerings. It is a viable alternative to HashiCorp’s Terraform, being forked from Terraform version 1.5.6.
The terraform plan
command does three things:
- Ensures the state is up to date by reading the current state of any already-existing remote infrastructure.
- Determines the deltas between the current configuration and the prior state data.
- Proposes a series of changes that will make the remote infrastructure match the current configuration.
If there aren’t any deltas, the output will report that no actions need to be taken.
Let’s look at the output resulting from executing plan
with a simple Terraform configuration script.
1. Prepare the configuration script
For this walkthrough, we’ll use a new Azure subscription that does not contain any resources. The listing for the configuration script is below.
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "=3.0.0"
}
}
}
provider "azurerm" {
features { }
}
resource "azurerm_resource_group" "tf-plan" {
name = "rg-tf-plan-example-centralus"
location = "centralus"
}
2. Run terraform plan
Run the following command on the command line to generate an execution plan.
Before doing so, be sure to set environment variables for your Azure Subscription Id and Tenant Id, using the subscription id and tenant id from your Azure subscription, respectively. For more details on the different ways, you can configure the AzureRM Terraform provider, see here.
terraform plan
3. Observe the output
The output contains a wealth of information, which is highlighted in the image above.
Terraform proposed a create action, as indicated by the green plus sign. Changes and deletions appear as yellow tilda and red minus sign, respectively. Terraform is also proposing to create a new resource group named “rg-tf-plan-example-centralus.”
The execution plan includes a summary count of each action in the proposal. In this case, there is one add, zero change, and zero destroy, actions.
4. Apply the changes
Apply the proposed changes by running the apply
command (make sure you confirm the actions by typing yes
when prompted):
terraform apply
In addition to creating the resource group, Terraform also updated the local state data. Terraform uses state data to remember the remote infrastructure configurations and create deltas when configurations change.
5. Make a change
Make the following change to the sample Terraform script. Add the following line to the end of the script, just before the closing curly brace:
tags = {name: "test"}
The resource group resource should now look like this:
resource "azurerm_resource_group" "tf-plan" {
name = "rg-tf-plan-example-centralus"
location = "centralus"
tags = {name: "test"}
}
6. Re-run terraform plan
Re-run the plan
command:
terraform plan
7. Observe the output again
When the plan
command executed, Terraform compared the stored state data for the rg-tf-plan-example-centralus resource to the changes made in the configuration script.
Looking at the output, you can see Terraform determined an update was necessary. The output also shows the proposed changes, including the actual change itself: the addition of the name
tag.
It’s also worth mentioning that with Spacelift you can use plan policies to make automated decisions based on Terraform plans. Plan policies are evaluated during a planning phase after a vendor-specific change preview command (e.g. terraform plan
) executes successfully. The body of the change is exported to JSON, and parts of it are combined with Spacelift metadata to form the data input to the policy.
Terraform doesn’t have any planning modes per se, but you can use different flags to achieve different behaviors.
Normal mode
The normal mode is running terraform plan
without any flags, which will display the actions Terraform will take based on the configuration files in that directory, without applying any changes.
You can also use plan with the -destroy
or -refresh-only
options, and all of these “modes” are mutually exclusive – this means that you cannot use more than one mode at a time.
Destroy mode
Destroy mode creates a plan which shows you all the remote objects that will be removed. Running the command in destroy mode does not actually perform any actions. A great use case for destroy mode is to delete a temporary development environment.
You activate destroy mode by including the -destroy
option on the command line:
terraform plan -destroy
There are times when you will intentionally change remote objects without the use of a Terraform script, i.e., directly via the Azure CLI. This can be necessary when troubleshooting a problem in the remote environment. In these cases, the state data will not match the remote object configuration and will need to be reconciled.
Refresh-only mode
Executing plan
in refresh-only mode creates a plan with the goal of showing you the deltas between the state data and the remote objects. You can activate refresh-only mode by including the -refresh-only
option on the command line.
Note: the -refresh-only
option is available only in Terraform v0.15.4 and later.
terraform plan -refresh-only
Using Terraform plan to create a new resource
Let’s see a plan for creating a new resource. As mentioned before, this is done by simply running terraform plan
without any arguments:
Step 1 – Prepare your Terraform configuration.
variable "region" {
description = "Azure location for the resource group"
type = string
default = "centralus"
}
provider "azurerm" {
features {}
}
resource "azurerm_resource_group" "tf-plan" {
name = "rg-tf-plan-example-centralus"
location = var.region
tags = { name : "test" }
}
Step 2 – Save the configuration to a main.tf file.
Step 3 – Go to the directory that contains this main.tf.
Step 4 – Run terraform plan
.
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# azurerm_resource_group.tf-plan will be created
+ resource "azurerm_resource_group" "tf-plan" {
+ id = (known after apply)
+ location = "centralus"
+ name = "rg-tf-plan-example-centralus"
+ tags = {
+ "name" = "test"
}
}
Plan: 1 to add, 0 to change, 0 to destroy.
Using Terraform plan to modify an existing resource
We will reuse the last example for changing the resource. I have applied the code in order to be able to make changes to it. Now let’s see the steps we will need to go through to make changes to a resource:
Step 1 – Go to your main.tf file.
Step 2 – Make a change to your configuration – I will add a new tag.
resource "azurerm_resource_group" "tf-plan" {
name = "rg-tf-plan-example-centralus"
location = var.region
tags = { name : "test", env : "dev" }
}
Step 3 – Run terraform plan
again from the location containing your main.tf file.
azurerm_resource_group.tf-plan: Refreshing state... [id=/subscriptions/subid/resourceGroups/rg-tf-plan-example-centralus]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
~ update in-place
Terraform will perform the following actions:
# azurerm_resource_group.tf-plan will be updated in-place
~ resource "azurerm_resource_group" "tf-plan" {
id = "/subscriptions/subid/resourceGroups/rg-tf-plan-example-centralus"
name = "rg-tf-plan-example-centralus"
~ tags = {
+ "env" = "dev"
"name" = "test"
}
# (1 unchanged attribute hidden)
}
Plan: 0 to add, 1 to change, 0 to destroy.
Using Terraform plan to destroy an existing resource
Let’s see a destroy plan in action for the same code example:
Step 1 – Go to the location containing the code base.
Step 2 – Run terraform plan -destroy
.
azurerm_resource_group.tf-plan: Refreshing state... [id=/subscriptions/subid/resourceGroups/rg-tf-plan-example-centralus]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
- destroy
Terraform will perform the following actions:
# azurerm_resource_group.tf-plan will be destroyed
- resource "azurerm_resource_group" "tf-plan" {
- id = "/subscriptions/subid/resourceGroups/rg-tf-plan-example-centralus" -> null
- location = "centralus" -> null
- name = "rg-tf-plan-example-centralus" -> null
- tags = {
- "name" = "test"
} -> null
}
Plan: 0 to add, 0 to change, 1 to destroy.
In addition to the alternate plan
modes described above, there are a couple of other categories of options available, those affecting behavior and those affecting outputs.
Options affecting how Terraform plan command behaves
Options that modify the plan
command’s behavior are listed below.
Option | Description |
-refresh=false
|
Ignores external changes to remote objects, i.e., those made via the CLI. This option will not sync the state data with the remote objects before checking for configuration changes. While this improves performance by making less remote API requests, ignoring external changes could result in incomplete or incorrect plans.
|
-replace=ADDRESS
|
Forces the replacement of the resource instead of an update action or no change at all. This will replace the resource at the given address. It can be applied multiple times to replace many objects at once.
|
-var 'NAME=VALUE'
|
Applies a value for an input variable. It can be applied multiple times, once per variable.
Note: Errors will occur if a space appears before or after the equals sign (e.g.,
|
-var-file=FILENAME
|
Applies values for one or more input variables as defined in a “tfvars” file. It can be used multiple times, once for each “tfvars” file.
|
-lock-timeout=DURATION |
Instructs Terraform to retry acquiring a lock for a period of time before returning an error. The duration value must be entered using the format nt where n is a number and t is a letter representing a unit of time, e.g., 3m is 3 minutes.
|
Options related to formatting and output
There will be use cases where you will need to change the plan
command’s output. The following options are available for those use-cases:
Option | Description |
-compact-warnings
|
Shows warnings as a summary unless the warnings include errors. If there are errors, the text will include useful information for troubleshooting.
|
-input=false
|
Disables prompts for input variables. that have not otherwise been assigned a value. This option is useful when automating Terraform scripts, like when run in a CI/CD pipeline.
|
-json
|
Returns output in JSON format.
|
-no-color
|
Removes color from the output.
|
-out=FILENAME |
Saves the plan to a file that can be passed to the Terraform apply command to execute the planned changes. Any filename is allowed, but the recommended naming convention is to name it “tfplan.” Do not use the “.tf” suffix, as this will lead Terraform to interpret the file as a configuration script and will fail.
Note: the file contains all the values associated with planned changes, including any sensitive data, in cleartext in the plan file. For this reason, you must consider impacts on security when saving plans to a file.
|
-parallelism=n |
By default, Terraform will run 10 operations concurrently, where possible. This option limits the number of concurrent operations to n.
|
-detailed-exitcode |
Includes detailed exit codes, which provide more granular information about the resulting plan. The list of codes is:
– 0 = Succeeded with empty diff (no changes)
|
Example – Using Terraform plan -out
Saving Terraform output plan as JSON
Let’s show the terraform plan as JSON. This is a two-step process:
First, save the plan to a file:
terraform plan -out=myplan.tfplan
Next, let’s use terraform show
command and save the output to a JSON file:
terraform show -json myplan.tfplan > myplan.json
Now, let’s view the content of myplan.json:
cat myplan.json
{
"format_version": "1.2",
"terraform_version": "1.5.7",
"variables": {
"region": {
"value": "centralus"
}
},
"planned_values": {
"root_module": {
"resources": [
{
"address": "azurerm_resource_group.tf-plan",
"mode": "managed",
"type": "azurerm_resource_group",
"name": "tf-plan",
"provider_name": "registry.terraform.io/hashicorp/azurerm",
"schema_version": 0,
"values": {
"location": "centralus",
"managed_by": null,
"name": "rg-tf-plan-example-centralus",
"tags": {
"name": "test"
},
"timeouts": null
},
"sensitive_values": {
"tags": {}
}
}
]
}
},
"resource_changes": [
{
"address": "azurerm_resource_group.tf-plan",
"mode": "managed",
"type": "azurerm_resource_group",
"name": "tf-plan",
"provider_name": "registry.terraform.io/hashicorp/azurerm",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"location": "centralus",
"managed_by": null,
"name": "rg-tf-plan-example-centralus",
"tags": {
"name": "test"
},
"timeouts": null
},
"after_unknown": {
"id": true,
"tags": {}
},
"before_sensitive": false,
"after_sensitive": {
"tags": {}
}
}
}
],
"configuration": {
"provider_config": {
"azurerm": {
"name": "azurerm",
"full_name": "registry.terraform.io/hashicorp/azurerm",
"expressions": {
"features": [
{}
]
}
}
},
"root_module": {
"resources": [
{
"address": "azurerm_resource_group.tf-plan",
"mode": "managed",
"type": "azurerm_resource_group",
"name": "tf-plan",
"provider_config_key": "azurerm",
"expressions": {
"location": {
"references": [
"var.region"
]
},
"name": {
"constant_value": "rg-tf-plan-example-centralus"
},
"tags": {
"constant_value": {
"name": "test"
}
}
},
"schema_version": 0
}
],
"variables": {
"region": {
"description": "Azure location for the resource group"
}
}
}
},
"timestamp": "2023-10-04T06:35:15Z"
}
Example – Using Terraform plan with variables
Let’s turn the resource group’s location attribute into a variable. This adds flexibility and reusability to the script, allowing us to specify the location value at runtime. Modify the sample script as follows.
Add a variable to the sample script named “region” just before the provider section and modify the azurerm_resource_group resource so the value for the location attribute comes from a variable.
Note: for simplicity, we are using one “.tf” file to define the resources and the variables. In practice, variables are declared in a separate file, typically named “variables.tf”. The script below shows the result of making this change.
variable "region" {
description = "Azure location for the resource group"
type = string
}
provider "azurerm" {
features { }
}
resource "azurerm_resource_group" "tf-plan" {
name = "rg-tf-plan-example-centralus"
location = var.region
tags = {name: "test"}
}
The “region” variable is declared as a string.
Re-run the Terraform plan
command. This time you will be prompted for the location in Azure where the resource group resides.
Run the plan
command again. This time, use the -var option to provide a value for region on the command line and notice that you are no longer prompted to supply a value for the variable.
terraform plan -var='region="centralus"'
Example – Using Terraform plan with tfvars
Now, let’s pass a tfvars file to the terraform plan that contains the value for the region:
cat vars.tfvars
region = “centralus”
terraform plan --var-file="vars.tfvars"
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# azurerm_resource_group.tf-plan will be created
+ resource "azurerm_resource_group" "tf-plan" {
+ id = (known after apply)
+ location = "centralus"
+ name = "rg-tf-plan-example-centralus"
+ tags = {
+ "name" = "test"
}
}
Plan: 1 to add, 0 to change, 0 to destroy.
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now.
For tfvars files, if you use the names “terraform.tfvars” or “*.auto.tfvars”, you won’t need to pass them directly to the plan/apply/destroy commands, as they will be read automatically by Terraform.
Resource targeting is like the -replace=ADDRESS option in that it tells Terraform to focus the plan only on resources that match the given address. Resource targeting includes all other objects that the selected resource(s) depend on, either directly or indirectly.
You can target resources using the -target=ADDRESS option.
Note: It is not recommended to use -target under normal circumstances.
Terraform enforces this point by including a warning message in the output. Run the following command to see the warning message embedded in the output:
terraform plan -var='region="centralus"' -target='azurerm_resource_group.tf-plan'
Instead of using the Terraform -target flag, split large configurations into several smaller configurations that can each be applied independently. Doing so allows a complicated configuration to be separated into more manageable parts.
1. Leverage terraform plan
as much as possible
Whenever you are making changes to your Terraform code base, it is a best practice to use terraform plan
before applying the changes per se. This is also valid for pushing changes to your code base, you should always check that your code works properly.
2. Use VCS for your Terraform code base
Although this is not a best practice for planning, storing your code in a VCS in conjunction with planning as much as possible will facilitate collaboration and minimize human-error.
3. Save your plan to an output file
After you run a terraform plan
, Terraform tells you it cannot guarantee it will take exactly these actions if you run a terraform apply
. This changes when you save the plan to an output file
4. Use Terraform Automation and Collaboration Software (TACOS)
Spacelift facilitates storing state remotely, shows plans in a prettified format, enables policies for better governance of your code, integrates with third party security tools for vulnerability scanning, and more.
The plan
command is very powerful despite not actually modifying any resources. Saving plans to output files enables automation and peer review. Supplying variable values on the command line or in files eliminates the need to hard code values, some of which may be sensitive. Using plan
to show deltas between state data and remote configurations can help illuminate changes made directly to remote objects, which may need to be refreshed.
With Spacelift, once the workspace is prepared by the Initializing phase, planning runs a vendor-specific preview command, in this case, terraform plan
, and interprets the results. The result of the planning phase is the collection of currently managed resources and outputs as well as planned changes. This is used as an input to plan policies (optional) and to calculate the delta – always. If you’re interested in finding out more about how to use Spacelift to manage and automate your Terraform deployments check out our Terraform documentation.
You should plan to run the plan
command prior to applying your changes. This will help you avoid late-night troubleshooting sessions for changes that have gone awry. Remember, failing to plan is planning to fail.
Manage Terraform Better and Faster
If you are struggling with Terraform automation and management, check out Spacelift. It helps you manage Terraform state, build more complex workflows, and adds several must-have capabilities for end-to-end infrastructure management.