top of page
Sep 23
8 min read

Updated: Sep 24

Lab 03 — Azure Governance Guardrails and Cost Control


azure administrator series


Lab 03 — Governance Guardrails. azure administrator series. Policy, locks and cost control.


ZIP SHA-256: 487f3470fc02d6d5ae855a9a1d2591ae41a0e9c4c879e4dcd09f5fa1b6c8ab64


Live-tested and cleaned up: baseline, exact policy denial, delete-lock denial, recovery twice and cleanup twice. Budget email delivery and management-group changes were not tested.


A deployment can fail because governance is working. Keep the guardrail in place, diagnose the rejected configuration, and recover by correcting the request—not by disabling the policy.


This is the third hands-on workshop in my series based on the Microsoft Azure Administrator (AZ-104) certification objectives. It follows scoped RBAC and identity lifecycle. It is independent training, not official Microsoft courseware or an exam guarantee. Allow 60–90 minutes.



What you will prove


  • Azure CLI deploys a healthy NSG baseline and submits a deliberately noncompliant request.

  • PowerShell validates exact objects, restores compliant desired state and removes the lab.

  • Portal checkpoints show tags, policy, budget, lock, activity events and successful deployments.

  • Policy and lock rejections are different administrative outcomes—not network or application outages here.


This addresses AZ-104 Policy, tags, resource locks, resource groups and cost management. Subscription and management-group administration remain read-only/guided. AZ-104 study guide


The isolated design


Existing subscription — inspect only
└── Disposable Lab 03 resource group (West Europe)
    ├── Required-tag policy: CostCenter = Training
    ├── Budget: 20 billing-currency units; notification disabled
    └── One unattached NSG with explicit tags
        └── CanNotDelete lock

Wrong desired tag → policy rejects request → NSG stays unchanged
Correct desired tag → deployment succeeds → guardrails remain
Delete NSG → lock rejects request → resource remains

No VM, public IP, NIC, subnet association, storage, traffic or flow logging is deployed. The NSG is a resource under governance, not a complete networking solution. Tags are applied explicitly to both RG and NSG; the exercise does not rely on tag inheritance.


Before you begin


Use Windows, PowerShell 7.5+, Azure CLI and a disposable subscription context. The compiled ARM template is included; Bicep source is available for inspection. You need effective permissions for resource groups, deployments, NSGs, policy assignments, locks and budgets. Contributor alone is not a general substitute for governance permissions. Preflight never grants roles, registers providers or alters inherited policy.


az account show --query '{name:name,state:state,tenantId:tenantId,id:id}' -o table
$PSVersionTable.PSVersion
az version

Compare your active tenant/subscription with the Portal privately. Do not publish the account output. Review current pricing and your agreement. The author run's operational ceiling was EUR20 including EUR5 cleanup reserve. This design has no metered workload; it does not guarantee that an existing subscription has no charges. Azure Policy · Cost Management pricing


1. Prepare private state


Extract and verify the ZIP, open pwsh in its folder, and read the README. Replace the subscription-name placeholder. The initializer creates a new directory restricted to your Windows account and SYSTEM.


$private = Join-Path $env:LOCALAPPDATA ('Workshopz-Lab03-' + [guid]::NewGuid().ToString('N'))
./Initialize-Lab03StateDirectory.ps1 -Path $private -Execute
$state = Join-Path $private 'state.json'
./Start-Az104Lab03.ps1 -Stage Preflight -StatePath $state -SubscriptionName 'YOUR SUBSCRIPTION NAME'

Preflight reads context, permissions, Network provider/region/API support, NSG quota, policies, locks, budgets, existing Advisor recommendations and visible management groups. Stop for unexpected restrictions; do not remove them. Raw reports remain private.


2. Deploy and validate the baseline


./Start-Az104Lab03.ps1 -Stage Deploy -StatePath $state -SubscriptionName 'YOUR SUBSCRIPTION NAME' -Execute -AcknowledgeCost
./Test-Az104Lab03.ps1 -StatePath $state -Phase Baseline

Setup records exact IDs. Azure CLI deploys the NSG template; PowerShell ARM requests configure the surrounding governance objects. The built-in Require a tag and its value on resources is assigned only at the new RG. No managed identity or remediation role is created.


In Portal, search for the exact RG from your private manifest. Overview must list one NSG in West Europe. These are real validation screenshots; your disposable names will differ.


Baseline: one lab NSG

Open Tags and confirm CostCenter=Training, lab/run identifiers and expiry. Inspect the NSG's tags too: resource-group metadata alone does not prove its resource is tagged correctly.


Explicit resource-group tags

Under Policies, inspect the exact assignment and parameters. Request enforcement and asynchronous compliance reporting are separate signals. The author run reached Compliant for its one resource; a new run may initially await evaluation. Policy deny effect


Lab policy compliance

