Week 14 - Infrastructure as Code
Why use Infrastructure as Code
Time to write infrastructure as code. In this chapter you author a real Bicep template that provisions a storage account (the same kind of resource you used for blob storage in Week 6, except now you create it as code), and you deploy it with the Azure CLI.
Bicep is a small language. You will meet its core building blocks in this chapter: resources (cloud objects you declare to create or update), variables (values computed inside the template for reuse), and outputs (values the deployment returns after success). Parameters are the deploy-time inputs from IaC concepts. You will also run the two CLI commands that log you in and deploy.
By the end of this chapter, you should be able to:
resource, param, var, and outputaz login and deploy with az deployment group createBicep is not Python or JavaScript. Those are general-purpose languages: you can build apps, scrape APIs, train models, or write games with them. Bicep is a DSL (domain-specific language): a small language built for one job.
Bicep's job is declaring Azure resources and their settings (storage accounts, containers, tags, and so on) so Azure Resource Manager can create or update them. You will not write loops that talk to the internet, or define classes, or run a web server in a .bicep file. You declare what should exist, deploy it, and let ARM do the work.
That limit is the point. A short vocabulary (param, resource, var, output, modules later) is easier to review in a pull request than an open-ended Python script that calls the Azure SDK. If you catch yourself wanting "real programming" inside the template, you are usually looking at the wrong tool for that step.
Work in the azure-bicep-reference repo (not the graded assignment). Clone once, then:
git clone <https://github.com/lassebenni/azure-bicep-reference.git>
cd azure-bicep-reference
git switch week-14-ch-3-bicep
Before you edit the file, install Microsoft's Bicep extension in VS Code. Open the Bicep extension on the Visual Studio Marketplace, or use the Extensions view / Command Palette → "Extensions: Install Extensions" and search Bicep (publisher Microsoft, id ms-azuretools.vscode-bicep). It gives you red/yellow squiggles for bad types and property names, plus autocomplete for resource types: the same help the struggle video shows later.

