Subchapter 7.9
references/sku-quota-validation.mdMarkdown9 KBView on GitHub
Pre-deploy quota and offer restriction checks. Read during prepare Step 5 before deploying.
For SKU selection (budget tiers, modifier rules, defaults), see sku-matrix.md.
⛔ Use
az rest, NOTaz quota list. Theaz quota listCLI extension triggers a full extension metadata scan on startup. If ANY installed extension has a permission error (common:azure-devopsWinError 5 on Windows), the entire command fails.az restis a built-in command that bypasses extension loading entirely and hits the same REST API. Quota increases are free — you only pay for resources actually used.
⛔
what-ifdoes NOT catch App Service quota errors.az deployment sub what-ifreturnsSucceededeven when the target SKU has limit=0. The quota rejection only surfaces at actualaz deployment sub createtime. This means the prepare-phase quota check is the ONLY pre-deploy safety net — do not skip or shortcut it.
⛔ Do NOT use
az vm list-usage,az appservice list-locations, ormcp_azure_mcp_quotafor quota checks. They return misleading data — see Anti-Patterns below.
Build the scan list dynamically — do NOT hardcode a fixed set of regions:
context.json.azure.region or context.json.overrides[] for a region preference. If stated, scan that region first.eastus2) and scan 4 alternates from the user’s likely geography (infer from subscription tenant location or ask).Global region pool: eastus2, eastus, westus2, centralus, westeurope, northeurope, australiaeast, japaneast, southeastasia, brazilsouth
Agent: adapt shell syntax to detected environment. PowerShell shown below; use equivalent syntax on bash/zsh (e.g.,
for region in eastus eastus2 ...; do ... done).
⛔ PowerShell
?in URLs: When buildingaz restURLs with variable interpolation, PowerShell may strip?from?api-version=. Always use the URL inline in double quotes (as shown below), NOT via a$urlvariable. If you must use a variable, wrap the?with a backtick:`?api-version=.
Use az rest for all quota checks. Query BOTH limit AND usage (limit alone is insufficient).
App Service: Query quota + usages endpoints per region:
$sub = '{subscriptionId}'; $sku = '{sku}'
@('{userRegion}','{alt1}','{alt2}','{alt3}') | ForEach-Object {
$limit = az rest --method get --url "https://management.azure.com/subscriptions/$sub/providers/Microsoft.Web/locations/$_/providers/Microsoft.Quota/quotas/$sku?api-version=2023-02-01" --query "properties.limit.value" -o tsv 2>$null
$used = az rest --method get --url "https://management.azure.com/subscriptions/$sub/providers/Microsoft.Web/locations/$_/providers/Microsoft.Quota/usages/$sku?api-version=2023-02-01" --query "properties.usages.value" -o tsv 2>$null
# limit=0 with used=-1 is the API's "Free tier not offered here" sentinel — treat limit<=0 as BLOCKED and clamp negative usage so 0-(-1) does NOT become a false-positive 1.
$ln = if ($limit) { [int]$limit } else { $null }; $un = if ($used) { [int]$used } else { 0 }
$avail = if ($null -eq $ln) { 'unknown' } elseif ($ln -le 0) { 0 } else { $ln - [math]::Max(0, $un) }
Write-Host "$_ : $sku limit=$limit available=$avail"
}Container Apps: /usages gives usage+limit in one call:
az rest --method get --url "https://management.azure.com/subscriptions/$sub/providers/Microsoft.App/locations/{region}/usages?api-version=2024-03-01" --query "value[?name.value=='ManagedEnvironmentCount'].{used:currentValue, limit:limit}" -o jsonStatic Web Apps: No Microsoft.Quota provider — Free plan caps at ~10 apps/subscription (per docs; may vary, treat as guideline). Count existing Free apps:
az staticwebapp list --query "length([?sku.name=='Free'])" -o tsvAt/near cap → treat SWA Free as UNAVAILABLE (no self-service increase — raises need a support request). Fall back per After Checking.
Storage — default limit 250 accounts/region. Rarely exhausted — skip programmatic check unless the plan requires multiple storage accounts.
Key Vault — no quota API exists (returns NotFound). Default limit ~1000 vaults/subscription. Skip programmatic check.
available > 0 → AVAILABLE. available = 0 / limit <= 0 → BLOCKED (a limit=0, used=-1 response is the API sentinel for “Free tier not offered in this region” — the script clamps it so it does not read as available). 404/empty → fallback candidate.az rest fails → quotaValidation: { verified: false, method: "unverifiable" }.assumptions[] note). User picks — never silently relocate (region may be a data-residency/latency requirement).assumptions[] note stating why (e.g., “No F1 quota in {checkedRegions} and SWA Free cap reached”).prepare-plan.json.quotaValidation: { verified: true, method: "cli", verifiedRegion, verifiedSku, checkedRegions[], failedResources[] }.⛔
what-if/validatedo NOT catchLocationIsOfferRestricted. Use capabilities API.
| Provider | API Version |
|---|---|
| PostgreSQL | 2022-12-01 |
| MySQL | 2023-12-30 |
$sub = '{subscriptionId}'; $provider = 'Microsoft.DBforPostgreSQL'; $apiVer = '2022-12-01'
@('{userRegion}','{alt1}','{alt2}','{alt3}') | ForEach-Object {
$result = az rest --method get --url "https://management.azure.com/subscriptions/$sub/providers/$provider/locations/$_/capabilities?api-version=$apiVer" --query "value[0].supportedFlexibleServerEditions[0].name" -o tsv 2>$null
if ($result) { Write-Host "$_ : $provider AVAILABLE ($result)" } else { Write-Host "$_ : $provider BLOCKED (offer restricted)" }
}For MySQL: change
$provider = 'Microsoft.DBforMySQL'and$apiVer = '2023-12-30'.
⛔ JMESPath MUST start with value[0].. URL MUST include /locations/{region}/. Empty/null response = BLOCKED. Write results to quotaValidation.offerRestrictions[].
⛔ Select the engine version deterministically from the capabilities payload — match the app’s detected version, upgrading only to the nearest compatible release. The payload lists supported versions at
value[0].supportedFlexibleServerEditions[0].supportedServerVersions[].name(e.g. MySQL:5.7,8.0.21,8.4,9.5). Using the detected DB version passed by the caller (fromcontext.json.detectedServices[]):
- If the exact detected version (or its exact patch) is in the supported list → use it.
- Else use the lowest supported version whose major ≥ the detected major (detected
5.7, supported[5.7, 8.0.21, 8.4, 9.5]→8.0.21). Picking the lowest compatible major — not the newest — avoids the 60+ minute provisioning hangs seen on brand-new majors (e.g.9.x) and keeps compatibility with the app’s driver/ORM. Return it in the quota output’s per-serviceversionfield; the orchestrator copies it toprepare-plan.json.services[].versionat plan-write (exact patch required — see prepare-schemas.tsversion). Record the bump inassumptions[]if the detected version was upgraded.
⛔ az quota list (extension failures), az vm list-usage (wrong layer), az appservice list-locations (ignores quota), mcp_azure_mcp_quota (misleading), what-if/validate (false positives).
If quotaValidation.verified == false at deploy gate: re-run Per-Provider Scripts above. Pass → update quotaValidation. Fail → present alternatives. az rest fails → warn and proceed.
When delegating from prepare Step 5, provide: subscriptionId, SKU list from prepare-plan.json.services[].sku, preferred region + fallbacks, list of managed database services, and this file’s content. See subagent-quota.md for the template.