Since I started using ARM templates, and later Bicep, for infrastructure as code more than 5 years ago, there has been a filed issue for being able to do "what-if" deployments without requiring write permissions.
For people with previous experience with Terraform (including myself), being able to do a plan or a form of dry-run of a deployment in a PR before merging to the main branch is often a desired pattern. This also applies to developers running their dry-runs locally. To be able to do this securely, it's definitely a requirement that the PR branch or local developer environment does not actually have write access to your production deployment environment.
So, after 5+ years of not much development, I recently learned in the latest Bicep community call that there actually was a recent feature released to support this. The only "official" news I've seen of this is the ambiguous changelog entry from Azure PowerShell:
- Added ValidationLevel Parameter to WhatIf and Validate cmdlets for deployments
For Az.Resources 7.10, if we dig into the documentation for the new parameter, we see:
-ValidationLevel
Sets the validation level for validate/what-if. ValidationLevel can be Template (Skips provider validation), Provider (Performs full validation), or ProviderNoRbac (Performs full validation using RBAC read checks instead of RBAC write checks for provider validation).
Since there are few examples of using this and it's not very well documented elsewhere in the what-if documentation regarding what permissions are needed and such - let's see how it works!
Note
As of July 3rd, 2025, it's not released for Azure CLI (will be released in the upcoming 2.76 according to MS).
The following resources will be set up in this demo:
Azure:
- A resource group (for demo purposes, to scope the role assignments used)
- Two user-assigned managed identities - one with only Reader permissions and one with Contributor permissions
- A Custom RBAC role that is similar to the Reader role but also has
deployments/whatIf/action
anddeployments/validate/action
.
GitHub:
- A PR environment with environment secrets for the PR user-assigned identity
- A main environment with environment secrets for the deploy user-assigned identity, including branch protection to only allow access from the main branch.
---
title: Demo Environment
---
flowchart LR;
subgraph Azure;
Reader["PR Managed Identity"]--Custom What-if role-->RG["Azure Resource Group"];
Deployer["Main Managed Identity"]--Contributor Role-->RG;
end;
subgraph GitHub;
subgraph GitHubPR["PR Environment"];
prSecrets["Environment Secrets"];
Reader-. Credentials .->prSecrets;
end;
subgraph GitHubMain["Main Environment"];
mainBranch["Branch Protection: Main"];
mainSecrets["Environment Secrets"];
Deployer-. Credentials .->mainSecrets;
end;
GitHubPR ~~~ GitHubMain;
end;
After this is set up, two separate workflows referring to the two environments will be used:
- pr-validate-yaml: triggers upon pull requests and runs a what-if resource group deployment against the Azure resource group - using the dedicated managed identity which does not have write permissions
- main-deploy.yaml: triggers upon push to the main branch and runs a resource group deployment against the Azure resource group - using the other managed identity which has write permissions
These workflows use the Azure/login and Azure/bicep-deploy actions.
- An Azure Subscription with Owner permissions
- PowerShell 7.x
- Azure PowerShell (Az) version 13.4.0 or newer (requires Az.Resources 7.10 or newer)!
To be able to set this up, you need to fork this repository, clone it, and do the following steps from your own fork:
To set up the environment in your Azure subscription, open a terminal locally and run to deploy the prereqs Bicep file:
Connect-AzAccount # Select the correct subscription after logging in
# Configure variables
$ghUserName = '<your GitHub username>'
# Deploy locally:
$deploy = New-AzSubscriptionDeployment -Name "demo-whatif-env" -Location norwayeast `
-TemplateFile ./bicep/prereqs/main.bicep -ghUserName $ghUserName
Keep the terminal open after running the command to ensure we can use the outputs from the $deploy
variable:
$deploy.Outputs | ConvertTo-Json
{
"subscriptionId": {
"Type": "String",
"Value": "<sub id>"
},
"tenantId": {
"Type": "String",
"Value": "<tenant id>"
},
"deployerClientId": {
"Type": "String",
"Value": "<client id>"
},
"prClientId": {
"Type": "String",
"Value": "<client id>"
}
}
-
Create two environments with the following names:
Azure
Azure-PR
-
Create two repo level secrets with the values for your Azure subscription and tenant:
AZURE_SUBSCRIPTION_ID
AZURE_TENANT_ID
-
For both environments, create two environment level secrets with the values for the two user-assigned managed identities:
AZURE_CLIENT_ID
Tip
For steps 2 and 3, you can run the following with GitHub CLI if you have it installed and set up:
# Set repo level secrets - replace with your values!
gh secret set AZURE_SUBSCRIPTION_ID --body $deploy.Outputs.subscriptionId.Value
gh secret set AZURE_TENANT_ID --body $deploy.Outputs.tenantId.Value
# Set environment level secrets - replace values!
gh secret set AZURE_CLIENT_ID --body $deploy.Outputs.deployerClientId.Value --env Azure
gh secret set AZURE_CLIENT_ID --body $deploy.Outputs.prClientId.Value --env Azure-PR
- For the
Azure
environment:- Select Deployment branches and tags and choose Selected branches and tags
- Only allow
main
with Ref type branch
This will ensure that we can only run actual deployments from the main
🔒
- Create a new branch and edit the
allowedoutboundaddresses
parameter value in the demo Bicep file - Save the edits, commit, and push to a new remote branch
- Create a pull request from the newly created branch to the
main
branch - Verify that the 'Azure what-if' workflow is running against the PR
You can also run this workflow manually using a workflow dispatch run.
Status check:
Example output:
Note
Note that when running the 'Azure What-If' workflow, it runs in the Azure-PR
environment, which uses a managed identity that does not have write permissions.
- Merge the pull request (or push a Bicep change to
main
) - Verify that the 'Azure Deploy' workflow is running against the latest commit on the
main
branch
You can also run this workflow manually using a workflow dispatch run.
Note
Note that when running the 'Azure Deploy' workflow, it runs in the 'Azure' environment, which uses the managed identity with Contributor permissions.
Delete the resource group, including the managed identities, their credentials, and other created resources by running:
Remove-AzResourceGroup -Name "demo-whatif-gh-rg"
-
There are still other fundamental issues with what-if (noise, resource provider support, nested modules limitations) that need improvements before a Bicep what-if is really comparable to a Terraform Plan. Kudos to the Bicep team for pushing and doing a lot of improvements in this area!
-
There might be more improvements coming soon to do more secure PR-level validations with Bicep snapshots. See a demo repo here: https://github.com/anthony-c-martin/bicep-snapshot-demo.
-
If you want to simulate the same what-if behavior when running locally or something else than Azure/bicep-deploy, you will need to add the
-ValidationLevel ProviderNoRbac
argument to the deployment cmdlet (see docs forNew-AzResourceGroupDeployment
).