.claude/skills/terraform-style-guide/SKILL.md
Comprehensive guide for Terraform code style, formatting, and best practices based on HashiCorp's official standards and Azure Verified Modules (AVM) requirements. Use when writing or reviewing Terraform configurations, formatting code, organizing files and modules, establishing team conventions, managing version control, ensuring code quality and consistency across infrastructure projects, or developing Azure Verified Modules.
npx skillsauth add agentdevsl/agentpane terraform-style-guideInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Adopting and adhering to a style guide keeps your Terraform code legible, scalable, and maintainable. This guide is based on HashiCorp's official Terraform style conventions and best practices, enhanced with Azure Verified Modules (AVM) requirements for Azure-specific Terraform development.
Note on AVM Requirements: The Azure Verified Modules section provides requirements specific to Azure module development. While these requirements are mandatory for AVM certification, many of the patterns and practices have broader applicability to Terraform module development across all cloud providers and can be adopted to improve code quality, consistency, and maintainability in any Terraform project.
Always follow these fundamental practices:
terraform fmt before committing code to version controlterraform validate to catch syntax and configuration errors# for comments (avoid // and /* */ style comments)count and for_each judiciously with clear intentConsider using Git pre-commit hooks to automatically run terraform fmt and terraform validate:
#!/bin/bash
# .git/hooks/pre-commit
terraform fmt -recursive
terraform validate
Terraform has specific formatting conventions that the terraform fmt command automates.
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "example-instance"
}
}
# Good - aligned equals signs
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
subnet_id = "subnet-12345678"
tags = {
Name = "web-server"
Environment = "production"
}
}
# Bad - unaligned
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
subnet_id = "subnet-12345678"
}
resource "aws_instance" "example" {
# Meta-arguments first
count = 3
# Standard arguments
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
# Blocks last
root_block_device {
volume_size = 20
}
}
variable "instance_count" {
description = "Number of instances to create"
type = number
default = 1
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t2.micro"
}
Organize your Terraform code into these standard files:
| File | Purpose |
| -------------- | ------------------------------------------------ |
| terraform.tf | Terraform and provider version requirements |
| providers.tf | Provider configurations |
| main.tf | Primary resources and data sources |
| variables.tf | Input variable declarations (alphabetical order) |
| outputs.tf | Output value declarations (alphabetical order) |
| locals.tf | Local value declarations |
Example terraform.tf:
terraform {
required_version = ">= 1.7"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.34.0"
}
}
}
Example providers.tf:
provider "aws" {
region = var.aws_region
default_tags {
tags = {
ManagedBy = "Terraform"
Project = "MyProject"
}
}
}
# ❌ Bad - includes resource type, uses hyphens, mixed case
resource "aws_instance" "webAPI-aws-instance" {
# ...
}
# ✅ Good - descriptive noun, underscores, lowercase
resource "aws_instance" "web_api" {
# ...
}
# ❌ Bad - too generic
variable "name" {
type = string
}
# ✅ Good - specific and clear
variable "application_name" {
type = string
}
Variables should clearly indicate their purpose:
variable "vpc_cidr_block" {
description = "CIDR block for the VPC"
type = string
}
variable "enable_dns_hostnames" {
description = "Enable DNS hostnames in the VPC"
type = bool
default = true
}
Define a data source before the resource that references it for better readability:
# Data source first
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
}
}
# Resource that uses it second
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t2.micro"
}
Follow this standard ordering for resource parameters:
count or for_each (meta-arguments)lifecycle block (if needed)depends_on (if required, as last resort)resource "aws_instance" "web" {
# 1. Meta-arguments
count = var.instance_count
# 2. Non-block parameters
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
subnet_id = aws_subnet.public.id
# 3. Block parameters
root_block_device {
volume_size = 20
volume_type = "gp3"
}
tags = {
Name = "web-${count.index}"
}
# 4. Lifecycle
lifecycle {
create_before_destroy = true
}
# 5. depends_on (avoid if possible)
# depends_on = [aws_iam_role_policy.example]
}
Every variable must include:
type - the data typedescription - clear explanation of purposeOptional but recommended:
default - default value if applicablesensitive - mark as true for secretsvalidation - for uniquely restrictive requirementsvariable "instance_type" {
description = "EC2 instance type for the web server"
type = string
default = "t2.micro"
validation {
condition = contains(["t2.micro", "t2.small", "t2.medium"], var.instance_type)
error_message = "Instance type must be t2.micro, t2.small, or t2.medium."
}
}
variable "database_password" {
description = "Password for the database admin user"
type = string
sensitive = true
}
variable "availability_zones" {
description = "List of availability zones for resource placement"
type = list(string)
}
variable "tags" {
description = "Common tags to apply to all resources"
type = map(string)
default = {}
}
Every output must include:
description - clear explanation of the valueOptional attributes:
sensitive - mark as true to hide from console outputdepends_on - explicit dependencies if neededoutput "instance_id" {
description = "ID of the EC2 instance"
value = aws_instance.web.id
}
output "instance_public_ip" {
description = "Public IP address of the EC2 instance"
value = aws_instance.web.public_ip
}
output "database_password" {
description = "Database administrator password"
value = aws_db_instance.main.password
sensitive = true
}
Organize variables alphabetically in variables.tf and use .tfvars files for environment-specific values:
# terraform.tfvars (or dev.tfvars, prod.tfvars)
instance_type = "t2.micro"
instance_count = 3
availability_zones = ["us-west-2a", "us-west-2b"]
Use local values sparingly to avoid unnecessary complexity. Locals are appropriate when:
locals {
# Good use case - computing a reusable value
common_tags = merge(
var.tags,
{
Environment = var.environment
ManagedBy = "Terraform"
Project = var.project_name
}
)
# Good use case - naming a complex expression
vpc_id = var.create_vpc ? aws_vpc.main[0].id : data.aws_vpc.existing[0].id
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
tags = local.common_tags
}
# ❌ Bad - unnecessary local for a simple reference
locals {
instance_type = var.instance_type
}
# ✅ Good - use the variable directly
resource "aws_instance" "web" {
instance_type = var.instance_type
}
Always define a default provider configuration first, then aliases:
# Default provider
provider "aws" {
region = "us-west-2"
}
# Aliased provider for another region
provider "aws" {
alias = "east"
region = "us-east-1"
}
# Using the aliased provider
resource "aws_instance" "east_web" {
provider = aws.east
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
}
For modules that use multiple providers, specify via the providers meta-argument:
module "vpc_replication" {
source = "./modules/vpc"
providers = {
aws.primary = aws
aws.secondary = aws.east
}
}
Choose the appropriate meta-argument based on your use case:
Use for_each when:
Use count when:
Avoid count for:
for_each with a set or map instead)# Using for_each with a map
variable "instances" {
type = map(object({
instance_type = string
ami = string
}))
default = {
web = {
instance_type = "t2.micro"
ami = "ami-0c55b159cbfafe1f0"
}
api = {
instance_type = "t2.small"
ami = "ami-0c55b159cbfafe1f0"
}
}
}
resource "aws_instance" "servers" {
for_each = var.instances
ami = each.value.ami
instance_type = each.value.instance_type
tags = {
Name = each.key
}
}
# Reference: aws_instance.servers["web"].id
# Using for_each with a set
variable "subnet_cidrs" {
type = set(string)
default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
}
resource "aws_subnet" "private" {
for_each = var.subnet_cidrs
vpc_id = aws_vpc.main.id
cidr_block = each.value
tags = {
Name = "private-${each.key}"
}
}
# Conditional resource creation
variable "enable_monitoring" {
type = bool
default = false
}
resource "aws_cloudwatch_metric_alarm" "cpu" {
count = var.enable_monitoring ? 1 : 0
alarm_name = "high-cpu-usage"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = 300
statistic = "Average"
threshold = 80
}
# Reference (when created): aws_cloudwatch_metric_alarm.cpu[0].id
❌ Avoid this pattern - Using count for simple numeric repetition:
# BAD: Don't use count for numeric repetition
variable "instance_count" {
type = number
default = 3
}
resource "aws_instance" "web" {
count = var.instance_count
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "web-${count.index}"
}
}
✅ Better approach - Use for_each with a set instead:
# GOOD: Use for_each for multiple similar resources
variable "instance_names" {
type = set(string)
default = ["web-1", "web-2", "web-3"]
}
resource "aws_instance" "web" {
for_each = var.instance_names
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = each.key
}
}
# Reference: aws_instance.web["web-1"].id
Why? Using for_each provides stable resource addresses that don't change when you add or remove instances from the middle of the list.
Never commit to version control:
terraform.tfstate, terraform.tfstate.backup).terraform.tfstate.lock.info).terraform directory (provider plugins and modules)*.tfplan, plan.out).tfvars files containing sensitive dataAlways commit:
.tf configuration files.terraform.lock.hcl (dependency lock file).gitignore fileAlways pin versions explicitly to ensure reproducible deployments:
terraform {
required_version = ">= 1.7"
required_providers {
aws = {
source = "hashicorp/aws"
version = "5.34.0" # Pin to exact version for stability
}
random = {
source = "hashicorp/random"
version = "~> 3.6" # Allow patch updates only
}
}
}
Version constraint operators:
= 1.0.0 - Exact version only>= 1.0.0 - Greater than or equal to~> 1.0 - Allow rightmost version component to increment (1.0, 1.1, but not 2.0)>= 1.0, < 2.0 - Version rangeUse the convention: terraform-<PROVIDER>-<NAME>
Examples:
terraform-aws-vpcterraform-azurerm-virtual-networkterraform-google-kubernetes-engineThree common approaches:
1. Separate Module Repositories (Recommended)
2. Logical Infrastructure Grouping
infra-networking, infra-compute, infra-databases3. Monorepo
Adopt GitHub Flow for simplicity:
# Create feature branch
git checkout -b feature/add-monitoring
# Make changes and commit
git add .
git commit -m "Add CloudWatch monitoring for EC2 instances"
# Push and create PR
git push origin feature/add-monitoring
Use separate workspaces for each environment:
# Development workspace: app-dev
# Staging workspace: app-staging
# Production workspace: app-prod
Terraform Cloud/HCP Terraform automatically manages state per workspace.
Never share full state files directly. State files contain sensitive information.
Alternatives for sharing data:
data "tfe_outputs" "vpc" {
organization = "my-org"
workspace = "networking-prod"
}
resource "aws_instance" "web" {
subnet_id = data.tfe_outputs.vpc.values.private_subnet_ids[0]
}
data "aws_vpc" "main" {
tags = {
Name = "main-vpc"
}
}
resource "aws_subnet" "app" {
vpc_id = data.aws_vpc.main.id
}
Protect credentials through:
Dynamic Provider Credentials (HCP Terraform)
HashiCorp Vault Integration:
data "vault_generic_secret" "database" {
path = "secret/database"
}
resource "aws_db_instance" "main" {
username = data.vault_generic_secret.database.data["username"]
password = data.vault_generic_secret.database.data["password"]
}
variable "database_password" {
description = "Database password (set via TF_VAR_database_password)"
type = string
sensitive = true
}
export TF_VAR_database_password="secure-password"
terraform apply
Write tests for modules using Terraform's native testing framework:
# tests/vpc.tftest.hcl
run "valid_vpc_cidr" {
command = plan
variables {
vpc_cidr = "10.0.0.0/16"
}
assert {
condition = aws_vpc.main.cidr_block == "10.0.0.0/16"
error_message = "VPC CIDR block did not match expected value"
}
}
run "vpc_enables_dns" {
command = plan
assert {
condition = aws_vpc.main.enable_dns_hostnames == true
error_message = "VPC should have DNS hostnames enabled"
}
}
Run tests:
terraform test
Use this checklist for code reviews:
terraform fmtterraform validatesensitive = true.gitignore excludes state files and secretsImportant: The following requirements are specific to AWS resource deployments and should be applied to all AWS Terraform configurations for consistency, cost tracking, and governance.
Severity: MUST | Requirement: AWS-TAG-001
All AWS resources that support tags MUST include at minimum an Application tag to identify the application or service the resource belongs to. This is critical for:
Every taggable AWS resource MUST include:
tags = {
Application = var.application_name # MANDATORY
# Additional tags as needed
Environment = var.environment
ManagedBy = "Terraform"
Owner = var.owner_email
CostCenter = var.cost_center
}
Best Practice: Configure default tags at the provider level to ensure all resources automatically inherit mandatory tags:
# providers.tf
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Application = var.application_name # MANDATORY
Environment = var.environment
ManagedBy = "Terraform"
Workspace = terraform.workspace
Repository = var.repository_url
}
}
}
Always define the application name variable:
variable "application_name" {
description = "Name of the application this infrastructure supports (REQUIRED for all resources)"
type = string
validation {
condition = length(var.application_name) > 0
error_message = "Application name is required and cannot be empty."
}
}
For complex tagging scenarios, use local values to manage tag inheritance:
locals {
# Mandatory tags that must be present on all resources
mandatory_tags = {
Application = var.application_name
}
# Common tags for all resources
common_tags = merge(
local.mandatory_tags,
{
Environment = var.environment
ManagedBy = "Terraform"
CreatedDate = timestamp()
}
)
# Merge with additional tags passed as variables
all_tags = merge(
local.common_tags,
var.additional_tags
)
}
# Usage in resources
resource "aws_instance" "example" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
tags = merge(
local.all_tags,
{
Name = "example-instance"
Type = "compute"
}
)
}
Implement validation to ensure required tags are present:
variable "tags" {
description = "Map of tags to apply to resources"
type = map(string)
validation {
condition = contains(keys(var.tags), "Application")
error_message = "The 'Application' tag is mandatory and must be included in the tags map."
}
}
Some AWS resources don't support tags directly. For these resources, document the application association in the resource name or description:
resource "aws_iam_policy_document" "example" {
# IAM policy documents don't support tags
# Include application name in the statement sid for traceability
statement {
sid = "${var.application_name}_S3Access"
# ...
}
}
resource "aws_iam_role" "example" {
name = "${var.application_name}-role" # Include app name in resource name
tags = {
Application = var.application_name # IAM roles do support tags
}
}
Severity: SHOULD | Requirement: AWS-PROV-001
AWS provider configurations SHOULD follow these guidelines:
terraform {
required_version = ">= 1.7"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0" # Use pessimistic constraint for stability
}
}
}
For multi-region deployments, use provider aliases with clear naming:
# Primary region (default provider)
provider "aws" {
region = var.primary_region
default_tags {
tags = {
Application = var.application_name
Region = var.primary_region
}
}
}
# Secondary regions with aliases
provider "aws" {
alias = "us_east_1"
region = "us-east-1"
default_tags {
tags = {
Application = var.application_name
Region = "us-east-1"
}
}
}
provider "aws" {
alias = "eu_west_1"
region = "eu-west-1"
default_tags {
tags = {
Application = var.application_name
Region = "eu-west-1"
}
}
}
For cross-account deployments, configure role assumption:
provider "aws" {
region = var.aws_region
assume_role {
role_arn = var.assume_role_arn
session_name = "${var.application_name}-terraform"
}
default_tags {
tags = {
Application = var.application_name
Account = var.target_account_id
}
}
}
Severity: SHOULD | Requirement: AWS-NAME-001
AWS resource names SHOULD follow these conventions for consistency and clarity:
Use the pattern: {application}-{environment}-{resource-type}-{identifier}
locals {
name_prefix = "${var.application_name}-${var.environment}"
}
resource "aws_s3_bucket" "data" {
bucket = "${local.name_prefix}-data-bucket"
tags = merge(
local.common_tags,
{
Name = "${local.name_prefix}-data-bucket"
}
)
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
tags = merge(
local.common_tags,
{
Name = "${local.name_prefix}-web-server"
}
)
}
resource "aws_rds_instance" "database" {
identifier = "${local.name_prefix}-postgres-db"
# ...
tags = merge(
local.common_tags,
{
Name = "${local.name_prefix}-postgres-db"
}
)
}
For resources that require DNS-compatible names (S3 buckets, CloudFront distributions), ensure names:
locals {
# Ensure DNS-compatible naming
dns_safe_name = lower(replace(var.application_name, "_", "-"))
bucket_name = "${local.dns_safe_name}-${var.environment}-${random_id.bucket.hex}"
}
resource "random_id" "bucket" {
byte_length = 4
}
resource "aws_s3_bucket" "example" {
bucket = local.bucket_name # Guaranteed to be DNS-compatible
tags = merge(
local.common_tags,
{
Name = local.bucket_name
}
)
}
Always include both the resource argument name and a Name tag for consistency:
resource "aws_security_group" "web" {
name = "${local.name_prefix}-web-sg" # Resource argument
description = "Security group for ${var.application_name} web servers"
tags = merge(
local.common_tags,
{
Name = "${local.name_prefix}-web-sg" # Name tag matches resource name
}
)
}
When testing AWS infrastructure:
# tests/aws_tags.tftest.hcl
run "verify_application_tag" {
command = plan
variables {
application_name = "test-app"
}
assert {
condition = aws_instance.example.tags["Application"] == "test-app"
error_message = "Application tag must be set on all resources"
}
}
run "verify_name_pattern" {
command = plan
variables {
application_name = "myapp"
environment = "dev"
}
assert {
condition = can(regex("^myapp-dev-", aws_instance.example.tags["Name"]))
error_message = "Resource names must follow the {app}-{env}-{type} pattern"
}
}
Add these items to your review checklist for AWS deployments:
Application tagdefault_tags with Application tag{application}-{environment}-{resource-type} patternImportant: The following requirements are mandatory for Azure Verified Modules but represent best practices that can enhance Terraform module development across any cloud provider.
Severity: MUST | Requirement: TFFR1
When building Resource or Pattern modules, module owners MAY cross-reference other modules. However:
source = "Azure/xxx/azurerm" with version = "1.2.3"git::https://xxx.yyy/xxx.git or github.com/xxx/yyy)Broader Applicability: Always use registry references with pinned versions for any module to ensure reproducibility and version control.
Severity: MUST | Requirement: TFFR3
For Azure Verified Modules, authors MUST only use the following Azure providers:
| Provider | Min Version | Max Version | | -------- | ----------- | ----------- | | azapi | >= 2.0 | < 3.0 | | azurerm | >= 4.0 | < 5.0 |
Requirements:
required_providers block to enforce provider versions~>)Example:
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
azapi = {
source = "Azure/azapi"
version = "~> 2.0"
}
}
}
Broader Applicability: Always specify provider versions with appropriate constraints for any cloud provider to ensure compatibility.
Severity: MUST | Requirement: TFNFR4
MUST use lower snake_casing for:
Example: snake_casing_example
Severity: SHOULD | Requirement: TFNFR6
Severity: MUST | Requirement: TFNFR7
count for conditional resource creationmap(xxx) or set(xxx) as resource's for_each collectionGood Example:
resource "azurerm_subnet" "pair" {
for_each = var.subnet_map # map(string)
name = "${each.value}-pair"
resource_group_name = azurerm_resource_group.example.name
virtual_network_name = azurerm_virtual_network.example.name
address_prefixes = ["10.0.1.0/24"]
}
Broader Applicability: Using typed collections with for_each ensures predictable behavior across all providers.
Severity: SHOULD | Requirement: TFNFR8
Order within resource/data blocks:
Meta-arguments (top):
providercountfor_eachArguments/blocks (middle, alphabetical):
Meta-arguments (bottom):
depends_onlifecycle (with sub-order: create_before_destroy, ignore_changes, prevent_destroy)Separate sections with blank lines.
Severity: SHOULD | Requirement: TFNFR9
Order within module blocks:
Top meta-arguments:
sourceversioncountfor_eachArguments (alphabetical):
Bottom meta-arguments:
depends_onprovidersSeverity: MUST | Requirement: TFNFR10
The ignore_changes attribute MUST NOT be enclosed in double quotes.
Good:
lifecycle {
ignore_changes = [tags]
}
Bad:
lifecycle {
ignore_changes = ["tags"]
}
Severity: SHOULD | Requirement: TFNFR11
For parameters requiring conditional resource creation, wrap with object type to avoid "known after apply" issues during plan stage.
Recommended:
variable "security_group" {
type = object({
id = string
})
default = null
}
Broader Applicability: This pattern prevents plan-time issues across all providers when using conditional resources.
Severity: MUST | Requirement: TFNFR12
Nested blocks under conditions MUST use this pattern:
dynamic "identity" {
for_each = <condition> ? [<some_item>] : []
content {
# block content
}
}
Broader Applicability: This is the standard Terraform pattern for conditional nested blocks.
Severity: SHOULD | Requirement: TFNFR13
Good:
coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
Bad:
var.new_network_security_group_name == null ? "${var.subnet_name}-nsg" : var.new_network_security_group_name
Broader Applicability: coalesce() and try() functions provide cleaner, more readable default value handling.
Severity: MUST | Requirement: TFNFR27
provider MUST NOT be declared in modules (except for configuration_aliases)provider blocks in modules MUST only use aliasBroader Applicability: This is a universal best practice for reusable Terraform modules.
Severity: MUST | Requirement: TFNFR14
Module owners MUST NOT add variables like enabled or module_depends_on to control entire module operation. Boolean feature toggles for specific resources are acceptable.
Severity: SHOULD | Requirement: TFNFR15
Variables SHOULD follow this order:
Severity: SHOULD | Requirement: TFNFR16
xxx_enabled instead of xxx_disabledSeverity: SHOULD | Requirement: TFNFR17
description SHOULD precisely describe the parameter's purpose and expected data typeobject types, use HEREDOC formatVariable and output descriptions MAY span multiple lines using HEREDOC format with embedded markdown for examples.
Severity: MUST | Requirement: TFNFR18
type MUST be defined for every variabletype SHOULD be as precise as possibleany MAY only be used with adequate reasonsbool instead of string/number for true/false valuesobject instead of map(any)Broader Applicability: Precise typing prevents errors and improves documentation across all Terraform code.
Severity: SHOULD | Requirement: TFNFR19
If a variable's type is object and contains sensitive fields, the entire variable SHOULD be sensitive = true, or extract sensitive fields into separate variables.
Severity: SHOULD | Requirement: TFNFR20
Nullable SHOULD be set to false for collection values (sets, maps, lists) when using them in loops. For scalar values, null may have semantic meaning.
Severity: MUST | Requirement: TFNFR21
nullable = true MUST be avoided unless there's a specific semantic need for null values.
Severity: MUST | Requirement: TFNFR22
sensitive = false MUST be avoided (this is the default).
Severity: MUST | Requirement: TFNFR23
A default value MUST NOT be set for sensitive inputs (e.g., default passwords).
Severity: MUST | Requirement: TFNFR24
deprecated_variables.tfDEPRECATED at the beginning of descriptionBroader Applicability: Clear deprecation management improves user experience for any module.
Severity: SHOULD | Requirement: TFFR2
Authors SHOULD NOT output entire resource objects as these may contain sensitive data and the schema can change with API or provider versions.
Best Practices:
name)sensitive = true for sensitive attributesfor_each, output computed attributes in a map structureExamples:
# Single resource computed attribute
output "foo" {
description = "MyResource foo attribute"
value = azurerm_resource_myresource.foo
}
# for_each resources
output "childresource_foos" {
description = "MyResource children's foo attributes"
value = {
for key, value in azurerm_resource_mychildresource : key => value.foo
}
}
# Sensitive output
output "bar" {
description = "MyResource bar attribute"
value = azurerm_resource_myresource.bar
sensitive = true
}
Broader Applicability: The anti-corruption layer pattern protects consumers from provider API changes.
Severity: MUST | Requirement: TFNFR29
Outputs containing confidential data MUST be declared with sensitive = true.
Severity: MUST | Requirement: TFNFR30
deprecated_outputs.tfoutputs.tfSeverity: MAY | Requirement: TFNFR31
locals.tf SHOULD only contain locals blockslocals blocks next to resources for advanced scenariosSeverity: MUST | Requirement: TFNFR32
Expressions in locals blocks MUST be arranged alphabetically.
Severity: SHOULD | Requirement: TFNFR33
Use precise types (e.g., number for age, not string).
Broader Applicability: Type precision improves code clarity and catches errors early.
Severity: MUST | Requirement: TFNFR25
terraform.tf requirements:
terraform blockrequired_version~> #.# or >= #.#.#, < #.#.# formatExample:
terraform {
required_version = "~> 1.6"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
Broader Applicability: Version constraints prevent compatibility issues across all Terraform projects.
Severity: MUST | Requirement: TFNFR26
terraform block MUST contain required_providers blocksource and versionsource MUST be in format namespace/nameversion MUST include minimum and maximum major version constraints~> #.# or >= #.#.#, < #.#.# formatSeverity: MUST | Requirement: TFNFR5
Required testing tools for AVM:
terraform validate/fmt/test)Broader Applicability: These tools provide comprehensive quality checks for any Terraform code.
Severity: SHOULD | Requirement: TFNFR36
For robust testing, prevent_deletion_if_contains_resources SHOULD be explicitly set to false in test provider configurations.
Severity: MUST | Requirement: TFNFR2
.terraform-docs.yml file MUST be present in the module rootBroader Applicability: Automated documentation ensures consistency and reduces maintenance burden.
Severity: MUST | Requirement: TFNFR34
New resources added in minor/patch versions MUST have a toggle variable to avoid creation by default:
variable "create_route_table" {
type = bool
default = false
nullable = false
}
resource "azurerm_route_table" "this" {
count = var.create_route_table ? 1 : 0
# ...
}
Broader Applicability: Feature toggles allow backward-compatible module evolution.
Severity: MUST | Requirement: TFNFR35
Breaking changes requiring caution:
Resource blocks:
dynamicmoved blockscount to for_each or vice versaVariable/Output blocks:
typedefault valuesnullable to falsesensitive from false to truedefaultvaluesensitive valueBroader Applicability: Understanding breaking changes is crucial for maintaining any public Terraform module.
Severity: MUST | Requirement: TFNFR3
Module owners MUST set branch protection policies on the default branch (typically main):
Broader Applicability: These protections ensure code quality for any collaborative project.
For Azure Verified Modules, add these items to your review checklist:
for_each uses map() or set() with static keysignore_changes not quotedenabled or module_depends_on variablesany)nullable = falsesensitive = false declarationsdeprecated_variables.tfsensitive = truedeprecated_outputs.tfterraform.tf has version constraints (~> format)required_providers block present with all providersprovider declarations in module (except aliases).terraform-docs.yml presentThis style guide combines HashiCorp's official Terraform conventions with cloud-specific requirements to provide comprehensive guidance for:
By following these guidelines, you'll create Terraform code that is:
Last Updated: November 15, 2024 Based on: HashiCorp Terraform Style Conventions, AWS Best Practices & Azure Verified Modules Requirements
development
AWS security assessment domains, risk rating framework, CIS/NIST reference baselines, and evidence-based finding format. Use when reviewing AWS security posture, assessing risk, or applying CIS/NIST baselines to Terraform configurations.
testing
--- name: "tf-runtask" description: "Retrieve and display Terraform Cloud/Enterprise run task results for a given run. Use this skill whenever the user asks about run task results, run task checks, task stage statuses, or wants to inspect what run tasks reported for a Terraform Cloud/Enterprise run. Triggers on phrases like "check the run tasks", "what did the run tasks say", "show run task results", "get task results for run-xxx", or any reference to run task outcomes on a specific run." source
devops
Research strategies for AWS documentation, provider docs, and public registry patterns. Use when researching AWS services, investigating provider resources, or studying public registry modules for design patterns.
development
Validation results summary template for Phase 4 output. Provides the format for reporting terraform test, validate, fmt, tflint, pre-commit, trivy, and security checklist results.