Cloud · Azure · Built twice

Azure Labs: Built Twice, Portal Then Bicep

Hands-on Azure labs, each built twice: by hand in the portal to see what the service really does, then from scratch as Bicep and compared. Lab 01 tested four Azure Policy rules and a delete lock until they broke, then rebuilt them as code in ten seconds. Lab 02, storage access tiers, is running.

Status
Lab 1 of 20 complete
Area
Cloud
Period
October 2026 to present
Role
Build, break, rebuild as code, document
Stack
Azure PolicyResource locksManaged identityAzure RBACBicepAzure CLIwhat-ifActivity logBlob Storage tiers
Lab 01 · built twice, then tested
Lab 01 built in the portal and as Bicep, then tested against four policies and a lockLeft, a portal window assigning a Deny policy that requires an owner tag, and a code window deploying the same rules as Bicep: what-if reports eight creates, and the deployment finishes in ten seconds. Centre, the resource group with P1 Modify inherit costCenter, P2 Deny needs costCenter, P3 Deny needs owner, P4 Deny East US only at subscription scope, and a CanNotDelete lock. Right, four requests: a network with an owner tag is created with costCenter added by P1; one without an owner tag is refused by P3; one in West US is refused by P4; a delete is refused by the lock.BY HAND · PORTALportal.azure.com · Policy · AssignAssign policy · P3DefinitionRequire a tag on resourcesEffectDenyTag nameownerCreate64 screenshots · each decision kept, every break recordedAS CODE · BICEPtargetScope = 'subscription'module gov 'modules/rg-governance.bicep' = { scope: rg // P1 to P3, the lock}$ az deployment sub what-if 8 to create · 0 to change$ az deployment sub create done in 10 secondsrg-lab-governance-taggingcostCenter=it-lab · owner=noble · lab tagsP1 · Modify · inherit a tagP2 · Deny · needs costCenterP3 · Deny · needs ownerP4 · Deny · East US only, subscription-wideLock · CanNotDelete+ costCenter✓✕ refused✕ refused✕ refused✓ created: vnet-p3-okcostCenter added by P1 before P2 checkedREQUESTSnew vnet · owner tagnew vnet · no owner tagnew vnet · West USdelete a vnet
The same rules built by hand in the portal, then rebuilt as Bicep: what-if showed eight creates and the deployment took ten seconds. Then the tests. P1 adds the missing costCenter tag before the Deny rules check, so a request with an owner tag is created; a missing owner tag, a West US request and a delete under the lock are all refused.
4Policy rules tested to failure
7Deliberate breaks, recorded
10 sTo rebuild it all as Bicep
1 / 20Labs complete
Replay

Follow four requests through the rules.

  1. Request
  2. P1Modify
  3. P2Deny
  4. ARMActivity log
  1. t+0.0sRequestCreate a virtual network with no tags.
  2. t+0.4sP1Adds costCenter, inherited from the resource group.
  3. t+0.8sP2Checks for costCenter, and finds the one P1 just added.
  4. t+1.2sARMRecords the order on the request: Modify, then the deny check.

Created. Modify runs before Deny, so the rule that adds wins.

Why build everything twice

Clicking through the portal is the fastest way to learn what a setting does. It is also impossible to repeat exactly, review, or keep in Git. Code is the opposite: repeatable and reviewable, but easy to write without understanding what it creates.

So every lab here is built twice. First by hand, with predictions written down before anything is created and something broken on purpose. Then from scratch as Bicep, deployed to a clean resource group, and compared with what the portal built. The differences between the two are usually where the lesson is.

Each lab follows the same five steps: build it in the portal, break something deliberately, export what Azure created, write the Bicep and deploy it, then write up the findings and tear everything down. Every resource carries environment, purpose, lab and autoDelete tags, and a script checks for anything left running.

Lab 01: governance and tagging

Three questions, each with a prediction written before the build:

QuestionPredictionWhat happened
Do resource group tags flow down to the resources inside?NoCorrect. A network created in a group with six tags carried none. Inheritance takes a policy, one per tag name.
What happens to resources that existed before a policy?Left aloneHalf right. Not changed, but reported non-compliant within minutes, and fixed only by a remediation task.
If one policy adds a tag and another denies resources missing it, which wins?The one that addsCorrect, and proven both ways. Modify runs before Deny; with the Modify rule switched off, the Deny rule refused the same request.

Four built-in policies made the test: P1 inherits costCenter from the group (Modify), P2 requires costCenter (Deny), P3 requires owner (Deny), and P4 allows East US only, assigned to the whole subscription. A CanNotDelete lock sat on the group.

Broken on purpose

AttemptResultRefused by
A network with no tags, P1 and P2 onCreated, costCenter addednothing
The same, P1 switched to Do not enforceRefusedP2
A network with no owner tagRefusedP3
A network in West USRefusedP4
A remediation task fixing an old networkFailedP3, because a remediation is just another update
Adding a tag to a network with no ownerRefusedP3, not the lock
Deleting a network under the lockRefusedthe lock

The remediation failure was the finding worth the lab. A remediation task applies a Modify rule by sending an ordinary update, and every Deny rule still checks that update. Adding the missing owner tag by hand let P1 add costCenter in the same request.

Rebuilt as code

The portal export was the first lesson: it held only the networks. Policy assignments and locks are extension resources, attached to the group rather than in it, so an exported template is not a backup of a group’s governance.

The Bicep rebuild deploys at subscription scope: the group and P4 from main.bicep, everything inside the group from a module. Built-in policy and role IDs are looked up, never remembered, and looking them up caught a wrong one. what-if listed eight creates and no changes; the deployment took ten seconds. Three things only the code build showed:

  • Rules and resources in one deployment race. A network created seconds after its rules came out untagged and unrefused, because neither rule had taken effect yet. Rules belong in their own deployment, before the resources they govern.
  • Tag Contributor is enough. The portal quietly gave P1’s managed identity Contributor. In code the role assignment is its own resource, and the narrow Tag Contributor role did the job, proven by the Activity log.
  • Redeploying re-runs Modify. A second deployment sent the network’s tags as owner only, and P1 put costCenter back inside that same request.

Lab 02, running: storage access tiers

The second lab asks what hot, cool, cold and archive storage really cost to keep and to read back, what it takes to bring an archived file back online, what moving a file to a colder tier too early costs, and, added mid-lab after noticing the portal quietly uses the account key, who can still read the files once the account’s keys are switched off. Predictions are written; the build is under way.

What is next

Twenty labs in three groups. Foundations: storage tiers, a virtual machine deep dive, a custom RBAC role, Entra ID users and groups at scale, Azure Files. Composition: a three-tier network, hub-and-spoke with Bastion, load balancer failover, monitoring and alerts, backup and restore, Key Vault with managed identity, VNet-to-VNet VPN, App Service slots, private endpoints. Applied: a Bicep CI/CD pipeline, a Defender for Cloud baseline, a cost guardrail, Conditional Access with PIM, and a full environment built and destroyed from one template.

Evidence

What it looks like when it is actually configured.

Screenshots from the build, named and captioned so each one can be traced to the configuration it proves.