Provision Validation
July 16, 2026 · View on GitHub
Local provision validation is a client-side check that runs automatically before every azd provision deployment. It analyzes the compiled ARM template and a Bicep deployment snapshot to detect common issues — such as missing permissions.
When It Runs
The validation pipeline executes inside BicepProvider.Deploy(), after the Bicep module has been compiled and parameters resolved, but before the template is sent to Azure for server-side validation or deployment.
azd provision
│
├── Compile Bicep module → ARM template + parameters
├── ► Local provision validation ← runs here
│ ├── Parse ARM template (schema, contentVersion, resources)
│ ├── Generate Bicep snapshot (resolved resource graph)
│ ├── Analyze resources (derive properties)
│ └── Run registered check functions
├── Server-side ARM preflight (Azure ValidatePreflight API)
└── Deploy
The user can disable local provision validation by setting validation.provision to "off" in their azd user configuration:
azd config set validation.provision off
Note:
validation.provisioncontrols only azd's local (client-side) provision validation described in this document. The separateprovision.preflightconfig key controls the server-side ARM preflight call (azd config set provision.preflight off). The two are independent.
Bicep Snapshots
Local provision validation depends on the bicep snapshot command (available in modern Bicep CLI versions). The snapshot produces a fully resolved deployment graph: all template expressions are evaluated, conditions are applied, copy loops are expanded, and nested deployments are flattened into a single flat list of predicted resources.
Why Snapshots Instead of Manual Parsing
An ARM template as compiled by bicep build still contains unresolved expressions like "[parameters('location')]", conditional resources, and nested deployment modules. Manually parsing these would require reimplementing the ARM expression evaluator. The Bicep snapshot command does this natively and returns the final, concrete set of resources that would be deployed.
Advantages of snapshots over manual template parsing:
| Aspect | Manual Parsing | Bicep Snapshot |
|---|---|---|
| Expression resolution | Not possible (e.g. [concat(...)]) | Fully resolved |
| Nested deployments | Must recursively extract | Flattened automatically |
| Conditional resources | Cannot evaluate condition expressions | Excluded when false |
| Copy loops | Cannot expand copy blocks | Expanded to individual resources |
| Resource IDs | Symbolic names only | Resolved resource IDs |
How Snapshots Are Generated
-
Determine the parameters file. If the module path is a
.bicepparamfile, it is used directly. Otherwise, a temporary.bicepparamfile is generated from the resolved ARM parameters usinggenerateBicepParam(). This file is placed next to the source.bicepmodule so that relativeusingpaths resolve correctly. -
Build snapshot options. The deployment target (
infra.Deployment) provides scope information:- Subscription-scoped deployments →
--subscription-idand--locationflags. - Resource-group-scoped deployments →
--subscription-idand--resource-groupflags.
- Subscription-scoped deployments →
-
Invoke
bicep snapshot. The Bicep CLI generates a<basename>.snapshot.jsonfile. azd reads it into memory and deletes the temporary file. -
Parse the snapshot. The JSON output contains a
predictedResourcesarray — each entry is a fully resolved resource with type, apiVersion, name, location, properties, and so on.
┌──────────────────────────┐
│ main.bicep │
│ + parameters │
└──────────┬───────────────┘
│
generateBicepParam()
│
▼
┌──────────────────────────┐
│ validation-*.bicepparam │ (temporary)
└──────────┬───────────────┘
│
bicep snapshot --subscription-id ... --resource-group ...
│
▼
┌──────────────────────────┐
│ validation-*.snapshot.json│
│ { │
│ "predictedResources": │
│ [ │
│ { type, name, ... } │
│ { type, name, ... } │
│ ] │
│ } │
└──────────────────────────┘
Check Pipeline
The validation system uses a pluggable pipeline of check functions. Each check receives a validationContext containing:
Console— for user interaction (prompts, messages).Props— derived properties from the resource analysis (e.g.HasRoleAssignments).ResourcesSnapshot— the raw JSON frombicep snapshot.SnapshotResources— the parsed[]armTemplateResourcelist from the snapshot.
Checks are registered via AddCheck() before calling validate(). They run in registration order. Each check is a ProvisionValidationCheck{RuleID, Fn}, where Fn returns:
nil(or an empty slice) — nothing to report (check passed).[]ProvisionValidationCheckResult, each with aSeverity(ProvisionValidationCheckWarningorProvisionValidationCheckError) and aMessage. A single check may emit multiple results.
Adding a New Check
To add a new validation check:
validator.AddCheck(ProvisionValidationCheck{
RuleID: "some_provider_problematic_resource",
Fn: func(ctx context.Context, valCtx *validationContext) ([]ProvisionValidationCheckResult, error) {
// Inspect valCtx.SnapshotResources, valCtx.Props, etc.
var results []ProvisionValidationCheckResult
for _, res := range valCtx.SnapshotResources {
if strings.EqualFold(res.Type, "Microsoft.SomeProvider/problematicResource") {
results = append(results, ProvisionValidationCheckResult{
Severity: ProvisionValidationCheckWarning,
Message: "This resource type requires additional configuration.",
})
}
}
return results, nil // empty slice = nothing to report
},
})
Built-in Checks
| Check | What It Does | Severity |
|---|---|---|
| Role assignment permissions | Detects Microsoft.Authorization/roleAssignments in the snapshot and verifies the current principal has roleAssignments/write permission on the subscription. | Warning |
UX Presentation
Results are displayed using the ProvisionValidationReport UX component (pkg/output/ux/provision_validation_report.go), which implements the standard UxItem interface. The report groups and orders findings: all warnings appear first, followed by all errors. Each entry is prefixed with the standard azd status icons.
Scenarios
Scenario 1: No Issues Found
All registered checks pass. No output is printed from the validation step. The deployment proceeds directly to server-side validation and then Azure deployment.
Validating deployment (✓) Done:
Creating/Updating resources ...
Scenario 2: Warnings Only
One or more checks return warnings but no errors. The warnings are displayed and the user is prompted to continue. The default selection is Yes — pressing Enter continues the deployment.
Validating deployment
(!) Warning: the current principal (abc-123) does not have permission
to create role assignments (Microsoft.Authorization/roleAssignments/write)
on subscription sub-456. The deployment includes role assignments and
will fail without this permission.
? Proceed with provisioning despite the warnings above? (Y/n)
If the user confirms (or accepts the default), deployment proceeds normally. If the user declines, the operation is canceled with a zero exit code (an intentional cancel, not a failure).
Scenario 3: Errors Only
One or more checks return errors. The errors are displayed and the deployment is immediately canceled — the user is not prompted. The CLI exits with a zero exit code.
Validating deployment
(x) Failed: critical configuration error detected in template
Validation detected errors, provisioning canceled.
Note: the exit code is zero because the provision validation successfully detected problems and intentionally canceled the deployment. This is not an unexpected internal failure — the CLI completed its task (validating and reporting errors) without encountering any execution errors itself.
Scenario 4: Warnings and Errors
When the report contains both warnings and errors, warnings are listed first and errors second. Because errors are present the deployment is canceled immediately — the warning prompt is skipped.
Validating deployment
(!) Warning: the current principal does not have permission to create
role assignments on this subscription.
(x) Failed: required parameter 'storageAccountName' is missing from
the deployment.
Validation detected errors, provisioning canceled.
Scenario 5: Check Function Returns an Error
If a check function itself fails (returns a Go error rather than []ProvisionValidationCheckResult), this is treated as an infrastructure failure. The CLI reports it as a hard error and exits with a non-zero code. This is distinct from a check returning a result with ProvisionValidationCheckError severity — that case means "we successfully detected a problem in the template", while an error return means "something went wrong while trying to run the check".
ERROR: local provision validation failed: validation check failed: <underlying error>
Exit Code Behavior
The exit code distinguishes between successful operation (the CLI did what it was supposed to do) and internal failure (the CLI could not complete its task).
Provision validation detecting errors and canceling provisioning is a successful outcome — the CLI performed the validation and correctly prevented a bad deployment. Only failures in the validation machinery itself produce a non-zero exit code.
| Outcome | Exit Code | Rationale |
|---|---|---|
| No issues | 0 | Deployment proceeds and succeeds. |
| Warnings only, user continues | 0 | User acknowledged warnings; deployment proceeds. |
| Warnings only, user declines | 0 | User chose to cancel; intentional, not a failure. |
| Errors detected | 0 | Validation successfully detected problems and canceled provisioning. |
| Check function error | 1 | Internal failure running a check (the validate function returned a non-nil error). |
File Layout
pkg/
├── infra/provisioning/bicep/
│ ├── provision_validation.go # Core pipeline, ARM types, parseTemplate, analyzeResources
│ ├── provision_validation_test.go # Unit tests for parsing, analysis, check pipeline
│ ├── role_assignment_check_test.go # Tests for the role assignment check
│ ├── generate_bicep_param_test.go # Tests for .bicepparam generation
│ └── bicep_provider.go # validateProvision() integration, checkRoleAssignmentPermissions
├── infra/provisioning/
│ └── validation_dispatcher.go # ValidationCheckDispatcher interface (DI decoupling)
├── output/ux/
│ ├── provision_validation_report.go # ProvisionValidationReport UxItem
│ └── provision_validation_report_test.go # Tests for ProvisionValidationReport
└── tools/bicep/
└── bicep.go # Snapshot() method, SnapshotOptions builder
Extension-Provided Checks
Extensions can contribute validation checks to the local provision validation
pipeline using the validation-provider capability. This allows extensions to
inspect the Bicep deployment data (ARM template, snapshot, parameters, location) and
return additional warnings or errors that are merged into the validation report.
How It Works
- The extension declares
validation-providerin itsextension.yamlcapabilities. - During startup, the extension registers one or more checks with a
check_type(e.g.,"arm-provision") and a stablerule_id. - When
BicepProvider.validateProvision()runs, after the built-in checks complete, it dispatches to all extension-registered checks matchingcheck_type: "arm-provision". - Each extension check receives a context map with:
resources_snapshot— Bicep snapshot JSON (predictedResources)predicted_resources— Parsed resource array from the snapshotarm_template— Compiled ARM template JSONarm_parameters— Resolved ARM parameters JSONenv_location— Azure location string
- The extension returns
ValidationCheckResultitems (severity, message, suggestion, links) which are appended to the validation report.
Extension Code Example
// In your extension's listen command:
host := azdext.NewExtensionHost(azdClient).
WithValidationCheck(azdext.ValidationCheckRegistration{
CheckType: "arm-provision",
RuleID: "my_naming_rule",
Factory: func() azdext.ValidationCheckProvider {
return &MyNamingCheck{}
},
})
// The check implementation:
type MyNamingCheck struct{}
func (c *MyNamingCheck) Validate(
ctx context.Context,
valCtx *azdext.ValidationContext,
req *azdext.ValidationCheckRequest,
) (*azdext.ValidationCheckResponse, error) {
resources, err := valCtx.ParsePredictedResources()
if err != nil || len(resources) == 0 {
return &azdext.ValidationCheckResponse{}, nil
}
// Inspect resources, return results...
return &azdext.ValidationCheckResponse{
Results: results,
}, nil
}
Failure Handling
If an extension check returns an error (the Validate method fails), the error is
logged as a warning but does not block the deployment. Only the extension's
results are omitted. Built-in check failures still follow the standard error behavior
described above.
Future Check Types
The check_type field is designed for extensibility. Two check types are
currently supported — "arm-provision" (Bicep-only, ARM-rich context) and
"provision" (provider-agnostic, lean context; see below). Future check types
(e.g., "project-config", "auth") can be added without changing the protocol.
Each check type defines its own context keys.
Provider-Agnostic Provision Checks ("provision")
The "arm-provision" dispatch above only runs for Bicep-provisioned
deployments, because its context is built from the compiled ARM template and
Bicep snapshot. Extensions that ship their own provisioning provider (e.g.
microsoft.foundry, the demo provider) — as well as the core Terraform
provider — never route through BicepProvider, so an "arm-provision" check
registered by such an extension would be dead code.
To let a check run before provisioning regardless of the provider, register
it under the "provision" check type instead. The provider-agnostic dispatch is
exposed as provisioning.Manager.RunProvisionValidation() and is invoked once
per azd provision / azd up by the command layer, before the layer
execution graph runs — not inside Manager.Deploy()/Manager.Preview().
Multi-layer provisioning (infra.layers[]) runs each layer's Deploy through
its own Manager concurrently, so dispatching per-Deploy would fire the
checks — and any warning confirmation prompt — once per layer with an identical,
env-scoped context. Hoisting the call to the action guarantees a single
dispatch and a single prompt:
azd provision
│
├── ► Provider-agnostic "provision" validation ← runs once, all providers
│ (provisionLayersGraph / up graph, before the layer graph)
│
├── per-layer graph (may run layers concurrently)
│ └── provider.Deploy()
│ └── (Bicep only) "arm-provision" validation
└── ...
On cancel (an error-severity finding, or a declined warning),
RunProvisionValidation returns provisioning.ErrProvisionValidationCanceled;
the action passes it through wrapProvisionError, which emits the shared
"Provisioning was cancelled." UX and maps it to internal.ErrAbortedByUser
(exit code 0). Any other error (a failure of the confirmation prompt itself) is
returned as-is, while a passing or skipped run returns nil. Both the deploy
and --preview paths route through the same single call.
Lean, Provider-Agnostic Context
Because Terraform and extension providers do not produce an ARM template, the
"provision" context is intentionally lean and carries no template,
parameters, or resource snapshot. Each check receives:
env_name— the azd environment namesubscription_id— the target Azure subscription idenv_location— the Azure location stringresource_group— the target resource group name (empty for subscription-scoped)target_scope—"subscription"or"resourceGroup"
Typed accessors (EnvName(), SubscriptionID(), EnvLocation(),
ResourceGroup(), TargetScope()) are available on azdext.ValidationContext.
Shared Behavior
The "provision" dispatch reuses the same machinery as "arm-provision":
parallel dispatch, the uniform provision validation report, severity/cancel
semantics (WARNING prompts to continue; ERROR cancels with exit code 0), an
equivalent 60s dispatch timeout (provisionValidationTimeout in
provision_validation.go, mirroring the Bicep path's
extensionValidationTimeout), and the validation.provision config gate (off
disables both dispatch sites). Registration still requires the
validation-provider capability.
Extension Code Example
host := azdext.NewExtensionHost(azdClient).
WithValidationCheck(azdext.ValidationCheckRegistration{
CheckType: azdext.ValidationCheckTypeProvision, // "provision"
RuleID: "resource_group_location",
Factory: func() azdext.ValidationCheckProvider {
return &MyLocationCheck{}
},
})
Registering Both Check Types
An extension can register both check types at once — they are independent and
both fire when applicable. The microsoft.azd.demo extension does exactly this as
a reference:
demo_warningunder"arm-provision"— inspects the Bicep snapshot; runs only for Bicep and is skipped gracefully when no snapshot is available.demo_provision_warningunder"provision"— reads the lean provision context; runs before provisioning for every provider, including the demo provider.
Because registrations are keyed by check_type + rule_id, the same extension can
safely register a check under each type.