Install
$ agentstack add skill-hashi-demo-lab-claude-skill-hcp-terraform-terraform-style-guide ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Terraform Style Guide
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.
Table of Contents
- [Code Style Fundamentals](#code-style-fundamentals)
- [Code Formatting Standards](#code-formatting-standards)
- [File Organization](#file-organization)
- [Naming Conventions](#naming-conventions)
- [Resource Organization](#resource-organization)
- [Variables and Outputs](#variables-and-outputs)
- [Local Values](#local-values)
- [Provider Configuration and Aliasing](#provider-configuration-and-aliasing)
- [Dynamic Resource Creation](#dynamic-resource-creation)
- [Version Control](#version-control)
- [Workflow Standards](#workflow-standards)
- [Multi-Environment Management](#multi-environment-management)
- [State and Secrets Management](#state-and-secrets-management)
- [Testing and Policy](#testing-and-policy)
- [Azure Verified Modules (AVM) Requirements](#azure-verified-modules-avm-requirements)
- [Module Cross-Referencing](#module-cross-referencing)
- [Azure Provider Requirements](#azure-provider-requirements)
- [AVM Code Style Standards](#avm-code-style-standards)
- [AVM Variable Requirements](#avm-variable-requirements)
- [AVM Output Requirements](#avm-output-requirements)
- [AVM Testing Requirements](#avm-testing-requirements)
- [Breaking Changes & Feature Management](#breaking-changes--feature-management)
Code Style Fundamentals
Core Principles
Always follow these fundamental practices:
- Execute
terraform fmtbefore committing code to version control - Execute
terraform validateto catch syntax and configuration errors - Use
#for comments (avoid//and/* */style comments) - Name resources with descriptive nouns using underscores, excluding the resource type
- Define dependent resources after their dependencies for better readability
- Include type and description for all variables
- Include descriptions for all outputs
- Use
countandfor_eachjudiciously with clear intent
Automation with Git Hooks
Consider using Git pre-commit hooks to automatically run terraform fmt and terraform validate:
#!/bin/bash
# .git/hooks/pre-commit
terraform fmt -recursive
terraform validate
Code Formatting Standards
Terraform has specific formatting conventions that the terraform fmt command automates.
Indentation
- Use two spaces per nesting level
- Never use tabs
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "example-instance"
}
}
Alignment
- Align equals signs for consecutive single-line arguments at the same nesting level
- Separate different argument groups with blank lines
# 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"
}
Block Organization
- Arguments precede blocks within a resource
- Separate with one blank line between arguments and blocks
- Meta-arguments come first, followed by standard arguments, then blocks
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
}
}
Spacing
- Use single blank lines to separate logical groups of arguments
- Top-level blocks (resources, data sources, modules) require blank lines between them
- Do not use excessive blank lines
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"
}
File Organization
Standard File Structure
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"
}
}
}
Naming Conventions
General Rules
- Use descriptive nouns and underscores to separate multiple words
- Exclude the resource type from the resource name (redundant)
- Use lowercase for all names
- Be specific and meaningful
Examples
# ❌ 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
}
Variable Naming
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
}
Resource Organization
Dependency Order
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"
}
Parameter Order Within Resources
Follow this standard ordering for resource parameters:
countorfor_each(meta-arguments)- Resource-specific non-block parameters (alphabetically or logically grouped)
- Resource-specific block parameters
lifecycleblock (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]
}
Variables and Outputs
Variable Declaration Standards
Every variable must include:
type- the data typedescription- clear explanation of purpose
Optional but recommended:
default- default value if applicablesensitive- mark as true for secretsvalidation- for uniquely restrictive requirements
variable "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 = {}
}
Output Declaration Standards
Every output must include:
description- clear explanation of the value
Optional attributes:
sensitive- mark as true to hide from console outputdepends_on- explicit dependencies if needed
output "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
}
Variable Files Organization
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"]
Local Values
Usage Guidelines
Use local values sparingly to avoid unnecessary complexity. Locals are appropriate when:
- Avoiding repetition of complex expressions
- Giving meaningful names to intermediate values
- Computing values used multiple times
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
}
Anti-patterns to Avoid
# ❌ 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
}
Provider Configuration and Aliasing
Default Provider First
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"
}
Module Provider Configuration
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
}
}
Dynamic Resource Creation
count vs for_each
Choose the appropriate meta-argument based on your use case:
Use for_each when:
- Resources need distinct argument values
- You want to reference resources by key instead of index
- Resources are based on a map or set
- Preference for_each over count
Use count when:
- Conditional resource creation (0 or 1)
Avoid count for:
- Simple numeric repetition (use
for_eachwith a set or map instead)
for_each Examples
# 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}"
}
}
count Examples
# 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
Anti-pattern: count for Numeric Repetition
❌ 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.
Version Control
.gitignore Configuration
Never commit to version control:
- State files (
terraform.tfstate,terraform.tfstate.backup) - Lock info files (
.terraform.tfstate.lock.info) .terraformdirectory (provider plugins and modules)- Saved plan files (
*.tfplan,plan.out) .tfvarsfiles containing sensitive data
Always commit:
- All
.tfconfiguration files .terraform.lock.hcl(dependency lock file).gitignorefile- README and documentation files
Workflow Standards
Version Pinning
Always 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, -
Examples:
terraform-aws-vpcterraform-azurerm-virtual-networkterraform-google-kubernetes-engine
Repository Strategy
Three common approaches:
1. Separate Module Repositories (Reco
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: hashi-demo-lab
- Source: hashi-demo-lab/claude-skill-hcp-terraform
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.