CAMPUX Cloud Bootcamp Phase Three · Class Twenty
Phase Three — DevOps Core
Reading 40 min · Drills 6 · 2 Labs
Aligned to AZ-400
Class Twenty

Infrastructure as code: Bicep

Everything you have built by clicking can be written down instead — and the moment it is text in the repository, the portal stops being the source of truth and starts being just one way to look at it.

§1

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, the environment cannot quietly diverge from what was agreed — and where it has, a deployment pulls it back. 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.

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

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.

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

§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-campux-prod \
    --template-file main.bicep

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.

§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'Policies and subscriptions across many subsaz deployment mg create
'tenant'Management groups at the top of the hierarchyaz 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.

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

Watch · Microsoft Learn

The official module, and a CAMPUX overview

Read or work the module first; watch the overview to see it move
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

CAMPUX overview video

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.

Lab 1 · Author and deploy

From a file to a real resource

~12 minutes · Azure CLI · a subscription you can create resources in

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.

  1. 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.
  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'
      properties: { allowBlobPublicAccess: false }
    }
    
    output storageId string = storage.id
    What to notice: uniqueString makes the name globally unique from the RG id, so the file works without you hand-picking a free name.
  3. 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.
  4. 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.
Lab 2 · Idempotency & change

Run it twice, then change one line

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

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
Note · a GRS upgrade and the storage account itself are billable while they exist — the final az group delete is what keeps this lab free. Run it before you walk away.
On the job

"Build me another one" becomes a parameter

You · Cloud Engineer · "set up another environment"

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

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. In what-if output, + creates, ~ modifies, and - deletes. A delete against a production database almost never reflects real intent — it usually means the resource was dropped from the template, or a scope or name is wrong, and applying the deployment would destroy it. 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.

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?
// storage-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, 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.

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

Five things worth carrying out of this class

  1. 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.
  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 is the plan before the change: + creates, ~ modifies, - deletes. Never apply what you have not first seen described — especially the delete lines.
  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 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.