» Upgrading to Terraform v0.12

Terraform v0.12 is a major release focused on configuration language improvements and thus includes some changes that you'll need to consider when upgrading. The goal of this guide is to cover the most common upgrade concerns and issues.

For most users, upgrading configuration should be completely automatic. Some simple configurations will require no changes at all, and most other configurations can be prepared by running the automatic upgrade tool. Please read on for more information and recommendations on the upgrade process.

» Upgrade to Terraform 0.11 first

We strongly recommend completing an upgrade to the latest Terraform v0.11 release first. This will give you an opportunity to address any changes required for the previous major version upgrades separately, rather than making multiple changes at once.

In particular, if you are upgrading from a Terraform version prior to v0.9, you must first upgrade to Terraform v0.9 and switch to initializing with terraform init, because v0.12 no longer includes the functionality for automatically migrating from the legacy remote state mechanism.

This guide focuses on changes from v0.11 to v0.12. Each previous major release has its own upgrade guide, so please consult the other guides (available in the navigation) to upgrade step-by-step to v0.11 first.

Terraform v0.11.14 (and any subsequent v0.11 releases) also include some additional functionality to help smooth the upgrade, which we will use later in this guide.

Prior versions of Terraform are available from the releases server.

» Pre-upgrade Checklist

Terraform v0.11.14 introduced a temporary helper command terraform 0.12checklist, which analyzes your configuration to detect any required steps that will be easier to perform before upgrading.

To use it, first upgrade to Terraform v0.11.14. Then, perform the following steps:

  • terraform init to ensure your working directory is fully initialized and all required plugins are installed and selected.
  • terraform apply to ensure that your real infrastructure and Terraform state are consistent with the current configuration. The instructions produced by the checklist command assume that configuration and state are synchronized.
  • terraform 0.12checklist to see if there are any pre-upgrade steps in the checklist.

If all is well, the final command will produce a message like this:

Looks good! We did not detect any problems that ought to be
addressed before upgrading to Terraform v0.12

This tool is not perfect though, so please check the v0.12 upgrade
guide for additional guidance, and for next steps:
    https://www.terraform.io/upgrade-guides/0-12.html

As the message suggests, the next step in that case is to read the remainder of this page to prepare for and carry out the upgrade.

However, the checklist command may instead produce a list of one or more tasks that we recommend you perform before upgrading to Terraform 0.12, because they are easier to perform with a fully-functional Terraform 0.11 than with a Terraform 0.12 that has encountered compatibility problems.

The tasks it may suggest you perform could include:

  • Upgrading any provider versions that are not compatible with Terraform v0.12. We recommend upgrading to the latest version of each provider before upgrading because that will avoid changing many things in one step.
  • Renaming any resources or provider aliases that have names that start with digits, because that is no longer valid in Terraform 0.12.
  • Upgrading any external modules the configuration uses which themselves have the above problems.

In each case, the tool will give some direction on how to perform the task it is suggesting.

The output from terraform 0.12checklist is in Markdown format so that it can easily be pasted into a Markdown-compatible issue tracker, should you want to track the necessary tasks or share the work with other team members.

After all of the tasks are complete, run terraform 0.12checklist one more time to verify that everything is complete. If so, continue reading the following sections to complete the upgrade!

» Upgrading to Terraform 0.12

Before switching to Terraform 0.12, we recommend using Terraform v0.11.14 (or any later v0.11 release) to perform one last terraform init and terraform apply to ensure that everything is initialized and synchronized.

Once terraform apply shows no changes pending, switch over to a Terraform v0.12 release and run terraform init again to upgrade the working directory metadata to v0.12 format. (Once you've done this, you'll need to delete the .terraform directory if you wish to return to Terraform v0.11, but no real infrastructure or persisted state will be upgraded yet.)

It is possible that your configuration may be using configuration constructs that are not Terraform v0.12 compatible and thus require upgrade. In that case, terraform init will produce the following message:

Terraform has initialized, but configuration upgrades may be needed.

