├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── conditional_resource_or_module_instance ├── README.md ├── conditional_resource_creation_count │ ├── README.md │ └── main.tf └── conditional_resource_creation_foreach │ ├── README.md │ └── main.tf ├── looping_for_resource_blocks ├── README.md ├── dynamic_n_instances │ ├── README.md │ └── main.tf └── dynamic_singleton │ ├── README.md │ ├── main.tf │ └── variables.tf ├── looping_for_resources_or_modules ├── README.md ├── count_index_antipattern │ ├── README.md │ └── main.tf ├── map_of_objects │ ├── README.md │ ├── main.tf │ └── terraform.tf └── set_of_objects_antipattern │ ├── README.md │ ├── main.tf │ └── terraform.tf ├── nested_maps ├── README.md └── flatten_nested_map │ ├── README.md │ └── main.tf └── passing_references ├── README.md ├── references_by_object ├── README.md └── main.tf └── references_by_string_antipattern ├── README.md └── main.tf /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | 8 | # Crash log files 9 | crash.log 10 | crash.*.log 11 | 12 | # Exclude all .tfvars files, which are likely to contain sensitive data, such as 13 | # password, private keys, and other secrets. These should not be part of version 14 | # control as they are data points which are potentially sensitive and subject 15 | # to change depending on the environment. 16 | *.tfvars 17 | *.tfvars.json 18 | 19 | # Ignore override files as they are usually used to override resources locally and so 20 | # are not checked in 21 | override.tf 22 | override.tf.json 23 | *_override.tf 24 | *_override.tf.json 25 | 26 | # Include override files you do wish to add to version control using negated pattern 27 | # !example_override.tf 28 | 29 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 30 | # example: *tfplan* 31 | *tfplan* 32 | 33 | # Ignore CLI configuration files 34 | .terraformrc 35 | terraform.rc 36 | 37 | # Ignore lock files 38 | .terraform.lock.hcl 39 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Robust Terraform module design 2 | 3 | This repository shows design patterns that we have found useful when writing Terraform modules. 4 | The patterns are not specific to any particular cloud provider, but the examples are written for Azure. 5 | The patterns are sometimes not the most efficient or beautiful way of doing things, but instead have been created with robustness in mind. 6 | 7 | ## Content 8 | 9 | - [Conditional resource or module instance](./conditional_resource_or_module_instance/) 10 | - [Looping for resource blocks](./looping_for_resource_blocks/) 11 | - [Looping for resources or modules](./looping_for_resources_or_modules/) 12 | - [Nested maps](./nested_maps/) 13 | - [Passing references](./passing_references) 14 | 15 | ## About the authors 16 | 17 | These design practices have been developed by the following teams at Microsoft: 18 | 19 | - [Azure Landing Zones](https://aka.ms/alz/tf) 20 | - [Azure Verified Modules](https://aka.ms/avm) 21 | - Azure Terraform Engineering 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /conditional_resource_or_module_instance/README.md: -------------------------------------------------------------------------------- 1 | # Conditional resource or module instance 2 | 3 | - [back home](../) 4 | 5 | This pattern shows how to create **either zero or one** resource or module instance, based on a variable. 6 | 7 | This is a very common pattern, typically using the `count` meta-argument. 8 | 9 | We have two examples: 10 | 11 | - [Conditional resource creation using count](./conditional_resource_creation_count/) 12 | - [Conditional resource creation using for_each](./conditional_resource_creation_for_each/) 13 | -------------------------------------------------------------------------------- /conditional_resource_or_module_instance/conditional_resource_creation_count/README.md: -------------------------------------------------------------------------------- 1 | # Conditional resource creation using count 2 | 3 | - [back to parent](../) 4 | - [back home](../../) 5 | 6 | This is the preferred way to create resources conditionally. 7 | You can use the `one()` function to retrieve the zero index value for outputs, etc. 8 | 9 | You would use a boolean value to control the count. 10 | This can either be directly from an input variable, or by using a test expression. 11 | We recommend that you test for null or not null. 12 | -------------------------------------------------------------------------------- /conditional_resource_or_module_instance/conditional_resource_creation_count/main.tf: -------------------------------------------------------------------------------- 1 | # Here we use a boolean value to simulate the condition of a variable being set to true or false. 2 | # We use this in the `terraform_data.conditional_boolean` resource to conditionally create the resource. 3 | locals { 4 | create_resource = true 5 | } 6 | 7 | # It is also common to use a null test for resource creation. 8 | # Here we show how to conditionally create a resource if the caller does not override a variable default. 9 | # We use this in the `terraform_data.conditional_null_string` resource to conditionally create the resource. 10 | variable "my_input" { 11 | type = string 12 | default = null 13 | description = "Shows conditional resource creation using count with null test." 14 | } 15 | 16 | # This resource will only be created if the `create_resource` local is set to true. 17 | resource "terraform_data" "conditional_boolean" { 18 | count = local.create_resource ? 1 : 0 19 | input = "foo" 20 | } 21 | 22 | # This resource will only be created if the `my_input` variable is `null`. 23 | resource "terraform_data" "conditional_null_string" { 24 | count = var.my_input == null ? 1 : 0 25 | input = "foo" 26 | } 27 | 28 | # In this output, the `one()` function is helpful to get the zeroth index of the resource. 29 | # We also need to use try to cater for the case where the resource is not created. 30 | output "conditional_boolean" { 31 | value = try(one(terraform_data.conditional_boolean).output, null) 32 | description = "Shows example of using one() function to return a single instance." 33 | } 34 | 35 | # In this output, the `one()` function is helpful to get the zeroth index of the resource. 36 | # We also need to use try to cater for the case where the resource is not created. 37 | output "conditional_null_string" { 38 | value = try(one(terraform_data.conditional_null_string).output, null) 39 | description = "Shows example of using one() function to return a single instance." 40 | } 41 | -------------------------------------------------------------------------------- /conditional_resource_or_module_instance/conditional_resource_creation_foreach/README.md: -------------------------------------------------------------------------------- 1 | # Conditional resource creation using for_each 2 | 3 | - [back to parent](../) 4 | - [back home](../../) 5 | 6 | Count is usually preferred but it is possible to conditionally create resources or module calls using for_each. 7 | 8 | You will need to access the resource instance using a well-known key. 9 | -------------------------------------------------------------------------------- /conditional_resource_or_module_instance/conditional_resource_creation_foreach/main.tf: -------------------------------------------------------------------------------- 1 | 2 | locals { 3 | create_resource = true 4 | conditional_key = "enabled" 5 | } 6 | 7 | resource "terraform_data" "conditional" { 8 | for_each = local.create_resource ? toset([local.conditional_key]) : toset([]) 9 | input = "foo" 10 | } 11 | 12 | output "example" { 13 | value = try(terraform_data.conditional[local.conditional_key].output, null) 14 | description = "Shows example of using map key to return a single instance." 15 | } 16 | -------------------------------------------------------------------------------- /looping_for_resource_blocks/README.md: -------------------------------------------------------------------------------- 1 | # Looping for resource blocks 2 | 3 | - [back home](../../) 4 | 5 | Some Terraform resource schemas define one or more blocks to define the target resource. 6 | A block is a nested configuration structure that is defined within a resource block. 7 | It is identified by the label for the block followed by `{}`. 8 | Note there is no equals sign. 9 | 10 | Typically blocks are either singletons (i.e. there is only one block of that type allowed per resource), or they are a list of blocks (i.e. there can be multiple blocks of that type per resource). 11 | 12 | ```hcl 13 | resource "my_resource" "example" { 14 | my_block { 15 | # ... 16 | } 17 | } 18 | ``` 19 | 20 | ## Patterns 21 | 22 | - [Dynamic `n` instances](./dynamic_n_instances/) 23 | - [Dynamic zero or one instance](./dynamic_singleton/) 24 | -------------------------------------------------------------------------------- /looping_for_resource_blocks/dynamic_n_instances/README.md: -------------------------------------------------------------------------------- 1 | # Dynamic N instances 2 | 3 | This pattern shows how to create N instances of a resource block using a set of objects. 4 | Unlike resources or modules, we do not have to use a map with an arbitrary key. 5 | 6 | Do test this pattern does not produce similar results to that of the count index antipattern. 7 | Terraform will remove and re-create all instances of the block if there are any changes, it is down to the provider to handle this properly. 8 | In the case of this example the provider does not re-create all of the subnets. 9 | -------------------------------------------------------------------------------- /looping_for_resource_blocks/dynamic_n_instances/main.tf: -------------------------------------------------------------------------------- 1 | provider "azurerm" { 2 | features {} 3 | } 4 | 5 | # These resources ensure we deploy to a random region and use a random suffix for our resource names 6 | module "regions" { 7 | source = "Azure/regions/azurerm" 8 | version = ">= 0.4.0" 9 | } 10 | 11 | resource "random_integer" "region_index" { 12 | min = 0 13 | max = length(module.regions.regions) - 1 14 | } 15 | 16 | resource "random_pet" "example" { 17 | length = 2 18 | } 19 | 20 | module "naming" { 21 | source = "Azure/naming/azurerm" 22 | version = ">= 0.4.0" 23 | suffix = [random_pet.example.id] 24 | } 25 | 26 | # This is the interesting part of the example 27 | resource "azurerm_resource_group" "example" { 28 | name = module.naming.resource_group.name 29 | location = module.regions.regions[random_integer.region_index.result].name 30 | } 31 | 32 | locals { 33 | address_prefix = "192.168.0.0/16" 34 | subnets = toset([ 35 | { 36 | name = "subnet0" 37 | address_prefix = cidrsubnet(local.address_prefix, 8, 0) 38 | }, 39 | { 40 | name = "subnet1" 41 | address_prefix = cidrsubnet(local.address_prefix, 8, 1) 42 | }, 43 | { 44 | name = "subnet2" 45 | address_prefix = cidrsubnet(local.address_prefix, 8, 2) 46 | }, 47 | ]) 48 | } 49 | 50 | resource "azurerm_virtual_network" "example" { 51 | name = module.naming.virtual_network.name 52 | address_space = [local.address_prefix] 53 | resource_group_name = azurerm_resource_group.example.name 54 | location = azurerm_resource_group.example.location 55 | 56 | # Here we use a set of objects to create multiple subnets. 57 | # For dynamics we don't needs to use a map, or a set of strings like we do for resources & modules. 58 | dynamic "subnet" { 59 | for_each = local.subnets 60 | content { 61 | name = subnet.value.name 62 | address_prefix = subnet.value.address_prefix 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /looping_for_resource_blocks/dynamic_singleton/README.md: -------------------------------------------------------------------------------- 1 | # Dynamic singleton 2 | 3 | - [back home](../../) 4 | - [back to parent](../) 5 | 6 | This pattern is used to create zero or one blocks within a resource, based on a variable or other condition. 7 | 8 | > Note this example does not create a valid plan. It is intended to show the pattern only. 9 | -------------------------------------------------------------------------------- /looping_for_resource_blocks/dynamic_singleton/main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | thing_enabled = true 3 | } 4 | 5 | resource "my_resource" "example" { 6 | input = "foo" 7 | 8 | # Here we construct a dynamic block where there can be either zero or one instances. 9 | # We do this by using a `for_each` expression that returns either an empty set or a set with one element. 10 | # In this scenario it is common to not use the `my_block.value` expression at all, and instead use 11 | # references to other values. 12 | dynamic "my_block" { 13 | for_each = local.lifecycle_enabled ? toset([1]) : toset([]) 14 | 15 | content { 16 | replace_triggered_by = [""] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /looping_for_resource_blocks/dynamic_singleton/variables.tf: -------------------------------------------------------------------------------- 1 | variable "boot_diagnostics_enabled" { 2 | type = bool 3 | default = false 4 | } 5 | 6 | variable "boot_diags_storage_account_uri" { 7 | type = string 8 | default = null 9 | } 10 | -------------------------------------------------------------------------------- /looping_for_resources_or_modules/README.md: -------------------------------------------------------------------------------- 1 | # Looping for resources or modules 2 | 3 | - [back home](../../) 4 | 5 | It is extremely common to want to create zero to many resources or modules, based on a variable. 6 | The only robust pattern for doing this is to use a map of objects, with an arbitrary key. 7 | 8 | Terraform also thinks this is the case, but hides the guidance in an error message for some reason: 9 | 10 | ***When working with unknown values in for_each, it's better to define the map keys statically in your configuration and place apply-time results only in the map values.*** 11 | 12 | ## Patterns 13 | 14 | - [Map of objects](./map_of_objects/) 15 | 16 | ## Antipatterns 17 | 18 | - [Set of objects antipattern](./set_of_objects_antipattern/) 19 | - [Count index antipattern](./count_index_antipattern/) 20 | -------------------------------------------------------------------------------- /looping_for_resources_or_modules/count_index_antipattern/README.md: -------------------------------------------------------------------------------- 1 | # Count index antipattern 2 | 3 | - [back to parent](../) 4 | - [back home](../../) 5 | 6 | Do not use count to create multiple resources or modules which are almost identical. 7 | Use for_each instead. 8 | 9 | The reason is that use of count makes it difficult to add or remove resources or modules in the middle of the list. 10 | Typically this will cause Terraform to destroy and recreate resources or modules after the change. 11 | 12 | > ***Hint*** Use a map of objects instead 13 | 14 | ## Example 15 | 16 | 1. Run `terraform init` to initialize the directory. 17 | 1. Run `terraform apply` to create the resources. 18 | 1. Observe 4 `terraform_data.arrrgh_dont_do_this` resources are created with indices 0-3. 19 | 1. Simulate a change to the deployemnt by commenting out the object with index 1 in `main.tf` - this is clearly marked in the code. 20 | 1. Run `terraform plan` and observe that Terraform will unnecessarily re-create resources due to the index changing. 21 | 22 | ## Result 23 | 24 | You should observe the following plan, which needlessly destroys and recreates resources: 25 | 26 | ```text 27 | Terraform will perform the following actions: 28 | 29 | # terraform_data.arrrgh_dont_do_this[1] must be replaced 30 | -/+ resource "terraform_data" "arrrgh_dont_do_this" { 31 | ~ id = "e4b4ea81-6ec8-ed2c-9213-84debd49cf37" -> (known after apply) 32 | ~ input = { 33 | ~ attr = "bar2" -> "baz2" 34 | ~ name = "corgi-bar" -> "corgi-baz" 35 | # (1 unchanged attribute hidden) 36 | } 37 | ~ output = { 38 | - attr = "bar2" 39 | - attr2 = "data2" 40 | - name = "corgi-bar" 41 | } -> (known after apply) 42 | ~ triggers_replace = "corgi-bar" -> "corgi-baz" 43 | } 44 | 45 | # terraform_data.arrrgh_dont_do_this[2] must be replaced 46 | -/+ resource "terraform_data" "arrrgh_dont_do_this" { 47 | ~ id = "0277f94a-12df-8394-f62f-4267edea1552" -> (known after apply) 48 | ~ input = { 49 | ~ attr = "baz2" -> "fiz2" 50 | ~ name = "corgi-baz" -> "corgi-fiz" 51 | # (1 unchanged attribute hidden) 52 | } 53 | ~ output = { 54 | - attr = "baz2" 55 | - attr2 = "data3" 56 | - name = "corgi-baz" 57 | } -> (known after apply) 58 | ~ triggers_replace = "corgi-baz" -> "corgi-fiz" 59 | } 60 | 61 | # terraform_data.arrrgh_dont_do_this[3] will be destroyed 62 | # (because index [3] is out of range for count) 63 | - resource "terraform_data" "arrrgh_dont_do_this" { 64 | - id = "44f5f320-3c06-0b65-56fb-789815158cc1" -> null 65 | - input = { 66 | - attr = "fiz2" 67 | - attr2 = "data4" 68 | - name = "corgi-fiz" 69 | } -> null 70 | - output = { 71 | - attr = "fiz2" 72 | - attr2 = "data4" 73 | - name = "corgi-fiz" 74 | } -> null 75 | - triggers_replace = "corgi-fiz" -> null 76 | } 77 | 78 | Plan: 2 to add, 0 to change, 3 to destroy. 79 | ``` 80 | -------------------------------------------------------------------------------- /looping_for_resources_or_modules/count_index_antipattern/main.tf: -------------------------------------------------------------------------------- 1 | resource "random_pet" "name" { 2 | length = 1 3 | } 4 | 5 | # This simulates some data that might be returned from an external data source/lookup 6 | locals { 7 | list_of_objects = [ 8 | { 9 | name = "${random_pet.name.id}-foo" 10 | attr = "foo2" 11 | }, 12 | { # run apply, then comment out this object and run plan 13 | name = "${random_pet.name.id}-bar" # ...and you'll see that other resources are destroyed and recreated 14 | attr = "bar2" # ...because the name value of the object at list indices 1 & 2 changed 15 | }, # ... 16 | { 17 | name = "${random_pet.name.id}-baz" 18 | attr = "baz2" 19 | }, 20 | { 21 | name = "${random_pet.name.id}-fiz" 22 | attr = "fiz2" 23 | }, 24 | ] 25 | 26 | # This simulates some more data that might be returned from an external data source/lookup 27 | # and is a very bad idea. 28 | # Trying to ensure consistent ordering of data in multiple lists is error prone. 29 | list_of_objects_data = [ 30 | "data1", 31 | "data2", 32 | "data3", 33 | "data4" 34 | ] 35 | } 36 | 37 | 38 | # This works, but what happens when you apply the config, then remove the second element from the list_of_objects? 39 | resource "terraform_data" "arrrgh_dont_do_this" { 40 | count = length(local.list_of_objects) 41 | input = { 42 | name = local.list_of_objects[count.index].name 43 | attr = local.list_of_objects[count.index].attr 44 | 45 | # This is looking up data from a separate list and is _really_ fragile. 46 | # If you do have to use more than one daya object to retrieve data for a resource, 47 | # then you should use maps and ensure the keys are consistent and match those used in the 48 | # for_each loop on the resource. 49 | attr2 = local.list_of_objects_data[count.index] 50 | } 51 | # This simulates a real cloud resource, where a change of a key attribute will cause the resource to be destroyed and re-created. 52 | triggers_replace = local.list_of_objects[count.index].name 53 | } 54 | -------------------------------------------------------------------------------- /looping_for_resources_or_modules/map_of_objects/README.md: -------------------------------------------------------------------------------- 1 | # Map of objects 2 | 3 | - [back to parent](../) 4 | - [back home](../../) 5 | 6 | Using a map of objects, with a arbitrary key is the most robust way of looping for resources or modules. 7 | 8 | Terraform also thinks this is the case, but hides the guidance in an error message for some reason: 9 | 10 | ***When working with unknown values in for_each, it's better to define the map keys statically in your configuration and place apply-time results only in the map values.*** 11 | -------------------------------------------------------------------------------- /looping_for_resources_or_modules/map_of_objects/main.tf: -------------------------------------------------------------------------------- 1 | # Using an arbitrary map key is the most robust way of avoiding errors with for_each expressions. 2 | locals { 3 | map_of_objects = { 4 | foo = { 5 | name = random_pet.name["foo"].id 6 | attr = "foo1" 7 | } 8 | bar = { 9 | name = random_pet.name["bar"].id 10 | attr = "bar1" 11 | } 12 | } 13 | } 14 | 15 | # Here we simulate values that aren't known until apply time. 16 | resource "random_pet" "name" { 17 | for_each = toset(["foo", "bar"]) 18 | length = 2 19 | } 20 | 21 | resource "terraform_data" "map_of_objects" { 22 | for_each = local.map_of_objects 23 | input = { 24 | name = each.value.name 25 | attr = each.value.attr 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /looping_for_resources_or_modules/map_of_objects/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.6.0" 3 | required_providers { 4 | random = { 5 | source = "hashicorp/random" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /looping_for_resources_or_modules/set_of_objects_antipattern/README.md: -------------------------------------------------------------------------------- 1 | # Set of objects antipattern 2 | 3 | - [back to parent](../) 4 | - [back home](../../) 5 | 6 | Do not use a set of objects to create multiple resources or modules which are almost identical. 7 | Use a map with an arbitrary key instead. 8 | 9 | The reason is that it is common for module consumers to want to construct inputs to a module using data from other resources. 10 | This will often result in a situation where the keys of the map are not known until apply time, which causes Terraform to fail. 11 | 12 | Terraform does not publish this recommendation in public documentation but does provide guidance in the error message: 13 | 14 | > ***When working with unknown values in for_each, it's better to define the map keys statically in your configuration and place apply-time results only in the map values.*** 15 | 16 | ## Example 17 | 18 | 1. Run `terraform init` to initialize the directory. 19 | 1. Run `terraform apply` to create the resources. 20 | 21 | ## Result 22 | 23 | Observe the following error: 24 | 25 | ```text 26 | │ Error: Invalid for_each argument 27 | │ 28 | │ on main.tf line 24, in resource "terraform_data" "map_from_set": 29 | │ 24: for_each = { for obj in local.set_of_objects : obj.name => obj } 30 | │ ├──────────────── 31 | │ │ local.set_of_objects is set of object with 2 elements 32 | │ 33 | │ The "for_each" map includes keys derived from resource attributes that cannot be determined until apply, and so Terraform cannot determine the full set of keys 34 | │ that will identify the instances of this resource. 35 | │ 36 | │ When working with unknown values in for_each, it's better to define the map keys statically in your configuration and place apply-time results only in the map 37 | │ values. 38 | │ 39 | │ Alternatively, you could use the -target planning option to first apply only the resources that the for_each value depends on, and then apply a second time to 40 | │ fully converge. 41 | ``` 42 | 43 | ## Additional nuance 44 | 45 | This antipattern does not always become apparent as an issue during module development due to the iterative nature of module development. 46 | To simulate this issue, we can use the following steps: 47 | 48 | 1. Run `terraform init` to initialize the directory. 49 | 1. Run `terraform apply -target='random_pet.name'` to simulate module development by creating the dependent resource first. 50 | 1. Run `terraform apply` to give the false impression that the module will deploy without error (we know this to be untrue). 51 | 52 | To prevent this, ***Always run `terraform destroy` before running `terraform apply`.*** during module development. 53 | -------------------------------------------------------------------------------- /looping_for_resources_or_modules/set_of_objects_antipattern/main.tf: -------------------------------------------------------------------------------- 1 | resource "random_pet" "name" { 2 | length = 1 3 | } 4 | 5 | # This simulates some data that might be returned from an external data source/lookup 6 | locals { 7 | set_of_objects = toset([ 8 | { 9 | name = "${random_pet.name.id}-foo" 10 | attr = "foo2" 11 | }, 12 | { 13 | name = "${random_pet.name.id}-bar" 14 | attr = "bar2" 15 | }, 16 | ]) 17 | } 18 | 19 | # This errors because the for_each expression contains map keys that are not known in advance. 20 | resource "terraform_data" "map_from_set" { 21 | # This line constructs a map from the set by using one of the fields as the key. 22 | # However this fails because the obj.name values are not known prior to apply. 23 | # This is a common occurrence and should be avoided. 24 | for_each = { for obj in local.set_of_objects : obj.name => obj } 25 | input = { 26 | name = each.key 27 | attr = each.value.attr 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /looping_for_resources_or_modules/set_of_objects_antipattern/terraform.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.6.0" 3 | required_providers { 4 | random = { 5 | source = "hashicorp/random" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /nested_maps/README.md: -------------------------------------------------------------------------------- 1 | # Nested maps 2 | 3 | - [back home](../) 4 | 5 | In complex modules it is a common pattern to have a map of objects, some of the values of the objects themselves are maps. 6 | 7 | For example, your module may create a number of resources, each of which can have a number of sub-resources. 8 | In Azure, a good example of this is resources and role assignments. 9 | 10 | ## Example Input 11 | 12 | ```hcl 13 | variable "my_input" { 14 | type = map(object({ 15 | name = string 16 | my_nested_map = map(object({ 17 | name = string 18 | })) 19 | })) 20 | } 21 | ``` 22 | 23 | ## Patterns 24 | 25 | - [Flatten nested map](./flatten_nested_map/) 26 | -------------------------------------------------------------------------------- /nested_maps/flatten_nested_map/README.md: -------------------------------------------------------------------------------- 1 | # Flatten nested map 2 | 3 | - [back to parent](../) 4 | - [back home](../../) 5 | 6 | In this pattern, we use a local to flatten a nested map into a flattened map. 7 | -------------------------------------------------------------------------------- /nested_maps/flatten_nested_map/main.tf: -------------------------------------------------------------------------------- 1 | # Here we show an input variable with a nested map type. 2 | # We have supplied a default value for the variable. 3 | variable "nested_map" { 4 | type = map(object({ 5 | name = string 6 | child_map = map(object({ 7 | child_name = string 8 | })) 9 | })) 10 | default = { 11 | first_parent = { 12 | name = "first_parent" 13 | child_map = { 14 | first_child = { 15 | child_name = "first_child_from_first_parent" 16 | } 17 | second_child = { 18 | child_name = "second_child_from_first_parent" 19 | } 20 | } 21 | } 22 | second_parent = { 23 | name = "second_parent" 24 | child_map = { 25 | first_child = { 26 | child_name = "first_child_from_second_parent" 27 | } 28 | second_child = { 29 | child_name = "second_child_from_second_parent" 30 | } 31 | } 32 | } 33 | } 34 | } 35 | 36 | # Here we use a local value to flatten the nested map into a single map. 37 | # We use nested for loops to iterate over parent map variable, then again over the child map variable. 38 | # 39 | locals { 40 | flattened_map = { 41 | for item in flatten( # 4. We use the flatten function to flatten the nested list into a single list. 42 | [ 43 | for parent_key, parent_value in var.nested_map : [ # 1. We create a list using a for expression over the parent map variable. Each list item is another list, which we will flatten later. 44 | for child_key, child_value in parent_value.child_map : { # 2. We create a nested list using a for expression over the child map variable. 45 | parent_key = parent_key # 3. Each list item is an object with the following attributes: 46 | child_key = child_key # The parent and child keys are needed to construct the resultant map key. 47 | child_value = child_value # The child value is used to construct the resultant map value. 48 | } 49 | ] 50 | ] 51 | ) : "${item.parent_key}/${item.child_key}" => item.child_value # 5. We use the other half of the for expression on line 41 to construct the resultant map. 52 | } # The key is constructed using the parent and child keys and the value is the child value. 53 | } 54 | 55 | # Here we use a for expression to safely iterate over the flattened map. 56 | resource "terraform_data" "nested_map" { 57 | for_each = local.flattened_map 58 | 59 | input = each.value.child_name 60 | } 61 | -------------------------------------------------------------------------------- /passing_references/README.md: -------------------------------------------------------------------------------- 1 | # Passing references 2 | 3 | - [back home](../) 4 | 5 | This pattern shows how to pass references to resources or modules in a robust way. 6 | This is a very common pattern, typically using the `count` meta-argument to conditionally create resources or modules. 7 | 8 | E.g. passing an Azure resource id for a resource so that we can create a child resource. 9 | 10 | If we use a string variable to pass the resource id, this value may be unknown at the time of the plan. 11 | Therefore the count expression will be unknown, and the plan will fail. 12 | 13 | We should use an object instead, and pass the object's attributes to the child resource. 14 | 15 | Example of how to do this in a robust manner: 16 | 17 | ```terraform 18 | variable "my_resource_id" { 19 | type = object({ 20 | id = string 21 | }) 22 | default = null 23 | } 24 | 25 | # We can safely evaluate the count expression, because the object is never known after apply. 26 | # The `id` value may be unknown, but the parent object is always knowable. 27 | resource "terraform_data" "my_resource" { 28 | count = var.my_resource_id != null ? 1 : 0 29 | input = var.my_resource_id.id 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /passing_references/references_by_object/README.md: -------------------------------------------------------------------------------- 1 | # References by object pattern 2 | 3 | - [back to parent](../) 4 | - [back home](../../) 5 | 6 | In this pattern, we pass references to resources or modules using an object and simulate the is property being known after apply. 7 | This the recommended pattern for passing resource references to resources or modules. 8 | -------------------------------------------------------------------------------- /passing_references/references_by_object/main.tf: -------------------------------------------------------------------------------- 1 | # This simulates the common situation where an incoming 2 | # resource id is used as a condition in a count expression. 3 | # It is not known until after apply. 4 | resource "random_pet" "resource_reference" { 5 | length = 1 6 | } 7 | 8 | # We wrap the resource id in an object to allow it to be 9 | # evaluated as null or not null at plan time. 10 | # (this would be an object variable with `default = null` in a real module) 11 | locals { 12 | resource_id = { 13 | id = random_pet.resource_reference.id 14 | } 15 | } 16 | 17 | # This will work because the count expression 18 | # be evaluated at plan time as the object is known, 19 | # even if the `id` value is not. 20 | resource "terraform_data" "this_works" { 21 | count = local.resource_id != null ? 1 : 0 22 | input = local.resource_id.id 23 | } 24 | -------------------------------------------------------------------------------- /passing_references/references_by_string_antipattern/README.md: -------------------------------------------------------------------------------- 1 | # References by string antipattern 2 | 3 | - [back to parent](../) 4 | - [back home](../../) 5 | 6 | In this antipattern, we pass references to resources or modules by string and simulate the value being known after apply. 7 | -------------------------------------------------------------------------------- /passing_references/references_by_string_antipattern/main.tf: -------------------------------------------------------------------------------- 1 | # This simulates the common situation where an incoming 2 | # resource id is used as a condition in a count expression. 3 | # It is not known until after apply. 4 | resource "random_pet" "resource_reference" { 5 | length = 1 6 | } 7 | 8 | # This will fail because the count expression cannot 9 | # be evaluated at plan time. 10 | resource "terraform_data" "oops" { 11 | count = random_pet.resource_reference.id == null ? 1 : 0 12 | input = "foo" 13 | } 14 | --------------------------------------------------------------------------------