Lab 04 — ARM and Bicep Deployment Operations
azure administrator series

ZIP SHA-256: 4483484c84d7819f33c58fc6d0aa9d16c11d6cb60d46e4e952e1370acb0ac231
Live-tested and cleaned up: Bicep baseline, storage-only ARM tag change, actual missing-parent deployment failure, recovery twice, export/decompile/rebuild and cleanup twice. Safety context/interruption tests use offline doubles. No application or connectivity test is claimed.
90–120 minutes · Azure CLI + PowerShell + Portal · AZ-104
Deploy a small, secure baseline, make a controlled ARM template change, diagnose a failed subnet deployment, and recover by correcting its Bicep parent reference. Then compare deployment-history export with current-resource export and review a decompiled template.
This is an infrastructure-deployment failure, not an application outage. There are no VMs, public IPs, workloads, private endpoints, or application data in this workshop.
The exercise maps to the AZ-104 objectives for interpreting, modifying, deploying, exporting, and converting ARM templates and Bicep files. It is independent training, not an official Microsoft exam lab. AZ-104 study guide.
The scenario
A platform administrator has deployed an empty virtual network and a locked-down storage account. A later subnet deployment fails, although the original resources still exist. Your job is to inspect the deployment evidence, explain why a valid template can fail in Azure, and deploy exactly the intended subnet without rebuilding the baseline.
Healthy baseline → ARM tag change → failed subnet deployment → corrected child deployment → export and review → cleanup.
Before you start
Use a disposable subscription or an explicitly approved training scope. This release was designed for Windows and PowerShell 7.5 or newer because private manifests use Windows ACLs and JSON date handling. Install Azure CLI, Bicep CLI, Az.Accounts 2.12.1 or newer, and Az.Resources 6.5.3 or newer from their official sources before starting. Scripts do not install or upgrade tools, register providers, grant permissions, or change policies.
Sign in separately with az login and Connect-AzAccount. Select the same tenant and subscription in both tools. The default subscription name is Visual Studio Enterprise Subscription; pass your own exact name with -SubscriptionName if appropriate. Successful sign-in alone is not enough: preflight checks context, effective permissions, provider registration, West Europe support, quota, subscription locks, inherited policy assignments, current retail pricing, and inherited Defender for Storage pricing. Review the private policy inventory before deployment. Azure validation remains authoritative for policies evaluated against the actual request.
The lab needs resource-group, deployment, VNet/subnet, and storage management permissions plus read access for inspection. Use existing authorized permissions; do not grant yourself a broad role just to make the exercise pass. Never remove a policy, lock, or deny assignment to force deployment.
Cost boundary: EUR20 total operational ceiling, with EUR5 reserved for cleanup and EUR15 for exercises. An empty unpeered VNet and empty Standard_LRS storage account are expected to incur negligible usage charges; EUR0.25 is a conservative planning allowance, not a billing guarantee or a billing cap. Retail pricing is refreshed before deployment; subscription-specific discounts and delayed billing cannot be inferred from it. No paid Defender plan is enabled. Stop new exercises after five hours and clean up before six. No budget alert or automatic cost-enforcement mechanism is created.
1. Inspect the package and prepare private state — 10 minutes
Extract the ZIP into a new folder. Verify its public SHA-256 first, then run the package verifier from the extracted folder. Read the scripts before running them. Keep the state folder outside any synchronized or published content directory.
pwsh
Set-Location '<extracted-lab-folder>'
./Verify-Lab04Package.ps1
./Initialize-Lab04StateDirectory.ps1 -Path 'C:\LabPrivate\Lab04' -Execute
$state = 'C:\LabPrivate\Lab04\state.json'
./Start-Az104Lab04.ps1 -Stage Preflight -StatePath $state
./Test-Lab04Safety.ps1 -StatePath $stateThe initializer creates a new private directory accessible only to the current user and SYSTEM. It does not weaken an existing directory's permissions. The private manifest records exact IDs, ownership tags, deployment names, phase, expiry, and pending operations. The safety script uses offline context doubles and a real local exclusive file lock; it does not test a different live tenant or intentionally interrupt Azure.
Every mutation requires -Execute. Running a stage without it describes the operation; it does not apply the template. Do not share state, tokens, raw exports, unsanitized logs, or screenshots containing subscription/account identifiers. The scripts never list storage keys or create SAS tokens.
2. Read Bicep and its compiled ARM JSON — 15 minutes
Open templates/baseline.bicep beside templates/baseline.json.
Concept | What to inspect |
Scope | targetScope = 'resourceGroup'; deployments target the recorded group. |
Parameters | Names and ownership tags are inputs; location defaults to West Europe. |
Variables | The storage-only ARM template has a training revision variable used in tags. |
Resources | The baseline declares a VNet and StorageV2 account with explicit API versions. |
Outputs | Resource IDs are returned privately; outputs are not security boundaries. |
Parent/child | A subnet belongs to a VNet; the child deployment uses an existing parent reference. |
az bicep build --file ./templates/baseline.bicep
az bicep build --file ./templates/subnet-fault.bicep
az bicep build --file ./templates/subnet-recovery.bicepA successful build proves local syntax and type checks, not that a referenced Azure resource exists. An existing declaration references a resource; it does not create that parent. In the compiled subnet template, inspect the full child resource type and the expression that combines the parent and child names.
All deployments use Incremental mode. Incremental does not mean “patch only the properties I happened to write.” Declared resources are reapplied; omitted properties can be reset by a resource provider. In particular, do not replay the empty-VNet baseline after creating the subnet. Recovery deploys only the child subnet. Deployment modes.
3. Deploy and verify the healthy baseline — 15 minutes
./Start-Az104Lab04.ps1 -Stage Deploy -StatePath $state
./Start-Az104Lab04.ps1 -Stage Deploy -StatePath $state -Execute -AcknowledgeCost
./Test-Az104Lab04.ps1 -StatePath $state -Phase BaselineSetup checks globally available storage naming and refuses to adopt an existing group. It creates one fresh tagged group, then uses Azure CLI validation, what-if, and an Incremental Bicep deployment. Private what-if evidence must show only the two intended creates. Stop if a policy, permission failure, unexpected change, or unfamiliar resource appears.
Expected baseline: VNet 10.44.0.0/16, zero subnets; empty Standard_LRS StorageV2 account; public network access disabled, anonymous blob access disabled, shared-key access disabled, HTTPS required, TLS 1.2, and no network allow rules. No data-plane calls are needed.
Portal checkpoint: open the recorded resource group, confirm its two resources, and inspect Deployments → baseline deployment → Inputs and Template. Inspect the VNet's address space/subnets and storage networking/configuration without retrieving keys.



