Terraform 0.12 beta makes provider schemas part of migration
The first 0.12 beta exposes the connection between a richer configuration language, provider protocol changes, and automatic configuration upgrades.
Terraform configurations have accumulated ways to express things the language did not directly support. Some exploit loose distinctions between a map argument and a nested block. Others wrap expressions in list brackets to obtain the expected shape. The first 0.12 beta replaces several of these workarounds with explicit language constructs, but that requires Terraform to understand more about what a provider expects.
The February 28 beta announcement invites configuration testing and feedback on the 0.12upgrade command. HashiCorp warns against using the prerelease on production infrastructure. Provider compatibility is still incomplete, so the beta is also a test of the boundary between Terraform Core and the plugins that implement resources.
I find this boundary particularly interesting because Terraform's reusable language is only useful when it preserves the meaning of the APIs underneath it. A migration that produces attractive syntax while changing the interpretation of a resource would be a poor result. The beta's schema work helps explain why the language upgrade cannot be evaluated independently of providers.
Why provider compatibility comes first
The upgrade guide at the beta tag explains that the new language requires changes to the provider protocol. Terraform CLI can obtain the resource type schemas from a provider. Those schemas make validation more informative and resource configuration more predictable.
An older provider binary cannot supply the new protocol merely because its resource names have stayed the same. The guide says providers built before this change cannot be used with 0.12. The updated SDK supports both the old and new protocols, allowing a compatible provider release to work with an existing 0.11 configuration before the CLI itself changes.
That gives migration a useful ordering. Provider upgrades can happen while the old language remains in use, making it possible to identify provider behavior changes separately from configuration conversion. The announcement says officially supported providers will be available through terraform init, while development snapshots provide another option during beta testing. A snapshot remains prerelease software with its own possible bugs.
For provider authors, the guide's immediate advice is to update the vendored Terraform packages to a 0.12 tag and check acceptance tests. Configuration snippets inside those tests may themselves require conversion. This is a consequence of the provider SDK containing both implementation support and examples of the language the provider consumes.
An argument is different from a block
The migration guide uses an instance resource to distinguish two kinds of configuration. A tags argument assigns a map with user-defined keys. An ebs_block_device block describes an object whose supported arguments come from the resource schema. Although both involve braces, they make different statements about the resource.
In the older decoder, some configurations could interchange argument syntax and block syntax. That flexibility allowed users to construct nested blocks dynamically through expressions even though the language did not formally support the operation. The guide identifies subtle bugs and limitations in that arrangement. Version 0.12 requires the syntax to match the provider's declared structure.
The upgrade tool uses the schema to decide whether to add or remove an equals sign. This is more than a text replacement rule. The same visible brace can introduce a map value or begin a nested configuration block, and only the resource definition tells the tool which interpretation is correct.
A dynamic block gives the previously improvised operation a defined form. The following fragment follows the beta guide's example and belongs inside a resource that supports these block arguments.
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
}
}
Here iteration generates nested configuration blocks. It does not declare multiple resource instances. The distinction matters because the schema still governs the contents of each generated block. The upgrade tool can translate the earlier workaround, although the guide notes that people may be able to simplify its division between static and generated blocks.
Values can retain their structure
First-class expressions remove the need to put every reference inside a quoted interpolation sequence. An assignment such as ami = var.ami states the reference directly. Lists and maps can also be constructed inside expressions with ordinary collection syntax, reducing the need for the older list and map function forms.
The more consequential change is how references represent whole objects. According to the beta guide, a resource without count can be referenced as a single object. A resource with count becomes a list of objects, including when the count happens to be one. The expression must select an instance before accessing one of its attributes.
output "first_instance_id" {
value = aws_instance.example[0].id
}
This illustrative output assumes aws_instance.example declares count and contains at least one instance. The explicit index follows the collection type. An unindexed .id no longer receives the older special treatment that could alias the first element. The guide also allows length(aws_instance.example) to ask for the number of instances directly.
Remote state output references get a corresponding structural change. Exported values now live under an outputs attribute. A reference to a VPC output becomes data.terraform_remote_state.vpc.outputs.vpc_id, and the complete outputs value can be used as a map. Keeping the exported values together makes their place in the data source explicit.
Conversion cannot infer every intention
The upgrade tool marks ambiguous situations with TF-UPGRADE-TODO comments. That is a useful limit on automation. Existing configuration may depend on a decoder quirk, and a tool can recognize the old shape without knowing why its author selected it.
One change cannot be solved by rewriting syntax alone. Older map variables merged a supplied value with the default map. In 0.12 an explicit value replaces the default, consistent with other variable types. The guide recommends an explicit merge expression when both sets of keys are required. Because the old behavior depends on how the variable is supplied, a module's source cannot reveal every affected invocation.
I prefer explicit expressions for these decisions because reusable modules need behavior that their readers can inspect. That preference does not remove the migration work. It means the resulting configuration should state the combination of values that the module actually requires.
Module versioning also becomes part of the change. The beta guide recommends publishing upgraded reusable modules under a new version and treating incompatible language updates as a major version change. Callers can then remain on their existing module release until they adopt the new language. The beta makes it possible to inspect those boundaries while the provider releases and conversion tool are still being tested.