Naming Azure Resources in Terraform: A CAF Compliant Approach
How to implement Microsoft CAF naming conventions in Terraform using locals, including handling Storage Accounts, Key Vaults, and other resources with tight character limits.
In Terraform, build names from a shared
localsblock so one pattern drives every resource, for examplename = "kv-${var.workload}-${var.environment}-${var.region}-001". Usesubstr()to enforce tight limits like the 24-character cap on storage accounts and Key Vaults.
Infrastructure-as-code makes Azure resource names more consequential, not less. When you deploy with Terraform, the name in your .tf file is the name in Azure, and unlike manually-created resources, IaC deployments are expected to be consistent, repeatable, and reviewable in pull requests. A naming convention isn’t optional here; it’s the foundation of a maintainable Terraform codebase.
This guide shows how to implement CAF compliant Azure naming in Terraform using locals, handle the awkward edge cases (Storage Accounts, Key Vaults, VMs), and avoid the pitfalls that trip up most teams.
Why names matter more in Terraform
When you create a resource manually in the Azure portal, you can see the name field and second-guess yourself before clicking Create. In Terraform, names are generated by string expressions buried in .tf files, by the time you catch an error, terraform apply has already run.
More critically: most Azure resources cannot be renamed after creation. If your naming pattern is wrong, fixing it means terraform destroy followed by terraform apply, with all the state management, dependency re-creation, and potential downtime that implies.
Define your naming convention in locals before writing a single resource block.
The locals naming pattern
The CAF naming pattern follows this component order:
{type}-{company}-{department}-{workload}-{environment}-{region}-{instance}
In Terraform, centralise this in a locals block:
locals {
company = "contoso"
department = "finance"
workload = "payments"
env = "prod"
region = "eus"
instance = "001"
# Standard name: type + all components
name_base = "${local.company}-${local.department}-${local.workload}-${local.env}-${local.region}-${local.instance}"
# Names per resource type
resource_group_name = "rg-${local.name_base}"
vnet_name = "vnet-${local.name_base}"
aks_name = "aks-${local.name_base}"
acr_name = "cr-${local.name_base}"
app_service_name = "app-${local.name_base}"
sql_server_name = "sql-${local.name_base}"
}
Then in your resource blocks:
resource "azurerm_resource_group" "main" {
name = local.resource_group_name
location = "eastus"
}
resource "azurerm_kubernetes_cluster" "main" {
name = local.aks_name
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
# ...
}
Handling tight length limits
Some resource types have character limits that are easy to exceed. These need their own truncated locals.
Storage Accounts, 24 chars, lowercase, no hyphens
Storage Accounts are the most constrained: 24 characters maximum, lowercase letters and numbers only, no hyphens. The standard name_base approach won’t work.
locals {
# Storage Account: strip hyphens, lowercase, max 24 chars
# Drop department to fit, or truncate workload
storage_name = lower(substr(
"st${local.company}${local.workload}${local.env}${local.instance}",
0, 24
))
}
For contoso + payments + prod + 001 this gives: stcontosopaymentsprod0 (22 chars, fits).
If your company or workload names are longer, trim one first:
locals {
# Explicit shorter variant for storage
storage_name = "st${substr(local.company, 0, 6)}${substr(local.workload, 0, 8)}${local.env}${local.instance}"
}
Key Vault, 24 chars, alphanumerics + hyphens
Key Vault allows hyphens but still has a 24-character limit. Drop lower-priority components:
locals {
# Key Vault: drop department and region, keep type+company+workload+env+instance
key_vault_name = "kv-${local.company}-${local.workload}-${local.env}-${local.instance}"
}
For contoso + payments + prod + 001: kv-contoso-payments-prod-001 = 28 chars, still over. Drop company:
key_vault_name = "kv-${local.workload}-${local.env}-${local.instance}"
# kv-payments-prod-001 = 20 chars ✓
Windows Virtual Machines, 15 chars
Windows VM names are restricted to 15 characters (NetBIOS limit). Use only abbreviation + minimal identifiers:
locals {
# VM: type (2) + workload short (4) + env (4) + instance (3) = 13 chars
vm_name = "vm${substr(local.workload, 0, 4)}${local.env}${local.instance}"
# vmpaypprod001 = 13 chars ✓
}
Validating names with precondition
Terraform 1.2+ supports precondition blocks in resource lifecycle. Use them to catch naming violations at plan time:
resource "azurerm_storage_account" "main" {
name = local.storage_name
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
account_tier = "Standard"
account_replication_type = "LRS"
lifecycle {
precondition {
condition = length(local.storage_name) <= 24
error_message = "Storage account name '${local.storage_name}' exceeds 24 characters."
}
precondition {
condition = can(regex("^[a-z0-9]+$", local.storage_name))
error_message = "Storage account name must be lowercase alphanumerics only."
}
}
}
This fails loudly at terraform plan instead of at terraform apply, saving you from a partial deployment.
Reusable naming module
For multi-workload environments, extract naming into a Terraform module:
# modules/naming/variables.tf
variable "company" { type = string }
variable "workload" { type = string }
variable "env" { type = string }
variable "region" { type = string }
variable "instance" { type = string, default = "001" }
# modules/naming/outputs.tf
output "resource_group" { value = "rg-${var.company}-${var.workload}-${var.env}-${var.region}-${var.instance}" }
output "key_vault" { value = "kv-${var.workload}-${var.env}-${var.instance}" }
output "storage_account" { value = lower(substr("st${var.company}${var.workload}${var.env}${var.instance}", 0, 24)) }
output "aks" { value = "aks-${var.company}-${var.workload}-${var.env}-${var.region}-${var.instance}" }
# Usage
module "names" {
source = "./modules/naming"
company = "contoso"
workload = "payments"
env = "prod"
region = "eus"
}
resource "azurerm_resource_group" "main" {
name = module.names.resource_group
location = "eastus"
}
Generating the names
Before writing your locals, use AzureNamer to generate CAF compliant names for every resource type in your deployment. Enter your company, workload, environment, and region once, then export a ready-made Terraform locals block. The parameterized export produces exactly the pattern this guide describes, variables plus derived name expressions, so one file serves dev, staging, and production.
Keep reading
Azure Bicep Naming Conventions: CAF Compliant Resource Names
How to generate and enforce CAF compliant Azure resource names in Bicep templates, including the azure-naming module, string functions, and practical examples.
Read →Azure API Management Naming Conventions: Service, APIs, and Products
CAF naming for Azure API Management. The service is a globally unique azure-api.net endpoint (1 to 50 chars); APIs, products, backends, and named values inside are function-named. Patterns and examples.
Read →Azure Service Bus Naming Conventions: Namespaces, Queues, and Topics
CAF naming for Azure Service Bus. The namespace is a globally unique servicebus.windows.net endpoint (6 to 50 chars); queues, topics, and subscriptions inside are relaxed. Patterns and examples.
Read →Try AzureNamer
Generate CAF compliant names for all 200+ Azure resource types, free, no login required.
Open the Generator →