ARM Templates
Overview
Infracost supports Azure Resource Manager (ARM) deployment templates in JSON. Templates are detected automatically by their $schema (any …/deploymentTemplate.json# URL, covering resource-group, subscription, management-group and tenant scopes), and, as a fallback for nested templates that omit the schema, by resources whose type starts with Microsoft.
Bicep is supported by transpiling to ARM JSON first — see Bicep below. The following outlines support for the various ARM template features:
| Feature | Support |
|---|---|
| Resources (array form) | ✅ |
| Resources (symbolic-name object form) | ✅ |
Parameters and defaultValue | ✅ |
Parameters files (*.parameters.json) | ✅ |
| Inline parameter overrides | ✅ |
| Variables | ✅ |
| Outputs | ✅ |
| Template functions (expressions) | ✅ |
copy loops (resource, property, variable) | ✅ |
copy loops (output) | ✅ |
condition | ✅ |
dependsOn and implicit dependencies | ✅ |
reference() | ✅ |
| Nested templates | ✅ |
| Linked templates (local / relative path) | ✅ |
| Linked templates (remote HTTP/HTTPS) | ✅ |
User-defined functions (functions) | ✅ |
| Deployment scopes (RG / sub / MG / tenant) | ✅ |
| Template Specs | ❌ |
| Bicep (direct) | ❌ |
Bicep @metadata / expected load | ❌ |
Resources may be written either as an array ("resources": [ … ]) or, using the newer symbolic-name syntax, as an object keyed by symbolic name ("resources": { "myVm": { … } }). Both are supported.
Where a template omits an optional cost-relevant property (an SKU capacity, a node count, a load-balancer SKU), Infracost applies Azure's documented default for that property rather than pricing it as zero, and marks the value as a default so policies can tell inferred values from authored ones.
Bicep
The parser consumes ARM JSON only — it does not parse .bicep files directly, and it does not run the Bicep transpiler for you. This mirrors how the CLI handles CDK: parse the generated template, don't run the toolchain (which can have side effects and requires the relevant CLI to be installed).
To cost a Bicep file, transpile it to ARM JSON first, then point Infracost at the output:
# Compile Bicep to an ARM JSON template
bicep build main.bicep --outfile main.json
# or, via the Azure CLI
az bicep build --file main.bicep --outfile main.json
# Then scan the directory containing the generated JSON
infracost scan .
This requires the bicep CLI on your PATH. If you point Infracost at a .bicep file directly, it returns an actionable error telling you to transpile first rather than a cryptic JSON parse failure.
Any Bicep @metadata decorators (including expected-load metadata) are not read. expectedLoad is stripped when Bicep compiles to ARM JSON, and the parser does not extract it.
Function support
ARM template expressions ("[ … ]") are evaluated during the scan. The following built-in functions are supported:
| Category | Functions | Support |
|---|---|---|
| Numeric | add, sub, mul, div, mod, min, max, range, int, float | ✅ |
| Logical & comparison | equals, less, lessOrEquals, greater, greaterOrEquals, if, and, or, not, bool, true, false, null | ✅ |
| String | concat, format, substring, replace, trim, split, join, toLower, toUpper, startsWith, endsWith, indexOf, lastIndexOf, padLeft, string, base64, base64ToString, base64ToJson, dataUri, dataUriToString, uri, uriComponent, uriComponentToString, uniqueString, guid, newGuid | ✅ |
| Array & object | array, createArray, createObject, coalesce, contains, empty, length, first, last, take, skip, union, intersection, flatten, shallowMerge, items, objectKeys, tryGet, json | ✅ |
| Lambda (higher-order) | lambda, lambdaVariables, map, mapValues, filter, reduce, sort, toObject, groupBy | ✅ |
| Date | dateTimeAdd, dateTimeFromEpoch, dateTimeToEpoch, utcNow | ✅ |
| Networking (CIDR) | cidrSubnet, cidrHost, parseCidr | ✅ |
| Copy | copyIndex | ✅ |
| Parameters & variables | parameters, variables | ✅ |
| Deployment & scope | resourceGroup, subscription, tenant, managementGroup, deployment, environment, pickZones | ✅ |
| Resource | resourceId, subscriptionResourceId, tenantResourceId, managementGroupResourceId, extensionResourceId, reference, listKeys, listSas, listAccountSas, listCallbackUrl, listSecrets, deployer | ✅ |
User-defined functions declared in the template's functions section are also supported and evaluated like built-ins.
Function, parameter, variable and object-property names are resolved case-insensitively, matching ARM — so parameters('VmSize') resolves a vmSize declaration and sku.name reads a "Sku": { "Name": … } block. format() honors .NET numeric format specifiers (for example format('{0:D3}', 5) → 005), and string functions such as substring and length count Unicode characters rather than bytes.
reference, listKeys, the list* family (listSas, listAccountSas, listCallbackUrl, listSecrets), deployer, environment and deployment depend on deployment-time state that isn't available during static analysis. Their results are treated as open objects: fields we do know (for example environment()'s Azure public-cloud suffixes, or a reference()d resource's authored properties) resolve to their real values, and any field we can't know resolves to an "unknown" that keeps a deeper property chain evaluating instead of failing. A reference() to a resource stands in with that resource's id, so composed values stay distinct. Values that only exist in live Azure state — secrets, connection strings, generated endpoints — are not real.
A call to an unrecognized function produces a non-critical warning and is left unresolved rather than failing the scan; this is rare in practice and seldom affects the cost estimate.
Deployment scope context
ARM functions like resourceGroup(), subscription() and tenant() resolve against the deployment scope. Infracost fills these with synthetic defaults (for example location defaults to eastus) so templates evaluate without a live Azure connection. Display-form locations ("East US") are normalized to their canonical form (eastus) for price lookups.
When a resource's region comes from one of these synthetic defaults — the ubiquitous "location": "[resourceGroup().location]" pattern with no configured location — it is marked as synthetic, so region-specific FinOps recommendations are not made from a region the template never stated. Set location in the config below to price against your real region. You can override the scope values per project in an infracost.yml config file:
version: 0.1
projects:
- path: .
azure:
subscription_id: 00000000-0000-0000-0000-000000000000
tenant_id: 00000000-0000-0000-0000-000000000000
resource_group_name: my-rg
location: westeurope
management_group_id: my-mg
Parameters
Parameter values are resolved in this order, matching az deployment precedence (later wins):
- The parameter's
defaultValuein the template. - A parameters file. Infracost auto-discovers a sibling parameters file next to the root template —
<template>.parameters.json,<template>.params.json, or a bareparameters.json/params.json— and recognizes per-environment files in the common shapes (<template>.<env>.parameters.json,<template>.parameters.<env>.json,parameters.<env>.json, …). Environment names come from your configured environments, falling back to the standard set (dev,staging,prod, …); each environment is priced separately with its own parameter values. You can also point at a file explicitly. - Inline parameter overrides supplied to the scan.
Nested and linked templates
- Nested templates (
Microsoft.Resources/deploymentswith an inlinetemplate) are expanded and priced, in both outer- and inner-scope evaluation modes. This includescopyloops over deployments and copy loops declared inside an inner-scope nested template. - Linked templates referenced by a local
relativePathorfile://URI are resolved and priced, provided the target stays within the scanned directory. - Linked templates referenced by an
http/httpsURL whose file ships in the scanned directory are resolved from the local copy — no network access. This covers the ubiquitous_artifactsLocationstaging pattern, including the_artifactsLocationdefault of[deployment().properties.templateLink.uri], where templates compose sibling paths withuri(...)even though the files live in the repo. - Genuinely-remote linked templates (an
http/httpstemplateLink.uriwith no local copy) are fetched over the network. Fetching is on by default and can be disabled; it is SSRF-guarded (connections to loopback, private, link-local and cloud-metadata addresses are refused at dial time), size-capped, and bounded by a per-request timeout. - Parameters passed to a nested or linked deployment are resolved from either inline
properties.parametersor an external parameters file referenced byproperties.parametersLink(resolved through the same local/remote loader astemplateLink).
Usage-based resources
Usage values are matched to ARM resources by address. An ARM resource's address is its type followed by its evaluated name, for example Microsoft.Web/sites/my-function-app or Microsoft.Storage/storageAccounts/mystorageacct. Use those addresses as keys in infracost-usage.yml; usage values written against a Terraform address (azurerm_linux_function_app.my_app) do not carry over.
Limitations
Template Specs
Template Specs (Microsoft.Resources/templateSpecs) are not expanded. Resources defined inside a Template Spec are not priced.
Tagging policies
Tags declared on a Microsoft.Resources/resourceGroups resource within the scanned template (common in subscription-scope deployments) are inherited by the resources deployed into that resource group, mirroring how provider default_tags work in Terraform — a resource's own tags win on conflict. This means resource-group-level tagging that lives in your templates is visible to tagging policies.
As with CloudFormation, Infracost tagging policies flag resources whose tag values are invalid, but do not flag missing tags on ARM resources. Tags applied entirely outside the template — at the subscription level, on pre-existing resource groups, or via Azure Policy — are invisible to static analysis, so missing tags are not checked in order to avoid false positives.