Check Your IaC Deployment with the Terraform `plan` Command
Tags: terraform, devops, iac
Published: 2022-09-28 15:53:42

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`](https://www.terraform.io/cli/commands/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
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"
}
```

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](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs).

`terraform plan`

![Output from plan command](https://res.cloudinary.com/redfive/image/upload/v1662926747/Dev.to/terraform-plan/tf_plan_initial_output_zevbej.png)

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](https://www.terraform.io/cli/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:

```terraform
resource "azurerm_resource_group" "tf-plan" {
  name     = "rg-tf-plan-example-centralus"
  location = "centralus"
  tags = {name: "test"}
}
```

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](https://res.cloudinary.com/redfive/image/upload/v1662936044/Dev.to/terraform-plan/tf_plan_delta_output_egcteb.png)

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](https://res.cloudinary.com/redfive/image/upload/v1662987035/Dev.to/terraform-plan/tf_plan_destroy_output_zlkbhc.png)

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](https://res.cloudinary.com/redfive/image/upload/v1662997336/Dev.to/terraform-plan/tf_plan_refresh_output_x04pfk.png)

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

```terraform
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.

![prompt for variables](https://res.cloudinary.com/redfive/image/upload/v1663810893/Dev.to/terraform-plan/tf_var_prompt_vavb9m.png)

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](https://res.cloudinary.com/redfive/image/upload/v1663527894/Dev.to/terraform-plan/tf_target_warning_pihtps.png)

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.