4. Modify ARM JSON without broadening scope — 10 minutes
Read templates/storage-update.json. The explicit training change is the trainingRevision variable from the baseline value 01 to 02, applied as the storage tag TrainingRevision. The template includes only the storage account and retains its security properties. Compare it with the compiled baseline rather than deleting security properties to shorten the template.
./Update-Az104Lab04Arm.ps1 -StatePath $state
./Update-Az104Lab04Arm.ps1 -StatePath $state -Execute
./Test-Az104Lab04.ps1 -StatePath $state -Phase BaselineAzure CLI validates and previews the ARM template; PowerShell New-AzResourceGroupDeployment applies it. The script compares a canonical set of baseline properties before and after, excluding only the intended revision. Review the private what-if delta and stop on unrelated changes. This is a controlled comparison of declared invariants, not a claim that every server-maintained metadata field is byte-identical.
Portal checkpoint: storage account → Tags shows TrainingRevision = 02. The successful ARM deployment's template shows the same security settings. The VNet remains empty.

5. Submit the Azure-side failure — 10 minutes
For a spoiler-free attempt, read CHALLENGE.md before opening the fault template or solution. This is the only deliberately failing exercise; there is no separate compilation-error task.
./Set-Az104Lab04Fault.ps1 -StatePath $state
./Set-Az104Lab04Fault.ps1 -StatePath $state -Execute
./Test-Az104Lab04.ps1 -StatePath $state -Phase FaultThe fault template is valid Bicep. This one deliberately broken stage submits directly to Azure instead of using the normal remote validation/what-if gate, so you can inspect an actual service rejection. It references a nonexistent parent inside the same lab group. The fault is accepted as proved only when the actual response identifies the missing parent and an appropriate not-found error, and the healthy resource fingerprint remains unchanged. An authorization failure or generic deployment failure does not count.
Portal checkpoint: Deployments → failed deployment → Operation details. Inspect the inner error, target resource, and correlation context. Then inspect the resource group's Activity log for the failed operation. A deployment may fail before any child-resource operation is emitted; inspect the deployment error itself if the operations list is empty. Activity log events may arrive later.