Visual Studio Marketplace card for the Microsoft Bicep extension (ms-azuretools.vscode-bicep), with Install.
Fill in main.bicep from the sample below (or the TODOs on the branch). When stuck, compare with week-14-ch-3-bicep-solution.
A Bicep template is a text file (.bicep) that declares what should exist. Here is a complete one that creates a storage account:
// main.bicep: save this file, then deploy it with az (next section)
// param = input at deploy time
param location string = resourceGroup().location // default: same region as the resource group
param storageName string // required: pass with --parameters storageName=...
// var = value reused inside this file (not passed at deploy time)
var storageKind = 'StorageV2'
// resource = what should exist (type + API version after @)
resource storage 'Microsoft.Storage/storageAccounts@2026-04-01' = {
name: storageName // from the parameter above
location: location // from the parameter above
sku: {
name: 'Standard_LRS' // cheapest general-purpose tier
}
kind: storageKind // from the variable above
}
// output = value returned after a successful deploy
output storageId string = storage.id
Four building blocks, all visible above:
param declares an input you supply at deploy time. storageName has no default, so you must pass it; location defaults to the resource group's region.resource keyword. The string 'Microsoft.Storage/storageAccounts@2026-04-01' is the resource type and API version (the date suffix that pins the ARM schema); the object after = is its configuration.var (here storageKind).resource (storage here) is the symbolic name: how this file refers to the resource. It is not the name in Azure, which is name:. You choose it like a variable name: letters, digits and _, not starting with a digit, unique within the file. It never reaches Azure at all, because the compiler drops it when it emits ARM JSON.output, here the storage account's resource ID, so other templates or scripts can use it.https://gist.githack.com/lassebenni/8a0ec8a6ebf40d200372575174451e39/raw/week_14__bicep_anatomy_visual.html
That is the whole language surface you need to start. Everything else is more resource types with more properties.
One question this raises if you are used to Python: where did resourceGroup() come from, and why is there no import at the top?
Bicep has no imports for this. The functions ship with the language, so resourceGroup(), uniqueString(), subscription() and the rest are simply available. Resource types are not part of the language at all: they come from Azure, and every one publishes a schema listing its properties and API versions.
So you never memorise this, you look it up. Three places, in the order you will actually reach for them:
| You need | Go to |
|---|---|
| What properties does this resource type take? | Bicep resource reference |
| What functions exist, and what do they return? | Bicep functions |
| The answer while you are still typing | The Bicep VS Code extension: Ctrl+Space completes types, API versions, and properties |
The extension is the fastest of the three because it reads the same schema the reference pages are generated from. If it does not offer a property, that property does not exist for that resource type, which makes it a quick way to check whether an example you found online (or an LLM suggested) is real.
Bicep templates deploy through the Azure CLI. You need a recent CLI with Bicep support (built in since Azure CLI 2.20+). Check with az bicep version. If that command is missing, upgrade the CLI or run az bicep install.
<aside>
📘 Recap from Week 6: az login and reading az output are the same skills from Azure CLI and the portal. This week you use them to deploy a .bicep file instead of listing resources someone else created.
</aside>
First authenticate:
az login
az login opens a browser for interactive sign-in, which is right for local development. Pipelines use a different non-human identity; Bicep in practice covers that when you need it.
Then deploy the template into a resource group. Your student account cannot create resource groups, so deploy into the shared class group rg-hyf-students.
The command looks like one phrase, az deployment group create, but each word is a separate idea:
You are not creating something called a "deployment group." You are creating a deployment at resource-group scope. Bicep needs that because Azure does not run .bicep files by itself. ARM must receive a deployment (template + parameters + target group) and apply it. The class already has rg-hyf-students; your deploy only creates resources inside it.
Worth being precise about what happens where, because half of it never touches Azure:
https://gist.githack.com/lassebenni/9d7d895b7d9d84a82ece2cb4d91f1a32/raw/week_14__deploy_pipeline_visual.html
The Azure CLI ships the Bicep compiler, so turning your .bicep into ARM JSON happens on your machine, with no network and no login. (Try it: az bicep build --file main.bicep --stdout works signed out.) Azure receives that JSON, never the .bicep file. One detail that surprises people: resourceGroup().location is not resolved during compilation. It survives into the JSON as an expression, and Azure evaluates it at deploy time.
This is why every command names a place. Azure has no default: you saw the tenant, subscription, and resource group in Week 6, and each level is something you point at by name.
https://gist.githack.com/lassebenni/00775afb167c4c55ace0f41048f849d3/raw/week_14__scope_hierarchy_visual.html
The visual also names the sign-in errors that look like template bugs and are not. If az account show returns an id equal to your tenantId, or a name like N/A(tenant level account), you are signed in at tenant level with no subscription selected, and every deploy fails with SubscriptionNotFound no matter how correct your Bicep is.
The Azure CLI keeps one default subscription across every tenant you have ever signed into, so a personal Azure for Students account can quietly win. Fix it by choosing explicitly:
az account list --all --output table
az account set --subscription "<the shared HYF subscription>"
az account show --query "{name:name, id:id}" -o table
Do this before you blame the template. A wrong subscription produces SubscriptionNotFound; a right subscription without the class role produces AuthorizationFailed. Two different errors, two different fixes.
Run these commands from the repo root of azure-bicep-reference (the folder that contains main.bicep after Anatomy of a Bicep file):
cd azure-bicep-reference # skip if your terminal is already here
export CLASS_RG=rg-hyf-students # resource group that receives the resources
# az deployment group create = start an ARM deployment at resource-group scope
# --resource-group which resource group (rg-hyf-students)
# --template-file the Bicep file to compile and apply
# --parameters fill the required param in main.bicep
az deployment group create \
--resource-group "$CLASS_RG" \
--template-file main.bicep \
--parameters storageName=sthyf<yourname>
The command reads main.bicep, fills in the parameters, and compiles the Bicep to ARM JSON. Then it asks Azure to make reality match. It prints the deployment result, including any output values. Look for "provisioningState": "Succeeded". Exact CLI wrapping varies by version; the fields below are what matter:
{
"provisioningState": "Succeeded",
"outputs": {
"storageId": {
"type": "String",
"value": "/subscriptions/.../resourceGroups/rg-hyf-students/providers/Microsoft.Storage/storageAccounts/sthyfyourname"
}
}
}
Your full CLI printout is longer. If provisioningState is Succeeded and outputs.storageId points at your account name, the deploy worked.
<aside>
⚠️ Storage account names must be globally unique across all of Azure, 3 to 24 characters, lowercase letters and numbers only. sthyf<yourname> is a safe pattern. A name clash is the most common first-deploy error.
</aside>
With the naming rule in mind, deploy the template yourself.
<aside>
⌨️ Hands on: On branch week-14-ch-3-bicep, finish main.bicep, run az login, then run az deployment group create against $CLASS_RG with your own storageName. Confirm it reports Succeeded. Diff against week-14-ch-3-bicep-solution if you get stuck.
</aside>
If the flags feel noisy, walk through the same deploy in the terminal recording below.
https://gist.githack.com/lassebenni/d1f68e5e6541622a5cf116fad0c28e92/raw/week_14__az_bicep_deploy_terminal.html
Then open the Azure portal and find your storage account. Search for the storageName you used, or open $CLASS_RG and look under Resources. You should see the account with provisioning succeeded. In a shared class group you may also see classmates' accounts.

