Why write it down
In the earlier practical classes you have inspected Azure and built selected resources through the portal or CLI — a resource group here, a VNet there, a storage account with the right redundancy. It works, and it is exactly how you should have learned. But a portal click leaves almost no trace of intent: six months later, nobody can say why that subnet is a /24, whether the firewall rule was deliberate or a debugging leftover, or how to rebuild any of it if the region burns down. Infrastructure as code is the fix — you describe the desired state of your Azure resources in text files, commit them to the repository, and let a tool make reality match the file.1
- Infrastructure as code
- Defining your cloud resources in declarative text — versioned, reviewed, and deployed like software — so that the file is the source of truth and the running environment is its output, not the other way round.
The payoff is a reviewable record. The file shows the settings the team chose; Git history can explain why they changed; and the same template can deploy to another environment with different parameters. This does not eliminate drift: a portal change can still alter a resource, and a property absent from the file may follow a service default. Rebuilding also requires permissions, data backups, dependencies, and time. Bicep is Azure's declarative language for ARM deployments: you describe resources rather than write each create or update step.2 The portal remains useful for inspection and emergency action. Reconcile any emergency change back into code once the incident is contained.
The anatomy of a Bicep file
A Bicep file has only a handful of moving parts, and once you can name them the language stops looking foreign. Here is a small but complete file — a storage account, parameterised — with every element you will use ninety percent of the time.
// main.bicep — a parameterised storage account
@description('Azure region for all resources')
param location string = resourceGroup().location
@allowed([ 'Standard_LRS', 'Standard_GZRS' ])
param redundancy string = 'Standard_LRS'
param namePrefix string
var storageName = '${namePrefix}${uniqueString(resourceGroup().id)}'
resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageName
location: location
sku: { name: redundancy }
kind: 'StorageV2'
properties: { allowBlobPublicAccess: false }
}
output storageId string = storage.id
- param
- An input you supply at deploy time — the thing that differs between dev and prod. Decorators like @allowed and @description constrain and document it.
- var
- A value computed inside the file — here, a name built from a prefix and a hash. Not an input; derived once, reused.
- resource
- The heart of it: a symbolic name, a type@apiVersion, and the properties that describe the desired state. This is the ARM resource of Class Sixteen, written cleanly.
- output
- A value the deployment hands back — an id, a hostname — so the next file or pipeline step can use it without guessing.
The file declares a desired state rather than a list of steps. Azure Resource Manager compares that declaration with the existing resource and applies the deployment. Repeating the same deployment usually reaches the same declared state, but it can still reapply properties, fail because of permissions or policy, or encounter a changed service default. Review the plan each time. The explicit allowBlobPublicAccess: false makes the Class Twelve security choice visible in a pull request.
The decorators are the @-prefixed lines above a parameter. @allowed restricts inputs to a listed set, @minLength and @maxLength bound values, and @description explains an input to the next engineer. @secure() marks a string or object as sensitive so Azure does not log its value in deployment history; still keep secrets out of source control, command history, outputs, and error messages. These checks catch some mistakes, not every invalid resource setting.
Modules — building bigger from small files
One file describing one storage account is a toy. Real infrastructure is dozens of resources, and cramming them into a single thousand-line file is the Bicep equivalent of a nine-hundred-line pull request — unreviewable and fragile. The answer is modules: a Bicep file can call another Bicep file the way a program calls a function, passing parameters in and receiving outputs back.
// main.bicep — composes reusable modules
param location string = resourceGroup().location
module network './modules/vnet.bicep' = {
name: 'networkDeploy'
params: {
location: location
addressSpace: '10.20.0.0/16'
}
}
module storage './modules/storage.bicep' = {
name: 'storageDeploy'
params: {
location: location
subnetId: network.outputs.appSubnetId // one module feeds the next
}
}
Read what that composition buys you. Each module is a small, self-contained file you can review, test, and reuse — the VNet module that builds Campux's 10.20.0.0/16 can be the same one a future project calls with different numbers. The network.outputs.appSubnetId line is the important trick: Bicep sees that storage depends on an output of network, so it works out the deployment order for you — network first, storage second — without you writing a single "do this before that" instruction. This is the same lesson as short-lived branches, one layer down: compose the system from small pieces that each fit in a reviewer's head, and let the tool handle how they join. A repository of well-named modules is what turns Class Seven's tenant sketch and Class Ten's VNet from diagrams into a platform that rebuilds itself on command.
The shape to hold in your head is a module as a function: parameters go in the top, resources are built inside, and outputs come out the bottom for the next module to consume. Figure 1 draws that interface, and the wiring between two modules that lets Bicep work out the order by itself.
Modules do not have to live beside the file that calls them. A team that has built a good vnet.bicep can publish it to a registry — a container registry Azure treats as a module store — and every project then references it by name and version, the same way code depends on a versioned library. That versioning is the point: pin br:campux.azurecr.io/bicep/vnet:1.2.0 and a fix to the module ships as 1.3.0 that no project inherits until it chooses to. One reviewed network module, versioned and shared, becomes the way an organisation stops re-solving subnetting in every repository — which is the same reuse argument as the modules themselves, told one level up.
what-if — the plan before the change
Declarative code has one frightening property: you describe an end state, hand it to Azure, and trust it to work out the steps. The first time you deploy a change to a live environment, "trust it" is not good enough — you want to see, before anything happens, exactly what will be created, changed, or deleted. Bicep gives you that preview with what-if.
$ az deployment group what-if \
--resource-group rg-bicep-example \
--template-file main.bicep \
--parameters namePrefix=campuxlab
Illustrative resource change: 1 to create.
+ Microsoft.Storage/storageAccounts/<generated-name> [create]
The example is illustrative, not a transcript from Azure; create the group before running it. + means create and ~ means modify. In Lab Two, changing a tag should produce a modify entry. A pipeline can run what-if on a PR and show it to a reviewer, but only if the team configures it. Read diagnostics too: Azure may be unable to evaluate part of a deployment.3
This Bicep deployment uses incremental mode. A resource omitted from the file is not deleted merely because you redeploy it; for example, an older campuxtemp account outside this file could remain. Bicep deployments do not use --mode Complete. For managed removal of resources with Bicep, learn deployment stacks and preview their changes separately. A clean incremental preview does not tell you which unmanaged resources can be deleted.
what-if is a prediction, not a guarantee. Some expressions cannot be resolved before deployment; some modules can be omitted from analysis, and computed properties can look changed even when the deployed value will stay the same. Treat an unevaluated or ignored resource as a gap to investigate, not as approval to deploy. Compare the template, parameter values, diagnostics, permissions, and live resource before a consequential change.
Play it through
Someone changes a setting in the portal. Redeploy the template and watch the click undone, then decide what the truth about your infrastructure actually is. It plays on its own and stops when it needs your hands.
Deployment scopes
One last thing decides where a Bicep file acts. A file that creates a storage account runs against a resource group; a file that creates the resource groups themselves has to run one level up. That level is the deployment scope, declared with targetScope and matched by the command you run.
The file is the truth.
| targetScope | What it can create | Command |
|---|---|---|
| 'resourceGroup' (default) | Resources inside one RG — VNets, storage, VMs | az deployment group create |
| 'subscription' | Resource groups themselves, and policy assignments | az deployment sub create |
| 'managementGroup' | Policy and RBAC applied across many subscriptions | az deployment mg create |
| 'tenant' | Management groups, new subscriptions, and tenant-wide RBAC | az deployment tenant create |
The mental model is the hierarchy from Class Seven, now made operational: you deploy at a level to create things that live at that level. Most of your day is 'resourceGroup' — it is the default, so you rarely write it. You reach for subscription scope on the day you want the resource groups themselves in code rather than clicked into being, which is exactly what Build I asked you to govern by hand. A subscription-scope file can create an RG and, in the same deployment, hand off to a module that fills it — the whole environment, from empty subscription to running platform, described in one reviewable place. That is the destination of this class: not a storage account written in Bicep, but an entire Azure footprint that lives as text.
Scopes also compose, which is what makes that whole-footprint deployment possible from one entry point. A file at 'subscription' scope creates the resource group, then calls a module and hands it that group as its scope: — the module runs at resource-group scope and fills the group the parent just made, all in a single deployment. So the levels are not four separate worlds you deploy in turn; they nest, exactly like the Class Seven hierarchy, and a parent deployment can reach down a level through a module. This is why "the whole environment from an empty subscription" is not a slogan: one subscription-scope main.bicep can declare the groups, then compose the resource-group-scope modules that populate each one, and what-if previews the entire chain before a single resource exists. The file is not just the truth about one storage account; it can be the truth about the estate.
Clicks become code
The tenant design from Class Seven and the 10.20.0.0/16 VNet from Class Ten were, until now, decisions living in diagrams and portal blades. This class they become files. You write a vnet.bicep module for the address space and its app, data, and management subnets, a storage.bicep module carrying the Class Twelve rule allowBlobPublicAccess: false, and a main.bicep that composes them and passes one module's subnet id into the next. The Build I governance that was clicked into place now has a written form the team can review.
Every change can follow the flow of the last three classes: a short-lived branch, a pull request, and — when the pipeline is configured — a what-if posted for review before application. The network declaration becomes reproducible, but restoring an actual service still needs data, secrets, dependencies, permissions, and a tested recovery process. The commit can explain why a subnet was chosen; it cannot replace the operational runbook.
Loops, conditions, and pointing at what already exists
So far every resource in the file has been written out once, by hand. Real estates are rarely that tidy. Campux's vnet.bicep declares three subnets — app, data, and management — and copy-pasting a subnet block three times is exactly the repetition that lets one of the three drift out of step with the other two. Bicep answers with a loop: describe the shape once, list the things that vary, and let the language stamp out the rest.
// vnet.bicep — three subnets from one description
param subnets array = [
{ name: 'app', prefix: '10.20.1.0/24' }
{ name: 'data', prefix: '10.20.2.0/24' }
{ name: 'mgmt', prefix: '10.20.3.0/24' }
]
resource vnet 'Microsoft.Network/virtualNetworks@2023-09-01' = {
name: 'vnet-campux'
properties: {
addressSpace: { addressPrefixes: [ '10.20.0.0/16' ] }
subnets: [for s in subnets: {
name: s.name
properties: { addressPrefix: s.prefix }
}]
}
}
The [for s in subnets: { ... }] reads as "one of these for every entry in the array." Adding a fourth subnet is now a one-line edit to data, not a copy of a block — and because the shape is written once, all four subnets are guaranteed to share it. You can loop over a count as easily as a list: [for i in range(0, 3): ...] stamps out three of something numbered 0 to 2, which is how you would build a fixed pool of identical VMs. The loop belongs to the thing that varies; the description stays single and reviewable.
Its partner is the condition. Not every environment wants every resource — dev does not need the jump host that production locks itself behind, and paying for one idle in dev is money set on fire monthly. An if makes a resource conditional on a parameter, so the same file builds the right shape for each environment instead of forking into three near-identical templates that drift apart.
// conditional-resource sketch; add required Bastion properties before deployment
param environment string
param location string = resourceGroup().location
resource bastion 'Microsoft.Network/bastionHosts@2023-09-01' = if (environment != 'dev') {
name: 'campux-bastion'
location: location
// ...properties
}
One file, three environments, and the difference between them stated as data rather than duplicated as code. This is the same instinct as the parameter of §2, pushed one step further: the loop and the condition let a single reviewed template describe a family of environments, which is what turns "build me a staging copy" from a project into a flag.
Point at it without owning it.
The last piece of authoring is the existing keyword, and it fixes a specific trap. Sooner or later your file needs to refer to a resource it did not create and must not manage — the shared Key Vault the platform team owns, or a VNet already built by another module. Declare that resource normally and Bicep will try to bring it under this file's control, and the next deploy will attempt to overwrite settings that are not yours to touch. Marking it existing tells Bicep the opposite: look this up, let me read from it, but leave it alone.
// read the shared VNet's id without managing it
resource hub 'Microsoft.Network/virtualNetworks@2023-09-01' existing = {
name: 'vnet-campux-hub'
}
// use hub.id to peer or attach — but never change it
output hubId string = hub.id
The existing resource has no properties block because you are not declaring a desired state — you are borrowing a reference. It appears in no what-if line, because your file changes nothing about it. This is how one team's Bicep wires cleanly into another team's infrastructure: read the id you need, attach to it, and never let your file drift a resource that belongs to someone else. Ownership stays where it should, and the boundary is legible in the source.
The linter, and one file that governs the rest
A Bicep file that compiles is not the same as a Bicep file that is safe. The compiler checks that your syntax is legal; it does not check that you left an unused parameter behind, put a secret where anyone can read it, or gave a password a hardcoded default. That second layer of judgement is the linter — a set of rules the Bicep tooling runs automatically, in your editor as you type and again when the file builds, flagging the mistakes that pass the compiler but fail a careful review.
The rules are named and specific, and worth knowing by name because they are the review comments you would otherwise wait for a colleague to write. no-unused-params catches a parameter nobody reads — usually the fossil of a deleted resource. outputs-should-not-contain-secrets stops you handing a password back through an output, where it lands in deployment history in plain sight. secure-parameter-default refuses a default value on a @secure() parameter — because a default sits in the file, and a secret in the file is the exact leak §2 warned about. The linter will not catch every careless secret, so it is a floor and not a ceiling; but it enforces the same instinct the whole class runs on — that the reviewable file is the last place a credential should live — and it does it before a human has to.4
Which rules fire, and whether each is a quiet note or a build-breaking error, is decided by a single file: bicepconfig.json. Drop one in a folder and every Bicep file at or below it inherits its settings; the nearest file up the tree wins, so a repository can set a strict baseline and a subfolder can relax one rule without arguing with the rest.
// bicepconfig.json — govern the linter for the whole repo
{
"analyzers": {
"core": {
"rules": {
"no-unused-params": { "level": "warning" },
"secure-parameter-default": { "level": "error" },
"outputs-should-not-contain-secrets": { "level": "error" }
}
}
}
}
Each rule takes a level of off, info, warning, or error — and error is the setting that earns its keep, because a rule set to error fails the build, which fails the pull request check, which means the mistake never merges. That is the whole arc of the last four classes closing: a standard the team agreed on, written down, and enforced by a machine on every change rather than remembered by whoever happens to review. The same file is also where you register the registry aliases from §3, so br/campux:vnet:1.2.0 resolves to your ACR without spelling the full path each time. One config file, checked into the repository, is how a Bicep estate stops depending on everyone being careful and starts being careful by default.
Build your first Bicep file with Microsoft Learn
Build your first Bicep file
learn.microsoft.com/training/modules/build-first-bicep-file/
Microsoft Learn · Docs — What is Bicep?
learn.microsoft.com/azure/azure-resource-manager/bicep/overview
From a file to a real resource
This lab creates a real storage account, so confirm the subscription and use a new lab-only resource group. Keep the same Cloud Shell session for both labs; its LAB_RG variable holds the name for cleanup. If you cannot use a billable subscription, read the file and expected preview without deploying.
In Azure Cloud Shell (Bash), check the active subscription first. If it is wrong, select the intended subscription before continuing. Then create a uniquely named lab group:
az account show --query "{name:name,subscriptionId:id}" -o table LAB_RG="rg-bicep-$(date +%s)" echo "$LAB_RG" az group create --name "$LAB_RG" --location eastusWhat to notice: write down the printed group name. Only continue if you created this group yourself. The storage account in this file lands inside it.Create main.bicep with the §2 storage account (a text editor, or the VS Code Bicep extension for autocomplete):
param location string = resourceGroup().location param namePrefix string var storageName = '${namePrefix}${uniqueString(resourceGroup().id)}' resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' = { name: storageName location: location sku: { name: 'Standard_LRS' } kind: 'StorageV2' tags: { lesson: 'bicep' } properties: { allowBlobPublicAccess: false } } output storageId string = storage.idWhat to notice: uniqueString returns a deterministic 13-character hash from the group ID. It helps form a stable name, but does not guarantee global uniqueness; if Azure reports a name collision, use a different short lowercase prefix and rerun the preview.Preview before you change anything — run what-if:
az deployment group what-if \ --resource-group "$LAB_RG" \ --template-file main.bicep \ --parameters namePrefix=campuxlab
On screen: look for a + create entry for your storage account. The exact output can include other details or diagnostics. What-if has not created the account.Deploy it, then confirm the resource is really there:
az deployment group create \ --resource-group "$LAB_RG" \ --template-file main.bicep \ --parameters namePrefix=campuxlab az storage account list --resource-group "$LAB_RG" --query "[].name" -o tsv
The lesson: the account you described in text now exists in Azure. You did not click a single blade — the file was the instruction, and what-if showed you the consequence before it happened.
Run it twice, then change one line
Redeploy the same declared settings, then make one harmless tag change and preview it. Cloud services can still reapply properties or report noise, so do not treat a second deploy as proof that nothing happened.
Deploy the unchanged file a second time:
az deployment group create \ --resource-group "$LAB_RG" \ --template-file main.bicep \ --parameters namePrefix=campuxlab
What to notice: the same storage account should remain, rather than a new one being created. That repeatable result is the useful part of idempotency. It does not make every deployment risk-free.In main.bicep, change only the lab tag:
// before: tags: { lesson: 'bicep' } tags: { lesson: 'bicep-reviewed' }What to notice: this is a small reviewable change that avoids a billable storage redundancy conversion.Preview the edit before applying — what-if again:
az deployment group what-if \ --resource-group "$LAB_RG" \ --template-file main.bicep \ --parameters namePrefix=campuxlab
On screen: look for a ~ modify entry showing the tag change. If the preview has unexpected changes or diagnostics, stop and inspect them before applying anything.If the preview is expected, you can apply the tag edit using the same az deployment group create command. Then inspect and delete only the lab group you created:
echo "$LAB_RG" az resource list --resource-group "$LAB_RG" --query "[].{name:name,type:type}" -o table az group delete --name "$LAB_RG" --yes az group exists --name "$LAB_RG"What to notice: the last command should return false. If it does not, check the group in the portal. Never run the deletion against a group you did not create for this lab.
Zoom out: infrastructure as code moves the risk from the click to the review
You can compose Bicep modules. Now reason about what putting infrastructure in code does to the whole estate, because the value is not the syntax — it is that a change becomes a diff someone can catch before it lands.
A portal change is invisible until it drifts and breaks; a template change is a diff someone reviews — the earlier the feedback, the cheaper the mistake. What makes drift impossible rather than merely discouraged?
Modules compose by passing outputs to inputs, so a change to a shared module ripples to everything that uses it — power and blast radius in the same wire.
The value is reproducibility; the ceiling is how much of the estate is actually in code versus clicked in beside it and never written down.
what-if turns a deploy from a leap into a reviewed list of changes — but only for a team that reads it instead of rubber-stamping.
One long template is readable at three resources and unmaintainable at three hundred; composition into named modules is what survives growth.
The engineer who ships is asked "did it deploy?" The engineer who gets promoted is asked "and can you rebuild it from the commit?" — and could, from an empty resource group.
"Build me another one" becomes a parameter
"We need a staging environment by Thursday." Start with the reviewed Bicep files, supply staging-specific parameters, preview the changes, and deploy after checking permissions and costs. The infrastructure shape can be similar to production, but region, data, secrets, identity, network access, and capacity need their own choices. Reuse saves work; it does not make staging a safe production clone by itself.
Examination
Four drills, then two situations. The situations have no marking scheme — write your answer before you reveal the reasoning, or the exercise is worthless. Nothing is stored; this is between you and the page.
B. An output hands a value back out of the deployment — most often an id or hostname the next module or pipeline step needs. A describes a param (an input); C describes a var (internal only). Confusing these three is what makes early Bicep feel random: param in, var within, output back out. Get the direction of data flow right and composition in §3 stops being mysterious.
C. A delete line is a stop sign. It can appear in a complete-mode ARM JSON deployment or a deployment-stack preview; it is not the behavior of the default incremental Bicep deployment in this class. Verify the scope, resource name, template, and preview diagnostics before considering the change. What-if predicts a deletion; it does not guarantee every operation was fully evaluated.
Repeatability, reviewable diffs, and intent captured in history. The two false options are seductive and wrong: IaC is often slower than a single click for a one-off change — its payoff is repeatability and review, not raw speed, so "faster than clicking" is the wrong reason to adopt it. And it certainly does not remove the need to understand Azure — you still have to know what a storage account or subnet is; you are just writing it down. IaC multiplies the knowledge you have; it does not replace it.
// sql-with-secret.bicep
1. param location string = resourceGroup().location
2. param sqlAdminPassword string = 'P@ssw0rd-Campux-2026'
3. resource sql 'Microsoft.Sql/servers@2023-05-01-preview' = {
4. name: 'campux-sql'
5. properties: { administratorLoginPassword: sqlAdminPassword }
6. }
Line two. A real password sits in plaintext in a file that is about to be committed to the repository — which means it is now in the Git history forever, visible to everyone with read access, and impossible to fully erase by simply deleting the line later. This is the Class Nine lesson returning: the credential is the asset, and IaC makes leaking one easier if you are careless, because the file is designed to be shared.
The fix is the @secure() decorator on the parameter, with no default value: @secure() param sqlAdminPassword string. That keeps the secret out of the file, out of logs, and out of what-if output — supplied at deploy time from Key Vault or the pipeline instead. Code that is meant to be reviewed and shared is the last place a secret should live.
Concede the true part before you argue. They are right about the moment: for one urgent change, clicking is faster than editing code and waiting on a pipeline. Denying that makes you sound like a zealot. The disagreement is not about the next thirty seconds — it is about the next six months, and saying so out loud earns you the room to make the case.
Name what the portal fix costs after the incident. A click fixes the symptom and leaves the file — the source of truth — now lying. The next deploy from that unchanged template will quietly revert the fix, or the drift will confuse the next person debugging at 3am. The thirty-second save today is borrowed against an afternoon of "why does prod not match the code?" later. IaC is slower per change and far cheaper per year; that trade is the whole point.
Offer the pragmatic path, not purity. If the fire genuinely demands it, click the fix now to stop the bleeding — then immediately reflect it back into the Bicep and open the PR, so the file and reality agree again before anyone forgets. The rule is not "never touch the portal"; it is "the code is the truth, so never let a portal change outlive the incident." Speed now, reconciled truth right after.
Praise the code, then separate it from the process. Open by acknowledging what is genuinely good — the resources are right, it deploys clean. That is real, and leading with it keeps the review from feeling like an ambush. The problem is not in the diff; it is in how the diff reaches production, and naming that distinction is the whole review.
Make the risk concrete. A deployment from one laptop without a preview leaves reviewers unable to inspect the intended resource and property changes before they reach production. Even an incremental deployment can modify declared resources unexpectedly. A preview helps, though it may be incomplete or noisy.
Ask for a reviewable path. Request a pipeline that runs what-if on the PR, shows diagnostics, and gates production behind an appropriate approval. Also check the scope, parameters, and deployed result. The review covers the deployment path as well as the Bicep file.
Five things worth carrying out of this class
- Bicep makes intended infrastructure reviewable and repeatable; it still needs drift checks, correct parameters, and a tested recovery process.
- A Bicep file is params in, vars within, resources declared, outputs back out — a declaration of desired state, not a script of steps.
- Modules compose small files into big systems; passing one module's output into another lets Bicep work out the deployment order itself.
- What-if previews a deployment but can omit or misread parts; investigate unexpected changes and diagnostics before applying.
- targetScope decides where a file acts — resourceGroup (default), subscription, managementGroup, tenant — matched by az deployment group/sub/mg/tenant create.
- There is a real distinction between declarative tools (Bicep, Terraform, ARM) that describe the desired end state and let the engine find the steps, and imperative scripts (a raw az or PowerShell sequence) that spell the steps out. Declarative is what makes idempotency and what-if possible, because the tool can compare "what you asked for" against "what exists". You will still write imperative scripts — Class Twenty-Five — but for standing infrastructure, declarative is the default for good reasons. ↩
- Bicep is a transparent abstraction over ARM: every Bicep file compiles to an ARM JSON template (az bicep build shows you the result), and anything ARM can express, Bicep can. That is worth knowing because older material and some Azure features are documented in ARM JSON first — being able to read both, and to see Bicep as the humane surface over the JSON of Class Sixteen, keeps you from being stranded when a sample is in the older syntax. ↩
- what-if predicts rather than guarantees results. Microsoft documents unevaluated expressions, module short-circuiting, and expansion limits. Read diagnostics and investigate an Ignore or unexpected result instead of assuming a clean-looking preview covers the whole deployment. ↩
- The linter is a real safety net but a shallow one: it reasons about the file's structure, not its meaning, so it will not notice a plaintext password sitting in a default on an ordinary (non-@secure()) parameter — the Drill 04 bug. Treat the linter as catching the careless, not the determined; the secret discipline of §2 is a habit you still have to hold. What the config file guarantees is only that the rules you did enable are enforced on everyone equally, which is worth a great deal on its own. ↩
Deploy one Bicep resource with what-if first, change it safely, then grow the file with parameters and outputs.
Play the lesson above to stamp this class.