CLI inspection, using names loaded locally from your private manifest:
$s = Get-Content $state -Raw | ConvertFrom-Json
az deployment group show -g $s.ResourceGroup -n $s.Deployments.Fault
az deployment operation group list -g $s.ResourceGroup -n $s.Deployments.Fault
az monitor activity-log list --resource-group $s.ResourceGroup --offset 1hThese commands can display resource IDs and account details. Inspect privately; do not paste their raw output into a public post.
6. Correct the parent and prove repeatable recovery — 15 minutes
Compare subnet-fault.bicep and subnet-recovery.bicep. Correct the parent reference, not the existing VNet. Keep the child name, prefix, and security intent unchanged. The supplied recovery JSON is compiled from the corrected Bicep file.
./Restore-Az104Lab04.ps1 -StatePath $state
./Restore-Az104Lab04.ps1 -StatePath $state -Execute
./Test-Az104Lab04.ps1 -StatePath $state -Phase Recovery
./Restore-Az104Lab04.ps1 -StatePath $state -Execute
./Test-Az104Lab04.ps1 -StatePath $state -Phase RecoveryCLI validation and what-if precede the PowerShell deployment. Expect exactly one snet-training subnet, prefix 10.44.1.0/24, and defaultOutboundAccess = false. Its full resource ID and configuration must remain the same after the second recovery. No network interfaces, private endpoints, delegations, peering, or other workload associations should appear.
Private-subnet configuration is not an application connectivity test. There is no workload to test and no explicit outbound connectivity in this lab. Portal checkpoint: corrected VNet → Subnets, then the successful recovery deployment and template.



7. Export, decompile, inspect, and rebuild — 15 minutes
./Export-Az104Lab04Templates.ps1 -StatePath $state
./Export-Az104Lab04Templates.ps1 -StatePath $state -ExecuteThe script creates a new private working directory for each export. It uses az deployment group export for baseline, ARM update, and recovery, and az group export for the current group. It decompiles the recovery deployment template with az bicep decompile, then rebuilds it with az bicep build.
Deployment-history export answers “What template was submitted for this deployment?” Current-resource export answers “What representation can Azure generate from these resources now?” They are not interchangeable: the baseline deployment still describes an empty VNet even though the current group now contains a subnet. Exports can include generated names, defaults, read-only properties, unsupported-resource warnings, and environment-specific values. Microsoft export guidance.
Inspect original and recompiled child type, name expressions, parameters, API version, address prefix, and default outbound setting. Read all warnings. Decompilation is a starting point for reviewed code, not lossless restoration of your original symbols, comments, or formatting. Do not redeploy an export blindly or claim byte-for-byte equivalence. Decompilation guidance.
Portal checkpoint: recovery deployment → Template, then resource group → Export template. Keep downloaded raw files private. Portal exports are evidence of a representation, not a substitute for source control.


