Resource declaration code should be placed together by its scope. If the number of resources defined in a module is not large, or the types are highly cohesive (Eg. there are multiple resources belong to the same domain), they can be defined in the same main.tf
file. If it involves resources from multiple different domains, they should be placed in files which can represent their types. Eg. virtual_machines.tf
, network.tf
, storage.tf
etc. The core scope of a module should be defined in main.tf
.
For the definition of resources in the same file, the resources be depended on come first, after them are the resources depending on others.
Resources have dependencies should be defined close to each other.
For some duplicated or complicated expressions, to increase readability, we encourage authors to extract them out and referenced as independent local
s.
If the expression involves resource
or data
, the local
should be defined right below the most important definition block of the related resource
or data
. Under the same resource
or data
block, at most one local
block can exist, all the local
s defined here should be ranked in alphabetical order. No blank lines between 2 local
s.
We can use count
and for_each
to deploy multiple resources, but the improper use of count
can lead to unpredictable behaviors.
count
can be used only when creating a set of identical or almost identical resources. For example, if we use count
to iterate through a list(string)
, there is a great chance that it could be wrong because modifying the elements in the list will lead to a change of resources order, thus causing unpredictable issues.
Another way of using count
is to create some kind of resources under certain conditions, for example:
resource "azurerm_network_security_group" "this" {
count = local.create_new_security_group ? 1 : 0
name = coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
resource_group_name = var.resource_group_name
location = local.location
tags = var.new_network_security_group_tags
}
There are 3 types of assignment statements in a resource
or data
block: argument, meta-argument and nested block. The argument assignment statement is a parameter followed by =
:
location = azurerm_resource_group.example.location
or:
tags = {
environment = "Production"
}
Nested block is a assignment statement of parameter followed by {}
block:
subnet {
name = "subnet1"
address_prefix = "10.0.1.0/24"
}
Meta-arguments are assignment statements can be declared by all resource
or data
blocks. They are:
count
depends_on
for_each
lifecycle
provider
The order of declarations within resource
or data
blocks is:
All the meta-arguments should be declared on the top of resource
or data
blocks in the following order:
provider
count
for_each
Then followed by:
- required arguments
- optional arguments
- required nested blocks
- optional nested blocks
All ranked in alphabetical order.
These meta-arguments should be declared at the bottom of a resource
block with the following order:
depends_on
lifecycle
The parameters of lifecycle
block should show up in the following order:
create_before_destroy
ignore_changes
prevent_destroy
parameters under depends_on
and ignore_changes
are ranked in alphabetical order.
Meta-arguments, arguments and nested blocked are separated by blank lines.
dynamic
nested blocks are ranked by the name comes after dynamic
, for example:
dynamic "linux_profile" {
for_each = var.admin_username == null ? [] : ["linux_profile"]
content {
admin_username = var.admin_username
ssh_key {
key_data = replace(coalesce(var.public_ssh_key, tls_private_key.ssh[0].public_key_openssh), "\n", "")
}
}
}
This dynamic
block will be ranked as a block named linux_profile
.
Code within a nested block will also be ranked following the rules above.
The meta-arguments below should be declared on the top of a resource
block with the following order:
source
version
count
for_each
blank lines will be used to separate them.
After them will be required arguments, optional arguments, all ranked in alphabetical order.
These meta-arguments below should be declared on the bottom of a resource
block in the following order:
depends_on
providers
Arguments and meta-arguments should be separated by blank lines.
Values in ignore_changes
passed to provider
, depends_on
, lifecycle
blocks are not allowed to use double quotations
For resources have configurable tags
field, tags
should be always exposed to module users through variable
to ensure they are able to set tags
Sometimes we need to ensure that the resources created compliant to some rules at a minimum extent, for example a subnet
has to connected to at least one network_security_group
. The user may pass in a security_group_id
and ask us to make a connection to an existing security_group
, or want us to create a new security group.
Intuitively, we will define it like this:
variable "security_group_id" {
type = string
}
resource "azurerm_network_security_group" "this" {
count = var.security_group_id == null ? 1 : 0
name = coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
resource_group_name = var.resource_group_name
location = local.location
tags = var.new_network_security_group_tags
}
The disadvantage of this approach is if the user create a security group directly in the root module and use the id
as a variable
of the module, the expression which determines the value of count
will contain an attribute
from another resource
, the value of this very attribute
is "known after apply" at plan stage. Terraform core will not be able to get an exact plan of deployment during the "plan" stage.
For this kind of parameters, wrapping with object
type is recommended:
variable "security_group" {
type = object({
id = string
})
default = null
}
The advantage of doing so is encapsulating the value which is "known after apply" in an object, and the object
itself can be easily found out if it's null
or not. Since the id
of a resource
cannot be null
, this approach can avoid the situation we are facing in the first example.
Please use this technique under this use case only.
An example from the community:
resource "azurerm_kubernetes_cluster" "main" {
...
dynamic "identity" {
for_each = var.client_id == "" || var.client_secret == "" ? [1] : []
content {
type = var.identity_type
user_assigned_identity_id = var.user_assigned_identity_id
}
}
...
}
Please refer to the coding style in the example. If you just want to declare some nested block under conditions, please use:
for_each = <condition> ? [<some_item>] : []
The following example shows how to use "${var.subnet_name}-nsg"
when var.new_network_security_group_name
is null
or ""
coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")