How UX/UI Design Makes Us Fall in Love with Products

Decoding Terraform Best Practices

Whether you’re a seasoned infrastructure engineer or just starting into the world of infrastructure as code (IaC), this blog series aims to unravel some of Terraform’s best practices. From module design to state management, we’ll explore the dos and don’ts that can make or break your infrastructure automation journey.

What is Terraform?

Terraform, developed by HashiCorp, is an open-source Infrastructure as Code (IaC) tool. It aims to empower users in defining and provisioning infrastructure resources—think virtual machines, networks, storage, and more—using a declarative configuration language. With Terraform, coders gain the ability to manage their infrastructure in a version-controlled, predictable, and reproducible manner. So, when coders want to build cloud environments and orchestrate on-premises systems, Terraform is a tool they can use to shape digital spaces.

Let’s water it down for a clearer understanding of the same.

Imagine a user/coder is building a digital infrastructure. He/She/they will need virtual machines, networks, and storage to construct one. Instead of manually building each structure or aspect of the entire thing, Terraform lets programmers describe the infrastructure they are seeking using a special language. It is almost like writing down a recipe for a dish.

This language is declarative, and thus, the programmer is free to provide numerous conditions and elaborate what they want and Terraform figures out how to make it happen.

What are the Key Features of Terraform?

  • Declarative Configuration: Infrastructure is defined using a high-level configuration language (HashiCorp Configuration Language, or HCL) which describes the desired state of the infrastructure.
  • Infrastructure as Code (IaC): Terraform treats infrastructure as code, allowing you to version control your infrastructure configurations, collaborate with team members, and automate infrastructure provisioning and management.
  • Resource Graph: Terraform builds a dependency graph of all resources defined in the configuration file, enabling it to determine the order in which resources need to be provisioned or destroyed.
  • Execution Plans: Before making any changes to your infrastructure, terraform generates an execution plan that outlines what actions it will take to achieve the desired state, providing an opportunity to review and approve changes.
  • Provisioners: Terraform supports provisioners, which are used to execute scripts or commands on local or remote machines after resources are created, allowing for customization or configuration management.
  • Provider Plugins: Terraform uses provider plugins to interact with various infrastructure providers such as AWS, Azure, Google Cloud Platform, VMware, and many others, enabling you to manage resources across different cloud and on-premises environments.

Overall, terraform simplifies and automates the process of managing infrastructure, making it easier to provision and manage complex environments consistently and reliable

Why do Developers Prefer Terraform?

For starters, Terraform offers a very easy-to-build infrastructure environment. This works wonderfully for coders and developers with fewer years of experience. Beginners can describe simple things without getting too puzzled by complex syntaxes. In addition to this here is the list of reasons why developers prefer Terraform over other IaC tools.

  1. Platform Agnostic Compatibility:
  • Most IaC tools tie you to a specific cloud provider like a contractual spell. But Terraform is platform agnostic. Developers can use it in AWS (Amazon Web Services) and GCP (Google Cloud Platform), or any other cloud realm.
  • There is no need to learn different incantations for each cloud. Terraform uses the same language everywhere.
  1. Immutable Infrastructure Approach:
  • Imagine your digital infrastructure. It can be either mutable or immutable. Mutable Infrastructure is like a shapeshifting creature. When a lot of changes are made, the infrastructure fails to retain its original design. Bugs and performance issues crop up from nowhere.
  • Terraform’s Immutable Approach is similar to a solid stone fortress. Whenever a change is made, Terraform creates a brand-new configuration. The old infrastructure is replaced by a brand new one. The best part about this is the flexibility it offers by allowing us to keep previous versions in case the developer decides to roll back to one of the earlier infrastructures for some reason.

(Note: Casey, please note that I have not mentioned one point that most websites including IBM’s page have mentioned – Terraform is an open-source tool. According to the latest news, it is no longer open source.)

  1. Terraform’s Easy Language:
  • Terraform uses HCL (HashiCorp Configuration Language), a human-readable language.
  • It’s similar to having an instruction manual book for building infrastructure. Coders just have to declare what they want and Terraform will take care of the rest.

Best Guidelines to Work on Terraform: Follow a standard module structure

  • Terraform modules must follow the standard module structure. Start every module with a .tf file, where resources are located by default.
  • In every module, include a READ.md file in Markdown format. In the README.md file, include basic documentation about the module.
  • Place examples in an examples/ folder, with a separate subdirectory for each example. For each example, include a detailed README.md file.
  • Create logical groupings of resources with their files and descriptive names, such as network.tf, instances.tf, or loadbalancer.tf.
  • In the module’s root directory, include only Terraform (*.tf) and repository metadata files (such as README.md and CHANGELOG.md).
  • Place any additional documentation in a docs/ subdirectory.

Adopt a naming convention

  • Name all configuration objects using underscores to delimit multiple words. This practice ensures consistency with the naming convention for resource types, data source types, and other predefined values.
  • To simplify references to a resource that is the only one of its types (for example, a single load balancer for an entire module), name the resource main.
  • Provide meaningful resource names to differentiate resources of the same type from each other (for example, primary and secondary).
  • Make resource names singular.
  • In the resource name, don’t repeat the resource type.

