DEV Community

Paul Delcogliano
Paul Delcogliano

Posted on • Originally published at spacelift.io

Check Your IaC Deployment with the 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 lowered 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.

Applying infrastructure changes haphazardly can lead to unwanted changes to your environment. The Terraform plan command is available to bring visibility to your IaC deployments. The plan command reports on changes to infrastructure but it does not apply any of the proposed changes. Instead, it creates a reviewable execution plan, which you can use to confirm that the proposed changes are expected. The execution plan can be saved to a file, which can be applied later, or peer reviewed to enforce quality control. The ability to review the proposed changes prior to applying them can save you from making unexpected infrastructure changes. After you have confirmed the changes as expected, use the Terraform apply command to update the remote objects.

The 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.

Usage Examples

Let's look at the output resulting from executing plan with a simple terraform 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"
}
Enter fullscreen mode Exit fullscreen mode

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

Output from plan command

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 a 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.

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.

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"}
}
Enter fullscreen mode Exit fullscreen mode

Re-run the plan command:

terraform plan

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.

Planning Modes

The plan command can be run in three different modes, normal, which is the default and the mode we've used so far, destroy, and refresh-only. The modes are mutually exclusive. You cannot use more than one mode at a time.

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 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. 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 refresh output

Planning Options

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 that modify the plan command's behavior are listed below.

Options Affecting How the Plan Command Behaves

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=false State data is unlocked during the operation. Note: This can lead to concurrency issues especially if others are running commands against the same workspace.
-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.

Let's turn the 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 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"}
}
Enter fullscreen mode Exit fullscreen mode

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.

prompt for variables

Run the plan command again. This time, using 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"'

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 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 to 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)

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 warning from target option

Instead of using -target, 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.

Conclusion

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.

You should plan to include 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.

Oldest comments (0)