Terraform

Terraform Plan Command: Examples & How It Works

Terraform plan command

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.

What is a Terraform Plan?

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.

What does Terraform plan command do

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.

How to use Terraform plan

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

Terraform plan - initial 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.

terraform plan delta output

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 plan planning modes

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
terraform plan destroy output

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
terraform plan refresh output

Terraform plan use case examples

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.

Terraform plan parameters

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 "region = centralus").

 

-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)
– 1 = Error
– 2 = Succeeded with non-empty diff (changes present)

 

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.

terraform variable prompt

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

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'
terraform plan target warning

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.

Terraform plan best practices

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.

Key Points

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.

Start free trial

The Practitioner’s Guide to Scaling Infrastructure as Code

Transform your IaC management to scale

securely, efficiently, and productively

into the future.

ebook global banner
Share your data and download the guide