Azure Resource Manager (ARM)
The central management-plane handler. It serves the generic ARM surface
(subscriptions, resource groups, and the resource/provider listings) that the
hashicorp/azurerm Terraform provider, OpenTofu, and the Azure CLI expect, and it acts as the
fallthrough for management-plane paths not claimed by a more specific handler (AKS, SQL, Redis,
Managed Identity, and the other Microsoft.* providers).
HTTP-only: no Docker. ARM resource state (resource groups and the ARM shells) is in-memory and ephemeral; it does not persist across restarts regardless of
storage.mode. The tenant and subscription directory is the exception: it followsstorage.mode(see Tenants and subscriptions), and so do resource groups underresource-group-semantics(see Resource groups).
Disabling ARM disables the management plane
arm.enabled: false turns off /subscriptions and /providers routing, which the
provider-specific ARM services (Redis, SQL, AKS, …) depend on. Leave it enabled unless you are
deliberately testing a data-plane-only configuration.
Features
- Tenants and subscriptions: a directory of tenants (each in one cloud) and subscriptions (each
owned by one tenant, in one state).
GET /tenantsandGET /subscriptions(enumeration, used byaz login) list it; an unknown subscription id is registered on first use, or refused with404 SubscriptionNotFoundunderparity: strict - Locations:
GET /subscriptions/{sub}/locations, the region catalog of the subscription's cloud, read byaz account list-locations, the CLI's display-name location translation (-l "East US"), and Terraform'sdata.azurerm_location - Resource groups: CreateOrUpdate, Get, CheckExistence (
HEAD), Update (PATCH), Delete, List (/subscriptions/{sub}/resourceGroups/{rg}), scoped per subscription, names matched case-insensitively;tagsandmanagedByare kept; accepts bothresourceGroupsand the lowercaseresourcegroupsspelling. See Resource groups - Storage accounts: an ARM shell that bridges
Microsoft.Storage/storageAccountsto the live Blob and Queue backends, returning the well-known development account key - Key vaults and managed HSMs: ARM shells for
Microsoft.KeyVault/vaultsandmanagedHSMswhosevaultUriandhsmUripoint at the live Key Vault handler, kept in thearm-keyvaultstore under the global storage mode - Sites: an ARM shell for
Microsoft.Web/sitesand itsconfig/webthat provisions a Functions app fromlinuxFxVersion, kept in thearm-webstore under the global storage mode - Resource listing:
GET /subscriptions/{sub}/resourcesandPOST .../{namespace}/checkNameAvailability - Resource providers: every namespace floci-az serves, with its resource types and Azure's
api-versions, and
register/unregister(see Resource providers) - Provider fallthrough: management-plane paths for providers without a dedicated handler are answered here so Terraform reads and dependency lookups resolve
Endpoints
GET /metadata/endpoints?api-version={2022-09-01|2019-05-01|2018-01-01}
GET /tenants
GET /subscriptions
GET /subscriptions/{sub}
GET /subscriptions/{sub}/locations
PUT /subscriptions/{sub}/resourceGroups/{rg}
GET /subscriptions/{sub}/resourceGroups/{rg}
HEAD /subscriptions/{sub}/resourceGroups/{rg}
PATCH /subscriptions/{sub}/resourceGroups/{rg}
DELETE /subscriptions/{sub}/resourceGroups/{rg}
GET /subscriptions/{sub}/resourceGroups
GET /subscriptions/{sub}/operationresults/{operationId} # poll of a resource-group delete
GET /subscriptions/{sub}/resources
GET /subscriptions/{sub}/providers[/{namespace}]
POST /subscriptions/{sub}/providers/{namespace}/register
POST /subscriptions/{sub}/providers/{namespace}/unregister
POST /subscriptions/{sub}/providers/{namespace}/checkNameAvailability
# ARM shells (bridge to the live data-plane handlers)
PUT /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Storage/storageAccounts/{name}
PUT /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.KeyVault/vaults/{name}
azurerm_storage_account and azurerm_key_vault resolve to those two provider paths; the storage
account returns the well-known development key and the vault's vaultUri points at the live
Key Vault data plane.
Quickstart
Point Terraform at the emulator with a minimal azurerm provider block (see the
Terraform guide for the full setup):
provider "azurerm" {
features {}
# metadata_host / endpoints redirected at localhost:4577: see the Terraform guide
}
resource "azurerm_resource_group" "example" {
name = "my-rg"
location = "eastus"
}
Or drive it directly:
curl -s -X PUT \
"http://localhost:4577/subscriptions/00000000-0000-0000-0000-000000000001/resourceGroups/my-rg?api-version=2021-04-01" \
-H "Content-Type: application/json" \
-d '{"location":"eastus"}'
Configuration
floci-az:
parity: compat # compat | azure | strict
# parity-overrides:
# subscription-listing: true # default: on from azure
# strict-subscriptions: true # default: on from strict
# resource-group-semantics: true # default: on from azure
# resource-group-delete: true # default: on from azure
# strict-resource-groups: true # default: on from strict
# strict-providers: true # default: on from strict
# location-normalize: true # default: on from azure
# tenant-scoping: true # default: on from azure
# strict-locations: true # default: on from strict
# strict-names: true # default: on from strict
cloud:
default: AzureCloud
services:
arm:
enabled: true
default-subscription-id: "00000000-0000-0000-0000-000000000001"
storage:
services:
directory:
# mode: persistent # default: storage.mode
| Property | Env var | Default | Description |
|---|---|---|---|
services.arm.enabled |
FLOCI_AZ_SERVICES_ARM_ENABLED |
true |
Enables the ARM management plane. Disabling it turns off every ARM-based service |
services.arm.default-subscription-id |
FLOCI_AZ_SERVICES_ARM_DEFAULT_SUBSCRIPTION_ID |
00000000-0000-0000-0000-000000000001 |
The subscription that always exists, listed first by GET /subscriptions, so the one az login selects |
parity |
FLOCI_AZ_PARITY |
compat |
The Azure parity level, compat, azure or strict; see Parity levels |
parity-overrides.subscription-listing |
FLOCI_AZ_PARITY_OVERRIDES_SUBSCRIPTION_LISTING |
on from azure |
true: GET /subscriptions also lists subscriptions registered on first use, and GET /subscriptions/{sub} echoes the directory's id rather than the path's spelling |
parity-overrides.strict-subscriptions |
FLOCI_AZ_PARITY_OVERRIDES_STRICT_SUBSCRIPTIONS |
on from strict |
true: an unknown subscription is 404 SubscriptionNotFound. false: it is registered on first use |
parity-overrides.resource-group-semantics |
FLOCI_AZ_PARITY_OVERRIDES_RESOURCE_GROUP_SEMANTICS |
on from azure |
true: a resource-group re-PUT answers 200, a resource PUT under a missing group creates the group, and groups persist in the directory store |
parity-overrides.resource-group-delete |
FLOCI_AZ_PARITY_OVERRIDES_RESOURCE_GROUP_DELETE |
on from azure |
true: a resource-group DELETE deletes every resource in the group and answers 202 with a Location to poll; the generic listings also show Event Grid, Monitor and Email resources |
parity-overrides.strict-resource-groups |
FLOCI_AZ_PARITY_OVERRIDES_STRICT_RESOURCE_GROUPS |
on from strict |
true: a request for a resource under a group that does not exist is 404 ResourceGroupNotFound, and groups are kept in the directory store |
cloud.default |
FLOCI_AZ_CLOUD_DEFAULT |
AzureCloud |
The cloud of the default tenant and of a tenant that names none: AzureCloud, AzureUSGovernment or AzureChinaCloud. An unknown name fails startup. Today it changes which cloud tenants and request scopes resolve to and the default location of a new group; endpoints, token issuers, DNS suffixes and TLS names stay the public cloud's, and a startup warning says so |
cloud.<name>.default-location |
FLOCI_AZ_CLOUD_<NAME>_DEFAULT_LOCATION |
the cloud's first catalog location | The location a resource or group of that cloud gets when its request names none; see Resource locations |
tenants[n].* |
FLOCI_AZ_TENANTS_<n>__ID, ..._DISPLAY_NAME, ..._DEFAULT_DOMAIN, ..._CLOUD |
none | Extra tenants; see below |
subscriptions[n].* |
FLOCI_AZ_SUBSCRIPTIONS_<n>__ID, ..._TENANT_ID, ..._DISPLAY_NAME, ..._STATE |
none | Extra subscriptions; see below |
storage.services.directory.mode |
FLOCI_AZ_STORAGE_SERVICES_DIRECTORY_MODE |
storage.mode |
Storage mode of the tenant and subscription directory |
The default tenant comes from the Entra ID default-tenant-id setting
(00000000-0000-0000-0000-000000000002 by default), so GET /tenants, the subscription's tenantId
and issued tokens always agree. The Managed Identity system-assigned-scope
defaults to subscriptions/${floci-az.services.arm.default-subscription-id}, so it follows the
subscription when you change it.
Cloud metadata
GET /metadata/endpoints describes the cloud with every endpoint a client calls at the emulator's own base
URL (the request's scheme and host, or the X-Forwarded-Proto scheme behind a TLS proxy), in the shape the
api-version asks for:
| api-version | Shape | Read by |
|---|---|---|
2022-09-01, or any other, or none |
one object, named floci-az |
azurerm's metadata_host, az cloud register --endpoint-resource-manager |
2019-05-01 |
a list holding one cloud, named floci-az |
the az CLI's ARM_CLOUD_METADATA_URL |
2018-01-01 |
Azure's cloudEndpoint document |
azurerm's location validation |
The 2019-05-01 and 2018-01-01 answers start from Azure's own payload for the default cloud
(floci-az.cloud.default), so audiences and suffixes of services no client reaches through this document stay
Azure's. Setting ARM_CLOUD_METADATA_URL to <base-url>/metadata/endpoints?api-version=2019-05-01 is enough
for the az CLI: the one cloud listed becomes its active cloud without az cloud register or az cloud set, and
az login signs in against the emulator. The cloud is named floci-az rather than AzureCloud because az
replaces the login endpoint of a cloud named AzureCloud with Microsoft's. Use the https base URL: MSAL
refuses a plain-HTTP authority.
The 2018-01-01 cloudEndpoint lists Azure's ARM hosts verbatim and none of the emulator's. azurerm validates a
resource's location only against the entry whose endpoint is its own ARM host, so it keeps accepting any
non-empty location against floci-az, as before.
Tenants and subscriptions
The directory holds the default tenant and subscription, the ones listed under floci-az.tenants and
floci-az.subscriptions, and the subscriptions registered on first use:
floci-az:
tenants:
- id: 11111111-0000-0000-0000-000000000000
display-name: Contoso Gov # default: floci-az local
default-domain: contoso.example # default: floci-az.local
cloud: AzureUSGovernment # default: cloud.default
subscriptions:
- id: 22222222-0000-0000-0000-000000000000
tenant-id: 11111111-0000-0000-0000-000000000000 # default: the default tenant
display-name: contoso-dev # default: floci-az local
state: Enabled # Enabled, Warned, PastDue, Disabled or Deleted
Only id is required. An entry with the default tenant's or subscription's id overrides it in place,
for example to start the default subscription Disabled. A subscription must name the default tenant
or a configured one, and an unknown cloud or state fails startup with the property that holds it.
- Cloud: a tenant lives in exactly one cloud, and a subscription's cloud is its tenant's. A request
under
/subscriptions/{sub}resolves to that cloud. - Listing:
GET /subscriptionslists the caller's tenant's subscriptions. By default the caller's tenant is always the default tenant; undertenant-scopingit is thetidof the request's Bearer token, when the directory holds that tenant (see Tenant scoping). The default subscription comes first, then configured ones in config order, soaz login(which selects the firstEnabledsubscription) keeps selecting the default. Subscriptions registered on first use are listed after them only undersubscription-listing.GET /tenantslists every tenant, each reported as the caller'sHometenant. - Id echo: subscription ids are case-insensitive. By default
GET /subscriptions/{sub}echoes the id as the path spells it; undersubscription-listingit echoes the directory's spelling (the configured one, or the one the subscription was first used with). - Persistence: configured entries are rewritten from configuration at every start, and an entry no
longer configured is dropped. First-use entries survive a restart when
storage.services.directory.mode(orstorage.mode) persists.POST /_admin/resetdrops first-use entries. A first-use subscription whose tenant is no longer in the directory after a restart (the default tenant id changed, or a configured tenant was removed) moves to the default tenant.
Tenant scoping
With tenant-scoping (on from parity: azure) the tid claim of a request's Bearer token is read as the
caller's tenant. The token is decoded, never verified: the claim only picks which tenant's view the request
gets, and verifying tokens is strict auth's job.
GET /subscriptionslists that tenant's subscriptions, as Azure lists the subscriptions of the tenant a token was issued for. A tenant the directory does not hold, or a request without a readable token, is the default tenant. A tenant that owns no subscription sees the default subscription, so a client logged in to it (az login --tenant) still finds one to use.- A subscription used for the first time is registered under the caller's tenant when the directory holds it.
GET /tenantsstill lists every tenant, each asHome. Azure lists every tenant the identity can reach, and every tenant is reachable in the emulator, soaz loginwithout--tenantwalks every tenant and, with a token per tenant, finds each one's subscriptions.
Emulator choices, with no Azure source to settle them: the default-subscription fallback, and Home for every
tenant (no local source says which category a tenant reached as a guest gets).
Parity levels
floci-az.parity decides how closely floci-az follows Azure where Azure differs from what floci-az has
always answered. The levels nest, and every parity-overrides key defaults from the level, so one key
can also be set alone to opt in piecemeal:
| Level | What changes |
|---|---|
compat (default) |
Nothing a client could rely on: unknown subscriptions are registered internally on first use but not listed, ids are echoed as sent, and resource groups answer as they always have (every PUT 201, DELETE 200 without cascade) |
azure |
Azure's answers for the requests Azure accepts. Every parity override on: subscription-listing, resource-group-semantics, resource-group-delete, location-normalize, tenant-scoping |
strict |
azure plus Azure's refusals, every strict-* override: strict-subscriptions, strict-resource-groups, strict-providers, strict-locations, strict-names |
auth.mode plays no part in any of this. The startup banner prints parity: <level> followed by every
override set to a value its level would not give it. At the default level the line also names the
variable that changes it, since a misspelt variable is silently ignored.
When an override refuses a request, the response body is Azure's own and says nothing about floci-az; the
emulator's log has one INFO line per refusal naming the override that caused it. Under azure, a subscription
registered on first use is logged at WARN, because it is now listed where Azure would answer 404.
First use and strict subscriptions
| Request names a subscription that is | Default | strict-subscriptions (on from parity: strict) |
|---|---|---|
in the directory, Enabled or PastDue |
served | served |
in the directory, Warned, Disabled or Deleted |
GET, HEAD and DELETE served; PUT, PATCH and POST refused with 409 ReadOnlyDisabledSubscription |
same |
| not in the directory | registered on first use (Enabled, default tenant), then served; listed only under subscription-listing |
404 SubscriptionNotFound: The subscription '{id}' could not be found. |
By default any id is accepted, GUID or not, so Terraform and SDK clients configured with their own
subscription keep working. Strict subscriptions do not check the format: a malformed id is simply not in
the directory, and Azure documents SubscriptionNotFound for a wrong or wrongly formatted id too.
Emulator choices, with no Azure source to settle them:
- The refusal of a write on a read-only subscription: Azure's resource provider contract says only
GET and DELETE work on a warned or suspended subscription, but no local source gives the status and
code a client receives. floci-az answers
409 ReadOnlyDisabledSubscription. PastDueis a billing state the provider contract does not list; floci-az keeps it writable.Deletedis read-only likeDisabled. Azure cleans a deleted subscription's resources up; floci-az does not cascade them.
Resource groups
Resource groups follow the ResourceGroups operations of the Resources REST spec (the az CLI pins
2024-11-01, azurerm 2023-07-01; both declare the same status codes). Names are matched
case-insensitively, and a group answers with the spelling of its latest PUT; a PATCH in another casing
updates the group and keeps that spelling.
These hold in every configuration:
- Tags and
managedBy: a PUT keeps both and every read returns them; a group created without tags has notagskey. PATCHanswers200with the group.tagsandmanagedByreplace what they name (atagsobject replaces the whole tag set, as azurerm expects when it sends its full tag map); everything the body leaves out is kept. A missing group is404 ResourceGroupNotFound.HEADanswers204for a group and404without one.
The rest depends on the parity overrides:
| Behaviour | Default | Override |
|---|---|---|
| PUT of an existing group | 201, as for a new one |
resource-group-semantics: 200 |
| GET of a missing group | 404 ResourceGroupNotFound, Resource group '{rg}' could not be found., as Azure answers |
the same |
| Resource PUT under a missing group | the resource is created, no group appears | resource-group-semantics: the group is created once the resource is (only after a 2xx), in the location the resource reports, else the cloud's default location. strict-resource-groups: refused with 404 ResourceGroupNotFound before the resource's handler runs, as is a read, an action or a delete of a resource under a missing group |
DELETE |
200; only the group is removed, a missing group is 200 too |
resource-group-delete: see below |
| Persistence | in memory, lost on restart | resource-group-semantics or strict-resource-groups: kept in the directory store, so they follow storage.services.directory.mode |
Generic listings (.../resources) |
as before: Event Grid topics, Log Analytics workspaces, data collection endpoints and rules, and Communication and Email services are not listed | resource-group-delete: listed, so Terraform's prevent_deletion_if_contains_resources sees what a delete would remove |
Under resource-group-delete a DELETE of a group is the long-running operation the spec declares:
- Every resource the generic index holds in the group is deleted, the deepest ids first, by sending the
resource's own
DELETEthrough the same routing and handler a client request reaches, so each service applies its own side effects (a global name is released, a sidecar container stopped). A resource that is already gone counts as deleted; any other failure is logged and the delete goes on. - The group is removed and the request answers
202with aLocationof{scheme}://{host}/subscriptions/{sub}/operationresults/{id}?api-version=..., built from the request's own scheme and host, andRetry-After: 1. - The work is done by the time the
Locationis polled: it answers200with an empty body, the shape Azure's own poll ends with. A missing group is404 ResourceGroupNotFound.
While the delete runs, the group is marked as being deleted, so that no resource created meanwhile is
left behind: a PUT or PATCH of a resource under the group is refused with
409 ResourceGroupBeingDeleted, and the delete waits for any such request that was already running
before it lists the group's resources. The mark is removed when the delete ends, whether it removed the
group or failed. Without resource-group-delete nothing is marked and no request is refused.
Emulator choices, with no Azure source to settle them: a re-PUT with a different location replaces the
location, as floci-az always has (Azure documents the location as fixed at creation but no local source
gives the error it answers); Retry-After is 1 second (Azure sends none on this operation, and clients
then fall back to their own, much longer, poll interval); forceDeletionTypes is accepted and ignored;
a write under a group that is being deleted answers 409 with the message The resource group '{rg}' is
in deprovisioning state and cannot perform this operation. (Azure documents the
ResourceGroupBeingDeleted code, with the advice to wait for the deletion to complete, but not its
status or message).
Template deployments
Microsoft.Resources/deployments is served by its own handler; see
ARM Template Deployments.
Resource providers
GET /subscriptions/{sub}/providers lists every namespace floci-az serves, and only those: a namespace it
does not serve is left out, which azurerm reads as nothing to register, and any request naming one is
404 InvalidResourceNamespace. Each namespace carries the resource types floci-az serves in it, with
Azure's api-versions newest first, the regions a type can be created in by display name, and
registrationPolicy: RegistrationFree for Microsoft.Resources, RegistrationRequired for the rest.
register and unregister answer 200 with the Provider body in its final state, Registered or
Unregistered, at once and every time. Azure passes through Registering and Unregistering first, but
az provider register --wait and the Python SDK's auto-register policy poll without a limit, so a
transitional state would only make them wait.
By default every namespace is Registered, so nothing ever needs registering, as before. With
strict-providers (on from parity: strict) every namespace but Microsoft.Resources starts
NotRegistered in each subscription, and a request for one of its resource types is refused with Azure's
409 MissingSubscriptionRegistration until the namespace is registered. A DELETE is never refused, so
resources created before their namespace was unregistered can still be deleted, by a client or by a resource
group delete. The Go, Java and Python SDKs
catch that error, register the namespace and retry on their own; azurerm with
resource_provider_registrations set registers what it needs at configure time. Registrations then persist
in the directory store, alongside the subscriptions they belong to.
Emulator choices, with no Azure source to settle them: the status and message of
InvalidResourceNamespace (Azure documents the code for an unknown namespace, but no recording shows the
Providers API answering one), and the capabilities string (SupportsTags, SupportsLocation for a type
with a location of its own, None otherwise). defaultApiVersion, apiProfiles, zoneMappings and
authorizations are optional in Azure's answer and omitted.
Resource locations
A resource or resource group whose request names no location gets its cloud's default location: the cloud's
first location in the catalog (eastus for AzureCloud, usgovvirginia for AzureUSGovernment, chinanorth
for AzureChinaCloud), or the one configured for that cloud:
floci-az:
cloud:
AzureCloud:
default-location: westeurope # FLOCI_AZ_CLOUD_AZURECLOUD_DEFAULT_LOCATION
The cloud name is matched without regard to case, so the environment variable form works. The value may be a
name or a display name (West Europe) and is stored canonical; a location the cloud does not offer, or a cloud
the catalog does not hold, fails startup.
By default a resource's location is stored and echoed exactly as the client sent it, so "East US" and
eastus are two different values, and names built from it keep the spelling: a container group's FQDN,
an AKS cluster's FQDN and its MC_<group>_<cluster>_<location> node resource group.
With location-normalize (on from parity: azure) the request gate rewrites the location of every
PUT or PATCH body of a served resource type before its handler runs, for the cloud the subscription
belongs to, as Azure stores it:
- a display name or any casing becomes the canonical name:
"East US 2"is stored and echoed aseastus2, and the ACI and AKS names above use it - a global type (DNS zones, private DNS zones and their virtual network links, Communication and Email
services) is
globalwhatever the client sends, and a PUT that names no location getsglobal - a type whose location defaults to its group's gets the group's location when a PUT names none
With strict-locations (on from parity: strict) a PUT of a regional resource naming a location its
subscription's cloud does not offer is refused before its handler runs with Azure's
400 NoRegisteredProviderFound: "No registered resource provider found for location 'global is always accepted. A PATCH, a child resource and a global type are never refused.
Both run before policy evaluation, so policy conditions see the canonical location. A location a policy
append adds or changes is checked again after policy, the same way: normalized under location-normalize
and refused under strict-locations.
Emulator choices, with no Azure source to settle them: which declared types count as global follows each service's declaration; the supported locations in the message are every location of the cloud, where Azure lists the ones the type is offered in.
Name syntax
With strict-names (on from parity: strict) a PUT whose resource name breaks its type's naming rule
(resource-name-rules.md) is refused before its handler runs with 400 InvalidResourceName. Without it every name
passes, as floci-az has always accepted them. The types that declare their rule so far:
| Type | Rule |
|---|---|
Microsoft.ApiManagement/service |
1 to 50 letters, digits and hyphens, starting with a letter and ending with a letter or digit |
Microsoft.App/containerApps |
2 to 32 lowercase letters, digits and hyphens, starting with a letter and ending with a letter or digit |
Microsoft.Authorization/policyAssignments, policyDefinitions, policyExemptions, policySetDefinitions |
1 to 64 characters, none of <>%&:\?/, not ending with a space; an assignment's 24-character bound at management-group scope is not checked |
Microsoft.Cache/redis |
1 to 63 letters, digits and hyphens, starting and ending with a letter or digit, no consecutive hyphens |
Microsoft.Communication/communicationServices |
1 to 63 letters, digits, underscores and hyphens |
Microsoft.Communication/emailServices |
1 to 63 letters, digits and hyphens |
Microsoft.Communication/emailServices/domains |
1 to 253 characters |
Microsoft.Compute/virtualMachines |
1 to 64 characters with no spaces or special characters (_ included), not ending with a period or hyphen; Linux's bound, Windows' 15 is not checked |
Microsoft.ContainerInstance/containerGroups |
1 to 63 lowercase letters, digits and hyphens, no leading, trailing or consecutive hyphen |
Microsoft.ContainerRegistry/registries |
5 to 50 letters and digits |
Microsoft.ContainerService/managedClusters |
1 to 63 letters, digits, underscores and hyphens, starting and ending with a letter or digit |
Microsoft.ContainerService/managedClusters/agentPools |
1 to 12 lowercase letters and digits, starting with a letter; also checked for the pools in a cluster PUT body |
Microsoft.DBforMariaDB/servers |
3 to 63 lowercase letters, digits and hyphens, not starting or ending with a hyphen |
Microsoft.DBforMySQL/flexibleServers |
3 to 63 lowercase letters, digits and hyphens, starting with a letter or digit and not ending with a hyphen |
Microsoft.DBforPostgreSQL/flexibleServers |
3 to 63 letters, digits and hyphens, starting and ending with a letter or digit, no consecutive hyphens |
Microsoft.EventGrid/eventSubscriptions |
3 to 64 letters, digits and hyphens |
Microsoft.EventGrid/topics |
3 to 50 letters, digits and hyphens |
Microsoft.KeyVault/managedHSMs |
3 to 24 letters, digits and hyphens, starting with a letter, ending with a letter or digit, no consecutive hyphens |
Microsoft.KeyVault/vaults |
3 to 24 letters, digits and hyphens, starting with a letter, ending with a letter or digit, no consecutive hyphens |
Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials |
3 to 120 letters, digits, hyphens and underscores, starting with a letter or digit |
Microsoft.ManagedIdentity/userAssignedIdentities |
3 to 128 letters, digits, hyphens and underscores, starting with a letter or digit |
Microsoft.Network/networkInterfaces |
1 to 80 letters, digits, underscores, periods and hyphens, starting with a letter or digit and ending with a letter, digit or underscore |
Microsoft.Network/networkSecurityGroups |
1 to 80 letters, digits, underscores, periods and hyphens, starting with a letter or digit and ending with a letter, digit or underscore |
Microsoft.Network/publicIPAddresses |
1 to 80 letters, digits, underscores, periods and hyphens, starting with a letter or digit and ending with a letter, digit or underscore |
Microsoft.Network/virtualNetworks/subnets |
1 to 80 letters, digits, underscores, periods and hyphens, starting with a letter or digit and ending with a letter, digit or underscore |
Microsoft.Network/virtualNetworks |
2 to 64 letters, digits, underscores, periods and hyphens, starting with a letter or digit and ending with a letter, digit or underscore |
Microsoft.OperationalInsights/workspaces |
4 to 63 letters, digits and hyphens, starting and ending with a letter or digit |
Microsoft.Sql/servers/databases |
1 to 128 characters without <>*%&:\/? or control characters, not ending with a period or a space |
Microsoft.Sql/servers/firewallRules |
1 to 128 characters without <>*%&:;\/? or control characters, not ending with a period |
Microsoft.Sql/servers |
1 to 63 lowercase letters, digits and hyphens, not starting or ending with a hyphen |
Microsoft.Storage/storageAccounts |
3 to 24 lowercase letters and digits; checked with or without strict-names, answered 400 AccountNameInvalid as floci-az always has, before any name conflict |
Microsoft.Web/sites |
2 to 60 letters, digits and hyphens, not starting or ending with a hyphen (the Unicode characters Azure maps to Punycode are refused) |
The other types declare theirs as they move to the shared resource store. The code is an emulator choice: no local source records Azure's answer for a name outside the rule.
Resource names across subscriptions
Two subscriptions can use the same resource-group name, and most resources then coexist: each subscription gets its own identity, virtual network, DNS zone, Event Grid topic, AKS cluster, container group, Container Apps environment or email service under the same name.
Resources whose name is a global DNS name behave as in Azure: the first subscription to create the name
owns it. A create of the same name from another subscription or resource group is refused, that scope
reads the name as 404, and the name is free again once the owner deletes it. The same holds for child
calls: a storage account's containers, queues and listKeys answer 404 under any scope but the
owner's, and a path that names no resource group never reaches another subscription's resource.
Concurrent creates of one name from several subscriptions leave exactly one owner.
| Resource | Refused with |
|---|---|
Microsoft.Storage/storageAccounts |
409 StorageAccountAlreadyTaken, or 409 StorageAccountInAnotherResourceGroup within the same subscription |
Microsoft.KeyVault/vaults |
409 VaultAlreadyExists |
Microsoft.KeyVault/managedHSMs |
409 Conflict (see below) |
Microsoft.Web/sites |
409 in the App Service error shape (Code: Conflict, ExtendedCode: 54001) |
Microsoft.ContainerRegistry/registries |
409 AlreadyInUse |
Microsoft.Cache/redis |
409 NameNotAvailable |
Microsoft.Sql/servers |
400 NameAlreadyExists |
Microsoft.DBforPostgreSQL/flexibleServers, Microsoft.DBforMySQL/flexibleServers, Microsoft.DBforMariaDB/servers |
409 ServerNameAlreadyExists |
Microsoft.ApiManagement/service |
409 ServiceAlreadyExists |
Microsoft.Communication/communicationServices |
409 Conflict (see below) |
POST /subscriptions/{sub}/providers/Microsoft.Storage/checkNameAvailability, the
Microsoft.KeyVault equivalent and its checkMhsmNameAvailability, and the Microsoft.Web one for a site name
report a taken name as nameAvailable: false, reason AlreadyExists.
For Microsoft.Storage, malformed names report nameAvailable: false, reason
AccountNameInvalid. Storage account names must contain 3 to 24 lowercase letters or digits. A PUT
that creates a storage account with such a name is refused with 400 AccountNameInvalid.
The codes come from Microsoft's troubleshooting pages and real error payloads, because the REST specs
do not model create conflicts. No public source gives the code for Managed HSM or Communication Services,
so those two answer with ARM's generic Conflict. The status for the PostgreSQL, MySQL and MariaDB
servers is not publicly documented either; 409 is the likeliest.
Intentional deviations
- Unknown subscriptions exist on first use: unless
strict-subscriptionsis on, any subscription id a request names is registered and served (and listed undersubscription-listing), so a client configured with its own subscription id keeps working. ARM state is kept per subscription. - The caller's tenant is the default tenant unless
tenant-scopingis on, and even then the token is read for itstidonly, never verified. - Locations are a static catalog per cloud: a subscription lists the regions of the cloud its tenant
belongs to (the public cloud's 51, US Gov's 6 or China's 6), with names and display names from the
Azure SDK's region list; every entry is a physical region with zones 1 to 3 mapped to
<region>-az<n>.regionalDisplayNamerepeatsdisplayName, andgeography,geographyGroup,latitude,longitude,physicalLocationandpairedRegionare omitted. There are no edge zones, soincludeExtendedLocations=truereturns the same list. Location values on resources are echoed as sent and not validated unlesslocation-normalizeorstrict-locationsis on (see Resource locations). - ARM state is in-memory: resource groups and the ARM shells do not persist across restarts; only
the tenant and subscription directory follows
storage.mode, and resource groups too underresource-group-semantics. - Resource-group deletion does not cascade by default: deleting a group leaves resources created
under it behind (they still appear in
GET .../resources, so Terraform'sprevent_deletion_if_contains_resourcesworks).resource-group-deleteturns the cascade on. - Shared Key / ARM auth is accepted but not verified: any bearer token or Shared Key is honored, consistent with the rest of the emulator's permissive dev auth.