Terraform found syntax errors in the configuration that prevented full
initialization. If you've recently upgraded to Terraform v0.12, this may be
because your configuration uses syntax constructs that are no longer valid,
and so must be updated before full initialization is possible.

Terraform has installed the required providers to support the configuration
upgrade process. To begin upgrading your configuration, run the following:
    terraform 0.12upgrade

To see the full set of errors that led to this message, run:
    terraform validate

As mentioned in the message, Terraform has partially initialized the directory just enough to perform the configuration upgrade process, which is described in the following section.

We recommend running the configuration upgrade tool even if you do not see the above message, because it may detect and fix constructs that are syntactically correct but still need some changes to work as expected with Terraform v0.12.

» Upgrading Terraform configuration

Terraform v0.12 includes a new command terraform 0.12upgrade that will read the configuration files for a module written for Terraform 0.11 and update them in-place to use the cleaner Terraform 0.12 syntax and also adjust for use of features that have changed behavior in the 0.12 Terraform language.

Simple configuration files are likely to be understood by Terraform 0.12 as-is, because the language is still broadly compatible, but we recommend that everyone run the upgrade tool nonetheless. Even if your configuration is already compatible, the tool will update your configuration to use the cleaner syntax available in Terraform 0.12, which should improve readability.

To run the command, first make sure that your local working directory is synced with your version control system so that there are no changes outstanding. This will make it easier to review the changes that the upgrade tool is proposing, using the diff feature of your version control system.

With a fully-initialized working directory (all necessary providers and child modules installed), run terraform 0.12upgrade to begin the process. By default it will print some information about what it is about to do and prompt for confirmation:

This command will rewrite the configuration files in the given directory so
that they use the new syntax features from Terraform v0.12, and will identify
any constructs that may need to be adjusted for correct operation with
Terraform v0.12.

We recommend using this command in a clean version control work tree, so that
you can easily see the proposed changes as a diff against the latest commit.
If you have uncommitted changes already present, we recommend aborting this
command and dealing with them before running this command again.

Would you like to upgrade the module in the current directory?

If you answer yes, the .tf and .tfvars files in your current working directory will be rewritten in-place.

The upgrade tool may also print out warnings about constructs it wasn't able to migrate fully automatically; in that case, it will also emit comments into the rewritten source files containing the special marker TF-UPGRADE-TODO, as a prompt for a decision you'll need to make to complete the upgrade.

Once the upgrade tool has successfully completed and you've resolved any TF-UPGRADE-TODO prompts, use your version control tool to review the proposed changes and then run terraform plan to see the effect of those changes.

In most cases, terraform plan should report that no changes are required, because the updated configuration is equivalent to before.

The remaining sections below describe both some common changes that the upgrade tool is able to make automatically, and some other upgrade situations that the configuration tool may not be able to fully resolve. If you encounter any errors during the upgrade or during the subsequent terraform plan, the sections below may give some additional context for how to proceed.

Once you're happy with the updated configuration, commit it to version control in the usual way and apply it with Terraform 0.12.

» Remote state references

The terraform_remote_state data source has changed slightly for the v0.12 release to make all of the remote state outputs available as a single map value, rather than as top-level attributes as in previous releases.

In previous releases, a reference to a vpc_id output exported by the remote state data source might have looked like this:

data.terraform_remote_state.vpc.vpc_id

This value must now be accessed via the new outputs attribute:

data.terraform_remote_state.vpc.outputs.vpc_id

The upgrade tool will rewrite remote state references automatically to include the additional outputs attribute.

Where appropriate, you can also access the outputs attribute directly to work with the whole map as a single value:

data.terraform_remote_state.vpc.outputs

Another consideration for terraform_remote_state is that this data source must be able to parse the latest state snapshot for a separate Terraform configuration that may have been updated by a newer version of Terraform. To provide flexibility when upgrading decomposed environments that use terraform_remote_state, Terraform v0.11.14 introduced support for reading outputs from the Terraform v0.12 state format, so if you upgrade all of your configurations to Terraform v0.11.14 first you can then perform v0.12 upgrades of individual configurations in any order, without breaking terraform_remote_state usage.

