opentofu/website/upgrade-guides/0-11.html.markdown
2021-02-24 13:36:47 -05:00

12 KiB

layout page_title sidebar_current description
language Upgrading to Terraform 0.11 upgrade-guides-0-11 Upgrading to Terraform v0.11

Upgrading to Terraform v0.11

Terraform v0.11 is a major release and thus includes some changes that you'll need to consider when upgrading. This guide is intended to help with that process.

The goal of this guide is to cover the most common upgrade concerns and issues that would benefit from more explanation and background. The exhaustive list of changes will always be the Terraform Changelog. After reviewing this guide, we recommend reviewing the Changelog to check on specific notes about the resources and providers you use.

This guide focuses on changes from v0.10 to v0.11. Each previous major release has its own upgrade guide, so please consult the other guides (available in the navigation) if you are upgrading directly from an earlier version.

Interactive Approval in terraform apply

Terraform 0.10 introduced a new mode for terraform apply (when run without an explicit plan file) where it would show a plan and prompt for approval before proceeding, similar to terraform destroy.

Terraform 0.11 adopts this as the default behavior for this command, which means that for interactive use in a terminal it is not necessary to separately run terraform plan -out=... to safely review and apply a plan.

The new behavior also has the additional advantage that, when using a backend that supports locking, the state lock will be held throughout the refresh, plan, confirmation and apply steps, ensuring that a concurrent execution of terraform apply will not invalidate the execution plan.

A consequence of this change is that terraform apply is now interactive by default unless a plan file is provided on the command line. When running Terraform in automation it is always recommended to separate plan from apply, but if existing automation was running terraform apply with no arguments it may now be necessary to update it to either generate an explicit plan using terraform plan -out=... or to run terraform apply -auto-approve to bypass the interactive confirmation step. The latter should be done only in unimportant environments.

Action: For interactive use in a terminal, prefer to use terraform apply with out an explicit plan argument rather than terraform plan -out=tfplan followed by terraform apply tfplan.

Action: Update any automation scripts that run Terraform non-interactively so that they either use separated plan and apply or override the confirmation behavior using the -auto-approve option.

Relative Paths in Module source

Terraform 0.11 introduces full support for module installation from Terraform Registry as well as other private, in-house registries using concise module source strings like hashicorp/consul/aws.

As a consequence, module source strings like "child" are no longer interpreted as relative paths. Instead, relative paths must be expressed explicitly by beginning the string with either ./ (for a module in a child directory) or ../ (for a module in the parent directory).

Action: Update existing module source values containing relative paths to start with either ./ or ../ to prevent misinterpretation of the source as a Terraform Registry module.

Interactions Between Providers and Modules

Prior to Terraform 0.11 there were several limitations in deficiencies in how providers interact with child modules, such as:

  • Ancestor module provider configurations always overrode the associated settings in descendent modules.

  • There was no well-defined mechanism for passing "aliased" providers from an ancestor module to a descendent, where the descendent needs access to multiple provider instances.

Terraform 0.11 changes some of the details of how each resource block is associated with a provider configuration, which may change how Terraform interprets existing configurations. This is notably true in the following situations:

  • If the same provider is configured in both an ancestor and a descendent module, the ancestor configuration no longer overrides attributes from the descendent and the descendent no longer inherits attributes from its ancestor. Instead, each configuration is entirely distinct.

  • If a provider block is present in a child module, it must either contain a complete configuration for its associated provider or a configuration must be passed from the parent module using the new providers attribute. In the latter case, an empty provider block is a placeholder that declares that the child module requires a configuration to be passed from its parent.

  • When a module containing its own provider blocks is removed from its parent module, Terraform will no longer attempt to associate it with another provider of the same name in a parent module, since that would often cause undesirable effects such as attempting to refresh resources in the wrong region. Instead, the resources in the module resources must be explicitly destroyed before removing the module, so that the provider configuration is still available: terraform destroy -target=module.example.

The recommended design pattern moving forward is to place all explicit provider blocks in the root module of the configuration, and to pass providers explicitly to child modules so that the associations are obvious from configuration:

provider "aws" {
  region = "us-east-1"
  alias  = "use1"
}

provider "aws" {
  region = "us-west-1"
  alias  = "usw1"
}

module "example-use1" {
  source = "./example"

