Terraform Rules
Project structure rules
Use variables.tf for variables
Severity: 🟥 Won’t affect the state. Can be fixed in the scope of a PR
Description: Typically, when developing a Terraform project, one would separate variables into their own file to improve readability. This is especially valid for modules. Most of the public modules utilize this convention. This file is often named variables.tf. Clear resources separation allows easy & productive code review as well as improves code learning curve.
Example:
Don’t add anything to variables.tf except variables
Severity: 🟥 Won’t affect the state. Can be fixed in the scope of a PR
Description: Typically, when developing a Terraform project, one would separate variables into their own file to improve readability. This is especially valid for modules. Most of the public modules utilize this convention. Having other things defined in a variables.tf file besides variables leads to confusion. Clear resources separation allows easy & productive code review as well as improves code learning curve.
Example:
Use outputs.tf for outputs
Severity: 🟥 Won’t affect the state. Can be fixed in the scope of a PR
Description: Typically, when developing a Terraform project, one would separate outputs into their own file to improve readability. This is especially valid for modules and projects exposing their state to others. Most of the public modules utilize this convention. This file is often named outputs.tf.Clear resources separation allows easy & productive code review as well as improves code learning curve.
Example:
Don’t add anything to outputs.tf except outputs
Severity: 🟥 Won’t affect the state. Can be fixed in the scope of a PR
Description: Typically, when developing a Terraform project, one would separate outputs into their own file to improve readability. This is especially valid for modules and projects exposing their state to others. Most of the public modules utilize this convention. Having other things defined in an outputs.tf file besides outputs leads to confusion. Clear resources separation allows easy & productive code review as well as improves code learning curve.
Example:
Use providers.tf for providers
Severity: 🟥 Won’t affect the state. Can be fixed in the scope of a PR
Description: Typically, when developing a Terraform project, one would separate providers into their own file to improve readability. This is especially valid for root modules. This file is often named providers.tf. Clear resources separation allows easy & productive code review as well as improves code learning curve.
Example:
Don’t add anything to providers.tf except providers
Severity: 🟥 Won’t affect the state. Can be fixed in the scope of a PR
Description: Typically, when developing a Terraform project, one would separate providers into their own file to improve readability. This is especially valid for root modules. Having other things defined in a providers.tf file besides providers leads to confusion. Clear resources separation allows easy & productive code review as well as improves code learning curve.
Example:
Use versions.tf for required providers versions
Severity: 🟥 Won’t affect the state. Can be fixed in the scope of a PR
Description: Typically, when developing a Terraform project, one would separate required_versions into their own file to improve readability. This is especially valid for modules. This file is often named versions.tf
Example:
Only required providers versions should be in versions.tf
Severity: 🟥 Won’t affect the state. Can be fixed in the scope of a PR
Description: Typically, when developing a Terraform project, one would separate variables into their own file to improve readability. This is especially valid for modules. Most of the public modules utilize this convention. Having other things defined in a variables.tf file besides variables leads to confusion.
Example:
Create new terraform project if you have more than 50 resources
Severity: 🟧 Affects the state. Could be fixed in the scope of another PR
Description: Large Terraform projects have pretty significant blast radius, as well as impact productivity because producing a terraform plan becomes a heavy operation that can lead to throttling from a cloud provider’s API and . Especially if you mix foundational resources, such as networking and databases. Splitting existing state into multiple state has to be approached with caution because it could lead to destructive actions such as deletion of resources and ultimately terraform’s state corruption. Ideally you’d account for it during the design face but we understand that it’s not always possible.
Example: For example you manage your organization’s structure via terraform and you have the following files structure:
And between groups.tf, permissions.tf and projects.tf there are 50 different resources managed by terraform, such as folders, projects, IAM groups, etc defined explicitly. Update to this project affects all resources managed and can potentially lead to human-error and accidental deletion. The better structure would look like:
Such a structure would allow to separate projects management from permissions management and as the result improve the potential blast radius. Another example: For example you manage everything related to your networks in a single terraform project:
Such a project can easily go over 50 resources. However splitting them is a significant challenge. So, this rule is not exactly set in stone, and if something is too complicated of a task that overshadows the benefits (especially if for example you make changes to this project once every 3-4 months) then this recommendation could be ignored.
Naming conventions
Use {type}_{name}_{attribute} for output names
Severity: 🟧 Affects the state. Could be fixed in the scope of another PR
Description: When upstream outputs are used, they help developers identify what type & resource they are coming from. This is helpful when you have many things exposed in your module or state. Typical naming strategy is to use the attribute’s name, however more clear one is to also mention resource type without a provider (like instance, instead of aws_instance), and resource name (not to confuse with the attribute your resource could have called “name”).
Example:
Use {name}_{attribute} as output name when referencing a module
Severity: 🟧 Affects the state’s outputs. Potentially affects downstream. Should be treated as an API schema change
Description: When upstream outputs are used, they help developers identify what type & resource they are coming from. This is helpful when you have many things exposed in your module or state. Typical naming strategy is to use the attribute’s name, however more clear one is to also mention module name (like vpc), and module’s output name.
Example:
Always prefer snake case
Severity: 🟧 Will trigger resource/module recreation if applied to existing resources. Potentially affects downstream with outputs.
Description: Snake case is being used by majority of the providers. This is practically the standard for naming conventions of terraform resources and variables/outputs. It’s recommended to use it instead of “-“ and CamelCase.
Example:
Don’t repeat resource type in a resource name
Severity: 🟧 Will trigger resource/module recreation if applied to existing resources.
Description: Terraform resource names are used to identify the purpose or the number of resources being created. Repeating the resource type in any manner doesn’t really help. Such as using group as a name for azurerm_resource_group resource type. If it’s a single resource group you’re creating, the more suitable name is this and if you create many, then names such as these or projects should be used (just as an example).
Example:
Prefer “this” when naming a single terraform resource
Severity: 🟧 Will trigger resource/module recreation if applied to existing resources.
Description: Terraform doesn’t pose any restrictions on resource naming, however for the transparency purposes, especially in the modules, it’s better to name a resource this if it’s the only resource type you’re using, such as VPC.
Example:
Use descriptive names if can’t use this
Severity: 🟧 Will trigger resource/module recreation if applied to existing resources.
Description: Terraform doesn’t pose any restrictions on resource naming, however for the transparency purposes, especially in the modules. this should reference only to a single module/resource you’re creating. If count/for_each are used or you have multiple copies of the same resource type such as aws_subnet then this isn’t a clear enough name.
Example:
Code quality improvements
Meta-arguments for_each, if used, should be the first attribute in a block
Severity: 🟥 Doesn’t affect the state. Could be fixed in the scope of an existing PR
Description: for_each helps to create multiple resources using the same terraform block. Either by using resources/data/modules or dynamic blocks. Listing it first right after { is crucial for the code readability. This is especially useful if you define a map for the aws_subnet attribute in the argument’s value and doesn’t abstract it with a “local” or by other means. This way the attribute stands out and it’s easy to review.
Example:
Meta-argument count, if used, should be the first attribute in a block
Severity: 🟥 Doesn’t affect the state. Could be fixed in the scope of an existing PR
Description: count helps to create multiple resources using the same terraform block. Mostly used for conditional resources creation. Listing it first right after { is crucial for the code readability. This way the attribute stands out and it’s easy to review.
Example:
Meta-argument depends_on should be the last of the resource’s attributes
Severity: 🟥 Doesn’t affect the state. Could be fixed in the scope of an existing PR
Description: Placing depends_on as the last argument in a terraform resource configuration ensures readability and clear separation from the resource-specific arguments (by adding a single empty line depends_on).
Example:
Meta-block lifecycle should be the last in a resource definition
Severity: 🟥 Doesn’t affect the state. Could be fixed in the scope of an existing PR
Description: Placing lyfecycle as the last block in a terraform resource configuration ensures readability and clear separation from the resource-specific blocks & attributes (by adding a single empty line before lyfecycle).
Example:
Group resources using for_each
Severity: 🟧 Will trigger resource/module recreation if applied to existing resources.
Description: If you copy paste the same resource & slightly change the value 4+ times, it’s better to group them all using for_each meta argument. This will ensure DRYness of the code without going extreme.
Example:
Define both description & type for a variable
Severity: 🟥 Doesn’t affect the state. Could be fixed in the scope of an existing PR
Description: This rule is by far the most essential, because your variables control what’s going on with the state. Even something obvious as “environment”, can and will be interpreted in many different ways: “is it stage, or staging?” or “is it included into resources prefixes?” that’s what description is for. Variable’s type in addition to being useful in “documentation” also helps terraform to throw meaningful errors if something doesn’t match.
Example:
Follow specific order for arguments in variables
Severity: 🟥 Doesn’t affect the state. Could be fixed in the scope of an existing PR
Description: This is essentially your state’s API documentation. It doesn’t mean you have to specify all of them, but if you do, they should be in the listed order to make sure it’s readable in the long run.
Example:
Set description for your output
Severity: 🟥 Doesn’t affect the state. Could be fixed in the scope of an existing PR
Description: This is essentially your state’s API documentation. Even something obvious as “environment”, can and will be interpreted in many different ways: “should I used it as a prefix everywhere?” that’s what description is for.
Example:
Don’t output plain strings
Severity: 🟥 Doesn’t affect the state. Could be fixed in the scope of an existing PR
Description: By using plain strings as values for your outputs you potentially disrupt dependncy graph. Consider moving the string to either local, variable, or retrieve it dynamically from resources you have.
Example: