Six months into a subscription, you run az resource list and stare at the wreckage: rg1, MyStorage, prod-vm, vnet-new, kv-test-final-2, a storage account someone called data and another called data2. Nobody can tell which resource belongs to which app, which is production, or who pays for it. The bill is a mystery, the on-call engineer can’t find the right VNet at 02:00, and a cleanup script almost deleted the wrong resource group because two were named rg-prod. None of this is a tooling failure. It is the slow, compounding cost of never deciding, on day one, how things are named — and writing that decision down where machines can enforce it.
A naming convention is a deterministic scheme that turns the question “what should I call this resource?” into a formula. The Cloud Adoption Framework (CAF) — Microsoft’s prescriptive guidance for adopting Azure at scale — publishes a recommended pattern and a canonical list of resource-type abbreviations precisely so every team doesn’t reinvent this. Done well, a name is self-describing: vm-shop-prod-eus-01 tells you it is a virtual machine, for the shop workload, in production, in East US, instance 01 — without opening a single blade. Paired with a small set of mandatory tags (the metadata key/value pairs that carry the facts a name can’t, like cost centre and owner), your estate becomes queryable, billable and governable.
This is a step-by-step implementation guide, not a philosophy lecture. By the end you will have designed a concrete naming scheme grounded in the real CAF abbreviations and the real, per-resource-type rules (storage accounts allow no hyphens and max 24 lowercase characters; a Key Vault name is globally unique and 3–24 characters; a resource group tolerates almost anything up to 90). You will build it three ways — in the portal, with the az CLI, and as reusable Bicep — and then make it stick with naming-aware Bicep functions, a tag taxonomy, and an Azure Policy that denies a non-compliant name before it is ever created. The goal is a convention a script can apply and a policy can enforce, so the wreckage above never accumulates again.
What problem this solves
The pain is rarely a single bad name; it is the absence of a system, which shows up months later as four distinct failures. Discoverability dies first: with hundreds of resources and no pattern, finding “the production database for the orders service” means opening blades and guessing, and the on-call engineer who doesn’t know the estate is paralysed. Cost allocation dies next: finance asks “what does the payments team spend?” and the answer is unknowable because nothing identifies the owner — names carry no cost centre and tags were never mandated, so the bill is one undifferentiated number. Safe automation dies third: scripts that target resources by name become dangerous when names collide or follow no rule (rg-prod exists three times across subscriptions), and a teardown job that matched the wrong one is a genuine outage waiting to happen. Audit and compliance dies last: an auditor asks you to prove every production resource is owned and classified, and without a convention there is no way to assert it across thousands of resources.
What breaks without a convention is not dramatic on day one — it is the steady accumulation of entropy that makes the estate progressively harder to operate, until someone declares a “tagging initiative” eighteen months in and discovers that retro-fitting names onto live resources is nearly impossible. You cannot rename most Azure resources. A storage account, a Key Vault, a VM, a public IP — the name is fixed at creation; to “rename” you create a new one and migrate, which for stateful resources is a project, not an edit. That single fact is why this belongs on day one: the cost of getting it wrong is not a quick fix later, it is a migration.
Who hits this: every team past the first handful of resources, and acutely any organisation running multiple subscriptions, multiple environments (dev/test/prod), or multiple workloads on shared infrastructure. It bites hardest where the Azure resource hierarchy spans management groups and many subscriptions, because that is exactly where ad-hoc naming makes the whole estate illegible. The fix is cheap if done first and expensive if done late — which is the whole argument for doing it now.
Learning objectives
By the end of this article you can:
- Explain the CAF recommended naming pattern and decompose any resource name into its component fields (type, workload, environment, region, instance).
- Use the canonical CAF resource-type abbreviations correctly and know where they live so you never guess (
vm,st,kv,rg,vnet,snet,pip,nsg, and more). - Handle the per-resource-type naming rules that differ wildly — length caps, allowed characters, case sensitivity, and global-uniqueness — and predict which resources will reject a generic name.
- Pair the naming scheme with a tag taxonomy, deciding what belongs in the name versus what belongs in tags.
- Generate compliant names deterministically in Bicep using string functions and a parameter-driven convention module.
- Create resources to the convention three ways — portal,
azCLI, and Bicep — with validation at each step. - Enforce the convention with Azure Policy (deny / audit) and the tag taxonomy with inherit / require policies, so non-compliant resources can’t be created.
- Right-size the effort: know which 20% of the convention (resource groups, the shared-services accounts, the cost-allocation tags) delivers 80% of the value.
Prerequisites & where this fits
You should be comfortable with the Azure resource hierarchy — management groups, subscriptions, resource groups and resources — and know how to run az in Cloud Shell, read JSON output, and deploy a basic Bicep file. Familiarity with Azure resource tags and the idea of Azure Policy effects (audit, deny, modify) will let you move faster, but this article reintroduces what it needs.
This sits in the Governance & Resource Organization track and is foundational to it. A naming convention is one of the first decisions in a CAF landing zone: the enterprise-scale landing zone reference architecture assumes a convention and bakes it into its templates. It pairs tightly with tagging strategy (names and tags are two halves of one metadata story) and with policy at scale (the convention is only real once a policy enforces it). It is upstream of almost everything: every VNet, storage account and VM you ever create inherits the decision you make here.
Where it fits in the bigger picture, layer by layer:
| Layer | What it contributes to naming | Who owns it | Why it matters |
|---|---|---|---|
| Management group | Optional org/business-unit prefix; policy scope | Platform / governance team | Where the deny policy is assigned so it applies estate-wide |
| Subscription | Often maps to environment or business unit | Platform team | A common field in the name (prod/dev) often mirrors the subscription |
| Resource group | The container; named first, sets the workload context | App / platform team | The highest-leverage name — get this right and grouping follows |
| Resource | The leaf; full convention applies per type | App / dev team | Where per-type rules (length, charset) actually bite |
| Tags | Metadata the name can’t carry (owner, cost centre, data class) | App + platform | The half of governance that makes the estate billable and auditable |
Core concepts
Five ideas make every later decision obvious.
A name is a formula, not a label. The CAF pattern composes a name from ordered, delimited fields so that any name can be both produced by a rule and parsed back into facts. The canonical components are: resource type (an abbreviation like vm), workload/application (a short app identifier like shop), environment (prod, dev, test), region (a short Azure region code like eus for East US), and an instance number (01, 02) to disambiguate copies. Not every field appears for every resource, but the order is fixed so names sort and scan consistently. The recommended delimiter is the hyphen (-) — vm-shop-prod-eus-01 — except where a resource type forbids hyphens, which is where the rules get interesting.
The abbreviation list is canonical — look it up, never invent. CAF publishes a maintained table of recommended abbreviations for every resource type. Using it means st always means storage account, kv always means Key Vault, vnet always a virtual network — across every team and every repo. Inventing your own (stor, keyv, virtnet) defeats the entire point, which is that a name is predictable. The single most important habit this article teaches is: when you need an abbreviation, look it up in the CAF table, copy it exactly.
Per-resource rules override your aesthetic. Azure does not enforce one naming rule; each resource provider enforces its own. Resource group: up to 90 characters, letters/digits/./_/-/parentheses, case-insensitive — generous. Storage account: 3–24 characters, lowercase letters and digits only — no hyphens, and globally unique across all of Azure. Key Vault: 3–24 characters, alphanumeric and hyphens, must start with a letter, globally unique. Virtual machine: 1–15 characters on Windows (the NetBIOS limit), 1–64 on Linux. Your beautiful st-shop-prod-eus-01 is illegal as a storage account name on three counts (hyphens, length, and the literal st- prefix). The convention must bend per type, and knowing which types are strict is half the battle.
Global uniqueness forces a uniqueness token. Resources that participate in public DNS — storage accounts (<name>.blob.core.windows.net), Key Vaults (<name>.vault.azure.net), App Services (<name>.azurewebsites.net), Azure SQL servers, Container Registries — must be unique not just in your tenant but across all of Azure. A pure formula like stshopprod will eventually collide with another customer. The fix is to append a short deterministic uniqueness suffix — commonly the last few characters of a hash of the resource group ID, which Bicep’s uniqueString() function produces — so the name stays predictable per deployment yet globally distinct.
Names and tags are complementary, not competing. A name is short, immutable and embedded in every reference; it carries the few facts you need to identify a resource at a glance (type, workload, env, region). A tag is a mutable key/value pair carrying the facts that change or are too long for a name: owner, costCenter, dataClassification, expiryDate, managedBy. The rule of thumb: if you need it to find or sort resources and it never changes, it goes in the name; if you need it to filter, bill or audit and it might change, it goes in a tag. Trying to cram cost centre and owner into the name is how you get 80-character monstrosities; that is what tags are for.
The vocabulary in one table
Pin down every moving part before the deep sections. The glossary repeats these for lookup; this is the mental model side by side:
| Term | One-line definition | Example | Where it lives |
|---|---|---|---|
| Naming convention | The deterministic formula for resource names | <type>-<workload>-<env>-<region>-<instance> |
A documented standard + Bicep module |
| CAF abbreviation | The canonical short code for a resource type | vm, st, kv, rg, vnet |
The CAF abbreviations table |
| Workload / app token | Short identifier for the application or service | shop, pay, core |
Chosen per project, kept stable |
| Environment token | Which lifecycle stage the resource serves | prod, dev, test, uat |
Standardised list across the org |
| Region token | Short code for the Azure region | eus (East US), weu (West Europe) |
A documented region map |
| Instance token | Disambiguates multiple copies | 01, 02 |
Per-resource, zero-padded |
| Uniqueness suffix | Deterministic token for globally-unique names | a1b2c3 (from uniqueString()) |
Appended to DNS-named resources |
| Tag | Mutable key/value metadata on a resource | costCenter=CC-4471 |
The resource’s tags property |
| Tag taxonomy | The agreed set of required + optional tag keys | owner, env, costCenter… |
A documented standard + policy |
| Delimiter | The separator between name fields | - (hyphen) |
The convention spec |
The CAF naming pattern, field by field
The CAF recommended pattern is an ordered sequence of fields joined by a delimiter. Not every field is mandatory for every resource, but when present they appear in this order. Here is each field: what it is, the values it takes, whether it’s required, and the trap that bites if you get it wrong.
| Field | Purpose | Example values | Required? | Gotcha |
|---|---|---|---|---|
| Resource type | Identifies what the resource is | vm, st, kv, rg, vnet, snet, pip, nsg |
Always | Use the CAF abbreviation exactly; never invent |
| Workload / app | Which application/service it serves | shop, pay, core, data |
Usually | Keep it short (≤ 8 chars) and stable forever |
| Environment | The lifecycle stage | prod, dev, test, uat, stage |
Usually | Standardise the exact spellings org-wide |
| Region | Where it is deployed | eus, eus2, weu, cus, sea |
When multi-region | Pick a region map and never deviate |
| Instance | Disambiguates copies | 01, 02, 001 |
When > 1 | Zero-pad so they sort (01 not 1) |
| Uniqueness suffix | Global-uniqueness token | a1b2c3 |
DNS-named types only | Only where global uniqueness is required |
Why the order is fixed (and matters)
The order is type → workload → environment → region → instance for a reason: it makes names sort usefully and scan predictably. When you list resources alphabetically, all VMs cluster (type first), then within VMs all shop resources cluster (workload second), and so on. If you put environment first you’d interleave VMs and storage accounts of the same environment — useless when you’re looking for “the production VMs.” The leading type abbreviation is also what lets a human glance at a name and immediately know the resource class, which is the single most common reason you read a name at all.
Field values: standardise the exact spellings
The convention is only deterministic if the vocabulary is fixed. “Production” might be prod, prd, production, p — pick one and forbid the rest, because vm-shop-prod and vm-shop-prd both existing is exactly the inconsistency you’re trying to kill. The same applies to regions and environments. Below is a sane starting vocabulary; the exact values matter less than choosing them once and enforcing them.
| Field | Recommended values | Avoid |
|---|---|---|
| Environment | prod, dev, test, uat, stage, sbox (sandbox) |
Mixing prd/prod, qa/test, capitalised variants |
| Region (examples) | eus, eus2, wus, wus2, cus, weu, neu, uks, sea, cin (Central India) |
Full region names; inconsistent abbreviations |
| Instance | 01–99 (zero-padded) |
Unpadded 1, 2; non-numeric suffixes for copies |
| Workload | 3–8 lowercase chars, stable for the app’s life | Renaming the workload token after resources exist |
Region codes: a practical map
There is no single Microsoft-blessed short region code list (the platform uses long names like eastus), so you adopt a consistent abbreviation map and document it. A common convention compresses the region name to 3–4 characters. Document the ones you use; here are the frequently-seen mappings.
| Azure region (long) | Common short code | Region (long) | Common short code |
|---|---|---|---|
| East US | eus |
West Europe | weu |
| East US 2 | eus2 |
North Europe | neu |
| West US | wus |
UK South | uks |
| West US 2 | wus2 |
UK West | ukw |
| Central US | cus |
Southeast Asia | sea |
| South Central US | scus |
Central India | cin |
The rule is not which codes you choose — it is that the map is written down and used everywhere, including in Bicep where you’ll map resourceGroup().location to the short code.
The abbreviation table you actually use
This is the heart of the convention: the CAF-recommended abbreviation per resource type. Memorise the common dozen; look up the rest. These are the canonical prefixes you place at the front of every name.
| Resource type | CAF abbreviation | Resource type | CAF abbreviation |
|---|---|---|---|
| Resource group | rg |
Virtual network | vnet |
| Virtual machine | vm |
Subnet | snet |
| VM scale set | vmss |
Network security group | nsg |
| Storage account | st |
Public IP address | pip |
| Key Vault | kv |
Network interface | nic |
| App Service plan | asp |
Load balancer (internal) | lbi |
| App Service / Web App | app |
Load balancer (external) | lbe |
| Function App | func |
Application Gateway | agw |
| Azure SQL server | sql |
Route table | rt |
| Azure SQL database | sqldb |
Private endpoint | pep |
| Cosmos DB account | cosmos |
Log Analytics workspace | log |
| Container Registry | cr |
Application Insights | appi |
| AKS cluster | aks |
User-assigned managed identity | id |
| Container Apps environment | cae |
Bastion host | bas |
A few placement notes that trip people up:
| Situation | Rule | Example |
|---|---|---|
| Abbreviation always leads | Type prefix is field 1, no exceptions | nsg-shop-prod not shop-nsg-prod |
| Resource group sets context | Name the RG first; resources inherit its workload/env | rg-shop-prod-eus → vm-shop-prod-eus-01 |
| Subnets live under a VNet | Subnet name need not repeat the full VNet name | snet-web, snet-data inside vnet-shop-prod-eus |
| Globally-unique types | Append the uniqueness suffix | stshopprodeusa1b2 (no hyphens) |
Where to find the authoritative list: the CAF “Abbreviation recommendations for Azure resources” page in Microsoft Learn. It is updated as new services launch — treat it as the source of truth and copy from it rather than relying on memory for the long tail.
Per-resource-type rules: where one convention isn’t enough
This is the section that separates a convention that works from one that throws InvalidResourceName on your third deployment. Every resource provider enforces its own length, character-set, case and uniqueness rules. Below is the reference table for the types you’ll hit most. st-shop-prod-eus-01 is a valid resource group name and an illegal storage account name — that difference is the whole point.
| Resource type | Length | Allowed characters | Case | Scope of uniqueness | The trap |
|---|---|---|---|---|---|
| Resource group | 1–90 | Alphanumeric, _, -, ., () |
Insensitive | Per subscription | Generous — but no trailing period |
| Storage account | 3–24 | Lowercase letters + digits only | Lowercase | Global | No hyphens; collides globally; very short cap |
| Key Vault | 3–24 | Alphanumeric + -; start with letter |
Insensitive | Global | Must start with a letter; global; no consecutive -- |
| Virtual machine (Windows) | 1–15 | Alphanumeric + -; can’t be all digits |
Insensitive | Per resource group | 15-char NetBIOS cap — brutal |
| Virtual machine (Linux) | 1–64 | Alphanumeric + -, . |
Insensitive | Per resource group | Longer than Windows; don’t assume one rule |
| Virtual network | 2–64 | Alphanumeric, _, -, . |
Insensitive | Per resource group | Hyphens fine; comfortable length |
| Subnet | 1–80 | Alphanumeric, _, -, . |
Insensitive | Per VNet | Scoped to its VNet, not the RG |
| Network security group | 1–80 | Alphanumeric, _, -, . |
Insensitive | Per resource group | Comfortable; hyphens fine |
| Public IP | 1–80 | Alphanumeric, _, -, . |
Insensitive | Per resource group | Comfortable |
| App Service / Web App | 2–60 | Alphanumeric + - |
Insensitive | Global (DNS) | .azurewebsites.net → global; needs suffix |
| Function App | 2–60 | Alphanumeric + - |
Insensitive | Global (DNS) | Same DNS rule as Web Apps |
| Azure SQL server | 1–63 | Lowercase alphanumeric + - |
Lowercase | Global (DNS) | Lowercase only; global |
| Container Registry | 5–50 | Alphanumeric only — no hyphens | Insensitive | Global | Like storage: no hyphens, global |
| Cosmos DB account | 3–44 | Lowercase alphanumeric + - |
Lowercase | Global | Lowercase; global |
| AKS cluster | 1–63 | Alphanumeric, _, - |
Insensitive | Per resource group | Comfortable; but node resource group gets auto-named |
Reading this table changes how you build the convention: hyphenated resources (rg, vm, vnet, nsg, kv, app) use the clean type-workload-env-region-instance form; no-hyphen resources (st, cr) drop the delimiters and become stworkloadenvregion plus a suffix; global resources (st, kv, app, func, sql, cr, cosmos) get the uniqueness suffix; and the 15-character Windows VM cap means your workload+env+region+instance must fit in 12 characters after vm-, which often forces abbreviation of the workload token itself.
The strict cases, called out
Three failure modes deserve their own callout because they’re the ones that surprise people in production:
| Strict case | Why it surprises | What to do |
|---|---|---|
Storage account st-shop-prod rejected |
The - and the length feel arbitrary after RGs accept them |
Drop hyphens, lowercase, ≤ 24 chars, add suffix: stshopproda1b2 |
| Windows VM name truncated/rejected at 15 | NetBIOS legacy limit; vm-payments-prod-eus-01 is 23 chars |
Shorten workload (pay), drop region if single-region: vm-pay-prod-01 |
| Globally-unique name collides intermittently | Works in dev, fails in prod when another tenant took the name | Always append uniqueString()-derived suffix for global types |
Tags: the half of governance the name can’t carry
A name identifies; a tag describes. Tags are key/value strings (max 512 chars on the value, 128 on the key, up to 50 tags per resource) that carry the facts you filter, bill and audit on. The discipline is the same as naming: agree a small required set, standardise the keys and allowed values, and enforce them. Resist the urge to over-tag — 50 tags is the ceiling, but five well-chosen mandatory tags beat twenty optional ones nobody fills in.
Here is a pragmatic baseline taxonomy — the mandatory keys, what each carries, and an example value.
| Tag key | Carries | Example value | Mandatory? | Why |
|---|---|---|---|---|
env |
Environment (mirrors the name field) | prod |
Yes | Filter and bill by lifecycle stage |
owner |
Team or individual responsible | payments-team |
Yes | Who to call; who to bill effort to |
costCenter |
Finance cost-allocation code | CC-4471 |
Yes | The single most important FinOps tag |
workload |
Application/service name | shop |
Yes | Roll cost up by application |
dataClassification |
Sensitivity of data handled | confidential |
Recommended | Drives security/compliance controls |
managedBy |
How it’s provisioned | bicep / terraform / manual |
Recommended | Flag drift; find click-ops resources |
expiryDate |
When a temp resource should die | 2026-09-30 |
For non-prod | Auto-cleanup of sandbox resources |
criticality |
Business impact tier | tier-1 |
Recommended | Prioritise during incidents |
What goes in the name vs the tag
The recurring decision, made explicit. The test: immutable + needed to identify at a glance → name; mutable or descriptive → tag.
| Fact | Name or tag? | Reasoning |
|---|---|---|
| Resource type | Name | Immutable; you read it constantly to identify the resource |
| Workload | Both | Name for at-a-glance ID; tag for cost roll-up queries |
| Environment | Both | Name for scanning; tag for billing/policy filtering |
| Region | Name | Immutable; needed to disambiguate copies |
| Owner / team | Tag | Changes as teams reorg; too long for a name |
| Cost centre | Tag | Pure metadata; changes; never used to find a resource |
| Data classification | Tag | Descriptive; drives policy, not identification |
| Creation date / expiry | Tag | Metadata; mutable |
Putting env and workload in both the name and a tag is deliberate, not redundant: the name makes them human-scannable, the tag makes them machine-queryable for cost and policy. The two systems reinforce each other. Tagging strategy deserves its own deep treatment — see Azure resource tags: strategy, governance & cost allocation for the full FinOps angle.
Architecture at a glance
Picture the convention as a small pipeline that turns intent into compliant, governed resources, with three checkpoints. At the left sits your intent: “I need a production storage account for the shop workload in East US.” That intent is just five facts — type, workload, environment, region, instance — plus the tag values (owner, cost centre, data class).
Those facts flow into the name builder — in practice a Bicep module or a documented formula. The builder applies the per-type rules: for a hyphenated type it joins the fields with -; for a no-hyphen global type like storage it strips delimiters, lowercases, truncates to 24 characters, and appends a uniqueString()-derived suffix for global uniqueness. The output is a deterministic, legal name (stshopprodeusa1b2) and a tag bag. The same facts, the same rule, the same name — every time, from any team.
The named, tagged request then hits the governance gate before anything is created: Azure Policy, assigned at the management-group or subscription scope. A deny policy pattern-matches the proposed name and rejects it if it doesn’t conform; require-tag policies reject the resource if mandatory tags are missing; inherit-tag policies copy costCenter and env down from the resource group so individual resources don’t have to repeat them. Only a request that passes all three — legal name, present tags — reaches the Azure Resource Manager and becomes a real resource. The result, at the right, is an estate where every resource name is parseable, every resource is owned and billable, and no non-compliant resource can sneak in, because the gate runs at create time, not in an after-the-fact audit. The mental model to hold: facts → formula → gate → governed resource, with the formula guaranteeing consistency and the gate guaranteeing enforcement.
Real-world scenario
Northwind Retail ran a single Azure subscription that had grown organically over three years to roughly 1,400 resources across four teams. There was no convention. Storage accounts were named data, datanew, prodstorage, nwstore01; resource groups included three different rg-prod variants and a RG-Test that actually held production databases. Tags existed on maybe 30% of resources, with keys like Owner, owner, OWNER and Team all in use. The breaking point came when finance, under a new cost-accountability mandate, asked each of the four teams to own their Azure spend — and nobody could produce the number, because nothing tied a resource to a team or cost centre.
The platform team ran a two-week remediation. Week one was design and inventory. They adopted the CAF pattern (<type>-<workload>-<env>-<region>-<instance>), copied the CAF abbreviation table verbatim, fixed the environment vocabulary (prod/dev/test/uat), and defined five mandatory tags (env, owner, costCenter, workload, dataClassification). Crucially, they accepted reality: existing resources could not be renamed, so the convention applied to all new resources, while a parallel effort retro-fitted tags (which can be applied to existing resources) onto the legacy estate. They exported the full inventory with az resource list to a spreadsheet and assigned each of the 1,400 resources an owner and cost centre by inspection and team interviews — tedious, but a one-time cost.
Week two was enforcement. They wrote a tag-inheritance policy so costCenter and env flow from each resource group down to its resources, and a require-tag policy (in audit mode first, to size the gap) for the five mandatory tags. The audit showed 64% non-compliance — exactly the legacy debt — which they burned down team by team using az resource tag bulk operations driven from the inventory spreadsheet. For new resources, they assigned a deny-effect policy that pattern-matches names against the convention’s regex and rejects non-conforming creates, plus a deny on resources missing mandatory tags. They flipped both to deny only after the audit gap was closed, so they didn’t block legitimate work mid-remediation.
The payoff was concrete. Within a month, a single Cost Management query grouped by the costCenter tag produced the four per-team spend numbers finance had asked for — the original trigger, now a non-event. The on-call rotation reported that finding “the production orders database” went from a five-minute hunt to reading the name. And a near-miss that had haunted them — a cleanup script that once almost deleted the wrong rg-prod — became impossible, because new resource groups were uniquely named (rg-orders-prod-eus, rg-payments-prod-eus) and the deny policy guaranteed it. The lesson Northwind drew, and the one to internalise: the design was the easy half-day; the value came entirely from enforcement. A convention nobody is forced to follow is a wiki page that rots.
Advantages and disadvantages
A convention is overwhelmingly worth it, but it is not free — the cost is up-front rigidity and a little ceremony. The honest trade-off:
| Advantages | Disadvantages |
|---|---|
| Names are self-describing — read intent without opening a blade | Up-front design effort before you create anything |
| Cost allocation works (group by tag → per-team spend) | Requires discipline/enforcement or it decays immediately |
| Safe automation — predictable names, no dangerous collisions | Immutability means mistakes early are expensive to fix |
| Audit/compliance becomes assertable across the estate | Per-type rules add complexity (no single formula fits all) |
| Onboarding is faster — new engineers read the estate | Global-uniqueness suffixes make names less “pretty” |
| Policy can enforce it at create time, not after the fact | Retro-fitting onto an existing estate is largely impossible (names) |
When the advantages dominate: any estate you intend to keep and grow, anything multi-team or multi-environment, anything where finance will eventually ask “who spends what,” and anything automated. That is nearly everything past a proof-of-concept. When the cost feels heaviest: a throwaway sandbox or a one-week spike, where the up-front design is overhead you won’t recoup — though even there, the tag discipline (especially expiryDate) pays for itself by letting you auto-clean the sandbox. The disadvantages are real but front-loaded: pay the design cost once, enforce with policy, and the convention runs itself. The failure mode is never “the convention was too strict” — it is “we wrote a convention and never enforced it,” which is the worst outcome because it has all the cost and none of the value.
Hands-on lab
This is the centerpiece. You will design a small convention and then create a complete, conformant slice of an estate three ways — portal, az CLI, and Bicep — for a fictional shop workload in prod, in East US. Then you’ll generate globally-unique names correctly, apply the tag taxonomy, and enforce both with Azure Policy. Everything here is free-tier-friendly: the resources used (resource group, VNet, NSG, storage account, Key Vault) cost nothing or near-nothing at rest, and you tear it all down at the end.
Prerequisites for the lab
| Requirement | How to get it | Verify |
|---|---|---|
| Azure subscription | Free account works | az account show -o table |
| Azure CLI ≥ 2.50 (or Cloud Shell) | Cloud Shell has it pre-installed | az version |
| Contributor on a subscription/RG | For creating resources | az role assignment list --assignee <you> -o table |
| Resource Policy Contributor (for the policy step) | For assigning policy | Needed only for Part 5 |
The convention we’ll implement in this lab:
| Field | Value for this lab |
|---|---|
| Pattern | <type>-<workload>-<env>-<region>-<instance> |
| Workload | shop |
| Environment | prod |
| Region | East US → eus |
| Instance | 01 |
| Mandatory tags | env, owner, costCenter, workload |
Set shell variables you’ll reuse (CLI/Cloud Shell):
# Lab variables — one place to change them
WORKLOAD="shop"
ENV="prod"
REGION="eastus" # Azure long name
REGION_SHORT="eus" # our documented short code
RG="rg-${WORKLOAD}-${ENV}-${REGION_SHORT}" # rg-shop-prod-eus
TAGS="env=${ENV} owner=payments-team costCenter=CC-4471 workload=${WORKLOAD} managedBy=cli"
echo "Resource group will be: $RG"
Expected output: Resource group will be: rg-shop-prod-eus.
Part 1 — Create the resource group (portal)
The resource group is named first because it sets the workload/environment context for everything inside it.
- In the Azure portal, search Resource groups → Create.
- Subscription: your subscription. Resource group: type
rg-shop-prod-eus(typerg, workloadshop, envprod, regioneus— no instance number; you rarely have two RGs for the same workload/env/region). - Region: East US.
- Click Next : Tags. Add the four mandatory tags:
env=prod,owner=payments-team,costCenter=CC-4471,workload=shop. - Review + create → Create.
Validation: the resource group appears in the list as rg-shop-prod-eus. Open it → Tags shows all four. If the portal rejected the name, you mistyped a character — RG names are forgiving, so this should pass cleanly.
Part 2 — Create the resource group and a VNet (az CLI)
Now the same operation, scripted — this is what you’ll actually use day to day. (If you created the RG in Part 1, the CLI create is idempotent and will simply confirm it.)
- Create (or confirm) the resource group with tags:
az group create \
--name "$RG" \
--location "$REGION" \
--tags $TAGS
Expected output: JSON with "provisioningState": "Succeeded" and a "tags" object containing your four tags.
- Create a virtual network to the convention (
vnet-shop-prod-eus):
VNET="vnet-${WORKLOAD}-${ENV}-${REGION_SHORT}" # vnet-shop-prod-eus
az network vnet create \
--resource-group "$RG" \
--name "$VNET" \
--location "$REGION" \
--address-prefixes 10.20.0.0/16 \
--subnet-name "snet-web" \
--subnet-prefixes 10.20.1.0/24 \
--tags $TAGS
Expected output: JSON with the VNet and an initial subnet snet-web. Note the subnet name follows the convention but does not repeat the full VNet name — subnets are scoped to their VNet, so snet-web and snet-data are clear enough.
- Add a second subnet and an NSG (
nsg-shop-prod-eus):
az network vnet subnet create \
--resource-group "$RG" --vnet-name "$VNET" \
--name "snet-data" --address-prefixes 10.20.2.0/24
NSG="nsg-${WORKLOAD}-${ENV}-${REGION_SHORT}" # nsg-shop-prod-eus
az network nsg create \
--resource-group "$RG" --name "$NSG" --location "$REGION" --tags $TAGS
Validation — list what you’ve built and confirm every name conforms:
az resource list --resource-group "$RG" \
--query "[].{name:name, type:type}" -o table
Expected output: a table listing vnet-shop-prod-eus (Microsoft.Network/virtualNetworks) and nsg-shop-prod-eus (Microsoft.Network/networkSecurityGroups). Every leading token (vnet, nsg) matches the CAF abbreviation table.
Part 3 — Hit the wall: globally-unique, no-hyphen names
Now create a storage account — and watch the generic formula fail, then fix it. This is the most important learning moment in the lab.
- Try the naive name (this should fail):
# This WILL fail — demonstrates why one formula isn't enough
az storage account create \
--name "st-${WORKLOAD}-${ENV}-${REGION_SHORT}" \
--resource-group "$RG" --location "$REGION" --sku Standard_LRS
Expected output: an error like The storage account named st-shop-prod-eus is invalid. ... can contain only lowercase letters and numbers, and must be between 3 and 24 characters. Three rules broken at once: hyphens, and (had you removed them) it’s also a global-uniqueness candidate.
- Build a legal, globally-unique name. Strip hyphens, lowercase, and append a short deterministic suffix from the subscription/RG to avoid global collisions:
# Deterministic 6-char suffix from the resource group id (stable per-RG)
SUFFIX=$(az group show -n "$RG" --query id -o tsv | sha1sum | cut -c1-6)
ST="st${WORKLOAD}${ENV}${SUFFIX}" # e.g. stshopprod3f9a2c (no region to stay <=24)
# Guard: enforce the 24-char limit and lowercase
ST=$(echo "$ST" | tr '[:upper:]' '[:lower:]' | cut -c1-24)
echo "Storage account name: $ST (length ${#ST})"
az storage account create \
--name "$ST" --resource-group "$RG" --location "$REGION" \
--sku Standard_LRS --tags $TAGS
Expected output: Storage account name: stshopprod3f9a2c (length 16) then JSON with "provisioningState": "Succeeded". The suffix makes it globally unique yet deterministic per resource group; dropping the region token keeps it under 24 characters.
- Create a Key Vault — global, hyphens-allowed, must start with a letter (so
kv-is fine), 3–24 chars:
KV="kv-${WORKLOAD}-${ENV}-${SUFFIX}" # kv-shop-prod-3f9a2c (19 chars, starts with letter)
KV=$(echo "$KV" | cut -c1-24)
echo "Key Vault name: $KV (length ${#KV})"
az keyvault create \
--name "$KV" --resource-group "$RG" --location "$REGION" --tags $TAGS
Expected output: Key Vault name: kv-shop-prod-3f9a2c (length 19) then a successful create. Note Key Vault keeps hyphens (unlike storage) — the per-type rules genuinely differ, which is exactly the point of Part 3.
Validation: confirm both global resources exist and their names are legal:
az resource list --resource-group "$RG" \
--query "[?contains(type,'Storage') || contains(type,'KeyVault')].{name:name, type:type}" -o table
Part 4 — Do it as reusable Bicep (the production way)
Hardcoding names in scripts doesn’t scale. Here you encode the convention once in Bicep so every deployment produces conformant names automatically. Create naming-lab.bicep:
// naming-lab.bicep — encodes the CAF convention as a parameterised template
@description('Short workload/app token, 3-8 lowercase chars')
@minLength(3)
@maxLength(8)
param workload string = 'shop'
@description('Environment token')
@allowed([ 'prod', 'dev', 'test', 'uat' ])
param env string = 'prod'
@description('Azure region (long name); RG location used if empty')
param location string = resourceGroup().location
// Map the long region name to our documented short code.
var regionMap = {
eastus: 'eus'
eastus2: 'eus2'
westus: 'wus'
westus2: 'wus2'
centralus: 'cus'
westeurope: 'weu'
northeurope: 'neu'
uksouth: 'uks'
southeastasia: 'sea'
centralindia: 'cin'
}
var regionShort = contains(regionMap, location) ? regionMap[location] : 'unk'
// Deterministic global-uniqueness suffix (6 chars) from the RG id.
var suffix = substring(uniqueString(resourceGroup().id), 0, 6)
// --- The convention, applied per type ---
// Hyphenated types: type-workload-env-region
var nameVnet = 'vnet-${workload}-${env}-${regionShort}'
var nameNsg = 'nsg-${workload}-${env}-${regionShort}'
// No-hyphen, global, <=24: strip delimiters, append suffix, lowercase, truncate
var nameSt = toLower(substring('st${workload}${env}${suffix}', 0, min(length('st${workload}${env}${suffix}'), 24)))
// Global with hyphens allowed, must start with a letter, <=24
var nameKv = substring('kv-${workload}-${env}-${suffix}', 0, min(length('kv-${workload}-${env}-${suffix}'), 24))
// Mandatory tag bag, reused on every resource.
var commonTags = {
env: env
owner: 'payments-team'
costCenter: 'CC-4471'
workload: workload
managedBy: 'bicep'
}
resource vnet 'Microsoft.Network/virtualNetworks@2023-11-01' = {
name: nameVnet
location: location
tags: commonTags
properties: {
addressSpace: { addressPrefixes: [ '10.30.0.0/16' ] }
subnets: [
{ name: 'snet-web', properties: { addressPrefix: '10.30.1.0/24' } }
{ name: 'snet-data', properties: { addressPrefix: '10.30.2.0/24' } }
]
}
}
resource nsg 'Microsoft.Network/networkSecurityGroups@2023-11-01' = {
name: nameNsg
location: location
tags: commonTags
}
resource st 'Microsoft.Storage/storageAccounts@2023-05-01' = {
name: nameSt
location: location
tags: commonTags
sku: { name: 'Standard_LRS' }
kind: 'StorageV2'
}
resource kv 'Microsoft.KeyVault/vaults@2023-07-01' = {
name: nameKv
location: location
tags: commonTags
properties: {
tenantId: subscription().tenantId
sku: { family: 'A', name: 'standard' }
enableRbacAuthorization: true
}
}
// Surface the generated names so you can verify the formula.
output vnetName string = nameVnet
output nsgName string = nameNsg
output stName string = nameSt
output kvName string = nameKv
- Preview with what-if (validates names and shows exactly what will change before you commit — never skip this):
# Use a SEPARATE RG so it doesn't clash with the CLI-created resources
RG_BICEP="rg-${WORKLOAD}-${ENV}-${REGION_SHORT}-bicep"
az group create -n "$RG_BICEP" -l "$REGION" --tags $TAGS
az deployment group what-if \
--resource-group "$RG_BICEP" \
--template-file naming-lab.bicep \
--parameters workload=shop env=prod
Expected output: a what-if diff showing + Create for four resources with names vnet-shop-prod-eus, nsg-shop-prod-eus, a stshopprod<suffix> storage account, and kv-shop-prod-<suffix>. Eyeball the generated names here — this is your last chance before they’re immutable.
- Deploy:
az deployment group create \
--resource-group "$RG_BICEP" \
--template-file naming-lab.bicep \
--parameters workload=shop env=prod \
--query "properties.outputs"
Expected output: the outputs object echoing the four generated names. The Bicep uniqueString() suffix will differ from the CLI sha1sum suffix — both are deterministic, just different functions; that’s expected.
Validation — prove the convention held:
az resource list --resource-group "$RG_BICEP" \
--query "[].{name:name, type:type}" -o table
Every name conforms, and you changed zero naming logic between runs — that’s the value of encoding the convention in Bicep once. To scale this further, factor the naming logic into a dedicated module; see authoring reusable Bicep with modules, loops & conditions.
Part 5 — Enforce it with Azure Policy
A convention nobody is forced to follow decays. Here you assign two policies: one that requires a tag, and (conceptually) one that denies non-conforming names. Start in audit to size the gap, then tighten.
- Require the
costCentertag using the built-in policy definition (this rejects, at create time, any resource group missing the tag). First find the built-in definition id:
# Built-in: "Require a tag on resource groups"
POLICY_ID=$(az policy definition list \
--query "[?displayName=='Require a tag on resource groups'].name" -o tsv)
echo "$POLICY_ID"
- Assign it at your resource-group (or subscription) scope, parameterised to the
costCentertag:
SUB_ID=$(az account show --query id -o tsv)
az policy assignment create \
--name "require-costcenter-tag" \
--display-name "Require costCenter tag on resource groups" \
--policy "$POLICY_ID" \
--scope "/subscriptions/$SUB_ID" \
--params '{ "tagName": { "value": "costCenter" } }'
Expected output: JSON describing the assignment. From now on, creating a resource group without a costCenter tag at that scope is denied by ARM — try it and you’ll get a RequestDisallowedByPolicy error naming this assignment.
- Test the deny:
# This SHOULD be rejected by the policy
az group create -n "rg-policytest-dev-eus" -l "$REGION" 2>&1 | head -5
Expected output: ... was disallowed by policy. ... RequestDisallowedByPolicy ... require-costcenter-tag. That is the convention enforcing itself — exactly what you want.
- For naming enforcement, the pattern is a custom policy with a
denyeffect whosepolicyRuleusesfield('name')withmatch/like/notLikeconditions encoding your regex-equivalent. Conceptually:
| Policy goal | Effect | Condition (sketch) |
|---|---|---|
RG name must start with rg- |
deny |
field('name') notLike rg-* |
Storage name must start with st |
deny |
field('name') notLike st* |
| Mandatory tags present | deny |
field('tags[costCenter]') exists false |
Inherit costCenter from RG |
modify |
append RG’s tag if missing |
Assign naming-deny policies in audit mode first (--params setting the effect to Audit if the definition supports it), review the compliance report after a day, fix the gap, then flip to Deny. Flipping straight to deny on a populated estate blocks legitimate work. For the full effect catalogue and how to author these rules, see Azure Policy effects explained: deny, audit, modify, deployIfNotExists, and for rolling policy across many subscriptions, Azure Policy & governance at scale.
Part 6 — Teardown
Clean up everything so the lab costs nothing.
# Remove the policy assignment first
az policy assignment delete --name "require-costcenter-tag" \
--scope "/subscriptions/$SUB_ID"
# Delete both resource groups (removes all child resources)
az group delete --name "$RG" --yes --no-wait
az group delete --name "$RG_BICEP" --yes --no-wait
# If you created the portal RG separately with the same name as $RG it's already covered.
# Verify nothing lingers:
az group list --query "[?starts_with(name,'rg-shop-prod')].name" -o table
Expected output: after the deletes complete, the final group list returns nothing. Key Vault has soft-delete on by default — it lingers in a recoverable state for the retention period; to fully purge in a lab (so the globally-unique name frees up), run az keyvault purge --name "$KV".
Lab validation summary — what you proved:
| You built | The convention element it exercised |
|---|---|
rg-shop-prod-eus (portal + CLI) |
The base hyphenated pattern; RG sets context |
vnet-/nsg-shop-prod-eus, snet-web/data |
Hyphenated types; subnet scoping |
stshopprod<suffix> |
No-hyphen + global-uniqueness handling |
kv-shop-prod-<suffix> |
Global + hyphens-allowed + must-start-with-letter |
naming-lab.bicep |
Convention encoded once, applied automatically |
require-costcenter-tag policy |
Enforcement at create time |
Common mistakes & troubleshooting
The failure modes that actually happen, with the exact symptom, how to confirm, and the fix.
| # | Symptom | Root cause | How to confirm | Fix |
|---|---|---|---|---|
| 1 | The storage account named X is invalid |
Hyphens/uppercase/length in a no-hyphen global type | Read the error; it lists the rule broken | Strip -, lowercase, ≤ 24 chars, add suffix |
| 2 | StorageAccountAlreadyTaken / KV name taken |
Global-uniqueness collision with another tenant | Name has no uniqueness suffix | Append uniqueString()-derived suffix |
| 3 | Windows VM create fails on name length | Name > 15 chars (NetBIOS cap) | vm-payments-prod-eus-01 is 23 chars |
Shorten workload (pay), drop region |
| 4 | RequestDisallowedByPolicy on a valid-looking create |
A naming/tag deny policy rejected it | Error names the assignment; check its rule | Conform the name/tags, or fix the policy rule |
| 5 | Two resources, same intent, different names | Vocabulary not standardised (prod vs prd) |
az resource list shows both spellings |
Standardise spellings; enforce with policy |
| 6 | Cost report can’t break down by team | Mandatory tags missing on legacy resources | Cost Management group-by-tag shows “untagged” | Bulk-apply tags via az resource tag; add require-tag policy |
| 7 | “We need to rename this resource group” | Belief that RGs/resources are renamable | Try the portal — there’s no rename button for most | Create new + migrate; accept names are immutable |
| 8 | Bicep deploy fails with name-too-long at a specific type | substring() not applied; type cap exceeded |
The error names the type and its limit | Apply substring(...min(length,cap)) per type |
| 9 | Subnet name collides | Two subnets named snet-web in one VNet |
Subnet scope is the VNet, not the RG | Make subnet names unique within the VNet |
| 10 | Policy assigned but not enforcing | Assigned at wrong scope, or in Audit not Deny | az policy assignment show → effect & scope |
Re-scope to MG/sub; set effect to Deny |
| 11 | Container Registry name rejected with hyphens | ACR forbids hyphens (like storage) | Error lists the charset | Drop hyphens: crshopprod<suffix> |
| 12 | uniqueString() value changed between deploys |
Different seed input (used RG name not RG id) | Two deploys produced different suffixes | Seed from a stable value (resourceGroup().id) consistently |
The three you’ll hit first
Mistake 1 — applying the hyphenated formula to storage/ACR. The error is explicit; believe it. st-shop-prod fails because storage allows only lowercase letters and digits. The fix is a type-aware builder (Part 3/4) that drops delimiters for these types. Don’t fight the platform — bend the convention.
Mistake 2 — forgetting the uniqueness suffix on global types. It passes in dev (you got there first) and fails in prod when another customer already owns stshopprod. Always append a deterministic suffix to storage, Key Vault, App Service, SQL server, ACR and Cosmos. Confirm with the StorageAccountAlreadyTaken / equivalent error.
Mistake 3 — flipping a naming/tag policy straight to Deny on a populated estate. You block every team’s legitimate work and generate a flood of RequestDisallowedByPolicy tickets. Always start in Audit, read the compliance report (az policy state summarize), close the gap, then set Deny.
Best practices
Crisp, production-grade rules distilled from doing this at scale:
- Adopt the CAF pattern and abbreviation table verbatim — don’t invent abbreviations; copy them from Microsoft Learn so every team and tool agrees.
- Name resource groups first — the RG sets workload/environment context, and getting it right makes everything inside it fall into place.
- Standardise the vocabulary once — fix the exact spellings of environments (
prod, notprd/production) and region codes, and forbid variants. - Make the convention type-aware — hyphenated for most, no-hyphen for storage/ACR, with a uniqueness suffix for all globally-unique types.
- Always append a deterministic uniqueness suffix to global-DNS resources; seed it from
resourceGroup().idso it’s stable per deployment. - Encode the convention in code, not a wiki — a Bicep module (or Terraform module / the
Azure/namingprovider) generates names so humans never hand-type them. - Keep five mandatory tags, not twenty —
env,owner,costCenter,workload, plusdataClassification; over-tagging guarantees blanks. - Use tag inheritance — let
costCenterandenvflow from RG to resources via amodifypolicy so nobody repeats them by hand. - Enforce with policy, audit-first — assign deny policies at the management-group scope, but always start in Audit, close the gap, then flip to Deny.
- Accept immutability — design before you create, because most names can’t be changed; the convention applies to new resources while tags retro-fit onto old ones.
- Put
envandworkloadin both name and tag — name for human scanning, tag for machine cost/policy queries; the redundancy is deliberate. - Right-size the effort — the highest-leverage 20% is resource-group naming + the cost-allocation tags + one deny policy; ship that first, refine the long tail later.
Security notes
Naming and tagging intersect security in concrete ways, even though they feel like “just metadata.”
| Concern | Risk if ignored | Mitigation |
|---|---|---|
| Information disclosure in names | Names appear in URLs, logs, error messages, public DNS — vm-classified-projectx-prod leaks project intel |
Don’t encode secrets or sensitive project codenames in names; use a neutral workload token |
| Data classification visibility | Without a dataClassification tag, you can’t find/secure sensitive resources |
Mandate dataClassification; drive policy (e.g. require private endpoints on confidential) |
| Least-privilege scoping | RBAC assignments are easier to scope correctly when RG names clearly map to teams/workloads | Name RGs per workload so role assignments target the right blast radius |
| Policy as the control plane | A convention without enforcement is advisory; nothing stops a non-compliant, untagged, unowned resource | Assign deny/require policies at MG scope so the platform enforces it, not goodwill |
| Auditability of ownership | An auditor asks “prove every prod resource is owned” — impossible without mandatory owner/costCenter |
Require owner + costCenter; audit compliance via az policy state |
| Soft-delete name reuse | A purged Key Vault/storage name stays reserved during soft-delete retention, blocking re-creation | Account for soft-delete in teardown/recreate; az keyvault purge to free a name in non-prod |
Two rules to internalise: names are public-ish (they surface in DNS, logs, support cases — never treat a name as a secret container), and the security value of tags is realised only through policy (a dataClassification tag nobody queries or enforces protects nothing). Tie classification tags to actual controls — for example, a policy that denies public network access on any resource tagged confidential.
Cost & sizing
A naming convention is free — it’s metadata; names and tags cost nothing to apply, and Azure Policy evaluation is free. The “cost” is human time up front, and the return is almost entirely on the cost side, which is the irony: a free practice is what makes your bill legible.
| Cost dimension | Figure | Note |
|---|---|---|
| Naming/tagging itself | ₹0 / $0 | Pure metadata; no charge |
| Azure Policy (audit/deny) | ₹0 / $0 | Policy evaluation and enforcement are free |
| Up-front design effort | ~0.5–1 day | One-time; the highest-ROI half-day in governance |
| Retro-fit tags on legacy estate | Hours–days, one-time | Proportional to resource count; bulk via az resource tag |
| Cost saved via allocation | Often 10–30% | Visibility (group-by-tag) reveals waste/orphans to cut |
| Global-uniqueness suffix | ₹0 / $0 | A few extra characters; no cost |
What drives the return: the costCenter and workload tags turn an undifferentiated bill into a per-team, per-application breakdown in Cost Management, which is the precondition for any FinOps work — you can’t optimise what you can’t attribute. Teams that adopt mandatory cost tags routinely find and cut 10–30% of spend simply by seeing orphaned and oversized resources for the first time. Right-sizing the convention effort: don’t try to perfect the long-tail abbreviations on day one. The minimum viable convention is: a resource-group naming rule, the five mandatory tags, tag inheritance, and one audit-mode policy. That’s a half-day and captures most of the value; the per-type refinements and the deny flip come later. There is no free-tier limit to worry about — every account can name, tag and apply policy at no charge, which removes the last excuse not to do this on day one.
Interview & exam questions
Mapped to AZ-104 (Administrator), AZ-305 (Solutions Architect) and the CAF/governance domains.
1. What is the CAF-recommended naming pattern, and why is field order fixed?
The pattern is <resource-type>-<workload>-<environment>-<region>-<instance>, joined by hyphens. Order is fixed (type first) so names sort and scan predictably — listing alphabetically clusters all resources of a type, then by workload, and the leading type abbreviation lets a human identify the resource class at a glance.
2. Why can’t you use one naming formula for every resource type? Each resource provider enforces its own rules. Storage accounts and Container Registries forbid hyphens and cap at short lengths; Windows VMs cap at 15 characters (NetBIOS); globally-unique DNS resources need a uniqueness token. A single hyphenated formula is rejected by these types, so the convention must be type-aware.
3. Which Azure resources require globally-unique names, and how do you guarantee uniqueness deterministically?
Resources with public DNS endpoints: storage accounts, Key Vaults, App Services/Function Apps, Azure SQL servers, Container Registries, Cosmos DB accounts. Guarantee uniqueness by appending a deterministic suffix derived from a stable seed — in Bicep, substring(uniqueString(resourceGroup().id), 0, n) — so the name is predictable per deployment yet globally distinct.
4. What’s the difference between what belongs in a name versus a tag? A name carries immutable facts you need to identify a resource at a glance (type, workload, env, region); it’s short and embedded in every reference. A tag carries mutable or descriptive facts you filter/bill/audit on (owner, cost centre, data classification). Rule: immutable + identifying → name; mutable + descriptive → tag.
5. Why is naming a day-one decision? Because most Azure resources cannot be renamed — the name is fixed at creation. To “rename” a storage account, Key Vault or VM you create a new one and migrate, which for stateful resources is a project. Retro-fitting a convention onto a live estate is largely impossible for names (though tags can be added later).
6. How do you enforce a naming convention so it doesn’t decay?
Azure Policy. A deny-effect policy whose rule matches field('name') against the expected pattern rejects non-conforming resources at create time; require-tag policies reject resources missing mandatory tags. Assign at the management-group scope, start in Audit to size the gap, then flip to Deny.
7. What is tag inheritance and why use it?
A modify-effect (or inherit) policy copies a tag — e.g. costCenter, env — from a resource group down to the resources inside it if missing. It means individual resources don’t have to repeat the value, reducing error and ensuring cost-allocation tags are present everywhere without manual effort.
8. Where does the canonical list of resource-type abbreviations come from?
The Cloud Adoption Framework’s “Abbreviation recommendations for Azure resources” page on Microsoft Learn. It’s the authoritative, maintained source; you copy from it rather than inventing abbreviations, so every team and tool agrees that st is storage, kv is Key Vault, etc.
9. A storage account create fails with “name is invalid.” What are the likely causes?
The name contains hyphens or uppercase (storage allows only lowercase letters and digits), exceeds 24 characters, is under 3, or — if the charset is fine — collides globally (StorageAccountAlreadyTaken). Fix: lowercase, strip hyphens, truncate to 24, and append a uniqueness suffix.
10. How would you retro-fit governance onto an existing untagged estate?
You can’t rename resources, but you can add tags. Export the inventory (az resource list), assign owner/cost-centre by inspection, bulk-apply tags (az resource tag / policy modify), and assign require-tag policies in Audit first to measure the gap, then Deny. New resources follow the naming convention going forward.
11. Why put environment and workload in both the name and a tag? The name makes them human-scannable (you read the name constantly); the tag makes them machine-queryable for cost reports and policy filtering. The duplication is deliberate — two systems (human reading, machine querying) each need the fact in their own format.
12. What’s the minimum viable naming/tagging convention to ship first?
A resource-group naming rule (the highest-leverage name), five mandatory tags (env, owner, costCenter, workload, dataClassification), tag inheritance for cost tags, and one deny/require policy in Audit mode. That half-day captures ~80% of the value; per-type refinements and the Deny flip follow.
Quick check
- Decompose the name
vnet-pay-prod-weu-02into its five fields. - Why is
st-payments-prod-eusan illegal storage account name? Give two reasons. - Name two resource types that require a globally-unique name and the Bicep function you’d use to make them unique.
- Should
costCenterlive in the resource name or a tag? Why? - You assigned a naming-deny policy and teams are flooded with
RequestDisallowedByPolicyerrors. What did you skip?
Answers
- Type =
vnet(virtual network), workload =pay, environment =prod, region =weu(West Europe), instance =02. - (a) It contains hyphens, which storage accounts forbid (lowercase letters and digits only); (b) at 21+ characters with the
paymentstoken it’s fine on length but the hyphens alone make it illegal — and it lacks a uniqueness suffix for the global namespace. The clean fix:stpayprod<suffix>, lowercase, ≤ 24 chars. - Any two of: storage account, Key Vault, App Service/Function App, Azure SQL server, Container Registry, Cosmos DB account. Use
uniqueString(resourceGroup().id)(typicallysubstring(uniqueString(...), 0, 6)) to derive a deterministic suffix. - A tag. It’s pure metadata used to filter and bill, it can change as finance reorganises, and you never use it to find a resource — all of which point to a tag, not the name.
- You flipped the policy straight to Deny on a populated estate without first running it in Audit to size and close the compliance gap. Re-assign in Audit, fix the non-conforming resources, then enable Deny.
Glossary
| Term | Definition |
|---|---|
| Naming convention | A deterministic, documented formula that turns a resource’s facts into its name. |
| Cloud Adoption Framework (CAF) | Microsoft’s prescriptive guidance for adopting Azure at scale, including the recommended naming pattern and abbreviation table. |
| CAF abbreviation | The canonical short code for a resource type (e.g. vm, st, kv, rg, vnet), published and maintained by Microsoft. |
| Workload token | A short, stable identifier for the application or service a resource serves (e.g. shop). |
| Environment token | A standardised code for the lifecycle stage (prod, dev, test, uat). |
| Region token | A short, documented code for an Azure region (e.g. eus for East US). |
| Instance token | A zero-padded number disambiguating multiple copies of the same resource (01, 02). |
| Uniqueness suffix | A deterministic token appended to globally-unique names to avoid collisions, typically from uniqueString(). |
| Global uniqueness | The requirement that a name be unique across all of Azure (applies to public-DNS resources like storage, Key Vault, App Service). |
| Tag | A mutable key/value metadata pair on a resource (max 50 per resource) carrying facts the name can’t, like owner or costCenter. |
| Tag taxonomy | The agreed set of required and optional tag keys and their allowed values. |
| Tag inheritance | A policy that copies a tag from a resource group down to its resources if missing. |
| Azure Policy | The Azure service that audits or enforces rules (e.g. deny non-conforming names, require tags) at create and evaluation time. |
| Deny effect | A policy effect that blocks a resource operation that violates the rule, at create time. |
| NetBIOS limit | The 15-character cap on Windows computer (VM) names, a legacy constraint that bites VM naming. |
uniqueString() |
A Bicep/ARM function returning a deterministic hash-based string from its inputs, used to seed uniqueness suffixes. |
Next steps
- Go deeper on the metadata half: Azure resource tags: strategy, governance & cost allocation and the tagging fundamentals.
- Make the convention enforceable everywhere: Azure Policy & governance at scale and Azure Policy effects explained.
- Place the convention inside the bigger picture: Azure Cloud Adoption Framework landing zones deep dive and the enterprise-scale landing zone.
- Encode names as reusable infrastructure: author reusable Bicep with modules, loops & conditions and deploy your first Bicep file from scratch.
- Ground the hierarchy the convention names: Azure resource hierarchy explained and resource group lifecycle & design.