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