  providers = {
    "aws" = "aws.use1"
  }
}

module "example-usw1" {
  source = "./example"

  providers = {
    "aws" = "aws.usw1"
  }
}

With the above configuration, any aws provider resources in the module ./example will use the us-east-1 provider configuration for module.example-use1 and the us-west-1 provider configuration for module.example-usw1.

When a default (non-aliased) provider is used, and not explicitly declared in a child module, automatic inheritance of that provider is still supported.

Action: In existing configurations where both a descendent module and one of its ancestor modules both configure the same provider, copy any settings from the ancestor into the descendent because provider configurations now inherit only as a whole, rather than on a per-argument basis.

Action: In existing configurations where a descendent module inherits aliased providers from an ancestor module, use the new providers attribute to explicitly pass those aliased providers.

Action: Consider refactoring existing configurations so that all provider configurations are set in the root module and passed explicitly to child modules, as described in the following section.

Moving Provider Configurations to the Root Module

With the new provider inheritance model, it is strongly recommended to refactor any configuration where child modules define their own provider blocks so that all explicit configuration is defined in the root module. This approach will ensure that removing a module from the configuration will not cause any provider configurations to be removed along with it, and thus ensure that all of the module's resources can be successfully refreshed and destroyed.

A common configuration is where two child modules have different configurations for the same provider, like this:

# root.tf

module "network-use1" {
  source = "./network"
  region = "us-east-1"
}

module "network-usw2" {
  source = "./network"
  region = "us-west-2"
}
# network/network.tf

variable "region" {
}

provider "aws" {
  region = "${var.region}"
}

resource "aws_vpc" "example" {
  # ...
}

The above example is problematic because removing either module.network-use1 or module.network-usw2 from the root module will make the corresponding provider configuration no longer available, as described in issue #15762, which prevents Terraform from refreshing or destroying that module's aws_vpc.example resource.

This can be addressed by moving the provider blocks into the root module as additional configurations, and then passing them down to the child modules as default configurations via the explicit providers map:

# root.tf

provider "aws" {
  region = "us-east-1"
  alias  = "use1"
}

provider "aws" {
  region = "us-west-2"
  alias  = "usw2"
}

module "network-use1" {
  source = "./network"

  providers = {
    "aws" = "aws.use1"
  }
}

module "network-usw2" {
  source = "./network"

  providers = {
    "aws" = "aws.usw2"
  }
}
# network/network.tf

# Empty provider block signals that we expect a default (unaliased) "aws"
# provider to be passed in from the caller.
provider "aws" {
}

resource "aws_vpc" "example" {
  # ...
}

After the above refactoring, run terraform apply to re-synchoronize Terraform's record (in the Terraform state) of the location of each resource's provider configuration. This should make no changes to actual infrastructure, since no resource configurations were changed.

For more details on the explicit providers map, and discussion of more complex possibilities such as child modules with additional (aliased) provider configurations, see Providers Within Modules.

Error Checking for Output Values

Prior to Terraform 0.11, if an error occurred when evaluating the value expression within an output block then it would be silently ignored and the empty string used as the result. This was inconvenient because it made it very hard to debug errors within output expressions.

To give better feedback, Terraform now halts and displays an error message when such errors occur, similar to the behavior for expressions elsewhere in the configuration.

Unfortunately, this means that existing configurations may have erroneous outputs lurking that will become fatal errors after upgrading to Terraform 0.11. The prior behavior is no longer available; to apply such a configuration with Terraform 0.11 will require adjusting the configuration to avoid the error.

Action: If any existing output value expressions contain errors, change these expressions to fix the error.

Referencing Attributes from Resources with count = 0

A common pattern for conditional resources is to conditionally set count to either 0 or 1 depending on the result of a boolean expression:

resource "aws_instance" "example" {
  count = "${var.create_instance ? 1 : 0}"

  # ...
}

When using this pattern, it's required to use a special idiom to access attributes of this resource to account for the case where no resource is created at all:

output "instance_id" {
  value = "${element(concat(aws_instance.example.*.id, list("")), 0)}"
}

Accessing aws_instance.example.id directly is an error when count = 0. This is true for all situations where interpolation expressions are allowed, but previously appeared to work for outputs due to the suppression of the error. Existing outputs that access non-existent resources must be updated to use the idiom above after upgrading to 0.11.0.