Cloud Accounts and the Resource Hierarchy: Tenants, Subscriptions, Resource Groups, Tags and Naming
In Azure every resource is a leaf of a tree made of a tenant, management groups, subscriptions and resource groups; the subscription is the billing boundary and the resource group the lifecycle boundary. For Bookstore we cover the prod/nonprod subscription split, a naming tool that catches global name limits, enforcing tags with Azure Policy, a first resource group with az CLI, free account credits and a budget alert on day one, with AWS equivalents.
Version note: The examples in this post were run with .NET 10 (SDK 10.0.404), Azure CLI 2.92.0 and ShellCheck 0.11.0. No Azure resources were created; CLI parameters were checked locally against --help output and behavior against Microsoft Learn (November 2026).
Guess First
For Bookstore, the online bookstore used as the shared example across this site's series, you have created three resource groups in Azure: rg-bookstore-dev-weu, rg-bookstore-staging-weu and rg-bookstore-prod-weu. All three are in the same subscription. You gave Emre the Contributor role on the dev resource group. Three questions: Can Emre see the database in the prod resource group? When you put the tag cost-center=cc-1042 on the resource group, does the App Service inside it get the tag? If someone deletes the dev resource group at the end of the month, what happens to the SQL database inside it? The answers are in the post.
Azure's Hierarchy: Tenant, Management Group, Subscription, Resource Group
Every resource in Azure is a leaf of a four-level tree. At the top is the Microsoft Entra ID tenant: the directory that holds the company's users, groups and application identities. The tenant is an identity boundary; a subscription always trusts exactly one tenant. Below the tenant come management groups. They are folders that group subscriptions, and role assignments and policies placed on them are inherited downwards. According to Learn, a directory can hold 10,000 management groups, the tree goes up to six levels deep excluding the root and the subscription level, and each subscription has exactly one parent. Every tenant has a root management group created automatically.
A subscription is the billing and quota boundary: the cost of every resource is charged to a subscription, and limits such as vCPU quotas are kept per subscription. A resource group is the lifecycle boundary: it holds resources that are created, changed and deleted together. A resource group has its own location, but that only decides where its metadata is stored; the resources inside it can live in other regions. Every resource ID carries the path through this tree:
/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-bookstore-dev-weu/providers/Microsoft.Web/sites/app-bookstore-dev-weu
Microsoft.Web in the middle of that path is the name of the resource provider. Every request, whether it comes from the portal, the CLI or Terraform, first goes to Azure Resource Manager, the layer that receives all management requests in Azure. ARM validates the token through Entra ID, applies role assignments and Azure Policy rules, and then forwards the request to the right resource provider. Because authorization and rules go through one place, Azure RBAC in the next post and the policies in this one behave the same way in every tool.
The first answer to the guess follows from this: a role assignment applies at the scope where you grant it and below. Emre's Contributor role covers only rg-bookstore-dev-weu; he won't even see the prod resource group in the list. The answer to the third question is harsh: deleting a resource group deletes every resource in it, including the database and the storage account.
The Same Structure on AWS: Organizations, OUs and Accounts
On AWS the tree has a different shape. At the top is an organization in AWS Organizations, accounts are grouped under OUs, and SCPs attached to an OU take effect downwards by limiting the widest set of actions possible in an account. The real difference is the account: an AWS account is the billing boundary and the security and resource boundary at the same time. In Azure these roles are spread out: identity in the tenant, billing in the subscription, lifecycle in the resource group. AWS has no direct equivalent of the resource group; tags and CloudFormation stacks are used to manage resources together. That's why AWS teams usually open a separate account per environment, while on Azure the subscription plays that role. Users are managed with IAM and IAM Identity Center on top of it.
Separating Environments: Subscription or Resource Group?
The setup in the guess, three environments in one subscription, is acceptable as a start for a small team, but it has two weak spots. First, anyone you grant access at the subscription level also reaches prod. Second, a large VM accidentally created in dev eats the quota and the budget that prod also uses. For Bookstore we recommend two subscriptions: sub-bookstore-prod and sub-bookstore-nonprod. Dev and staging share the nonprod subscription, each in its own resource group; prod is alone. That way, who can reach prod is visible in one place, and you can give nonprod a loose budget and prod a strict approval process.
Split resource groups by lifecycle too. In Bookstore, rg-bookstore-dev-weu holds the application itself (App Service, SQL, storage), and when the environment is deleted they all go together. The shared Key Vault managed by the platform team, or the storage account holding the Terraform state, live in separate resource groups; the rg-bookstore-tfstate-weu you saw in the Infrastructure as Code: Terraform series is an example. Deleting the application resource group must not delete the state file.
A Naming Standard
Names can't be changed later; for most resources, renaming means deleting and recreating the resource. So the standard has to be written before the first resource. Bookstore's rule uses the abbreviations recommended by the Cloud Adoption Framework: <type>-<workload>-<environment>-<region>; the locals.tf in the Terraform series uses the same pattern. We turned the rule into code once, so that everyone produces the same name and limit violations show up early instead of at deploy time:
// Bookstore naming and tagging convention: <abbreviation>-<workload>-<environment>-<region>
// Run: dotnet run tools/cloud/naming.cs -- [workload] [environment...]
using System.Text.RegularExpressions;
string workload = args.Length > 0 ? args[0] : "bookstore";
string[] environments = args.Length > 1 ? args[1..] : ["dev", "prod"];
const string Region = "westeurope";
const string RegionShort = "weu";
// Abbreviations follow the Cloud Adoption Framework list; limits follow "Naming rules and restrictions for Azure resources".
NamingRule[] rules =
[
new("Resource group", "rg", 1, 90, @"^[\w\-\.\(\)]*[\w\-\(\)]$", Scope.Subscription),
new("App Service plan", "asp", 1, 60, "^[a-zA-Z0-9-]+$", Scope.ResourceGroup),
new("Web app", "app", 2, 60, "^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$", Scope.Global),
new("Container Apps environment", "cae", 2, 32, "^[a-z][a-z0-9-]*[a-z0-9]$", Scope.ResourceGroup),
new("Container app", "ca", 2, 32, "^[a-z][a-z0-9-]*[a-z0-9]$", Scope.ResourceGroup),
new("Storage account", "st", 3, 24, "^[a-z0-9]+$", Scope.Global, Compact: true),
new("SQL logical server", "sql", 1, 63, "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$", Scope.Global),
new("Key vault", "kv", 3, 24, "^[a-zA-Z](?!.*--)[a-zA-Z0-9-]*[a-zA-Z0-9]$", Scope.Global),
new("User-assigned identity", "id", 3, 128, "^[a-zA-Z0-9][a-zA-Z0-9_-]*$", Scope.ResourceGroup),
];
Dictionary<string, string> baseTags = new()
{
["app"] = workload,
["owner"] = "platform-team@example.com",
["cost-center"] = "cc-1042",
};
int errors = 0;
foreach (string environment in environments)
{
Console.WriteLine($"== {environment} ({Region})");
foreach (NamingRule rule in rules)
{
string name = rule.Compact
? $"{rule.Abbreviation}{workload}{environment}{RegionShort}".Replace("-", "").ToLowerInvariant()
: $"{rule.Abbreviation}-{workload}-{environment}-{RegionShort}".ToLowerInvariant();
string? problem = rule.Check(name);
errors += problem is null ? 0 : 1;
string scope = rule.Scope == Scope.Global ? "global" : rule.Scope.ToString().ToLowerInvariant();
Console.WriteLine($" {rule.Type,-27} {name,-40} {name.Length,3}/{rule.MaxLength,-3} {scope,-14} {problem ?? "OK"}");
}
Dictionary<string, string> tags = new(baseTags) { ["environment"] = environment };
Console.WriteLine(" tags: " + string.Join(" ", tags.Select(t => $"{t.Key}={t.Value}")));
}
return errors == 0 ? 0 : 1;
enum Scope { Global, Subscription, ResourceGroup }
record NamingRule(string Type, string Abbreviation, int MinLength, int MaxLength, string Pattern, Scope Scope, bool Compact = false)
{
public string? Check(string name) =>
name.Length < MinLength || name.Length > MaxLength ? $"ERROR length must be {MinLength}-{MaxLength}"
: !Regex.IsMatch(name, Pattern) ? "ERROR invalid characters"
: null;
}
dotnet run tools/cloud/naming.cs produces names for dev and prod by default; the output for the first environment:
== dev (westeurope)
Resource group rg-bookstore-dev-weu 20/90 subscription OK
App Service plan asp-bookstore-dev-weu 21/60 resourcegroup OK
Web app app-bookstore-dev-weu 21/60 global OK
Container Apps environment cae-bookstore-dev-weu 21/32 resourcegroup OK
Container app ca-bookstore-dev-weu 20/32 resourcegroup OK
Storage account stbookstoredevweu 17/24 global OK
SQL logical server sql-bookstore-dev-weu 21/63 global OK
Key vault kv-bookstore-dev-weu 20/24 global OK
User-assigned identity id-bookstore-dev-weu 20/128 resourcegroup OK
tags: app=bookstore owner=platform-team@example.com cost-center=cc-1042 environment=dev
One column matters in particular: the scope. Resources marked global must have a name that is unique across all of Azure, because the name becomes a DNS record: the storage account becomes stbookstoredevweu.blob.core.windows.net and the Key Vault kv-bookstore-dev-weu.vault.azure.net. A storage account name accepts only lowercase letters and digits and is at most 24 characters; you can't use hyphens. As soon as you try to open a separate workload for institutional orders, the limits show up:
$ dotnet run tools/cloud/naming.cs -- bookstore-institutional staging
== staging (westeurope)
Resource group rg-bookstore-institutional-staging-weu 38/90 subscription OK
App Service plan asp-bookstore-institutional-staging-weu 39/60 resourcegroup OK
Web app app-bookstore-institutional-staging-weu 39/60 global OK
Container Apps environment cae-bookstore-institutional-staging-weu 39/32 resourcegroup ERROR length must be 2-32
Container app ca-bookstore-institutional-staging-weu 38/32 resourcegroup ERROR length must be 2-32
Storage account stbookstoreinstitutionalstagingweu 34/24 global ERROR length must be 3-24
SQL logical server sql-bookstore-institutional-staging-weu 39/63 global OK
Key vault kv-bookstore-institutional-staging-weu 38/24 global ERROR length must be 3-24
User-assigned identity id-bookstore-institutional-staging-weu 38/128 resourcegroup OK
tags: app=bookstore-institutional owner=platform-team@example.com cost-center=cc-1042 environment=staging
The exit code is 1. The fix is to add a workload abbreviation to the standard: a short name like bkinst fits both the storage and the Key Vault limits. If you wire this into a CI step from day one, a long name stops the pull request instead of the deployment.
Tags and Enforcing Them with Azure Policy
A tag is a key/value pair attached to a resource. Bookstore's standard has four tags: app, environment, owner and cost-center; the Terraform series added managed_by=terraform. According to Learn, a resource, resource group or subscription can have at most 50 tags; a tag name can be at most 512 characters and a value 256 (on storage accounts the name limit is 128). Tag names are case-insensitive, values are not: environment=Prod and environment=prod are two separate lines in a cost report. Tags are stored as plain text and end up in cost reports and exports; never put a password or personal data in them.
The second answer to the guess: no, resources don't inherit the tags of their resource group. Turning that into a rule is Azure Policy's job. A script applied once to Bookstore's subscription assigns two built-in policies:
#!/usr/bin/env bash
# Runs once per subscription: tag rules for every Bookstore resource group and resource.
set -euo pipefail
: "${AZURE_SUBSCRIPTION_ID:?Set AZURE_SUBSCRIPTION_ID to the target subscription}"
SCOPE="/subscriptions/${AZURE_SUBSCRIPTION_ID}"
az account set --subscription "$AZURE_SUBSCRIPTION_ID"
# Built-in "Require a tag on resource groups": deny a resource group without cost-center
az policy assignment create \
--name require-cost-center-on-rg \
--display-name "Require cost-center tag on resource groups" \
--policy 96670d01-0a4d-4649-9c89-2d3abc0a5025 \
--params '{ "tagName": { "value": "cost-center" } }' \
--scope "$SCOPE"
# Built-in "Inherit a tag from the resource group if missing": modify effect, needs an identity
az policy assignment create \
--name inherit-cost-center \
--display-name "Inherit cost-center tag from the resource group" \
--policy ea3f2387-9b95-492a-a190-fcdc54f7b070 \
--params '{ "tagName": { "value": "cost-center" } }' \
--scope "$SCOPE" \
--mi-system-assigned --location westeurope \
--identity-scope "$SCOPE" --role Contributor
The first assignment is the "Require a tag on resource groups" definition: ARM rejects a request to create a resource group without a cost-center tag. The second is "Inherit a tag from the resource group if missing": it adds the resource group's tag to a resource that lacks it. This policy's effect is modify, so it changes resources; that's why the assignment must have its own managed identity, and that identity needs the Contributor role. The --mi-system-assigned, --identity-scope and --role parameters are there for that. Policies apply to resources that are created or updated; for resources that existed before the assignment you have to run a separate remediation task.
A First Resource Group with the Portal, Cloud Shell and az CLI
There are three ways to manage Azure and all three call the same ARM API. The portal is fastest when you do something for the first time and want to see what's possible. Cloud Shell is a terminal that opens inside the portal with az and PowerShell ready and the session already signed in; if you want your files to persist, you attach a storage account. Locally you install the Azure CLI; on Windows with winget install Microsoft.AzureCLI or Microsoft's MSI package. For this post we ran the CLI from its official container image:
$ az version
{
"azure-cli": "2.92.0",
"azure-cli-core": "2.92.0",
"azure-cli-telemetry": "1.1.0",
"extensions": {}
}
This series leaves managing infrastructure as code to the Infrastructure as Code: Terraform series; here we use the CLI to see what each step corresponds to in ARM. The script that creates the resource group is short:
#!/usr/bin/env bash
# Creates the resource group of one Bookstore environment with the standard tags.
set -euo pipefail
: "${AZURE_SUBSCRIPTION_ID:?Set AZURE_SUBSCRIPTION_ID to the target subscription}"
ENVIRONMENT="${1:?Usage: 02-resource-group.sh <dev|staging|prod>}"
LOCATION="westeurope"
RG="rg-bookstore-${ENVIRONMENT}-weu"
az account set --subscription "$AZURE_SUBSCRIPTION_ID"
az group create \
--name "$RG" \
--location "$LOCATION" \
--tags app=bookstore environment="$ENVIRONMENT" \
owner=platform-team@example.com cost-center=cc-1042 \
--output table
The : "${AZURE_SUBSCRIPTION_ID:?...}" line is deliberate: if no subscription ID is given, the script stops without doing anything. A command going to whatever subscription happens to be selected in the CLI is one of the most common accidents in Azure; the Terraform series explained AzureRM 4.0 making the subscription ID mandatory for the same reason. We checked the scripts with bash -n and ShellCheck; we did not run them against Azure. In your own subscription you can run bash infra/cli/02-resource-group.sh dev.
Free Accounts, Credits and a Budget Alert on Day One
You don't have to spend money to experiment. According to the page we checked on 14 November 2026, the Azure free account gives a 200 dollar credit for the first 30 days, free monthly amounts of more than 20 popular services for 12 months to new customers, and permanent free amounts on more than 65 services (for example 100,000 vCore seconds of Azure SQL Database serverless and 1 million Functions requests a month). AWS changed its model for accounts opened from 15 July 2025: a new customer gets up to 200 dollars in credits, and the free plan ends 6 months after sign-up or when the credits run out, whichever comes first. These offers change often; check the provider's current page before you sign up.
The first thing to do on the day you open the account is a budget alert. In the portal you go to Budgets under Cost Management and define a monthly amount at the subscription scope; two conditions that send an e-mail when actual spend reaches 80 percent and forecast spend reaches 100 percent of the amount are a good start. Two things to know. A budget only notifies, it doesn't stop resources; if you want to stop the spending, you have to connect the alert to automation through an action group, which is the subject of post 10. The CLI side isn't complete yet either: the az consumption budget create command is in preview and its --help output has no parameter for e-mails or thresholds. So create budgets either in the portal or with an IaC tool such as Terraform.
Finally, a table: the AWS equivalent of each Azure layer.
| Azure | AWS | What it's for |
|---|---|---|
| Entra ID tenant | IAM Identity Center directory | Identity boundary holding users and groups |
| Management group | Organizations, OU | Grouping accounts, applying rules from above |
| Subscription | Account | Billing and quota boundary (also the security boundary on AWS) |
| Resource group | No direct equivalent: tags, CloudFormation stacks | Managing resources that live together |
| Azure Policy | SCP, AWS Config rules | Rejecting or correcting disallowed settings |
| Cost Management budget | AWS Budgets | Notifying at a spending threshold |
In Production
Put a delete lock on the prod resource group. A CanNotDelete resource lock prevents the resource group and the resources in it from being deleted, even if you are an Owner; the lock has to be removed first. It is the last barrier against deleting the wrong resource group in the wrong browser tab.
Try policies in audit mode first. A policy assigned with --enforcement-mode DoNotEnforce blocks nobody but reports non-compliant resources. If you assign a policy with a deny effect directly to an existing subscription, the deployment pipelines running that morning break.
Check quotas early. A new subscription's regional vCPU quota may not be enough to scale Bookstore during a campaign week; a quota increase is a support request and may not be resolved in minutes.
Common Pitfalls
Putting everything into one resource group. Dev and prod, or the application and the shared Key Vault, living in the same resource group makes it impossible to separate access and to cleanly delete an environment.
Opening the company subscription with a personal credit card. When the card owner leaves the company, the ownership and billing of the subscription stay tied to one person. Company accounts should be opened under the company's billing agreement (a Microsoft Customer Agreement or through a partner), and ownership should go to a group, not a person.
Skipping the name with "we'll fix it later". Storage account, SQL server and Key Vault names are global and can't be changed; a wrong name means moving the data to a new resource.
Relying on tags for reporting without enforcing them. Tags set without a policy look fine in the first month; by the third month half of the resources don't have them.
A Small Experiment
★☆☆ Add the abbreviations for Log Analytics workspace (log) and Application Insights (appi) to tools/cloud/naming.cs; find their limits on Learn's "Naming rules and restrictions" page.
★★☆ Add an assignment to the policy script that forces the environment tag to take only the values dev, staging and prod. Find a built-in definition that does the job, or write your own policy definition in JSON.
★★★ Draw the management group tree for Bookstore: platform and application groups under the root, prod and nonprod subscriptions. Write which policy and which role assignment you would put on each level; compare the same structure on AWS with OUs and SCPs.
In the next post we look at who can access what inside this tree: Entra ID, Azure RBAC, managed identities and Bookstore.Api reaching storage without an access key.