Author a Bicep module from scratch — params, a loop, outputs — and deploy it.
Why write it down
For nineteen classes you have built Azure by clicking — 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.
State the payoff as the three things clicking fails to give you. No drift: when the file is authoritative, every property it declares has one agreed value — and where the environment has wandered, redeploying the file pulls those properties back into line. (What the file does not mention, a default deployment leaves alone; §4 returns to that boundary.) No archaeology: the "why" lives in the code and its Git history, so a decision made today is legible in a year instead of being reverse-engineered from a running system under incident pressure. Repeatability: the same file builds dev, test, and prod identically, and rebuilds any of them from nothing — the difference between a nine-hour recovery and a nine-minute one. Bicep is Azure's own language for this: a clean, declarative file that compiles down to the ARM JSON you met in Class Sixteen.2 The portal does not stop existing; it stops being the truth. The file is the truth, and the portal becomes one window onto it.
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.
Notice what the file is: a declaration of the end state, not a script of steps. You do not tell Bicep "if the account is missing, create it, otherwise update the SKU" — you state the account you want and Bicep works out the difference. That declarative nature is the whole point, and it is why the same file is safe to run a hundred times. Notice too allowBlobPublicAccess: false sitting right there in the source: the security decision from Class Twelve is now reviewable in a pull request, not buried in a portal blade nobody revisits.
The decorators — the @-prefixed lines above a parameter — are worth a second look, because they turn a parameter from a hole anyone can fill into a contract the file enforces. @allowed restricts the value to a fixed set, so a typo like Standard_LSR fails at compile time rather than deploying something wrong. @minLength and @maxLength bound a name or an array; @description documents the input for the next engineer and for the auto-generated parameter form. The one that matters most in production is @secure(): mark a string or object parameter secure and its value is kept out of deployment logs and history, which is the difference between a password that lives for a deploy and a password that lives forever in a log file. Decorators are how a Bicep file stops trusting its caller to be careful and starts refusing bad input on its own — the same instinct as the branch protection of Class Nineteen, moved into the template.
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-campux-prod \
--template-file main.bicep \
--mode Complete
Resource changes: 1 to create, 1 to modify, 1 to delete.
+ Microsoft.Storage/storageAccounts/campuxlogs [create]
~ Microsoft.Network/virtualNetworks/vnet-hub [modify]
addressSpace.addressPrefixes: ["10.20.0.0/16"] => ["10.20.0.0/20"]
- Microsoft.Storage/storageAccounts/campuxtemp [delete]
Read that output like a pilot reads a checklist, because the symbols are the whole story: + creates, ~ modifies, and - deletes. That delete line is why what-if exists. A parameter typo that shrinks an address space or drops a resource looks harmless in the source and catastrophic in production, and this preview is where you catch it — before, not after. In a real pipeline the what-if runs automatically on every pull request and its output is posted for a human to read, so approving the PR means approving a named list of changes rather than a hopeful guess.3 It is the same instinct as reviewing a diff: never change what you have not first seen described.
The flag on the last line of that command deserves a paragraph of its own, because it decides whether delete lines can appear at all. Deployments run in incremental mode by default: resources you add or change are applied, but a resource you remove from the file is simply left running — unmanaged, unmentioned, and still on the invoice. The file stops describing it; Azure does not touch it. Delete lines like campuxtemp appear only in complete mode, where the template is the whole truth and anything absent from it is removed — or under deployment stacks, the newer mechanism built to manage exactly this lifecycle. Know which mode a pipeline runs before you trust its preview: an incremental what-if that shows no deletes is not promising your resources are safe. It never looked.
Trust the preview, but not blindly. what-if is a best-effort comparison, and it has known rough edges: it occasionally reports a property as changing when nothing meaningfully will — "noise" from fields the provider normalises or fills in server-side — and for a handful of resource types it cannot see far enough to predict a change perfectly. Read it the way a pilot reads weather radar: a strong signal of what is coming, not a guarantee of every gust. The delete lines and the large modifies are reliable and are the ones that matter; the occasional spurious ~ on a tag or a computed field is worth learning to recognise rather than fear. The habit the tool builds is the valuable part — that no change reaches production without a human first reading a description of it — and that habit survives the tool's imperfections intact.
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 goes through the flow of the last three classes: a short-lived branch, a pull request, and — wired in next class — a what-if posted for a reviewer before anything is applied. Campux can now rebuild its network from an empty resource group in minutes, and answer "why is this subnet a /24?" by reading a commit instead of guessing. The nine-hour outage of Class One had no such file to rebuild from; the Campux of Class Twenty does. That gap, written in Bicep, is the Build I artifact finally maturing from a governed subscription into a reproducible one.
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.
// a bastion in every environment except dev
param environment string
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.
The official module, and a CAMPUX overview
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
A short walkthrough of a Bicep file's anatomy, a module composing two resources, and a what-if reading before a deploy will live here. Video to be added.
From a file to a real resource
You watched the template put the portal click back above. Now do it for real. Write the storage account from §2 as a Bicep file, preview it with what-if, and deploy it for real. Use a throwaway resource group so cleanup is one command.
Sign in and make a throwaway resource group to deploy into:
az login az group create --name rg-bicep-lab --location eastus
What to notice: the resource group is your deployment scope. Everything this file creates will land inside it, so deleting the group at the end removes it all.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' properties: { allowBlobPublicAccess: false } } output storageId string = storage.idWhat to notice: uniqueString makes the name globally unique from the RG id, so the file works without you hand-picking a free name.Preview before you change anything — run what-if:
az deployment group what-if \ --resource-group rg-bicep-lab \ --template-file main.bicep \ --parameters namePrefix=campuxlab
On screen: a single + create line for the storage account. Nothing exists yet — this is the plan, exactly what a reviewer would approve on a PR.Deploy it, then confirm the resource is really there:
az deployment group create \ --resource-group rg-bicep-lab \ --template-file main.bicep \ --parameters namePrefix=campuxlab az storage account list --resource-group rg-bicep-lab --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
Feel the two properties that make declarative code safe: deploying the same file changes nothing, and changing the file shows up in the plan before it is applied.
Deploy the unchanged file a second time:
az deployment group create \ --resource-group rg-bicep-lab \ --template-file main.bicep \ --parameters namePrefix=campuxlab
What to notice: it succeeds and creates nothing new. The file describes a desired state that already matches reality, so there is nothing to do. That is idempotency — the property that lets a pipeline redeploy without fear.Change one line in main.bicep — upgrade the redundancy:
// sku: { name: 'Standard_LRS' } -> sku: { name: 'Standard_GRS' }What to notice: a one-line edit is a reviewable diff. On a real team this goes through a PR, and the reviewer sees exactly this change to the redundancy of a production account.Preview the edit before applying — what-if again:
az deployment group what-if \ --resource-group rg-bicep-lab \ --template-file main.bicep \ --parameters namePrefix=campuxlab
On screen: now a ~ modify line, showing the SKU moving from Standard_LRS to Standard_GRS. The preview names the exact change — no surprises when you apply it.Apply the change if you like, then clean up everything in one command:
az group delete --name rg-bicep-lab --yes --no-wait
The lesson: same file, no change; changed file, a previewed change; and the whole environment removable in one line because it was disposable by design. Idempotency plus what-if is why teams trust a pipeline to deploy infrastructure unattended.
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 copy of production by Thursday." Clicking it together would take days and drift immediately. You deploy the Bicep that already describes production, point it at a new resource group, and staging exists in minutes — identical, because it came from the same file. Infrastructure as code turns "build me another one" from a project into a parameter.
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. In what-if output, + creates, ~ modifies, and - deletes. A delete line also tells you something about the deployment itself: it is running in complete mode or through a deployment stack, because the default incremental mode never deletes anything. So this preview is saying the template no longer describes a database that exists — a dropped resource, a wrong scope, a wrong name — and that applying it will destroy the real one. This is exactly the accident what-if exists to catch. You do not proceed; you find out why the file stopped describing something that should exist.
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, not abstract. Deploying straight from a laptop with no preview means a typo that shrinks an address space or drops a resource reaches production with nothing in between to catch it. Point at §4's delete line: the one time this matters, it matters catastrophically, and "it deployed fine on my machine" is exactly the story that precedes an outage. The gap is not hypothetical; it is the missing what-if.
Ask for the small change that closes it. Request — as a question, not a decree — that deploys run through the pipeline, with what-if on the PR and production behind an approval, the way the last three classes built. Frame it as protecting them: the day a bad template would have deleted a database, the preview is what saves their weekend and their reputation. Good code deployed recklessly is still a risk; the review's job is the whole path, not just the file.
Five things worth carrying out of this class
- Infrastructure as code makes the file the source of truth: no drift, no archaeology, and repeatable rebuilds. The portal becomes a window, not the record.
- 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 is the plan before the change: + creates, ~ modifies, - deletes. Never apply what you have not first seen described — especially the delete lines.
- 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 is accurate but not infallible — for a few resource types and some property changes it can report a change that will not really happen, or occasionally miss a nuance, because it relies on each resource provider implementing the preview correctly. Treat it as a strong safety net, not a guarantee: it will catch the catastrophic delete you care about, but read a surprising result with judgement rather than blind trust. The direction — always preview before you apply — is not negotiable. ↩
- 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. ↩