Skip to main content

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:

FeatureSupport
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.

note

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:

CategoryFunctionsSupport
Numericadd, sub, mul, div, mod, min, max, range, int, float
Logical & comparisonequals, less, lessOrEquals, greater, greaterOrEquals, if, and, or, not, bool, true, false, null
Stringconcat, 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 & objectarray, 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
DatedateTimeAdd, dateTimeFromEpoch, dateTimeToEpoch, utcNow
Networking (CIDR)cidrSubnet, cidrHost, parseCidr
CopycopyIndex
Parameters & variablesparameters, variables
Deployment & scoperesourceGroup, subscription, tenant, managementGroup, deployment, environment, pickZones
ResourceresourceId, 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.

note

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):

  1. The parameter's defaultValue in the template.
  2. A parameters file. Infracost auto-discovers a sibling parameters file next to the root template — <template>.parameters.json, <template>.params.json, or a bare parameters.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.
  3. Inline parameter overrides supplied to the scan.

Nested and linked templates

  • Nested templates (Microsoft.Resources/deployments with an inline template) are expanded and priced, in both outer- and inner-scope evaluation modes. This includes copy loops over deployments and copy loops declared inside an inner-scope nested template.
  • Linked templates referenced by a local relativePath or file:// URI are resolved and priced, provided the target stays within the scanned directory.
  • Linked templates referenced by an http/https URL whose file ships in the scanned directory are resolved from the local copy — no network access. This covers the ubiquitous _artifactsLocation staging pattern, including the _artifactsLocation default of [deployment().properties.templateLink.uri], where templates compose sibling paths with uri(...) even though the files live in the repo.
  • Genuinely-remote linked templates (an http/https templateLink.uri with 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.parameters or an external parameters file referenced by properties.parametersLink (resolved through the same local/remote loader as templateLink).

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.