8. Clean up and verify absence — 10 minutes
./Remove-Az104Lab04.ps1 -StatePath $state
./Remove-Az104Lab04.ps1 -StatePath $state -Execute
./Remove-Az104Lab04.ps1 -StatePath $state -Execute
./Export-Lab04Evidence.ps1 -StatePath $state -OutputPath './my-run-evidence.json' -ExecuteCleanup checks exact recorded IDs, ownership, associations, data-object inventories, and unexpected lab-scoped assignments before deleting anything. It removes the recorded VNet/subnet and storage account, then the empty group and its deployment history. These disposable resources are permanently deleted; Activity Log retention is separate. Repeated cleanup verifies absence rather than adopting or deleting something by name prefix.
If an unexpected resource, association, policy, role assignment, lock, or ownership change appears, stop. Do not remove unrelated objects to make cleanup pass. Investigate and obtain direction for any scope expansion.
Interrupted operations and limits
An exclusive manifest lock prevents concurrent stage runs. Never delete a held lock. A pending operation blocks further mutation: inspect the exact recorded deployment with az deployment group show and its operations, reconcile the actual resources and ownership, and retain the private evidence. Do not blindly clear Pending or rerun baseline. This release deliberately fails closed; uncertain-operation reconciliation is a manual operator procedure, not automatic recovery. If the operation is still running, wait for its actual terminal state before deciding. For a partially deployed run, a qualified operator must reconcile the manifest against the exact successful resources before using cleanup; never infer ownership from a prefix.
The package does not change Conditional Access, MFA, existing policies, licenses, role assignments, provider registration, or authentication settings. It does not prove workload connectivity or actual-phone behavior. See VALIDATION.md for what was genuinely tested and what remains outside scope.
Continue the series
Previous: Lab 03 — Governance Guardrails.
Next: Lab 05 — not published yet.
Use CHALLENGE.md, HINTS.md, SOLUTION.md, and KNOWLEDGE-CHECK.md to practice before reading the answers.
Challenge — A subnet deployment fails
The baseline VNet and storage account exist. A subsequent subnet deployment reports failure. The VNet remains empty; storage retains TrainingRevision 02 and its security configuration.
Identify the actual Azure error and affected target. Show that the healthy baseline did not change. Repair only the intended subnet deployment, without recreating the VNet or widening access. Prove a second recovery produces no duplicate, then compare deployment-history and current-resource exports. Clean up only recorded resources.
Success evidence: error diagnosis, unchanged baseline, one correctly configured private subnet, stable resource identity after repeat, reviewed export/decompile results, and verified cleanup. Do not call this a connectivity or application outage.
Knowledge check
What does a successful Bicep build prove—and what does it not prove?
Does an existing VNet declaration create a missing VNet?
Why use a storage-only ARM template for the tag change?
Does Incremental mode guarantee that omitted properties are preserved?
Why is replaying the empty-VNet baseline unsafe after recovery?
How do deployment-history and current-resource exports differ?
Why review and rebuild decompiled Bicep instead of redeploying it immediately?
What should happen when cleanup discovers an unrecorded resource or a pending deployment?
Progressive hints, the separate solution, knowledge-check answers and full validation report are included in the download. Try the challenge before opening the solution.
Lab 04 validation report
Core live validation performed on 24 September 2026 in West Europe. Results describe the disposable training deployment, not production suitability or workload connectivity.
Check | Result and evidence |
Context and preflight | CLI and PowerShell tenant/subscription contexts matched; permissions, providers, region, quota, locks, inherited policies and retail prices inspected. No grants, registrations, exemptions or upgrades performed. |
Baseline | Azure CLI Incremental Bicep deployment succeeded. Exactly two top-level resources; VNet 10.44.0.0/16 with no subnets; empty Standard_LRS storage with public network, anonymous blob and shared-key access disabled. |
ARM modification | PowerShell Incremental storage-only ARM deployment succeeded. TrainingRevision changed from 01 to 02; declared security and VNet invariants remained unchanged. What-if contained that effective tag change and provider NoEffect metadata; the out-of-template VNet was Ignore, not modified. |
Deliberate failure | Actual Azure response: outer DeploymentFailed, inner ResourceNotFound. The message identified the deliberately nonexistent VNet parent ending in -missing. The operation was Failed/NotFound. This was not an authorization error. |
Fault isolation | Healthy baseline fingerprint remained unchanged; no subnet was created. Activity Log later contained failed Create Deployment events; initial ingestion was delayed. |
Recovery | CLI validation and what-if followed by PowerShell deployment succeeded. Exactly one snet-training subnet, 10.44.1.0/24, defaultOutboundAccess false, no workload associations. Second recovery preserved the exact subnet ID and verified configuration. |
Export and conversion | Three deployment-history templates exported. Current-resource export represented seven resources: VNet, subnet, storage account and four implicit storage service children. No application data was created. Recovery template decompiled, inspected and rebuilt successfully. |
Conversion review | Child resource type, name expression, API version, prefix, default outbound setting and parameter semantics matched. Symbol/layout/parameter-order and type-case differences are not byte equivalence. The tool warned that decompilation is best effort and advertised a newer version; no upgrade was performed. Current export used provider-selected versions/defaults rather than reproducing the original authoring file. |
Safety guards | Wrong-context, mismatched-ID and pending-operation checks passed using offline doubles; concurrent-run check used a real exclusive local file lock. These are simulations, not a live wrong-tenant deployment or deliberate process crash. |
Cleanup | Exact recorded resources and resource group removed; no run-tagged active residuals. Repeated cleanup passed. Deployment history was removed with the empty group; Activity Log retention is separate. |
Implementation findings
The first run exposed a local reporting incompatibility with Az.Resources 6.5.3 after Azure had already completed the ARM update: the result object did not expose the assumed DeploymentId property. The pending-operation guard blocked continuation. The operator inspected the exact successful deployment, parameters, ownership and unchanged invariants before reconciling private state. The helper now records the known deployment name instead; a fresh isolated verification run successfully exercised the corrected ARM stage. This was a tooling correction, not another deliberately broken Azure scenario.
Two initially saved Portal screenshots contained unloaded panels and were rejected. Baseline and subnet captures were replaced during a separate isolated verification run using the same templates. Screenshots are genuine Portal captures; cropping and opaque privacy masks hide private identifiers without changing service outcomes. All raw captures remain private. The cover alone is AI-generated conceptual artwork.
Toolchain and limits
Tested with PowerShell 7.6.6, Azure CLI 2.88.0, Bicep 0.46.1, Az.Accounts 2.12.1 and Az.Resources 6.5.3. Final release gates include Bicep builds, PowerShell AST checks, zero ScriptAnalyzer error-severity findings, ZIP extraction and every packaged file's SHA-256 verification. Publication checks and the public ZIP hash are recorded separately in the release record to avoid a self-referencing ZIP checksum.
No VMs, public IPs, private endpoints, data, licenses, keys or SAS tokens were created. No authentication policies or role assignments were changed. No application outage, application recovery, outbound-connectivity test, production certification or actual-phone testing is claimed. Wix mobile preview is a layout check only.
Retail Hot LRS data-storage pricing was refreshed in EUR (first-tier reference EUR0.0168/GB-month); the account stored no data. Inherited Defender for Storage was Free. The resource-group cost query returned no rows at inspection time, which is not proof of a final zero invoice. The EUR0.25 planning allowance is not an enforced cap; the authorized EUR20 ceiling includes a EUR5 cleanup reserve.
Comments