Note that the config block should now be in the form of an assignment with the = sign:

data "terraform_remote_state" "default" {
  backend = "gcs"
  config = {
    bucket = "..."
  }
}

» Attributes vs. blocks

Terraform resource configurations consist of both arguments that set individual properties of the main object being described, and nested blocks which declare zero or more other objects that are modeled as being part of their parent. For example:

resource "aws_instance" "example" {
  instance_type = "t2.micro"
  ami           = "ami-abcd1234"

  tags = {
    Name = "example instance"
  }

  ebs_block_device {
    device_name = "sda2"
    volume_type = "gp2"
    volume_size = 24
  }
}

In the above resource, instance_type, ami, and tags are both direct arguments of the aws_instance resource, while ebs_block_device describes a separate EBS block device object that is connected to the parent instance.

Due to the design of the configuration language decoder in Terraform v0.11 and earlier, it was in many cases possible to interchange the argument syntax (with =) and the block syntax (with just braces) when dealing with map arguments vs. nested blocks. However, this led to some subtle bugs and limitations, so Terraform v0.12 now requires consistent usage of argument syntax for arguments and nested block syntax for nested blocks.

In return for this new strictness, Terraform v0.12 now allows map keys to be set dynamically from expressions, which is a long-requested feature. The main difference between a map attribute and a nested block is that a map attribute will usually have user-defined keys, like we see in the tags example above, while a nested block always has a fixed set of supported arguments defined by the resource type schema, which Terraform will validate.

The configuration upgrade tool uses the provider's schema to recognize the nature of each construct and will select the right syntax automatically. For most simple usage, this will just involve adding or removing the equals sign as appropriate.

A more complicated scenario is where users found that they could exploit this flexibility to -- with some caveats -- dynamically generate nested blocks even though this wasn't intentionally allowed:

  # Example of no-longer-supported workaround from 0.11 and earlier
  ebs_block_device = "${concat(map("device_name", "sda4"), var.extra_block_devices)}"

Terraform v0.12 now includes a first-class feature for dynamically generating nested blocks using expressions, using the special dynamic block type. The above can now be written like this, separating the static block device from the dynamic ones:

  ebs_block_device {
    device_name = "sda4"
  }
  dynamic "ebs_block_device" {
    for_each = var.extra_block_devices
    content {
      device_name = ebs_block_device.value.device_name
      volume_type = ebs_block_device.value.volume_type
      volume_size = ebs_block_device.value.volume_size
    }
  }

The configuration upgrade tool will detect use of the above workaround and rewrite it as a dynamic block, but it may make non-ideal decisions for how to flatten your expression down into static vs. dynamic blocks, so we recommend reviewing the generated dynamic blocks to see if any simplifications are possible.

Terraform v0.12 now also requires that each argument be set only once within a particular block, whereas before Terraform would either take the last definition or, in some cases, attempt to merge together multiple definitions into a list. The upgrade tool does not remove or attempt to consolidate any existing duplicate arguments, but other commands like terraform validate will detect and report these after upgrading.

» Integer vs. Float Number Types

From Terraform v0.12, the Terraform language no longer distinguishes between integer and float types, instead just having a single "number" type that can represent high-precision floating point numbers. This new type can represent any value that could be represented before, plus many new values due to the expanded precision.

In most cases this change should not cause any significant behavior change, but please note that in particular the behavior of the division operator is now different: it always performs floating point division, whereas before it would sometimes perform integer division by attempting to infer intent from the argument types.

If you are relying on integer division behavior in your configuration, please use the floor function to obtain the previous result. A common place this would arise is in index operations, where the index is computed by division:

  example = var.items[floor(count.index / var.any_number)]

Using a fractional number to index a list will produce an error telling you that this is not allowed, serving as a prompt to add floor:

Error: Invalid index

