Terraform coding standards, patterns, and governance for Azure deployments across Learning, Project, and Client workspaces. USE FOR: Terraform Azure code generation, module scaffolding, provider configuration, state management, variable patterns, tagging, naming conventions, validation, security, Terraform style, Terraform best practices, AzureRM provider, Terraform Azure governance. DO NOT USE FOR: local Hyper-V labs (use terraform-ad-lab), Bicep deployments (use bicep-azure), non-Azure Terraform providers.
Provide authoritative Terraform coding standards, patterns, and governance for all Azure deployments. This skill consolidates guidance from workspace-level governance documents, community best practices, and real-world patterns observed across the Learning, Project, and Client workspaces.
This skill synthesizes guidance from:
instructions/Terraform Style Guidelines.instructions.md — formatting, naming, best practicesinstructions/Azure Governance Guidelines.instructions.md — cross-workspace naming, tagging, cost, securityLearningAzure/.github/skills/terraform-scaffolding/SKILL.md — lab-specific scaffolding rules (R-120 through R-128)LearningAzure/.github/skills/shared-contract/SKILL.md — cross-cutting lab rules (R-001 through R-030)LearningAzure/Governance-Lab.md — Learning workspace governanceTCU/.github/instructions/terraform-governance.instructions.md — Client workspace governanceterraform fmt standard formatting (2-space indentation) before all reviews.terraform validate to check syntax before planning.tflint for linting and tfsec or equivalent for security scanning.terraform/
├── main.tf # Primary resource definitions or thin orchestration
├── variables.tf # Input variable declarations
├── outputs.tf # Output value declarations
├── providers.tf # Provider and version configuration
├── locals.tf # Local values (optional — use when computed values exist)
├── data.tf # Data sources (optional)
├── terraform.tfvars # Non-sensitive variable values
├── terraform.lock.hcl # Provider lock file (commit this)
└── modules/
└── <module-name>/
├── main.tf
├── variables.tf
└── outputs.tf
| File | Content |
|---|---|
providers.tf | terraform {} block with required_version and required_providers, provider configuration |
main.tf | Thin orchestration: locals, resource group, module calls, or direct resources |
variables.tf | All input variables with description, type, and optional validation |
outputs.tf | Outputs with description; use sensitive = true for secrets |
locals.tf | Computed values, common tags, name construction |
data.tf | Data sources for existing resources |
terraform.tfvars | Non-sensitive defaults; never store secrets here |
If main.tf or variables.tf grows too large, split by resource type (e.g., main.networking.tf, variables.networking.tf).
<EXAM>/hands-on-labs/<domain>/lab-<topic>/
├── README.md
├── terraform/
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ ├── providers.tf
│ ├── terraform.tfvars
│ └── modules/
│ └── <module>/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── validation/
└── <validation-script>.ps1
terraform {
required_version = ">= 1.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.0"
}
}
}
provider "azurerm" {
features {
resource_group {
prevent_deletion_if_contains_resources = false
}
}
subscription_id = var.subscription_id
}
~>).random provider when the deployment contains soft-delete resources requiring unique names.ARM_SUBSCRIPTION_ID environment variable — never hardcode in the provider block.Learning labs use lab_subscription_id variable name and include a subscription guard (see §11).
| Workspace | Pattern | Example |
|---|---|---|
| Learning | <exam>-<domain>-<topic>-tf | az104-networking-vnet-peering-tf |
| Project | project-<workspace>-<environment>-<purpose>[-<instance>]-tf | project-docwriter-lab-compute-tf |
| Client | client-<client>-<environment>-<purpose>[-<instance>] | client-tcu-lab-network-01 |
Pattern: <prefix>-<descriptive-name>[-<instance>]
Use Azure CAF-aligned abbreviations:
| Resource Type | Prefix | Example |
|---|---|---|
| Virtual Network | vnet | vnet-core-01 |
| Subnet | snet | snet-app-01 |
| Network Security Group | nsg | nsg-web-01 |
| Virtual Machine | vm | vm-web-01 |
| Storage Account | st | stdocwriterdata01 |
| Key Vault | kv | kv-secrets-01 |
| App Service Plan | asp | asp-web-01 |
| App Service | app | app-api-01 |
| Function App | func | func-processor-01 |
| SQL Server | sql | sql-main-01 |
| Log Analytics | log | log-monitor-01 |
| Application Insights | appi | appi-web-01 |
| Container Registry | acr | acrdocwriter01 |
| OpenAI Account | oai | oai-gpt-01 |
| AI Services | ais | ais-shared-01 |
| Cognitive Services | cog | cog-docwriter-7k3m |
snake_case for Terraform identifiers (variables, locals, resource names, module names).azurerm_resource_group.main not azurerm_resource_group.rg1.-tf) is mandatory and always last for resource groups.All resource names must be static and predictable. Random suffixes are only permitted for resources subject to soft-delete name reservation:
| Resource | Retention | Random Suffix Required |
|---|---|---|
| Cognitive Services | 48 hrs | Yes |
| Key Vault | 7–90 days | Yes |
| API Management | 48 hrs | Yes |
| Recovery Vault | 14 days | Yes |
Random suffix format: 4 lowercase alphanumeric characters via random_string.
resource "random_string" "suffix" {
length = 4
upper = false
special = false
}
locals {
common_tags = {
Environment = var.environment
Category = var.category # "Learning", "Project", "Client"
Workspace = var.workspace
Purpose = var.purpose
Owner = var.owner
DateCreated = var.date_created # Static YYYY-MM-DD — never use timestamp()
DeploymentMethod = "Terraform"
ManagedBy = "terraform"
}
}
DateCreated must be a static string. Never use timestamp() or any dynamic function.local.common_tags.common_tags as an explicit input to all modules.ExamCode, Client, CostCenter).locals {
common_tags = {
Environment = "Lab"
Project = "<EXAM>" # "AI-102", "AZ-104"
Domain = "<Domain>" # "Networking", "Compute"
Purpose = "<Purpose>" # "VNet Peering"
Owner = var.owner
DateCreated = var.date_created
DeploymentMethod = "Terraform"
}
}
Every variable must have:
description — clear, concise purposetype — explicit type declarationvalidation — where appropriate for safetyvariable "location" {
description = "Azure region for resource deployment"
type = string
default = "eastus"
validation {
condition = contains(["eastus", "eastus2", "westus2", "centralus"], var.location)
error_message = "Location must be a supported US region."
}
}
variable "date_created" {
description = "Date the resources were created (YYYY-MM-DD format)"
type = string
validation {
condition = can(regex("^\\d{4}-\\d{2}-\\d{2}$", var.date_created))
error_message = "Date must be in YYYY-MM-DD format."
}
}
sensitive = true..tfvars files or state.ephemeral secrets with write-only parameters when supported (Terraform v1.11+).locals.tf when local values exist.locals {
resource_group_name = "project-docwriter-tf"
resource_name_prefix = "${var.project_name}-${var.environment}"
common_tags = { ... }
}
depends_on (if explicit dependency required)count or for_each (instantiation logic)tags near the endlifecycle block lastSeparate sections with blank lines. Group related attributes together.
Use modules when 2+ related resource types are deployed together.
main.tfmodules/<module-name>/
├── main.tf # Resource definitions
├── variables.tf # Inputs — must accept tags map + resource IDs from other modules
└── outputs.tf # Resource IDs, endpoints, principal IDs
examples/ directory when distributing.README.md explaining usage.principal_id) as explicit inputs for RBAC.| Workspace | Backend | Notes |
|---|---|---|
| Learning | Local state only | Never configure remote backend |
| Project | Local or remote (Azure Storage) | Use Terraform workspaces as needed |
| Client | Remote (Azure Storage) | State locking via blob lease |
.tfstate files to version control; ensure .gitignore coverage.terraform.lock.hcl to ensure consistent providers.sensitive = true.sensitive = true..gitignore to exclude files containing sensitive information.tfsec, trivy, or checkov for security issues.For non-production environments, disable soft-delete when the provider allows it:
# Cognitive Account
purge_soft_delete_on_destroy = false # Unique names eliminate purge need
# Key Vault feature flags
cognitive_account {
purge_soft_delete_on_destroy = false
}
# Log Analytics
permanently_delete_on_destroy = true
# Recovery Vault
purge_protected_items_from_vault_on_destroy = true
# General
soft_delete_enabled = false
resource "random_string" "suffix" {
length = 4
upper = false
special = false
}
resource "azurerm_cognitive_account" "example" {
name = "cog-${var.topic}-${random_string.suffix.result}"
...
}
data "azurerm_subscription" "current" {}
resource "terraform_data" "subscription_guard" {
lifecycle {
precondition {
condition = data.azurerm_subscription.current.subscription_id == var.lab_subscription_id
error_message = "DEPLOYMENT BLOCKED — wrong subscription detected."
}
}
}
terraform init
terraform validate
terraform fmt -check
# Capacity tests for constrained services
terraform plan
# Review plan output
terraform apply
Always review terraform plan output before applying, especially for production.
| Resource Type | Default SKU |
|---|---|
| Virtual Machine | Standard_B2s |
| Storage Account | Standard_LRS |
| Load Balancer | Basic |
| Public IP | Basic |
| SQL Database | Basic / S0 |
| Managed Disk | Standard_HDD |
| Bastion | Developer |
| AI Services | F0 → S0 fallback |
All VMs must include auto-shutdown:
| Setting | Default Value | Client Override |
|---|---|---|
| Time | 0800 (8:00 AM) | TCU: 1600 (4:00 PM) |
| Time Zone | Central Standard Time | — |
Use azurerm_dev_test_global_vm_shutdown_schedule.
DateCreated tag.for_each for collections (maps/sets) — provides stable resource addresses.count for 0-1 conditional resources.# Conditional resource
resource "azurerm_public_ip" "pip" {
count = var.create_public_ip ? 1 : 0
...
}
# Multiple resources from a map
resource "azurerm_subnet" "subnets" {
for_each = var.subnet_configs
name = each.key
...
}
Use dynamic blocks for optional nested objects:
resource "azurerm_network_security_group" "nsg" {
...
dynamic "security_rule" {
for_each = var.security_rules
content {
name = security_rule.value.name
priority = security_rule.value.priority
direction = security_rule.value.direction
access = security_rule.value.access
protocol = security_rule.value.protocol
source_port_range = security_rule.value.source_port_range
destination_port_range = security_rule.value.destination_port_range
source_address_prefix = security_rule.value.source_address_prefix
destination_address_prefix = security_rule.value.destination_address_prefix
}
}
}
depends_on when absolutely necessary.depends_on where the dependent resource is already referenced implicitly.lifecycle {
ignore_changes = [tags] # Prevent drift on externally managed tags
prevent_destroy = true # Protect critical resources
}
ignore_changes for attributes managed externally.moved blocks for renames to avoid resource replacement.lifecycle blocks at the end of resource definitions.Include in all .tf files:
# -------------------------------------------------------------------------
# Program: <filename>
# Description: <purpose>
# Context: <workspace context>
# Author: Greg Tate
# Date: <YYYY-MM-DD>
# -------------------------------------------------------------------------
local-exec provisioners unless absolutely necessary.terraform import as a regular workflow pattern.When rules conflict, apply this precedence (highest wins):
Governance-Lab.md, terraform-governance.instructions.md)instructions/Azure Governance Guidelines.instructions.md)instructions/General Coding Guidelines.instructions.md).tftest.hcl extension.terraform validate for syntax checks.terraform plan for pre-deployment verification.