Use variables carefully

  • Declare all variables in variables.tf.
  • Give variables descriptive names that are relevant to their usage or purpose.
  • Variables must have descriptions. Descriptions are automatically included in a published module’s auto-generated documentation. Descriptions add additional context for new developers that descriptive names cannot provide.
  • Give variables defined types.
  • When appropriate, provide default values:
    • For variables that have environment-independent values (such as disk size), provide default values.
    • For variables that have environment-specific values (such as project_id), don’t provide default values. This way, the calling module must provide meaningful values.
  • Use empty defaults for variables (like empty strings or lists) only when leaving the variable empty is a valid preference that the underlying APIs don’t reject.
  • Store variables in a tfvars file
    • For root modules, provide variables by using a .tfvars variables file. For consistency, name variable files terraform.tfvars.
    • Don’t specify variables by using alternative var-files or var=’key=val’ command-line options. Command-line options are ephemeral and easy to forget. Using a default variables file is more predictable.

 Expose outputs

  • Organize all outputs in an outputs.tf file.
  • Provide meaningful descriptions for all outputs.
  • Document output descriptions in the README.md file.
  • Output all useful values that root modules might need to refer to or share. Especially for open source or heavily used modules, expose all outputs that have potential for consumption.
  • Don’t pass outputs directly through input variables, because doing so prevents them from being properly added to the dependency graph. To ensure that implicit dependencies are created, make sure that outputs reference attributes from resources.

Use data sources

  • Put data sources next to the resources that reference them. For example, if you are fetching an image to be used in launching an instance, place it alongside the instance instead of collecting data resources in their file.
  • If the number of data sources becomes large, consider moving them to a dedicated data.tf file.

 Use built-in formatting

  • All Terraform files must conform to the standards of terraform fmt.

Use count for conditional values

  • To instantiate a resource conditionally, use the count meta-argument.

Use each for iterated resources

  • If you want to create multiple copies of a resource based on an input resource, use the for each meta-argument.

Use environment directories

To share code across environments, and reference modules. Typically, this might be a service module that includes the base shared Terraform configuration for the service. In service modules, hard-code common inputs only require environment-specific inputs as variables.

Each environment directory must contain the following files:

  • A backend.tf file, declaring the Terraform backend state location
  • A main.tf file that instantiates the service module

Each environment directory (dev, qa, prod) corresponds to a default Terraform workspace and deploys a version of the service to that environment.

Use a default branching strategy

For all repositories that contain Terraform code, use the following strategy by default:

  • The main branch is the primary development branch and represents the latest approved code.
  • Development happens on feature and bug-fix branches off the main branch.
    • Name feature branches feature/$feature_name.
    • Name bug-fix branches fix/$bugfix_name.
  • When a feature or bug fix is complete, merge it back into the main branch with a pull request.
  • To prevent merge conflicts, rebase branches before merging them.

Never commit secrets

  • Never commit secrets to source control, including in Terraform configuration. Instead, upload them to a system like Secret Manager and reference them by using data sources.
  • Keep in mind that such sensitive values might still end up in the state file and might also be exposed as outputs.

Implement an automated pipeline

  • To ensure consistent execution context, execute Terraform through automated tooling. If a build system (like Jenkins) is already in use and widely adopted, use it to run the terraform plan and terraform apply commands automatically.

Avoid importing existing resources

  • Where possible, avoid importing existing resources (using terraform import), because doing so can make it challenging to fully understand the provenance and configuration of manually created resources. Instead, create new resources through Terraform and delete the old resources.
  • In cases where deleting old resources would create significant toil, use the terraform import command with explicit approval. After a resource is imported into Terraform, manage it exclusively with Terraform.

Don’t modify Terraform state manually

  • The Terraform state file is critical for maintaining the mapping between Terraform configuration and Cloud resources. Corruption can lead to major infrastructure problems. When modifications to the Terraform state are necessary, use the Terraform

Use remote state

  • We recommend using the Cloud Storage state backend. This approach locks the state to allow for collaboration as a team. It also separates the state and all the potentially sensitive information from version control.
  • Make sure that only the build system and highly privileged administrators can access the bucket that is used for remote state.
  • To prevent accidentally committing development state to source control, use gitignore for Terraform state files.

Don’t store secrets in the state

  • There are many resources and data providers in Terraform that store secret values in plaintext in the state file. Where possible, avoid storing secrets in the state.

Mark sensitive outputs

  • Instead of attempting to manually encrypt sensitive values, rely on Terraform’s built-in support for sensitive state management. When exporting sensitive values to output, make sure that the values are marked as sensitive.

Clean up all resources

  • To destroy all remote objects managed by a particular configuration, use the terraform destroy command. After you run the terraform destroy command, also run additional clean-up procedures to remove any resources that Terraform failed to destroy. Do this by deleting any projects used for test execution or by using a tool like the project cleanup module.

Recent blog posts:

Unleashing Excellence with Business Process Engineering
How Business Process Improvement (BPI) Maximizes Efficiency in Retail
The Future of Banking: AI and Predictive Analytics
How to Achieve Customer-Driven Business Transformation with LeanOps