The given key does not identify an element in this collection value: indexing a
sequence requires a whole number, but the given index (0.5) has a fractional
part.

Unfortunately the automatic upgrade tool cannot apply a fix for this case because it does not have enough information to know if floating point or integer division was intended by the configuration author, so this change must be made manually where needed.

» Referring to List Variables

In early versions of Terraform, before list support became first-class, we required using seemingly-redundant list brackets around a single expression in order to hint to the language interpreter that a list interpretation was desired:

  # Example for older versions of Terraform; not valid for v0.12
  example = ["${var.any_list}"]

This strange requirement was subsequently lifted after the introduction of first-class list support, but we retained compatibility with this older usage for a transitional period by including some fixup logic that would detect when list brackets contain list expressions and automatically flatten to a single list.

As part of implementing the first-class expressions support for v0.12, we needed to finally remove that backward-compatibility mechanism to avoid ambiguity in the language, so an expression like the above will now produce a list of lists and thus produce a type checking error for any argument that was expecting a list of some other type.

The upgrade tool is able to recognize most simple usage of this pattern and rewrite automatically to just refer to the list directly:

  example = var.any_list

However, an unintended side-effect of this compatibility mechanism was to also flatten mixed lists of single-value and list expressions into a single list automatically. We didn't intend for this to be a part of the language, but in retrospect it was an obvious consequence of how the compatibility mechanism was implemented. If you have expressions in your modules that produce a list of strings by using list brackets with a mixture of string and list-of-string sub-expressions, you will need to rewrite this to explicitly use the flatten function to make the special treatment more obvious to the reader:

  example = flatten([
    "single string",
    var.any_list,
  ])

The configuration upgrade tool unfortunately cannot make this change automatically, because it doesn't have enough information to know for certain which interpretation was intended for a given list.

For complex examples that the upgrade tool is not able to adjust automatically, subsequent Terraform operations may produce an error message like the following:

Error: Incorrect attribute value type

  on redundant-list-brackets.tf line 9, in resource "aws_security_group" "foo":
   9:     cidr_blocks = ["${var.cidr_blocks}"]

Inappropriate value for attribute "cidr_blocks": element 0: string required.

This message is reporting that Terraform has understood this expression as a list of lists, and therefore element zero is a list rather than a string. To fix the error, remove the redundant list brackets and possibly add a flatten function call as described above, for more complex cases.

» Reserved Variable Names

In preparation for new features planned for future releases, Terraform 0.12 reserves some additional names that can no longer be used as input variable names for modules. These reserved names are:

When any of these names is used as the label of a variable block, Terraform will now generate the following error:

