Skip to content
CAMPUX Cloud Bootcamp Phase Three · Class Twenty
Phase Three — DevOps Core
4 drills · 2 situations · 2 Labs
Aligned to AZ-400
Class Twenty

Infrastructure as code: Bicep

Describe an Azure resource in a file, preview the change, and deploy it. You will also learn what the file does not control, so a portal change or an incomplete preview cannot surprise you later.

Phase Three · DevOps Core · Class 20 of 42 · 27 min · Assumes Class 16 and Class 17

After this you can deploy one Bicep resource with what-if first, change it safely, then grow the file with parameters and outputs.

New to this? Read Class 16 and Class 17 first. Already done them, or know the material? Carry on.

§1

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.

§2

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.

Later · §3

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.

Figure 1: a module drawn as a function. On the left, main.bicep composes two modules. The vnet.bicep module box has three rows: inputs (location, addressSpace), what it builds (a VNet and three subnets), and outputs (appSubnetId). Below it, the storage.bicep module box takes inputs (location, subnetId), builds a storage account, and outputs storageId. A red pen line carries vnet.bicep's appSubnetId output down into storage.bicep's subnetId input, and a note reads: Bicep reads this wiring and deploys network first, storage second. main.bicep composes vnet.bicep in — location, addressSpace builds — VNet + 3 subnets out — appSubnetId storage.bicep in — location, subnetId builds — storage account out — storageId params in appSubnetId feeds subnetId a module is a function: params in, outputs out Bicep reads the wiring — network first, storage second
Figure 1 Each module is a small file with a clean interface: inputs at the top, a resource in the middle, outputs at the bottom. Wire one module's output into another's input and you have stated a dependency without writing an order — Bicep reads appSubnetId flowing into subnetId and deploys the network before the storage that needs it. Compose the estate from pieces this size and every one of them fits in a reviewer's head.

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.

§4

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

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.

§5

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.

Table 1 — Scopes and their deploy commands
targetScopeWhat it can createCommand
'resourceGroup' (default)Resources inside one RG — VNets, storage, VMsaz deployment group create
'subscription'Resource groups themselves, and policy assignmentsaz deployment sub create
'managementGroup'Policy and RBAC applied across many subscriptionsaz deployment mg create
'tenant'Management groups, new subscriptions, and tenant-wide RBACaz 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.

Case File · Campux Retail

Clicks become code

campux-platform grows its first real Bicep

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.

Fig. 2 · Clicks become code — main.bicep composes the estate
main.bicep composes modules vnet.bicep app / data / mgmt subnets 10.20.0.0/16 storage.bicep allowBlobPublicAccess: false subnet id rg-campux-platform VNet + subnets storage account what-if → apply one file rebuilds it — read the commit
The Class 7 tenant and Class 10 network become reviewable files. main.bicep composes vnet.bicep and storage.bicep; a what-if previews the planned resource changes. Rebuilding a working application still requires its data and other dependencies.
Later · §6

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.

Later · §7

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.

Read · Microsoft Learn

Build your first Bicep file with Microsoft Learn

Work through the official module, then use the two labs below to practise
Microsoft Learn · Module

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

Lab 1 · Author and deploy

From a file to a real resource

~12 minutes · Azure Cloud Shell in Bash mode · permission to create a resource group and storage account · billable resources

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.

  1. 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 eastus
    What 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.
  2. 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.id
    What 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.
  3. 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.
  4. 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.
Lab 2 · Idempotency & change

Run it twice, then change one line

~10 minutes · same RG · watch what-if tell the difference

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
Note · the storage account can incur charges while it exists. Deleting the group ends the lab resources after Azure completes the deletion; it does not guarantee the lab cost was zero. If a command fails halfway through, use the printed group name to find and remove your lab resources.
Think in systems

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.

Feedback loops

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?

Dependencies & coupling

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 constraint

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.

Second-order effects

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.

What breaks when it scales

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.

On the job

"Build me another one" becomes a parameter

You · Cloud Engineer · "set up another environment"

"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.

Class Twenty

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.

Drill 01Recall · anatomy
In a Bicep file, what is the role of an output?
Marked

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.

Drill 02Recall · what-if
A what-if preview shows a line beginning with - against a production database. What does it mean, and what do you do?
Marked

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.

Drill 03Select three
Which three are genuine benefits of infrastructure as code over clicking in the portal?
Marked

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.

Drill 04Spot the error
This Bicep snippet passes review at a glance, but one line is a serious mistake. Which?
// 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.  }
Marked

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.

Situation 01Write before you reveal
Mid-incident, a teammate snaps: "This is why IaC is a waste of time — I could have fixed this in the portal in thirty seconds, but now I have to edit a file, open a PR, and wait for a deploy. The code is slowing us down." What do you say?
Concede the true part first — then name what the thirty-second fix actually costs later.
Reasoning

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.

Situation 02Write before you reveal
A colleague asks you to review their first Bicep PR. It deploys cleanly and the resources are correct, but there is no what-if anywhere in the process — they deploy straight from their laptop to production. How do you handle the review?
The code is fine. The process around it is the finding — say so kindly.
Reasoning

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.

Examination record · first attempt
0/4
Class Twenty · Complete
Retain this much

Five things worth carrying out of this class

  1. Bicep makes intended infrastructure reviewable and repeatable; it still needs drift checks, correct parameters, and a tested recovery process.
  2. A Bicep file is params in, vars within, resources declared, outputs back out — a declaration of desired state, not a script of steps.
  3. Modules compose small files into big systems; passing one module's output into another lets Bicep work out the deployment order itself.
  4. What-if previews a deployment but can omit or misread parts; investigate unexpected changes and diagnostics before applying.
  5. targetScope decides where a file acts — resourceGroup (default), subscription, managementGroup, tenant — matched by az deployment group/sub/mg/tenant create.
Notes
  1. 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. ↩
  2. 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. ↩
  3. 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. ↩
  4. 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. ↩