From ae1ccb8d4c54979c2ea6f6c8846811fea452b2e2 Mon Sep 17 00:00:00 2001 From: Brian McClain Date: Fri, 22 Mar 2024 09:26:14 -0400 Subject: [PATCH] Update style guide --- website/data/language-nav-data.json | 7 +- website/docs/language/style.mdx | 725 +++++++++++++++++++++++++ website/docs/language/syntax/style.mdx | 70 --- 3 files changed, 730 insertions(+), 72 deletions(-) create mode 100644 website/docs/language/style.mdx delete mode 100644 website/docs/language/syntax/style.mdx diff --git a/website/data/language-nav-data.json b/website/data/language-nav-data.json index 2d8cf205ae..60f088fdf7 100644 --- a/website/data/language-nav-data.json +++ b/website/data/language-nav-data.json @@ -26,8 +26,7 @@ { "title": "JSON Configuration Syntax", "path": "syntax/json" - }, - { "title": "Style Conventions", "path": "syntax/style" } + } ] }, { @@ -1089,6 +1088,10 @@ "title": "Upgrading to Terraform v1.7", "path": "upgrade-guides" }, + { + "title": "Style Guide", + "path": "style" + }, { "title": "v1.x Compatibility Promises", "path": "v1-compatibility-promises" diff --git a/website/docs/language/style.mdx b/website/docs/language/style.mdx new file mode 100644 index 0000000000..cfafca47ef --- /dev/null +++ b/website/docs/language/style.mdx @@ -0,0 +1,725 @@ +--- +page_title: Style Guide - Configuration Language +description: >- + Learn recommended style conventions for the Terraform language and + workflows. +--- + +# Style Guide + +The flexibility of Terraform's configuration language gives you many options to choose from as you write your code, structure your directories, and test your configuration. We recommend that your organization adopt a style guide to apply to your Terraform development. While some decisions depend on your organization’s needs or personal preferences, there are some common patterns that we suggest you adopt. These style suggestions will help keep your Terraform code consistent, legible, scalable, and maintainable. + +This article discusses best practices, as well as some considerations to keep in mind as you develop your organization's style guide. The article is split into two sections. The first section covers code style recommendations, such as formatting and resource organization. The second section covers operations and workflow recommendations, such as lifecycle management through meta-arguments, versioning, and sensitive data management. + +## Code Style + +Writing Terraform code in a consistent style makes it easier to read and maintain. This section reviews code style patterns that we recommend that you adopt. + +- Run `terraform fmt` and `terraform validate` before committing your code to source control. +- Use `#` for single-line comments. +- Use nouns for resource names and do not include the resource type in the name. +- Use underscores to separate multiple words in a resource name. Wrap resource type and name in double quotes in your resource definition. +- Define dependent resources after those they depend on so that your code builds on itself. +- Include a type and description for every variable. +- Include a default value for optional variables. +- Include a value and description for every output. +- Use descriptive nouns to name your input variables, outputs, and local values and use underscores to separate multiple words. +- Avoid overuse of variables and local values. +- Always include a default provider configuration. +- Use `count` and `for_each` sparingly. + +## Syntax conventions + +The Terraform parser allows you some flexibility in how you lay out the elements in your configuration files, but the Terraform language also has some idiomatic style conventions which we recommend users always follow for consistency between files and modules written by different teams. + +- Indent two spaces for each nesting level +- When multiple arguments with single-line values appear on consecutive lines at the same nesting level, align their equals signs: + + + + ```hcl + ami = "abc123" + instance_type = "t2.micro" + ``` + + + +- When both arguments and blocks appear together inside a block body, place all of the arguments together at the top and then place nested blocks below them. Use one blank line to separate the arguments from the blocks. +- Use empty lines to separate logical groups of arguments within a block. +- For blocks that contain both arguments and "meta-arguments" (as defined by the Terraform language semantics), list meta-arguments first and separate them from other arguments with one blank line. Place meta-argument blocks last and separate them from other blocks with one blank line. Refer to [dynamic resource count](#dynamic-resource-count) for more information on meta-arguments. + + + + ```hcl + resource "aws_instance" "example" { + # meta-argument first + count = 2 + + ami = "abc123" + instance_type = "t2.micro" + + network_interface { + # ... + } + + # meta-argument block last + lifecycle { + create_before_destroy = true + } + } + ``` + + + +- Top-level blocks should always be separated from one another by one blank line. Nested blocks should also be separated by blank lines, except when grouping together related blocks of the same type (like multiple `provisioner` blocks in a resource). +- Avoid grouping multiple blocks of the same type with other blocks of a different type, unless the block types are defined by semantics to form a family. (For example: `root_block_device`, `ebs_block_device` and `ephemeral_block_device` on `aws_instance` form a family of block types describing AWS block devices, and can therefore be grouped together and mixed.) + +## Code formatting + +The `terraform fmt` command helps ensure your Terraform code formatting is consistent by applying a subset of the [Terraform language style conventions](https://developer.hashicorp.com/terraform/language/syntax/style), such as those described in the [syntax style recommendations](#syntax-style). When you run this command, Terraform modifies your code to remove trailing whitespace, align arguments, and fix indentation. By default, the `terraform fmt` command will only modify your Terraform code in the directory that you execute it in, but you can include the `-recursive` flag to modify code in all subdirectories as well. + +We recommend that you run `terraform fmt` before each commit to source control. You can use mechanisms such as [Git pre-commit hooks](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) to automatically run this command each time you commit your code. + +If your Microsoft VS Code or another development environment or text editor that supports the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/), we recommend that you use the [Terraform Language Server](https://github.com/hashicorp/terraform-ls). The Terraform Language Server enables syntax highlighting, syntax validation, and automatic code formatting. + +## Code validation + +The `terraform validate` command checks that your code is syntactically valid and internally consistent, regardless of any provided variables or existing state. The `validate` command does not check if argument values are valid for a specific provider, but it will verify that they are the correct type. + +The `terraform validate` command is safe to run automatically and frequently. You can configure your text editor to run this command as a post-save check, define it as a pre-commit hook in the Git repository, or run it as a step in a CI/CD pipeline. + +For more information, refer to the [Terraform `validate` documentation](https://developer.hashicorp.com/terraform/cli/commands/validate). + +## File names + +We recommend the following file naming conventions: + +- `backend.tf`: Contains your [backend configuration](https://developer.hashicorp.com/terraform/language/settings/backends/configuration). You can define multiple `terraform` blocks in your configuration to separate your backend configuration from your Terraform and provider versioning configuration. +- `main.tf`: Contains all resource and data source blocks. +- `outputs.tf`: Contains all output blocks in alphabetical order. +- `providers.tf`: Contains all `provider` blocks and configuration. +- `terraform.tf`: Contains a single `terraform` block which defines your `required_version` and `required_providers`. +- `variables.tf`: Contains all variable blocks in alphabetical order. +- `override.tf`: Contains override definitions for your configuration. + +Terraform treats the file named `override.tf` and all files ending with `_override.tf` as a special use case. Terraform will load all other `.tf` files first, and then use the configuration in the override files to modify those resources. Use these files sparingly and add comments to the original resource definitions, as these overrides make your code harder to read. Refer to the [override files](https://developer.hashicorp.com/terraform/language/files/override) documentation for more information. + +As your codebase grows, limiting it to just these files can become difficult to maintain. If your code becomes hard to navigate due to its size, we recommend that you split the resources and data sources into separate files by logical groups. For example, if your web application requires networking, storage, and compute resources, you might create the following files: + +- `network.tf`: VPC, subnets, load balancers, and all other networking resources +- `storage.tf`: Object storage and related permissions configuration +- `compute.tf`: Compute instances + +No matter how you decide to split your code, it should be immediately clear where a maintainer can find a specific resource or data source definition. + +As your configuration grows, you may need to separate it into multiple state files. The HashiCorp Well-Architected Framework provides more guidance about [workspace structure and scope](https://developer.hashicorp.com/well-architected-framework/operational-excellence/operational-excellence-workspaces-projects#workspace-structure). + +## Linting and static code analysis + +While Terraform does not have a built-in linter, many organizations find it helpful to adopt a linting tool into their Terraform development cycle. You can use a linter, such as [TFLint](https://github.com/terraform-linters/tflint), to enforce your organization's own coding best practices. A linter will use static code analysis to compare your Terraform code against a set of rules. Most linters ship with a default set of rules, but also allow you to write your own. + +## Comments + +While you should try to make your code easy to read, comments let you clarify any complexity for other readers. + +We recommend you use `#` for all comments. For multi-line comments, we recommend that you prefix each line with `#`. The `//` and `/* */` comment syntaxes are not considered idiomatic, but Terraform supports them to remain backwards-compatible with earlier versions of HCL. + + + +```hcl + +# Each tunnel is responsible for encrypting and decrypting traffic exiting +# and leaving its associated gateway. +resource "google_compute_vpn_tunnel" "tunnel1" { + ## ... +``` + + + +## Resource naming + +Every resource must have a unique name. For consistency and readability, we recommend that you use a descriptive noun and separate words with underscores. Do not include the resource type in the resource identifier since the resource address already includes it. Wrap the resource type and name in double quotes. + +❌Bad: + + + +```hcl +resource aws_instance webAPI-aws-instance {...} +``` + + + +✅Good: + + + +```hcl +resource "aws_instance" "web_api" {...} +``` + + + +## Resource order + +The order of the resources and data sources in your code does not affect how Terraform builds them, so organize your resources for readability. Terraform will determine the creation order based on cross-resource dependencies. + +How you order your resources largely depends on the size and complexity of your code, but we recommend defining data sources alongside the resources that reference them. For readability, your Terraform code should “build on itself” — you should define a data source before the resource that references it. + +The following example defines an `aws_instance` that relies on two data sources, `aws_ami` and `aws_availability_zone`. For readability and continuity, it defines the data sources before the `aws_instance` resource. + + + +```hcl +data "aws_ami" "web" { + ##... +} + +data "aws_availability_zones" "available" { + ##... +} + +resource "aws_instance" "web" { + ami = data.aws_ami.web.id + availability_zone = data.aws_availability_zones.available.names[0] + ##... +} +``` + + + +We recommend that you order the parameters of each resource in the following order: + +1. If your resource uses one, the `count` or `for_each` parameter. +1. Your resource-specific non-block parameters. +1. Your resource-specific block parameters. +1. If required, a `lifecycle` block. +1. If required, the `depends_on` parameter. + +## Variables and outputs + +Input variables let you customize Terraform modules. While variables make your modules more flexible, overusing variables can make code difficult to read. When deciding whether to expose a variable for a resource setting, consider whether that parameter will change between deployments. + +We recommend that you use the following order when you define your variable parameters: + +1. Type +1. Description +1. Default (optional) +1. Sensitive (optional) +1. [Validation blocks](https://developer.hashicorp.com/terraform/language/values/variables#custom-validation-rules) + +Define a `type` and a `description` for every variable. If the variable is optional, define a reasonable `default`. For sensitive variables, such as passwords and private keys, set the `sensitive` parameter to `true`. Remember that Terraform will still store this value in plain text in its state, but it will not display it when you run `terraform plan` or `terraform apply`. Refer to [secrets management](#secrets-management) for more information on how to securely handle sensitive values. + +[Input variable validation](https://developer.hashicorp.com/terraform/language/values/variables#custom-validation-rules) lets you create additional rules for your variable values in addition to Terraform's type validation. We recommend that you only use variable validation when your variable values have uniquely restrictive requirements. For example, if your Terraform code requires two web instances to work, you can add a `validation` block to enforce this, as shown below. + + + +```hcl +variable "web_instance_count" { + type = number + description = "Number of web instances to deploy. This application requires at least two instances." + + validation { + condition = var.aws_instance_count > 1 + error_message = "This application requires at least two web instances." + } +} +``` + + + +Output values let you expose data about your infrastructure on the command line and make it easy to reference in other Terraform configurations. Like you would for variables, provide a `description` for each output. + +We recommend that you use the following order for your output parameters: + +1. Description +1. Value +1. Sensitive (optional) + +Every variable and output requires a unique name. For consistency and readability, we recommend that you use a descriptive noun and separate words with underscores. + + + + ```hcl +variable "db_disk_size" { + type = number + description = "Disk size for the API database" + default = 100 +} + +variable "db_password" { + type = string + description = "Database password" + sensitive = true +} + +output "web_public_ip" { + description = "Public IP of the web instance" + value = aws_instance.web.public_ip +} +``` + + + +## Variable values + +Terraform provides multiple ways to define values for your variables, and follows an order of precedence if you provide multiple values for the same variable. Terraform loads variables in the following order, with later sources taking precedence over earlier ones: + +- Environment variables. +- The `terraform.tfvars` file. +- The `terraform.tfvars.json` file. +- Any `*.auto.tfvars` or `*.auto.tfvars.json` files, processed in lexical order of their filenames. +- Any `-var` and `-var-file` options on the command line, in the order they are provided. This includes variables defined in a Terraform Cloud workspace or variable set. Refer to [precedence with priority variable sets](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/variables#precedence-with-priority-variable-sets) for more information. + +For sensitive variables, we recommend using environment variables or a secrets management service like HashiCorp Vault. This helps you avoid writing the values to a file and accidentally committing it to version control. + +## Local values + +Local values are useful when you need to reference an [expression](https://developer.hashicorp.com/terraform/language/expressions) or value multiple times. We recommend that you use local values in moderation, as overuse can make your code harder to read for future maintainers. Like resource, variable, and output names, we recommend local names be descriptive nouns and use underscores to separate multiple words. + +For example, you may want to append the region and environment (for example, development or test) to every web instance name. The following local value creates the suffix with these values. + + + +```hcl +locals { + name_suffix = "${var.region}-${var.environment}" +} +``` + + + +Then you can reference the local value in your resource definition. + + + +```hcl +resource "aws_instance" "web" { + ami = data.aws_ami.ubuntu.id + instance_type = "t3.micro" + + tags = { + Name = "web-${local.name_suffix}" + } +} +``` + + + +In general, there are two places we recommend that you define your local values: + +If you reference the local value in multiple locations, we recommend you define it in a file named `locals.tf`. +If the local is specific to a file, we recommend that you define it at the top of the file. + +For more information, refer to the [local values documentation](https://developer.hashicorp.com/terraform/language/values/locals) and the [Simplify Terraform configuration with locals](https://developer.hashicorp.com/terraform/tutorials/configuration-language/locals) tutorial. + +## Provider aliasing + +Provider aliasing lets you define multiple `provider` blocks for the same Terraform provider. For example, this is useful if you are using a provider that can only target a single region, but you want to deploy to multiple regions. The `provider` meta-argument for resources and the `providers` meta-argument for modules lets you specify which provider to use. + + + +```hcl +# The default provider configuration; resources that begin with `aws_` will use +# it as the default, and it can be referenced as `aws`. +provider "aws" { + region = "us-east-1" +} + +# Additional provider configuration for west coast region; resources can +# reference this as `aws.west`. +provider "aws" { + alias = "west" + region = "us-west-2" +} +``` + + + + + +```hcl +resource "aws_instance" "example" { + # This "provider" meta-argument selects the AWS provider + # configuration whose alias is "west", rather than the + # default configuration. + provider = aws.west + + # ... +} + + +module "aws_vpc" { + source = "./aws_vpc" + providers = { + aws = aws.west + } +} +``` + + + +Any provider block that does not define the `alias` parameter is considered the default provider configuration. We recommend that you always include a default provider configuration and that you define all of your providers in the same file. If you define multiple instances of a provider, define the default instance first. For non-default providers, define the `alias` as the first parameter of the `provider` block. + +## Dynamic resource count + +The `for_each` and `count` meta-arguments let you create multiple resources from a single `resource` block depending on run-time conditions. You can use these meta-arguments to make your code flexible and remove the need to duplicate resource blocks. If your instances are almost identical, `count` is appropriate. If some of their arguments need distinct values that cannot be directly derived from an integer, it is safer to use `for_each`. + +The `for_each` meta-argument accepts a `map` or `set` value, and Terraform will create an instance of that resource for each element in the value you provide. In the following example, Terraform creates an `aws_instance` for each of the strings defined in the `web_instances` variable: "ui", "api", "db" and "metrics". The example uses `each.key` to give each instance a unique name. The `web_private_ips` output uses a [for expression](https://developer.hashicorp.com/terraform/language/expressions/for) to create a map of instance names and their private IP addresses, while the `web_ui_public_ip` output addresses the instance with the key "ui" directly. + + + +```hcl +variable "web_instances" { + type = list(string) + description = "A list of instances for the web application" + default = [ + "ui", + "api", + "db", + "metrics" + ] +} + +resource "aws_instance" "web" { + for_each = toset(var.web_instances) + + ami = data.aws_ami.webapp.id + instance_type = "t3.micro" + + tags = { + Name = "web_${each.key}" + } +} + +output "web_private_ips" { + description = "Private IPs of the web instances" + value = { + for k, v in aws_instance.web : k => v.private_ip + } +} + +output "web_ui_public_ip" { + description = "Public IP of the web UI instance" + value = aws_instance.web["ui"].public_ip +} +``` + + + +The above example will create the following output: + + + +``` +web_private_ips = { + "api" = "172.31.25.29" + "db" = "172.31.18.33" + "metrics" = "172.31.26.112" + "ui" = "172.31.20.142" +} + +web_ui_public_ip = "18.216.208.182" +``` + + + +The `count` meta-argument lets you create multiple instances of a resource from a single resource block. In the following example, if the value of `var.web_instance_count` is 2, the code will create two EC2 instances, one named `web_0` and the other named `web_1`. + + + +```hcl +resource "aws_instance" "web" { + count = var.web_instance_count + + ami = data.aws_ami.ubuntu.id + instance_type = "t3.micro" + + tags = { + Name = "web_${count.index}" + } +} +``` + + + +A common practice to conditionally create resources is to use the `count` meta-argument with a [conditional expression](https://developer.hashicorp.com/terraform/language/expressions/conditionals). In the following example, Terraform will only create the `aws_instance` if `var.enable_metrics` is `true`. + + + +```hcl +variable "enable_metrics" { + description = "True if the metrics server should be deployed" + type = bool + default = true +} + +resource "aws_instance" "web" { + count = var.enable_metrics ? 1 : 0 + + ami = data.aws_ami.webapp.id + instance_type = "t3.micro" + ##... +} +``` + + + +We recommend that you use meta-arguments in moderation since you sacrifice explicit readability for an implicit understanding of how the configuration works. If the effect of the meta-argument is not immediately obvious, add a comment to make it clear to the reader. + +To learn more about these meta-arguments, refer to the [`for_each`](https://developer.hashicorp.com/terraform/language/meta-arguments/for_each) and [`count`](https://developer.hashicorp.com/terraform/language/meta-arguments/count) documentation. + +## .gitignore + +Define a `.gitignore` file for your repository to exclude files that you should not publish to version control, such as your state file. + +Always commit: + +- All Terraform code files +- Your `.terraform.lock.hcl` [dependency lock file](https://developer.hashicorp.com/terraform/language/files/dependency-lock) +- A `.gitignore` file that excludes the files listed below +- A `README.md` to describe the code, input variables, and outputs + +Exclude: + +- Your `terraform.tfstate` state file, including `terraform.tfstate.*` backup state files. +- Your `.terraform.tfstate.lock.info` file. Terraform creates and deletes this file automatically when you run a `terraform apply` command and contains info about your [state lock](https://developer.hashicorp.com/terraform/language/state/locking) +- Your `.terraform` directory, where Terraform downloads providers and child modules. +[Saved plan files](https://developer.hashicorp.com/terraform/cli/commands/plan#out-filename) that you create when you include the `-out` flat when you run `terraform plan`. +- Any `.tfvars` files that contain sensitive information. + +For an example, refer to [GitHub's Terraform .gitignore file](https://github.com/github/gitignore/blob/main/Terraform.gitignore). + +## Workflow Style + +Adopting a consistent design style can help with operational stability, security, and make it easier to enforce best practices. The second section of this article covers the design style patterns we recommend that you adopt. + +- Pin your Terraform, provider, and module versions. +- Name your module repositories using this three-part name `terraform--` when using the Terraform Cloud registry. +- Store local modules at `./modules/`. +- Use the [tfe_outputs](https://registry.terraform.io/providers/hashicorp/tfe/latest/docs/data-sources/outputs) data source or provider-specific data sources to share state between two state files. +- Use a secrets manager such as HashiCorp Vault or [dynamic provider credentials](https://developer.hashicorp.com/terraform/tutorials/cloud/dynamic-credentials) when possible so that you do not write secrets to your Terraform code. +- Write [tests](https://developer.hashicorp.com/terraform/language/tests) for your modules. +- Use a linter such as [TFLint](https://github.com/terraform-linters/tflint) to enforce your organization's own coding best practices. +- Use [policy enforcement](https://developer.hashicorp.com/terraform/cloud-docs/policy-enforcement) on Terraform Cloud to build guardrails for what your developers are and are not allowed to do. + +## Version pinning + +To prevent providers and modules upgrades from introducing unintentional changes to your infrastructure, use version pinning. + +For your providers, define the [required_providers block](https://developer.hashicorp.com/terraform/language/providers/requirements#requiring-providers) inside your `terraform` block to specify which provider version to use. Terraform [version constraints](https://developer.hashicorp.com/terraform/language/providers/requirements#version-constraints) support a range of accepted versions. We recommend that you pin your module to a specific major and minor version as shown in the example below to ensure stability. You can use looser restrictions if you are certain that the module does not introduce breaking changes outside of major version updates. + +We also recommend that you set a minimum required version of the Terraform binary by setting the `required_version` in your `terraform` block. This ensures that other operators use a version of Terraform that has all of the features your configuration relies on. + + + +```hcl +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "5.34.0" + } + } + + required_version = ">= 1.7" +} +``` + + + +The above example pins the version of the `hashicorp/aws` provider to version `5.34.0`, and requires that operators use Terraform `1.7` or newer. + +For modules sourced from a registry, use the `version` parameter in the `module` block to pin the version. For local modules, Terraform ignores the `version` parameter. + + + +```hcl +module "vault_starter" { + source = "hashicorp/vault-starter/aws" + version = "1.0.0" + ##... +} +``` + + + +## Module repository names + +The Terraform registry requires that repositories match a naming convention for all modules that you publish to the registry. Module repositories must use this three-part name `terraform--`, where `` reflects the type of infrastructure the module manages and `` is the main provider the module uses. The `` segment can contain additional hyphens, for example, `terraform-google-vault` or `terraform-aws-ec2-instance`. + +## Module structure + +Terraform modules allow you to create self-contained, reusable pieces of infrastructure-as-code. You can use modules to abstract away complex code and create multiple resources using a single declaration. + +Use modules to group together logically related resources. For example: + +- A networking module that defines a VPC, along with its subnets, gateway, and security groups. +- An application module defining all resources required for each deployment. This stack could include web servers, databases, storage, and supported networking. + +Review the [module creation recommended pattern documentation](https://developer.hashicorp.com/terraform/tutorials/modules/pattern-module-creation) and [standard module structure](https://developer.hashicorp.com/terraform/language/modules/develop/structure) for guidance on how to structure your modules. + +## Local modules + +Local modules are sourced from local disk rather than a remote module registry. We recommend publishing your modules to a module registry, such as the [Terraform Cloud private module registry](https://developer.hashicorp.com/terraform/cloud-docs/registry). A module registry lets you share modules across your organization for use by other developers. If you cannot use a module registry, organizing part of your code in local modules still reduces the burden of maintaining and updating your code. + +We recommend that you define child modules in the `./modules/` directory. + +## Repository structure + +How you structure your modules and Terraform configuration in version control significantly impacts versioning and operations. In general, we recommend that you store your actual infrastructure configuration separate from your module code. + +To structure your modules, ee recommend that you store each module in an individual repository. This lets you independently version each module and makes it easier to publish your modules in the private Terraform registry. + +To structure your infrastructure configuration, we recommend that you create repositories that group together logically-related resources. For example, a web application that requires compute, networking, and database resources would be grouped in a single repository. By separating your resources into groups, you limit the blast radius of changes from one configuration affecting another. + +Another approach is to group all modules and infrastructure configuration into a single repository. For example, a monorepo may define a collection of local modules for each component of the infrastructure stack, and deploy them in the root module. + + + +``` +. +├── modules +│ ├── function +│ │ ├── main.tf # contains aws_iam_role, aws_lambda_function +│ │ ├── outputs.tf +│ │ └── variables.tf +│ ├── queue +│ │ ├── main.tf # contains aws_sqs_queue +│ │ ├── outputs.tf +│ │ └── variables.tf +│ └── vpc +│ ├── main.tf # contains aws_vpc, aws_subnet +│ ├── outputs.tf +│ └── variables.tf +├── main.tf +├── outputs.tf +└── variables.tf +``` + + + +Monolithic repositories can complicate your CI/CD automation: since any code change triggers a deployment that operates on your entire repository, your workflow must target only the modified directories. You also lose the granular access control, since anyone with repository access can modify any file in it. + +Monolithic repositories make the repository the single source of truth, where the repository contains every change made to the infrastructure. This lowers the maintenance burden by reducing the number of repositories you must manage and track. If your organization requires a monolithic approach, Terraform Cloud and Terraform Enterprise let you scope a workspace to a specific directory in a repository, simplifying your workflows. + +## Branching strategy + +To collaborate on your Terraform code, we recommend using the [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow). This approach uses short-lived branches to help your team quickly review, test, and merge changes to your code. To make changes to your code, you would: + +1. Create a new branch from your main branch +1. Write, commit, and push your changes to the new branch +1. Create a pull request +1. Review the changes with your team +1. Merge the pull request +1. Delete the branch + +Terraform Cloud and Terraform Enterprise can run [speculative plans for pull requests](https://developer.hashicorp.com/terraform/cloud-docs/run/ui#speculative-plans-on-pull-requests). These speculative plans run automatically when you create or update a pull request, and you can use them to see the effect that your changes will have on your infrastructure before you merge them to your main branch. When you merge your pull request, Terraform Cloud will start a new run to apply these changes. + +## Multiple environments + +We recommend that your repository's `main` branch be the source of truth for all environments. For Terraform Cloud and Terraform Enterprise users, we recommend that you use separate workspaces for each environment. For larger codebases, we recommend that you split your resources across multiple workspaces to prevent large state files and limit unintended consequences from changes. For example, you could structure your code as follows: + + + +``` +. +├── compute +│ ├── main.tf +│ ├── outputs.tf +│ └── variables.tf +├── database +│ ├── main.tf +│ ├── outputs.tf +│ └── variables.tf +└── networking + ├── main.tf + ├── outputs.tf + └── variables.tf +``` + + + +In this scenario, you would create three workspaces per environment. For example, your production environment would have a "prod-compute", "prod-database", and "prod-networking" workspace. Read more about [Terraform workspace and project best practices](https://developer.hashicorp.com/well-architected-framework/operational-excellence/operational-excellence-workspaces-projects). + +If you do not use Terraform Cloud or Terraform Enterprise, we recommend that you use modules to encapsulate your configuration, and use a directory for each environment. The configuration in each of these directories would call the local modules, each with parameters specific to their environment. This also lets you maintain separate variable and backend configurations for each environment. + + + +``` +├── modules +│ ├── compute +│ │ └── main.tf +│ ├── database +│ │ └── main.tf +│ └── network +│ └── main.tf +├── dev +│ ├── backend.tf +│ ├── main.tf +│ └── variables.tf +├── prod +│ ├── backend.tf +│ ├── main.tf +│ └── variables.tf +└── staging + ├── backend.tf + ├── main.tf + └── variables.tf +``` + + + +## State sharing + +Since your state contains sensitive information, avoid sharing full state files when possible. + +If you use Terraform Cloud or Terraform Enterprise and a resource is managed in a separate workspace, use the [`tfe_outputs`](https://registry.terraform.io/providers/hashicorp/tfe/latest/docs/data-sources/outputs) data source to access outputs across workspaces. + +If you do not use Terraform Cloud or Terraform Enterprise but still need to reference data about other infrastructure resources, use data sources to query the provider. For example, you can use the [`aws_instance` data source](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/instance) to look up an AWS EC2 instance by its ID or tags + +If neither of these options are viable, you can use the [`terraform_remote_state` data source](https://developer.hashicorp.com/terraform/language/state/remote-state-data). If you use this data source with Terraform Cloud or Terraform Enterprise, configure which workspaces have access to your state using the principle of least privilege. Refer to [remote state sharing](https://developer.hashicorp.com/terraform/cloud-docs/workspaces/settings#remote-state-sharing) for more information. + +## Secrets management + +If you do not configure remote state storage, the Terraform CLI stores the entire state in plaintext on the local disk. State can include sensitive data, such as passwords and private keys. + +Terraform Cloud and Terraform Enterprise provide state encryption through HashiCorp Vault. + +- When using Terraform Enterprise, we recommend that you define and enforce a Sentinel policy to prevent use of the `local_exec` provisioner or external data sources. +- When using Terraform Cloud or Terraform Enterprise, use [dynamic provider credentials](https://developer.hashicorp.com/terraform/tutorials/cloud/dynamic-credentials) to avoid using long-lived static credentials. + + +If you use Terraform Community Edition, we recommend the following: + +- Configure provider credentials using provider-specific environment variables. +- Access secrets from a secrets management system such as HashiCorp Vault with the [Terraform Vault provider](https://registry.terraform.io/providers/hashicorp/vault/latest/docs). Be aware that Terraform will still write these values in plaintext to your state file. + +If you use a custom CI/CD pipeline, review your CI/CD tool's best practices for managing sensitive values. Most tools let you access sensitive values as environment variables. For more information, refer to your CI/CD documentation. + +- [Using secrets in GitHub Actions](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions) +- [Gitlab pipeline security](https://docs.gitlab.com/ee/ci/pipelines/pipeline_security.html) +- [Integrate Vault into your CI/CD pipeline](https://developer.hashicorp.com/well-architected-framework/security/security-cicd-vault) + +## Integration and unit testing + +Terraform tests let you validate your modules and catch breaking changes. We recommend that you write tests for your Terraform modules and run them just as you run your tests for your application code, such as pre-merge check in your pull requests or as a prerequisite step in your automated CI/CD pipeline. + +Tests differ from validation methods such as variable validation, preconditions, postconditions, and check blocks. These features focus on verifying the infrastructure deployed by your code, while tests validate the behavior and logic of your code itself. For more information, refer to the [Terraform test documentation](https://developer.hashicorp.com/terraform/language/tests) and the [Write Terraform tests tutorial](https://developer.hashicorp.com/terraform/tutorials/configuration-language/test). + +## Policy + +Policies are rules that Terraform Cloud enforces on Terraform runs. You can use policies to validate that the Terraform plan complies with your organization's best practices. For example, you can write policies that: + +- Limit the size of a web instance +- Check for required resource tags +- Block deployments on Fridays +- Enforce security configuration and cost management + +We recommend that you store polices in a separate VCS repository from your Terraform code. + +For more information, refer to the [policy enforcement documentation](https://developer.hashicorp.com/terraform/cloud-docs/policy-enforcement), as well as the [enforce policy with Sential](https://developer.hashicorp.com/terraform/tutorials/policy) and [detect infrastructure drift and enforce OPA policies](https://developer.hashicorp.com/terraform/tutorials/cloud/drift-and-opa) tutorials. +## Next steps +This article introduces some considerations to keep in mind as you standardize your organization’s Terraform style guidelines. Enforcing a standard way of writing and organizing your Terraform code across your organization ensures that it is readable, maintainable, and shareable. + +The [HashiCorp Well-Architected Framework](https://developer.hashicorp.com/well-architected-framework) provides in-depth information on [Terraform adoption and maturity](https://developer.hashicorp.com/well-architected-framework/operational-excellence/operational-excellence-terraform-maturity). diff --git a/website/docs/language/syntax/style.mdx b/website/docs/language/syntax/style.mdx deleted file mode 100644 index 8cb0366e8e..0000000000 --- a/website/docs/language/syntax/style.mdx +++ /dev/null @@ -1,70 +0,0 @@ ---- -page_title: Style Conventions - Configuration Language -description: >- - Learn recommended formatting conventions for the Terraform language and a - command to automatically enforce them. ---- - -# Style Conventions - -The Terraform parser allows you some flexibility in how you lay out the -elements in your configuration files, but the Terraform language also has some -idiomatic style conventions which we recommend users always follow -for consistency between files and modules written by different teams. -Automatic source code formatting tools may apply these conventions -automatically. - --> **Note**: You can enforce these conventions automatically by running [`terraform fmt`](/terraform/cli/commands/fmt). - -* Indent two spaces for each nesting level. - -* When multiple arguments with single-line values appear on consecutive lines - at the same nesting level, align their equals signs: - - ```hcl - ami = "abc123" - instance_type = "t2.micro" - ``` - -* When both arguments and blocks appear together inside a block body, - place all of the arguments together at the top and then place nested - blocks below them. Use one blank line to separate the arguments from - the blocks. - -* Use empty lines to separate logical groups of arguments within a block. - -* For blocks that contain both arguments and "meta-arguments" (as defined by - the Terraform language semantics), list meta-arguments first - and separate them from other arguments with one blank line. Place - meta-argument blocks _last_ and separate them from other blocks with - one blank line. - - ```hcl - resource "aws_instance" "example" { - count = 2 # meta-argument first - - ami = "abc123" - instance_type = "t2.micro" - - network_interface { - # ... - } - - lifecycle { # meta-argument block last - create_before_destroy = true - } - } - ``` - -* Top-level blocks should always be separated from one another by one - blank line. Nested blocks should also be separated by blank lines, except - when grouping together related blocks of the same type (like multiple - `provisioner` blocks in a resource). - -* Avoid grouping multiple blocks of the same type with other blocks of - a different type, unless the block types are defined by semantics to - form a family. - (For example: `root_block_device`, `ebs_block_device` and - `ephemeral_block_device` on `aws_instance` form a family of block types - describing AWS block devices, and can therefore be grouped together and - mixed.)