The Developer's Guide to HCL, Part 4: Best Practices
Write clean, scalable HCL. Learn best practices for project structure, module design, state management, code formatting, validation, and testing your IaC.
Writing high-quality HCL is essential for creating infrastructure configurations that are functional, understandable, and scalable. This section outlines best practices for structuring projects, managing state, writing cleaner code, and testing. Part 1 covered the basics, Part 2 covered syntax, and Part 3 outlined common issues.
Structuring HCL Projects and Modules
A well-organized project is a maintainable project.
- Standard File Layout: Adopt a consistent file structure.
main.tf
: Core resource definitions.variables.tf
: Input variable declarations.outputs.tf
: Output value definitions.versions.tf
: Terraform and provider version constraints.README.md
: Essential documentation for your module.
- Module Design Principles: Modules are the key to reusability.
- Keep Modules Focused: A module should do one thing well (e.g., manage a VPC, deploy a database).
- Use Clear Inputs/Outputs: Define variables and outputs clearly with types and descriptions.
- Avoid Hardcoding: Parameterize everything that might change between environments.
- Don't Hardcode Environment Logic: A module should be environment-agnostic. Pass in environment-specific configuration via variables rather than using conditional logic like
if var.env == "prod"
inside the module.
- Managing Multiple Environments (Dev/Staging/Prod):
- Directory-Based Separation: This is the most common and recommended approach. Create a separate directory for each environment. Each directory can then call shared, reusable modules with its own specific variable values (
.tfvars
) and backend state configuration. This provides strong isolation. - Avoid Terraform Workspaces for Environments: Workspaces are better for feature branches or temporary instances, not for strong isolation between long-lived environments like dev and prod. Using separate directories is safer. Also see Terraform Workspaces for Environment Management
- Directory-Based Separation: This is the most common and recommended approach. Create a separate directory for each environment. Each directory can then call shared, reusable modules with its own specific variable values (
For a deeper dive, see How to Structure Terraform.
Terraform State Management Strategies
Terraform state is the source of truth for your managed infrastructure. Protecting it is critical.
- Always Use Remote State: For any collaborative or production project, store your state file remotely. Local state is only for solo experiments.
- Benefits: Collaboration, reliability, and most importantly, state locking.
- Common Backends: AWS S3 with a DynamoDB table for locking, Azure Blob Storage, Google Cloud Storage, or Terraform Cloud.
- State Locking is Mandatory: Remote backends provide locking mechanisms to prevent multiple people from running
terraform apply
at the same time and corrupting the state file. If your backend doesn't support locking, don't use it. - Split Your State: Do not use a single, monolithic state file for all your infrastructure. A large state file is slow and a single error has a massive blast radius.
- Splitting Strategy: Decompose your state by environment, component, or service. For example, have separate state files for your networking, your databases, and your application services.
- Benefits: Faster plans/applies, reduced blast radius for errors, and better security boundaries.
Writing Cleaner HCL: Formatting and Validation
Clean code is trustworthy code.
terraform fmt
: Run this command automatically to enforce a canonical, consistent style. Integrate it into a pre-commit hook so code is always formatted before it enters version control.terraform validate
: Run this command to check for syntax errors and basic consistency. It's fast because it doesn't access the network. This should be the first step in your CI/CD pipeline.- Use Clear Naming Conventions: Your resource, variable, and output names should be descriptive and consistent.
aws_s3_bucket.customer_billing_reports_prod
is much better thanaws_s3_bucket.bucket
.
Testing HCL Configurations
Testing Infrastructure as Code is crucial for reliability.
- Static Analysis: This is your first line of defense.
terraform fmt
andterraform validate
are basic static analysis tools.- Use third-party tools like TFLint to check for provider-specific best practices, find potential errors, and enforce coding standards.
terraform plan
as a Dry Run: Theplan
is an essential preview, but it is not a test. It shows intent but doesn't guarantee success.- Integration Testing: For important modules, write automated tests that deploy real infrastructure.
- Terratest: A Go library from Gruntwork is the industry standard for this. It allows you to write tests that run
terraform apply
, make assertions about the created infrastructure (e.g., "Is port 443 open?"), and then runterraform destroy
. This provides the highest level of confidence that your module works as expected.
- Terratest: A Go library from Gruntwork is the industry standard for this. It allows you to write tests that run
Next up, a recap:

Key Sources
- Terraform Documentation - Modules: https://developer.hashicorp.com/terraform/language/modules - Official documentation on creating and using modules.
- Terraform Documentation - Backends: https://developer.hashicorp.com/terraform/language/settings/backends/configuration - The definitive guide to configuring remote state backends.
- Terraform State in Terraform Cloud: https://developer.hashicorp.com/terraform/cloud-docs/workspaces/state - Explains concepts of state management, including locking, in the context of Terraform Cloud.
- Terratest Website: https://terratest.gruntwork.io/ - The homepage for the popular IaC testing library, with links to documentation and examples.