Business Challenge
Post #27 argued that Terraform's plan is its strongest feature, because a routine-looking apply can contain a delete. Post #28 argued the same about a deployment stack update. Both posts pointed here.
Azure's answer is the what-if operation: it predicts the changes if the specified Bicep file is deployed and doesn't make any changes to existing resources. It works at all four scopes from post #24 — resource group, subscription, management group and tenant — and, as of a page dated 12 August 2026, for deployment stacks as well.
That last point deserves stating plainly, because I got it wrong yesterday. Post #28 said what-if was not supported for stacks, quoting the known issues page, which says exactly that and still did this morning. It is stale. A separate, newer, more specific page documents stack what-if in full. Post #28 has been corrected, and the episode is a useful reminder that a claim can be correctly sourced and still wrong — vendor documentation is not one consistent document, and the newer, more specific page is the one to believe.
With that settled, the interesting question is not whether Azure has a preview. It is how much to trust one. What-if is a prediction made without running anything, and the documentation is unusually candid about where the prediction breaks down:
- Five categories of expression it cannot evaluate at all, reported as-is rather than resolved.
- A note that some properties are incorrectly reported as deleted, which the output itself calls noise.
- Three limits at which it stops expanding and marks everything remaining
Ignore— a word that reads like "nothing happens here" and means "this was not analysed".
The failure mode is not what-if being wrong. It is a reader treating a clean what-if as proof that a deployment is safe, when the risky part was a module that short-circuited, a template spec that was never expanded, or a resource beyond the 500-nested-template ceiling. Knowing the blind spots is what turns the output into evidence.
Architecture
Preflight is a setting on the preview, not a separate tool
Preflight validation is where Resource Manager asks the resource providers whether the thing you described could actually be created — availability, quota, naming, permissions. It is exposed as a validation level on the what-if command:
| Level | What it checks | Cost of choosing it |
|---|---|---|
Provider | The default. Full validation: template syntax, resource definitions, dependencies, and permission checks confirming you can deploy everything in the template. | The slowest and the only one that answers "would this work". |
ProviderNoRbac | The same validation, but checks only read permission on each resource rather than full deployment permission. | Lets someone preview without deploy rights — useful for review, and it stops telling you whether the pipeline identity could actually run it. |
Template | Static validation only: syntax and structure, skipping preflight checks and permission checks. | Microsoft's own words: less thorough, potentially missing issues that could cause deployment failures. |
Worth noting for pipeline design: what-if has the same permission requirements as deploying — write access on the resources plus all operations on Microsoft.Resources/deployments. A preview is not a lower-privilege operation by default, which is exactly what ProviderNoRbac exists to change.
The seven change types, and the two that are not what they sound like
Five of the seven are self-explanatory: Create, Modify, NoChange, NoEffect for a read-only property the service ignores, and Delete — which only applies when using complete mode for JSON template deployment, so a Bicep incremental deployment will not show it.
Two carry more meaning than their names suggest.
Ignore is overloaded. Its ordinary meaning is that a resource exists but is not in the template, so it will not be touched. Its second meaning is that what-if gave up: when a nested-template expansion limit is reached, the remaining resources' change type is set to Ignore. Those two situations look identical in the output and mean opposite things — one is "nothing planned for this", the other is "not analysed".
Deploy is an admission of uncertainty. The resource will be redeployed and the properties may or may not change; the operation returns it when it doesn't have enough information to determine if any properties will change. It appears only under ResourceIdOnly, which is the trade that format makes: a shorter list, and no idea what is inside each item. FullResourcePayloads is the default and the one to keep.
What it cannot evaluate, and why the output is noisy
Five categories of expression are simply not evaluated during what-if, and appear in the output as-is:
- Nondeterministic functions such as
newGuid()andutcNow(). - Any reference to a secure parameter value.
- References to resources not deployed in the same template.
- References to resource properties not defined in the same template.
- Any resource function, such as
listKeys().
The consequence is stated directly: what-if can't resolve the reference function, so every time you set a property to a template expression that includes the reference function, what-if reports the property will change — because it compares a current value like true against an unresolved expression, and those never match.
There is a second source of false positives running the other way. Properties can be incorrectly reported as deleted when they aren't in the Bicep file, but are automatically set during deployment as default values. So a what-if output showing a dozen deletions may be showing a dozen defaults that will still be there afterwards. Microsoft calls this noise in its own output, and says these properties will be filtered out as the operation matures.
A resource short-circuits when its resource ID or API version can't be calculated outside the deployment context. Worse, short-circuiting of modules or nested deployment resources can also happen, resulting in all resources within the module being excluded from the what-if analysis results — an entire module missing from the preview. Recent tool versions surface a diagnostic when this happens; older ones do not, and the documentation is blunt that with an older CLI the issue still occurs — it just happens silently. If your what-if output looks suspiciously short, check the tool version before trusting it.
Where it stops expanding
Three ceilings, and everything past them becomes Ignore: 500 nested templates, 800 resource groups in a cross-resource-group deployment, and 5 minutes spent expanding. The second is the same 800 that bounds a resource group's deployment history — 800 − 800 = 0 difference between the two ceilings, which is a coincidence of Resource Manager's favourite number rather than a shared mechanism, but a handy way to remember both.
Separately and more sharply: the evaluation and expansion of templateLink aren't supported, so resources deployed through template specs never appear in what-if results at all. An estate that has standardised on template specs — which post #24 noted is the way to share templates with non-Bicep callers — has a preview with a permanent hole in it.
Stack what-if: the same prediction, stored as a resource
For a normal deployment, what-if is an operation: you run it, Azure returns the predicted changes, and nothing is stored afterward. For a stack it is a resource you create — a Microsoft.Resources/deploymentStacksWhatIfResults that you name, retrieve by ID, share and delete.
Three consequences of that design:
- It reports
Detachas well asDelete, which is the whole reason it matters for stacks — post #28'sactionOnUnmanagedecisions become visible before they are applied. - The stack does not have to exist. If it does not, what-if reports the stack itself as a new resource, so a first run can be previewed too.
- Results are resources, and they cost. Each one counts toward that scope's resource limits, and anything with a retention interval longer than
PT3Hisn't deleted automatically. The guidance is to keep retention at three hours or less and delete what you keep longer.
Stacks also get noise reduction, which is a genuinely clever answer to the false-positive problem above. When a stack is deployed or updated, Azure evaluates what-if at that moment and keeps the result as a baseline; later runs remove anything unchanged since that baseline as noise. The caveat is stated too — it doesn't remove every one — and it only applies to stacks deployed since the feature existed, so an old stack needs one update to establish its baseline.
Afterwards: history, and the error message that is not where you look
Deployment history answers the third question. Two operational facts matter more than the rest.
History is a budget. It is limited to 800 deployments per resource group, and as you near the limit, deployments are automatically deleted from the history. A pipeline deploying on every commit will silently lose its oldest records, so history is not an audit trail unless something else is copying it out.
The error is on the operation, not the deployment. Each deployment can include multiple operations, and when a deployment fails, the deployment operations include an error message. The deployment itself reports only that it failed; the useful text is one level down, reached by listing the operations and filtering to the Failed state. And every deployment carries a correlation ID, which is what support asks for — capturing it in pipeline logs costs nothing and saves a round trip later.
Why This Architecture Holds Up
A preview is only a control if something depends on it
The strongest way to use what-if is the one that removes the choice: --confirm-with-what-if shows the changes and prompts before continuing, so the preview and the approval are a single step. In automation the equivalent is capturing the result with --no-pretty-print for a JSON object and gating the apply on it.
Either shape beats running what-if as a separate advisory step, because a separate step is one a tired engineer skips. This is the same argument post #27 made about automatic approval and post #17 made about policy effects: the control has to be in the path, not beside it.
Calibrate the output before relying on it
Given the blind spots, a clean what-if means less than it appears to on some templates and almost exactly what it says on others. The difference is knowable in advance:
- A template full of
reference(),listKeys()andutcNow()will report changes that are not changes, every run. That is noise you learn to read past — and reading past noise is how a real change gets missed. - A template using template specs has resources that never appear at all.
- A deeply nested composition can hit the expansion ceilings and mark the remainder
Ignore.
None of these argue against using what-if. They argue for knowing which of them applies to your template, so that a clean result means what you think it means.
History is evidence, and it expires
The 800-deployment ceiling makes deployment history a rolling window rather than a record. For anything that needs a real audit trail — who deployed what, when, and what it changed — the durable copy has to live in the pipeline, not in Resource Manager. Correlation IDs and failed-operation messages are worth extracting at the time, because the deployment they belong to will eventually be deleted to make room.
Key Architecture Decisions
The shape that works
| Decision | What to do | Why |
|---|---|---|
| Wiring in the preview | --confirm-with-what-if interactively; capture and gate on it in pipelines |
A preview beside the deployment is a step people skip; one in the path is a control. |
| Validation level | Leave it at Provider; use ProviderNoRbac for reviewers without deploy rights |
Template skips preflight and permission checks, so it misses the failures worth catching. |
| Result format | Keep FullResourcePayloads |
ResourceIdOnly returns Deploy, which explicitly means it does not know what will change. |
| Reading the output | Treat Ignore as two different findings, and check the tool version |
Ignore also means an expansion limit was hit, and older CLI versions short-circuit silently. |
| Template specs | Know that what-if cannot see them | templateLink is not expanded, so those resources never appear in the result. |
| Stack previews | Run stack what-if before any update whose actionOnUnmanage deletes |
It reports Detach and Delete, which is the only way to see those before they happen. |
| Stack what-if results | Retention PT3H or less, and delete anything kept longer |
They are real resources counting against the scope's limits and are not cleaned up for you. |
| Old stacks | Deploy or update once to establish the noise-reduction baseline | Noise filtering compares against a baseline recorded at deploy time; without one, results stay noisy. |
| History | Extract correlation IDs and failed-operation messages into pipeline logs | History is capped at 800 per resource group and older entries are deleted automatically. |
Closing Thought
What-if is a better tool than its reputation, and the reason it has that reputation is visible in its own documentation: it tells you a property will change when it will not, it cannot see inside a template spec, and it marks an unanalysed resource with the same word it uses for one that will not be touched. Anyone who has been burned by a noisy diff learns to skim it, and skimming a diff is how the one real change gets past.
The remedy is not to trust it more or less, but to know its shape. Every one of the blind spots above is documented, deterministic and predictable from the template in front of you. A template without reference(), without template specs and without deep nesting produces a what-if you can take almost literally. A template with all three produces one that needs interpretation. Those are different artefacts, and treating them the same is the actual mistake.
Which is also the lesson from correcting post #28 this morning. The claim was quoted accurately from a Microsoft page that still says it. It was still wrong, because a newer page said otherwise and nothing reconciles the two. Sourcing tells you where a statement came from. It does not tell you that it is true, and the only defence is to keep reading past the first page that agrees with you.
#30 steps back up to the top: landing zones and the Cloud Adoption Framework accelerator — what the reference architecture actually assumes, which of its decisions are hard to reverse, and how much of the last twenty-nine posts it makes for you.
Comments