3. Budget configuration, not alert delivery


Open RG → Budgets → the recorded budget. Confirm scope, amount, monthly reset and end date. Azure returned EUR in this run: a budget of 20 means €20 and its 80% example is €16. The recipient is lab03-alerts@example.invalid; the notification is disabled. No real recipient, contact role, action group or webhook is used.


Budget with non-deliverable placeholder

The Portal summary shows the condition and address but does not expose the disabled flag. API readback and validation establish enabled=false; the screenshot alone does not prove it. Configuration was live-tested; threshold evaluation and delivery were not. Do not spend money to trigger an alert. Budgets do not stop consumption. Budget tutorial


4. Prove the delete lock


./Test-Az104Lab03.ps1 -StatePath $state -Phase Baseline -TestDeleteLock -Execute

The test sends a real DELETE to the exact NSG after checking its recorded lock. Passing means HTTP409 ScopeLocked, with the NSG still present. Portal labels CanNotDelete as Delete. Compliant updates are still allowed; this is not ReadOnly or an RBAC role. Resource locks concern control-plane operations, not every data-plane action. Resource locks


Resource-scoped delete lock

5. Introduce and diagnose the fault


For a challenge-first attempt, open challenge.md before the solution. The fault changes a submitted desired tag, not the healthy baseline resource. Ownership tags remain intact.


./Set-Az104Lab03Fault.ps1 -StatePath $state -Execute
./Test-Az104Lab03.ps1 -StatePath $state -Phase Fault

The script requires RequestDisallowedByPolicy naming the exact lab assignment. Missing permissions, an inherited policy, a lock error or another generic failure do not count. If enforcement has not propagated and the request succeeds, stop, inspect private evidence and restore compliant state; do not claim the guardrail worked.


Open the RG Activity log and select failed Create Deployment. In this run its summary showed InvalidTemplateDeployment with a policy-violation message. The saved CLI response contained the nested exact policy denial. Rejection occurred before a failed deployment resource persisted: use Activity log and the response, not an assumed failed row in Deployments.


Policy-violation message cropped to exclude identifiers

Actual activity outcomes

6. Recover without weakening governance


./Restore-Az104Lab03.ps1 -StatePath $state -Execute
./Test-Az104Lab03.ps1 -StatePath $state -Phase Recovery
./Restore-Az104Lab03.ps1 -StatePath $state -Execute

PowerShell resubmits the NSG template with CostCenter=Training. Policy and lock remain unchanged. Validation compares NSG tags/rules with baseline, checks zero associations and verifies all five expected objects. Repeating recovery reuses the same deployment and NSG, rather than creating duplicates.


Portal → Deployments shows baseline and recovery succeeded. This proves resource configuration, not application health: no application was deployed.


Successful baseline and recovery

7. Guided, read-only extensions


Inspect your subscription's directory and parent scope. Review Management groups and discuss where shared governance belongs. Creating a hierarchy or moving a subscription affects a wider boundary and is excluded. Production changes need an agreed hierarchy, permission checks, impact analysis and rollback planning.


Inspect existing Advisor recommendations for the applicable scope. Record scope, category and observation date; an empty result is not proof of optimization. Do not enable a service, generate recommendations or apply unrelated changes. Preflight verified read access only. These activities are not live management-group configuration tests.


8. Safety, evidence and cleanup


./Test-Az104Lab03Safety.ps1 -StatePath $state
./Remove-Az104Lab03.ps1 -StatePath $state -Execute
./Remove-Az104Lab03.ps1 -StatePath $state -Execute
./Export-Az104Lab03Evidence.ps1 -StatePath $state -OutputPath ./my-sanitized-evidence.json -Execute

Safety probes cover wrong-tenant refusal, ownership mismatch, pending-create refusal and competing file handles. They use local altered state and live read-only context, not an actual tenant switch or process crash. During authoring, a genuine interrupted setup was reconciled against its exact successful deployment after fixing JSON timestamp parsing.


Cleanup rechecks ownership and scope before deleting the recorded lock, budget, policy, NSG and RG. Unrelated objects cause a stop, not an expanded deletion. Exact-ID and run-tag checks then look for residuals. This permanently deletes disposable infrastructure and deployment history; Azure activity logs can remain under retention. Repeat cleanup to confirm safe absence.


The expiry tag is not an automatic deletion timer. New exercises stop after five hours; clean up before six. Keep the manifest. Never adopt objects by prefix or blindly clear a pending create. See README for interrupted-run handling.


Keep raw responses, identifiers and unredacted screenshots private. Consult validation-report.md for author-run results, knowledge-check.md for eight questions, and the separate hints/solution files.


Takeaway


RBAC answers who may act. Policy constrains configuration. Locks block particular operations. Budgets inform decisions without capping spending. Identify the exact rejection, then fix the request when the guardrail is correct.