Error: Invalid variable name

  on reserved-variable-names.tf line 2, in variable "count":
   2: variable "count" {

The variable name "count" is reserved due to its special meaning inside module
blocks.

The upgrade tool cannot automatically adjust for these reserved names, because it does not know what new name would be more appropriate. To proceed, you must unfortunately rename these input variables and make a new major release of the module in question, since renaming input variables is a breaking change.

» Type Constraints on Variables

In Terraform v0.11, variables were documented as accepting only strings, lists of strings, and maps of strings. However, in practice Terraform permitted lists of lists and lists of maps and other nested structures in some cases, even though it was then generally inconvenient to work with those values elsewhere in the module due to limitations of the index syntax, element function, and lookup function.

Terraform now allows various type constraints to be specified, as part of the language's new type system and generalized functions and operators. However, because lists and maps of non-string values were not officially supported in 0.11, existing configurations do not have enough information for the upgrade tool to know what element type was intended. It will therefore assume that lists and maps are of strings as documented, which will be incorrect for configurations using more complex structures. The result will be one of the following error messages:

Error: Invalid default value for variable

  on child_module/example.tf line 4, in variable "example":
   4:   default = [
   5:     {
   6:       "foo" = "bar"
   7:     },
   8:   ]

This default value is not compatible with the variable's type constraint:
element 0: string required.
Error: Invalid value for module argument

  on variables-incorrect-elem-type.tf line 4, in module "child":
   4:   example = [
   5:     {
   6:       "foo" = "bar"
   7:     },
   8:   ]

The given value is not suitable for child module variable "example" defined at
child/child.tf:1,1-19: element 0: string required.

To fix this, change the type argument from list(string) or map(string) to a more appropriate type constraint.

If you're not sure what type constraint to use yet, another option is to use the type constraint any, which will effectively disable validation and allow any value. We recommend using specific types where possible, but selecting any during upgrade may be preferable, so that the work to select and define a more precise type can be saved for a later change at your leisure, once upgrading is complete.

» Working with count on resources

The count feature allows declaration of multiple instances of a particular resource constructed from the same configuration. In Terraform v0.11, any use of count would generally lead to referring to the resource in question using the "splat expression" syntax elsewhere in the configuration:

aws_instance.example.*.id[0]

Because aws_instance.example itself was not directly referencable in Terraform v0.11, the expression system allowed some flexibility in how such expressions were resolved. For example, Terraform would treat aws_instance.example.id as an alias for aws_instance.example.*.id[0].

Terraform v0.12 allows referring to an entire resource as an object value, but that required making a decision on what type of value is returned by aws_instance.example. The new rules are as follows:

  • For resources where count is not set, a reference like aws_instance.example returns a single object, whose attributes can be accessed in the usual way, like aws_instance.example.id.

  • For resources where count is set -- even if the expression evaluates to 1 -- aws_instance.example returns a list of objects whose length is decided by the count. In this case aws_instance.example.id is an error, and must instead be written as aws_instance.example[0].id to access one of the objects before retrieving its id attribute value.

The splat syntax is still available and will still be useful in situations where a list result is needed, but we recommend updating expressions like aws_instance.example.*.id[count.index] to instead be aws_instance.example[count.index].id, which should be easier to read and understand for those who are familiar with other languages.

Another consequence of the new handling of count is that you can use the length function directly with references to resources that have count set:

length(aws_instance.example)

This replaces the v0.11 special case of aws_instance.example.count, which can no longer be supported due to aws_instance.example being a list.

The upgrade tool will automatically detect references that are inconsistent with the count setting on the target resource and rewrite them to use the new syntax. The upgrade tool will not rewrite usage of splat syntax to direct index syntax, because the old splat syntax form is still compatible.

Another count-related change is that Terraform now requires count to be assigned a numeric value, and will not automatically convert a boolean value to a number in the interests of clarity. If you wish to use a boolean value to activate or deactivate a particular resource, use the conditional operator to show clearly how the boolean value maps to a number value:

  count = var.enabled ? 1 : 0

» First-class expressions

Terraform v0.11 and earlier allowed expressions only within interpolation sequences, like "${var.example}". Because expressions are such an important part of Terraform -- they are the means by which we connect the attributes of one resource to the configuration of another -- Terraform v0.12 now allows you to use expressions directly when defining most attributes.

  ami = var.ami

The generalization of expression handling also has some other benefits. For example, it's now possible to directly construct lists and maps within expressions using the normal syntax, whereas in Terraform v0.11 we required using the list and map functions:

  # Old 0.11 example
  tags = "${merge(map("Name", "example"), var.common_tags)}"

  # Updated 0.12 example
  tags = merge({ Name = "example" }, var.common_tags)

The automatic upgrade tool will perform rewrites like these automatically, making expressions easier to read and understand.

» Default settings in connection blocks

Terraform v0.11 and earlier allowed providers to pre-populate certain arguments in a connection block for use with remote provisioners. Several resource type implementations use this to pre-populate type as "ssh" and host as one of the IP addresses of the compute instance being created.

While that feature was convenient in some cases, we found that in practice it was hard for users to predict how it would behave, since each provider had its own rules for whether to prefer public vs. private IP addresses, which network interface to use, whether to use IPv4 or IPv6, etc.

It also violated our design principle of "explicit is better than implicit": we think it's important that someone who is unfamiliar with a particular Terraform configuration (or with Terraform itself) to be able to read the configuration and make a good guess as to what it will achieve, and the default connection settings feature left an important detail unstated: how do the provisioners access the host?

With this in mind, Terraform v0.12 no longer performs any automatic population of connection blocks. Instead, if you are using any remote provisioners you should explicitly set the connection type and the hostname to connect to:

  connection {
    type = "ssh"
    host = self.public_ip
    # ...
  }

The automatic upgrade tool will detect existing connection blocks that are lacking these settings within resource types that are known to have previously set defaults, and it will write out an expression that approximates whatever selection logic the provider was previously doing in its own implementation.

Unfortunately in some cases the provider did not export the result of the possibly-rather-complex host selection expression as a single attribute, and so for some resource types the generated host expression will be quite complicated. We recommend reviewing these and replacing them with a simpler expression where possible, since you will often know better than Terraform does which of the instance IP addresses are likely to be accessible from the host where Terraform is running.

» Upgrades for reusable modules

If you are making upgrades to a reusable module that is consumed by many different configurations, you may need to take care with the timing of your upgrade and of how you publish it.

We strongly recommend using module versioning, either via a Terraform registry or via version control arguments in your module source addresses, to pin existing references to the old version of the module and then publish the upgraded version under a new version number. If you are using semantic versioning, such as in a Terraform registry, the updates made by the upgrade tool should be considered a breaking change and published as a new major version.

The migration tool will automatically add a >= 0.12.0 Terraform version constraint to indicate that the module has been upgraded to use v0.12-only features. By using version constraints, users can gradually update their callers to use the newly-upgraded version as they begin to use Terraform v0.12 with those modules.

For simpler modules it may be possible to carefully adapt them to be both 0.11 and 0.12 compatible at the same time, by following the upgrade notes in earlier sections and avoiding any v0.12-only features. However, for any module using a undocumented workarounds for v0.11 limitations it is unlikely to be possible to both update it for Terraform v0.12 and retain v0.11 compatibility at the same time, because those undocumented workarounds have been replaced with new features in Terraform v0.12.

» Map variables no longer merge when overridden

In prior versions of Terraform, a variable of type "map" had a special behavior where any value provided via mechanisms such as the -var command line option would be keywise-merged with any default value associated with the variable. This was useful in early versions of Terraform that lacked mechanisms for doing such merging explicitly, but since Terraform v0.10 introduced the concept of local values we consider it preferable to perform such merges manually so that they are explicit in configuration:

variable "example_map" {
  type = map(string)
  default = {}
}

locals {
  default_map_keys = {
    "a" = "b"
  }
  merged_map_keys = merge(local.default_map_keys, var.example_map)
}

In order to improve the consistency of variable handling across types, the map variable merging behavior is removed in Terraform v0.12. Because this mechanism was driven by command line options rather than configuration, the automatic upgrade tool cannot automatically handle it. If you are relying on the merging feature, you must reorganize your configuration to use explicit merging like in the above example, or else your default map value will be entirely overridden by any explicitly-set value.

» Upgrading remote Backend Configuration

Terraform Enterprise users, and users of the Terraform SAAS free tier, will need to run terraform init -reconfigure to upgrade to Terraform 0.12.

Terraform provides a message stating that terraform init is required; while there is no harm in running this command, the next error message will clarify that terraform init -reconfigure is required.

» Upgrading Sentinel policies

The Terraform Sentinel imports have been updated to work with Terraform 0.12. Care has been taken to ensure that the API is as backwards compatible as possible and most policies will continue to work without modification. However, there are some important changes and certain policies will need to modified.

More information on the changes can be found in our page on using Sentinel with Terraform 0.12.

It's strongly advised that you test your Sentinel policies after upgrading to Terraform 0.12 to ensure they continue to work as expected. Mock generation has also been updated to produce mock data for the Sentinel imports as they appear in Terraform 0.12.

For more information on testing a policy with 0.11 and 0.12 at the same time, see the section on testing a policy with 0.11 and 0.12 simultaneously.