Skip to content
Back to skills

3004 Terraform Code Best Practicesinstructions 23280dc6

ASecurity

Infrastructure as Code best practices for Azure Terraform templates. AVM-first, CAF naming, security baseline.

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added June 1, 2026
devopspythongobashsqlazureterraformgitdatabasebackendsecurity

Works with

  • cli
  • mcp

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned June 1, 2026

npx -y skills add tools-only/X-Skills --skill 3004-terraform-code-best-practicesinstructions_23280dc6 --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 3004 Terraform Code Best Practicesinstructions 23280dc6?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for 3004 Terraform Code Best Practicesinstructions 23280dc6
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-3004-terraform-code-best-practicesinstructions-232/badge)](https://www.skillsdirectory.com/skills/tools-only-3004-terraform-code-best-practicesinstructions-232)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
description: "Infrastructure as Code best practices for Azure Terraform templates. AVM-first, CAF naming, security baseline."
applyTo: "**/*.tf"
---

# Terraform Development Best Practices

## Quick Reference

| Rule          | Standard                                                                 |
| ------------- | ------------------------------------------------------------------------ |
| Region        | `swedencentral` (alt: `germanywestcentral`)                              |
| Unique suffix | `resource "random_string" "suffix" { length = 4; lower = true }` in root |
| AVM first     | **MANDATORY** - Use Azure Verified Modules where available               |
| Tags          | Environment, ManagedBy, Project, Owner on ALL resources                  |
| Provider      | Pin `azurerm` to `~> 4.0`                                                |
| State backend | Azure Storage Account — **NEVER** HCP Terraform Cloud                    |

> [!IMPORTANT]
> The 4 tags above are baseline defaults. Discovered Azure Policy constraints
> (`04-governance-constraints.md`) ALWAYS take precedence. See
> `terraform-policy-compliance.instructions.md`.

## File Structure (MANDATORY)

Every root module MUST follow this file layout:

| File           | Purpose                                      |
| -------------- | -------------------------------------------- |
| `main.tf`      | Root module resources and module calls       |
| `variables.tf` | Input variable declarations                  |
| `outputs.tf`   | Output value declarations                    |
| `providers.tf` | Provider configuration blocks                |
| `versions.tf`  | `terraform {}` block with required_providers |
| `locals.tf`    | Local value computations                     |
| `backend.tf`   | Remote state backend configuration           |

## Naming Conventions

### Resource Identifiers

Use `azurerm_resource_group.this` for singleton resources (single instance per module).
Use descriptive names for multiple instances: `azurerm_subnet.app`, `azurerm_subnet.data`.

```hcl
# Singleton resource pattern
resource "azurerm_resource_group" "this" {
  name     = "rg-${var.project}-${var.environment}"
  location = var.location
  tags     = local.tags
}
```

### Azure Resource Names

Use lowercase with hyphens for Azure resource names. Follow CAF abbreviations:

| Resource        | Pattern                        | Example                |
| --------------- | ------------------------------ | ---------------------- |
| Resource Group  | `rg-{project}-{env}`           | `rg-contoso-dev`       |
| Virtual Network | `vnet-{project}-{env}`         | `vnet-contoso-dev`     |
| Key Vault       | `kv-{short}-{env}-{suffix}`    | `kv-contoso-dev-a1b2`  |
| Storage Account | `st{short}{env}{suffix}`       | `stcontosodevа1b2`     |
| App Service     | `app-{project}-{env}-{suffix}` | `app-contoso-dev-a1b2` |
| SQL Server      | `sql-{project}-{env}-{suffix}` | `sql-contoso-dev-a1b2` |

> [!CAUTION]
> Storage Account names have a 24-char limit and no hyphens. Key Vault names have a
> 24-char limit. Always use `substr()` to trim long names.

## Unique Suffix Pattern (CRITICAL)

Generate ONCE in the root module, pass to ALL child modules:

```hcl
# versions.tf or locals.tf
resource "random_string" "suffix" {
  length  = 4
  lower   = true
  numeric = true
  special = false
}

locals {
  suffix = random_string.suffix.result

  # Length-constrained names
  kv_name  = "kv-${substr(var.project, 0, 8)}-${substr(var.environment, 0, 3)}-${local.suffix}"
  st_name  = "st${substr(replace(var.project, "-", ""), 0, 8)}${substr(var.environment, 0, 3)}${local.suffix}"
}
```

## Provider Configuration

```hcl
# versions.tf
terraform {
  required_version = ">= 1.9"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.0"
    }
  }
}
```

```hcl
# providers.tf
provider "azurerm" {
  features {}
  subscription_id = var.subscription_id
}
```

## State Backend (MANDATORY)

Use Azure Storage Account for remote state. **NEVER** use HCP Terraform Cloud.

```hcl
# backend.tf
terraform {
  backend "azurerm" {
    resource_group_name  = "rg-tfstate-prod"
    storage_account_name = "sttfstate{suffix}"
    container_name       = "tfstate"
    key                  = "{project}.terraform.tfstate"
  }
}
```

## Tags (MANDATORY)

> [!IMPORTANT]
> These 4 tags are the MINIMUM baseline. Azure Policy in your subscription may enforce
> additional tags. Always defer to `04-governance-constraints.md` for the actual required tag list.

```hcl
# locals.tf
locals {
  tags = merge(var.tags, {
    Environment = var.environment
    ManagedBy   = "Terraform"
    Project     = var.project
    Owner       = var.owner
  })
}
```

Pass `local.tags` to every resource and AVM module.

## Security Defaults (MANDATORY)

> [!IMPORTANT]
> The security settings below are baseline defaults. Discovered Azure Policy
> security constraints (`04-governance-constraints.md`) ALWAYS take precedence.
> See `terraform-policy-compliance.instructions.md`.

```hcl
# Storage Account
resource "azurerm_storage_account" "this" {
  # ...
  https_traffic_only_enabled    = true
  min_tls_version               = "TLS1_2"
  allow_nested_items_to_be_public = false
  shared_access_key_enabled     = false  # Policy may require this
}

# SQL Server
resource "azurerm_mssql_server" "this" {
  # ...
  minimum_tls_version          = "1.2"
  public_network_access_enabled = false
  azuread_administrator {
    azuread_authentication_only = true
  }
}
```

## RBAC Least Privilege (MANDATORY)

Do not grant broad built-in control-plane roles to runtime app identities.

### Forbidden by Default

The following roles are **not allowed** for application runtime identities unless
explicitly approved in a tracked exception:

- `Owner`
- `Contributor`
- `User Access Administrator`

### Approved Role Mappings

Use the smallest role and narrowest scope that satisfies the workload.

| Resource Type | Approved Role(s)                                                    | Required Scope Pattern             |
| ------------- | ------------------------------------------------------------------- | ---------------------------------- |
| Key Vault     | `Key Vault Secrets User`                                            | Specific Key Vault resource ID     |
| Storage Blob  | `Storage Blob Data Reader` / `Storage Blob Data Contributor`        | Storage account or container scope |
| SQL Database  | `SQL DB Contributor` (or DB-level Entra roles)                      | Database scope, not server scope   |
| Service Bus   | `Azure Service Bus Data Sender` / `Azure Service Bus Data Receiver` | Namespace or queue/topic scope     |
| Event Hubs    | `Azure Event Hubs Data Sender` / `Azure Event Hubs Data Receiver`   | Namespace or hub scope             |
| ACR Pull      | `AcrPull`                                                           | Specific registry scope            |

### SQL-Specific Rule

For app-to-SQL access, prefer:

1. Entra-based DB user and DB roles (`db_datareader`, `db_datawriter`) where possible
2. Otherwise `SQL DB Contributor` at database scope

Never assign `Contributor` at SQL server scope for app runtime identities.

### Exception Process

If a broad role is unavoidable, all of the following are required:

1. Inline comment marker on the role assignment:
   `RBAC_EXCEPTION_APPROVED: <ticket-or-ADR>`
2. Matching justification in implementation docs (ADR or implementation reference)
3. Time-bound review date and owner in the justification

Without all three, the configuration is non-compliant.

## Azure Verified Modules (AVM-TF)

**MANDATORY: Use AVM-TF modules for ALL resources where an AVM module exists.**

Raw `azurerm_*` resources are only permitted when no AVM module exists AND the user
explicitly approves. Document the rationale in the implementation reference.

```hcl
# ✅ Use AVM-TF for Key Vault
module "key_vault" {
  source  = "Azure/avm-res-keyvault-vault/azurerm"
  version = "~> 0.9"

  name                = local.kv_name
  resource_group_name = azurerm_resource_group.this.name
  location            = var.location
  tenant_id           = data.azurerm_client_config.current.tenant_id
  tags                = local.tags
}

# ❌ Only use raw azurerm_* if no AVM module exists
# Requires explicit user approval: "approve raw terraform"
```

### AVM-TF Module Source Format

```hcl
source  = "Azure/avm-res-{service}-{resource}/azurerm"
version = "~> {major}.{minor}"
```

Examples:

| Resource        | Source                                         |
| --------------- | ---------------------------------------------- |
| Key Vault       | `Azure/avm-res-keyvault-vault/azurerm`         |
| Storage         | `Azure/avm-res-storage-storageaccount/azurerm` |
| Virtual Network | `Azure/avm-res-network-virtualnetwork/azurerm` |
| App Service     | `Azure/avm-res-web-site/azurerm`               |

Use `mcp_terraform_get_latest_module_version` or `registry.terraform.io/modules/Azure`
to find the latest version before generating code. Update pinned minor version
(`~> X.Y`) to the latest available.

## Variables

```hcl
# variables.tf
variable "location" {
  description = "Azure region for all resources."
  type        = string
  default     = "swedencentral"

  validation {
    condition     = contains(["swedencentral", "germanywestcentral", "northeurope"], var.location)
    error_message = "Location must be an approved EU region."
  }
}

variable "environment" {
  description = "Deployment environment."
  type        = string
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

variable "tags" {
  description = "Additional tags to merge with baseline tags."
  type        = map(string)
  default     = {}
}
```

## Outputs

```hcl
# outputs.tf — every module must output BOTH ID and name
output "resource_group_id" {
  description = "Resource group resource ID."
  value       = azurerm_resource_group.this.id
}

output "resource_group_name" {
  description = "Resource group name."
  value       = azurerm_resource_group.this.name
}
```

## Managed Identity Pattern

Prefer SystemAssigned managed identity over access keys or connection strings:

```hcl
resource "azurerm_linux_web_app" "this" {
  # ...
  identity {
    type = "SystemAssigned"
  }
}

resource "azurerm_role_assignment" "app_kv" {
  scope                = module.key_vault.resource_id
  role_definition_name = "Key Vault Secrets User"
  principal_id         = azurerm_linux_web_app.this.identity[0].principal_id
}
```

## Lifecycle Rules

```hcl
# Avoid phantom diffs on externally-managed tags
lifecycle {
  ignore_changes = [tags["DateCreated"]]
}

# Prevent accidental deletion of stateful resources
lifecycle {
  prevent_destroy = true
}
```

## Resource Renaming Without Destroy

Use `moved` blocks instead of destroying and re-creating:

```hcl
moved {
  from = azurerm_key_vault.main
  to   = azurerm_key_vault.this
}
```

## Patterns to Avoid

| Anti-Pattern                    | Problem                       | Solution                               |
| ------------------------------- | ----------------------------- | -------------------------------------- |
| Hardcoded resource names        | Naming collisions             | Use `random_string.suffix`             |
| `count` for named resources     | Index-based drift on deletion | Use `for_each` with string keys        |
| Missing `description` on vars   | Poor documentation            | Document all input variables           |
| `>= 3.0` provider version range | Unintended major upgrades     | Use `~> 4.0` for minor-version pinning |
| HCP Terraform Cloud as backend  | Vendor lock-in                | Use Azure Storage Account backend      |
| Raw `azurerm_*` when AVM exists | Policy drift and maintenance  | Use AVM-TF modules or get approval     |
| `connection_string` auth        | Credential exposure           | Use managed identity RBAC              |

## Validation Commands

```bash
terraform fmt -recursive
terraform validate
terraform plan -out=plan.tfplan
terraform show -json plan.tfplan | python scripts/analyze_plan.py  # optional: set-diff analysis
```

Always run `terraform fmt` before committing. Always run `terraform validate` before planning.

## Cross-References

- **Policy compliance**: `.github/instructions/terraform-policy-compliance.instructions.md`
- **Governance discovery**: `.github/instructions/governance-discovery.instructions.md`
- **Terraform patterns**: `.github/skills/terraform-patterns/SKILL.md`
- **Azure defaults**: `.github/skills/azure-defaults/SKILL.md`

Files in this skill

  • README.md1.1 KB
  • skill.md12.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.