Lab 03 — Challenge


Your baseline training resource is healthy. A later deployment request has been rejected. A separate attempt to delete the resource also failed. Your task is to explain both outcomes and make the deployment succeed without weakening the existing governance.


Use only the original private state manifest and the recorded disposable lab scope. Do not remove policies, exemptions, inherited restrictions or locks during diagnosis. Do not change permissions. Leave unrelated resources untouched.


Submit a concise incident note answering: Which exact operation failed? What is its nested error? Which recorded object explains the decision? Did the existing NSG change? What is the smallest compliant recovery? How did you prove repeat recovery is safe?


Acceptance: successful compliant deployment; unchanged policy and lock; no added NSG rules/associations; no duplicate resources; verified cleanup. Budget delivery is outside the challenge. Do not include private IDs or raw logs in a public submission.


Open hints.md progressively. Keep solution.md closed until you have a diagnosis.


Lab 03 — Eight knowledge checks


  1. A user has permission to write an NSG, but its update is denied. Does permission alone prove the request should succeed? Explain.

  2. Does applying CostCenter to the RG prove the NSG has that tag?

  3. Which difference separates CanNotDelete from ReadOnly for the recovery request?

  4. Why is a generic Forbidden response insufficient evidence for this lab's policy test?

  5. Where can a rejected deployment be investigated if no failed deployment resource exists?

  6. Does a budget of20 guarantee that spending stops at20? What does the disabled fake recipient prove?

  7. What should cleanup do when the NSG is unexpectedly attached to a non-lab subnet?

  8. What evidence supports repeatable recovery and safe cleanup, and which safety scenarios were only local probes?


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 03 — Validation report


Author run: September 23, 2026. AzureCloud, West Europe; Windows PowerShell 7.6.6. Private identifiers and raw responses are intentionally excluded. evidence.json records actual stage timestamps.


Check

Actual result

Baseline

Passed: exact RG/NSG/policy/lock/budget, expected tags, unattached NSG, no custom rules

Policy fault

Passed: nested RequestDisallowedByPolicy matched the exact lab assignment; baseline configuration unchanged

Portal fault evidence

Actual failed Create Deployment activity event; InvalidTemplateDeployment summary explicitly said policy violation

Deployment-history nuance

Fault rejected before a failed deployment resource persisted; baseline and recovery records exist

Recovery

Passed twice; same resource/deployment IDs, compliant desired state, guardrails unchanged

Delete lock

Passed: real DELETE rejected HTTP409 ScopeLocked for the exact NSG; resource remained

Budget

20 EUR monthly; disabled 80% actual-cost notification to example.invalid; no real recipients/actions

Cleanup

Exact owned infrastructure and RG deleted; exact-ID and tagged-resource residual checks passed

Repeat cleanup

Passed: original group absent, no exact-ID or run-tagged residual resources

Wrong tenant / ownership / pending create / lock contention

Passed local guard probes with live read-only context. No real tenant switch or process-kill experiment

Mismatched manifest ID / enabled placeholder

Offline development probes rejected them

Genuine interruption

Initial baseline deployment succeeded but ownership validation stopped on JSON timestamp conversion. Exact successful deployment and original ownership were verified before resuming; no name-based adoption

PowerShell / Bicep

Syntax checks and one-resource/no-custom-rule template checks passed during development; release package is checked again before ZIP creation


Implementation findings


PowerShell's JSON timestamp conversion initially produced a type mismatch between an in-memory string tag and an ARM response DateTime. The release preserves JSON date strings explicitly and requires PowerShell 7.5+. Azure also normalized the notification key's initial letter; validation now accepts that key case normalization while retaining exact recipient/disabled-state checks. Neither finding was hidden or counted as a first-attempt pass.


Evidence limits


Budget currency was EUR in the subsequent readback and the genuine Portal screenshot. The initial create response omitted currency, so the journal's Budget.Currency is honestly recorded as Not returned; it is not a contradiction of the later screenshot.


No budget email was sent; no threshold evaluation or billing-latency test was performed. No metered workloads were introduced to force spend. Subscription hierarchy mutation, management-group creation, Advisor remediation, sign-in, networking reachability and application health are not tested. Advisor and hierarchy were read-only preflight checks/guided topics. No actual-phone test is claimed.


Screenshots are genuine Portal captures with deterministic crops excluding account contact details and subscription/tenant IDs; no AI editing was used on evidence. The budget screenshot displays a condition but not its enabled flag; the API validation proves disabled state. Compliance screenshots show observed state, not a guarantee of instantaneous evaluation. The cover is separately generated marketing artwork.


The downloadable ZIP must pass extraction, file-hash comparison and public-identifier scanning before publication. Public layout/mobile-preview verification is a publishing check, not an Azure acceptance test.


 
 
 

Comments


bottom of page