Azure portal Overview Essentials for storage account sthyfw14lab in resource group rg-hyf-students, showing Location westeurope and Provisioning state Succeeded. Your student account name will differ (sthyf…); look for the one you just deployed.
<aside>
💡 Recap: You declared a storage account in main.bicep, deployed it into $CLASS_RG with az deployment group create, and saw Succeeded. The live resource now matches the file.
</aside>
Every deployment is recorded. In the Azure portal, open the resource group and select Deployments (the portal blade that lists every deploy with its template, parameters, and result). Your run is listed there.
This history is one of the quiet benefits of IaC: an auditable record of every change, what was deployed, when, and with which inputs. Manual portal clicks never leave that behind.
Open a deployment and you get the template that ran, the parameters it was given, the outputs it returned, and the list of resources it touched.
In practice you will rarely need it. Two moments make it worth knowing: a deploy did something you did not expect and you want to see exactly what was sent, or you share a resource group and want to see what a teammate deployed while you were not looking. On rg-hyf-students the second one applies to you all week.

Azure portal Deployments blade for rg-hyf-students, listing a Succeeded deployment.
<aside> 🤓 Curious Geek: the API version in the type string
That @2026-04-01 on the resource type is the Azure REST API version, and pinning it is deliberate. Azure keeps old API versions working for years, so a template that deployed cleanly in 2023 still deploys the same way later, even after Azure adds new properties. Leaving the version out is not an option; pinning it is what makes your template reproducible over time, not just across environments.
</aside>
Details like that version string are exactly what a generated draft can get subtly wrong, so keep a critical eye on any help you get.
<aside> 💡 Using AI to help: Bicep resource-type strings and property names are easy to forget. An LLM is good at generating a starting template from "a Standard_LRS storage account in West Europe". Always check the result against the Bicep resource reference, because AI sometimes invents property names. (⚠️ no real data, no PII)
</aside>
https://lasse.be/simple-hyf-teach-widget/mcq.html?bank=week_14_ch3_bicep_quiz&embed=1
az deployment group create actually do?If writing and deploying the template still feels abstract, this short Microsoft walkthrough shows the Bicep VS Code extension (the one you installed above) and az deployment group create.
https://www.youtube.com/watch?v=atWVFV7Y4vY
The next chapter makes your template production-shaped: modules, secure parameters, what-if, and teardown. You are ready when:
Succeededparam, resource, var, output) in a Bicep fileaz deployment group create reference.Next up: Bicep in practice, where you refactor into modules, keep secrets out with secure parameters, preview changes with what-if, and tear resources down cleanly.