1. Define the change contract
In a fictional funds-service migration, the project needs a private network, storage and machines prepared for batch processing. Before writing code, define the destination, owners and observable outcome: which resources should exist, what configuration should they have and how will application operation be demonstrated? Retain template version, parameters, dependencies and execution identity. A branch named release does not identify immutable content. Analysis approved for one revision may not represent another revision or parameter set. The linter detects static issues under configured rules; it does not establish destination permissions or application capacity. If the team changes the candidate after approval, it must reconcile evidence with what will execute. The aim is to explain the decision’s exact scope and the risk still unproven to the change committee.
2. Separate scope, location and identity
Imagine the platform team creates the application group from a subscription deployment. Resources belonging to that group are deployed through a module with that scope, and the identity needs destination access. Deployment metadata location does not require every resource to reside in that region. If the same deployment name is already registered at another location, correct the name or reuse the previous location instead of redesigning the application. Referencing an account with existing does not create it either: check name, subscription and group when investigating NotFound. In cross-team review, request the full resource identifier and confirm its owner. Identical names in different destinations are particularly dangerous when test and production share naming conventions.
3. Read predictions with their gaps
What-if helps review differences before execution. A Modify caused by an unresolved expression needs investigation; an empty list accompanied by short-circuiting does not establish module stability. Keep diagnostics with the summary, identify external components that were not expanded and close missing analysis. Also check the validation level: in a compatible CLI, ProviderNoRbac does not establish write authorization and Template has static scope. The example team reviews a private connection before the window, but APS changes DNS afterward. Even with the same commit, the destination changed. Current impact needs review. For deployment stacks, consulted documentation disagrees about what-if availability. Confirm support in the environment and client being used before relying on it; treat supported results as predictions with limitations rather than a universal guarantee.
4. Declare state and real dependencies
Incremental distinguishes omitted resources from redeclared resources. Do not interpret the template as a patch that automatically preserves every absent property. Review nondefault values, API specifics and dependency effects. In a shared network, an apparently secondary property can affect another application. Conditions also need explicit intent: making the parent optional does not automatically make separately declared children optional. Guard references that would be invalid when the resource does not exist. Use symbolic references to express real dependencies and avoid serializing every module merely to document architecture. If concurrent runs share module deployment names at the same scope, outputs can cross over. Correct those deployment identities; this does not replace concurrency control when two runs actually change the same resource.
5. Decide how to handle drift
APS may have applied an authorized hotfix during an incident. Finding a difference between code and reality does not automatically determine which side is correct. Link drift to the incident, confirm the adjustment’s validity and expiry, and decide whether to update the baseline or revert the change. For settings inside supported machines, distinguish Audit, Apply and Monitor, and Apply and Autocorrect: observing a divergence does not prove correction. Introduce autocorrection with a tested package, appropriate scope and an operational decision about exceptions. In Azure Policy remediation, check the assignment managed identity’s roles. The code author’s privileges are not that identity’s privileges. Close the work with a later state observation, retaining configuration evidence and an exception owner where an exception remains necessary.
6. Handle lifecycle exit
A migration does not end when resources disappear from the template. For each old resource, record retention, transfer or deletion, ownership and expected cost. A stack using detachAll retains resources; a successful run can leave a temporary VM consuming budget. In this lesson’s case, an archive must remain and the VM may be deleted after dependency checks. Do not use deletion of the entire group as a shortcut: include all its contents in impact analysis. If managed inventory is out of sync, reconcile it before considering bypass. Also review protection at parent-group scope; denyDelete on stack resources is insufficient to assume the entire group is protected. FINOPS and RUN need observed final state, not merely a successful command message.
7. Plan recovery and service retirement
ARM on-error rollback has specific semantics: it redeploys earlier history in complete mode with earlier parameters and does not undo data migrations. In a shared group, a partial historical template needs particular attention. Define application, infrastructure and data recovery with compatibility tests and observable criteria. A file in Git does not establish that the whole system can recover within the required time. Also consider tooling lifecycle. Azure Deployment Environments documentation announces retirement on February 22, 2027. Understanding existing environments and project environment types remains relevant, but a multiyear proposal needs continuity assessment. Inventory definitions, permissions, subscriptions and automation dependencies before choosing an alternative; do not assume automatic migration merely because a catalog concept looks similar.
8. Exercise the decision and prepare RUN
Run the Python model with fictional data. Its first function compares candidate identity, the declared snapshot and completeness of expected modules. Change a parameter or mark the network as unanalyzed: the evidence stops meeting the exercise rule. The second function requires observed state and ownership consistent with the retain-or-delete decision. Try marking the VM as still present after claimed deletion. The model does not query Azure, authenticate inputs or calculate Bicep changes; it merely makes a review rule over supplied records inspectable. In real handover, provide inventory, configuration, pending decisions, recovery, owners and costs. Summarize the reasoning in four questions: which candidate, which destination, which analysis was completed and which state was actually observed?
"""Original fictional review model. No Azure calls or deployment simulation."""
from copy import deepcopy
def preview_matches(candidate, approval, current_snapshot, expected_modules):
# This checks supplied evidence only. A caller must obtain trustworthy inputs.
identity = ('template', 'parameters', 'scope', 'snapshot')
if any(candidate.get(k)!= approval.get(k) or not candidate.get(k) for k in identity):
return False
if candidate['snapshot']!= current_snapshot:
return False
modules = candidate.get('modules', {})
return (bool(expected_modules) and set(modules) == set(expected_modules)
and all(value == 'complete' for value in modules.values))
def handover_complete(resources):
# Fictional handover policy; 'detached' alone is deliberately insufficient.
if not resources or len({r['id'] for r in resources})!= len(resources):
return False
for r in resources:
if r['decision'] == 'retain':
if r['observed']!= 'present' or not r['owner'] or not r['cost_owner']:
return False
elif r['decision'] == 'delete':
if r['observed']!= 'absent' or not r['dependencies_checked'] or r['retention_required']:
return False
else:
return False
return True
candidate = dict(template='digest-A', parameters='digest-P', scope='subscription-X/group-Y',
snapshot='snapshot-10', modules={'network': 'complete', 'app': 'complete'})
approval = {k: candidate[k] for k in ('template', 'parameters', 'scope', 'snapshot')}
expected = {'network', 'app'}
assert preview_matches(candidate, approval, 'snapshot-10', expected)
for key in ('template', 'parameters', 'scope', 'snapshot'):
changed = deepcopy(candidate)
changed[key] = 'changed'
assert not preview_matches(changed, approval, 'snapshot-10', expected)
assert not preview_matches(candidate, approval, 'snapshot-11', expected)
for state in ('short-circuited', 'unsupported', 'unknown'):
changed = deepcopy(candidate)
changed['modules']['network'] = state
assert not preview_matches(changed, approval, 'snapshot-10', expected)
changed = deepcopy(candidate)
del changed['modules']['network']
assert not preview_matches(changed, approval, 'snapshot-10', expected)
changed = deepcopy(candidate)
changed['modules']['unexpected'] = 'complete'
assert not preview_matches(changed, approval, 'snapshot-10', expected)
assert not preview_matches(candidate, approval, 'snapshot-10', set)
resources = [dict(id='archive', decision='retain', observed='present', owner='APS',
cost_owner='Funds', dependencies_checked=True, retention_required=True),
dict(id='temp-vm', decision='delete', observed='absent', owner='APS',
cost_owner='Migration', dependencies_checked=True, retention_required=False)]
assert handover_complete(resources)
for key, value in (('owner', ''), ('cost_owner', ''), ('observed', 'absent')):
changed = deepcopy(resources)
changed[0][key] = value
assert not handover_complete(changed)
for key, value in (('observed', 'present'), ('dependencies_checked', False), ('retention_required', True)):
changed = deepcopy(resources)
changed[1][key] = value
assert not handover_complete(changed)
changed = deepcopy(resources)
changed[1]['decision'] = 'detached'
assert not handover_complete(changed)
assert not handover_complete([])
assert not handover_complete(resources + [deepcopy(resources[0])])
print('22 fictional IaC evidence checks passed')
A DNS change after preview reopens connectivity review; at closure, a detached VM still present prevents declaring its decommissioning complete.
Common pitfalls
Confusing absent analysis with NoChange; approving different inputs; treating incremental as a patch; confusing detach with delete; assuming resource rollback recovers data.
Related topics: Change governance and risk · Configuration and drift management · Decommissioning and FINOPS · Resilience and data recovery
Bind each decision to the actual candidate and destination. Close analysis gaps and confirm final state, including retained resources, ownership and recovery.
Reference: Use the Bicep linter · AZ-400 objectives 2026-07-27