Week 14 - Infrastructure as Code

Why use Infrastructure as Code

IaC concepts

Azure Bicep

Bicep in practice

Deploy Bicep from CI/CD

Practice

Assignment

Gotchas & Pitfalls

Glossary: Week 14

Career relevance: Week 14

History of IaC

Slides (PDF)

IaC concepts

Before you write a single line of Bicep, it helps to understand the ideas that make Infrastructure as Code work. They are what separate "a script that creates a resource" from "a template you can deploy a hundred times safely." This chapter covers the four that matter most: declarative desired state, idempotency, drift, and environments.

By the end of this chapter, you should be able to:

Declarative vs imperative

There are two ways to tell a computer to set up infrastructure.

An imperative (scripting how to reach the end state) approach lists the steps one by one: create the storage account, then create the container, then set this property. You have to handle "what if the resource already exists?" yourself.

After portal clicking, teams often took the next step: put Azure CLI commands in a bash file and commit it. You already used az in Week 6 to list resources. Creating them as a script looks like this:

<aside> ⚠️ Read only (illustrative script): do not run this block. The name is deliberately invalid (storage names allow no hyphens), so pasting it fails harmlessly instead of creating a resource nobody owns. az storage account create is what Bicep will do in later chapters. az storage container create --auth-mode login needs blob data-plane roles your student IaC role does not grant. Use the Hands-on below for a safe container practice.

</aside>

#!/usr/bin/env bash
# provision-storage.sh: each line is a step (illustrative only; do not run)
az storage account create \
  --name st-example-do-not-run \
  --resource-group "$CLASS_RG" \
  --location westeurope \
  --sku Standard_LRS

az storage container create \
  --name raw \
  --account-name st-example-do-not-run \
  --auth-mode login

Keeping the steps in a file is better than having them in your head: it lives in git and a teammate can read the steps. But it is still imperative, and the limit shows up as soon as you ask the script a question.

Run that account create a second time and it does not fail. Azure prints A storage account with the provided name is found. Will continue to update the existing account. and succeeds. That is convenient, and it is also the problem: the script quietly changed something and told you afterwards. Change --sku and it applies the new SKU the same way, with no chance to object.

Now ask the script what it would do before it does it. az storage account create and az storage account update have no preview or dry-run flag: they act, then report.

That is not a limitation of the CLI. The same az gives you a preview command on the deployment path, az deployment group what-if, which prints the diff and changes nothing. You will run it yourself in Bicep in practice; here it is only the contrast that matters. The difference is what the two commands have to work with: a deployment carries a declared desired state, so Azure can compare it against reality and describe the gap. A one-off create carries no such statement, so there is nothing to diff it against.

So the script can create and update, but it cannot tell you what currently differs from what you wrote. It also cannot notice a SKU someone changed in the portal, and every "only if it looks like this" rule is a case you must write and keep correct.

The recording below runs exactly that experiment against the class subscription: the same create twice, then a SKU change, then the preview that only the deployment path offers.

https://gist.githack.com/lassebenni/81b7f9633b36d2fbff623eb19667144c/raw/week_14__az_imperative_rerun_terminal.html

⌨️ Hands on: create a lab container

You can safely practise only the container step: create one blob container on the teacher-owned lab account.

Your student IaC role allows control-plane container create, list, and delete on sthyfw14lab inside $CLASS_RG (the container-rm commands below). It does not let you create resource groups. It also does not grant blob data-plane roles, so do not use az storage container create --auth-mode login here.

Sign in, create a uniquely named container, then list it. Running create a second time succeeds again (Azure updates the same resource); that is still an imperative step, not a desired-state declaration.

az login
export CLASS_RG=rg-hyf-students
export CLASS_STORAGE=sthyfw14lab
export MY_CONTAINER=lab-<your-github-handle>   # lowercase, e.g. lab-alice

az storage container-rm create \
  --storage-account "$CLASS_STORAGE" \
  -g "$CLASS_RG" \
  --name "$MY_CONTAINER" \
  --query name -o tsv

# Same create again: still succeeds (same container resource)
az storage container-rm create \
  --storage-account "$CLASS_STORAGE" \
  -g "$CLASS_RG" \
  --name "$MY_CONTAINER" \
  --query name -o tsv

az storage container-rm list \
  --storage-account "$CLASS_STORAGE" \
  -g "$CLASS_RG" \
  --query "[].name" -o tsv

Confirm the same result in the portal: open storage account sthyfw14labData storageContainers. You should see your lab-… name in the list (the screenshot uses the placeholder lab-yourhandle).

Azure portal Containers blade for sthyfw14lab, listing the blob container lab-yourhandle created with az storage container-rm create. Open your own account the same way: storage account → Data storage → Containers.

Azure portal Containers blade for sthyfw14lab, listing the blob container lab-yourhandle created with az storage container-rm create. Open your own account the same way: storage account → Data storage → Containers.

When you are done, delete your container so classmates are not left with leftovers:

az storage container-rm delete \
  --storage-account "$CLASS_STORAGE" \
  -g "$CLASS_RG" \
  --name "$MY_CONTAINER" \
  --yes

If the flags feel noisy, walk through the same create → re-run → list loop in the terminal recording below.

https://gist.githack.com/lassebenni/5cff19aeb9bc81488124265d95ef03dc/raw/week_14__az_container_lab_terminal.html

A declarative (describing the end state and letting the tool work out the steps) approach says: "there should be a storage account named X, in region Y, of tier Z." You do not say how to get there. The tool compares what you declared against what exists and works out the steps. Bicep is declarative, and so is almost every modern IaC tool. The same storage idea as one desired-state declaration looks like this:

resource storage 'Microsoft.Storage/storageAccounts@2026-04-01' = {
  name: 'sthyfdemo014'
  location: 'westeurope'
  sku: { name: 'Standard_LRS' }
  kind: 'StorageV2'
}

If you were to deploy that once, Azure would create the account. If you deploy it again without edits, that second deploy would do nothing because the account already exists. You will write and deploy real Bicep in the next chapter; here the point is only the shape: declare the goal, not the az create steps.

Imperative (az script) Declarative (Bicep)
You write Steps: create account, then container, then … Desired state: this storage should exist
Re-run Usually succeeds, but silently updates whatever you changed Safe: Azure applies only what still differs
Preview None: no way to ask what would change what-if prints the diff before you apply
Portal edit Script does not notice Re-deploy brings templated resources back in line
Remove a line Nothing happens; the resource stays Nothing happens either, teardown is explicit (Chapter 4)

<aside> 💡 You already know this split from data work: a SQL SELECT is declarative (you describe the result; the database plans and runs the steps), while Python that loops, filters, and joins by hand is imperative (you list each step yourself). Bicep is the SQL side for infrastructure.

</aside>

Declarative wins for infrastructure because you care about the end state, not the steps. You want "this should exist," and you want that to be true whether you are starting from nothing or from a half-built environment.

Azure Resource Manager

Portal clicks, az commands, and Bicep deploys look different. They still call the same Azure Resource Manager (ARM) (Azure's HTTP control plane for creating and updating resources).

The portal is a website on top of those APIs. The CLI wraps the APIs in commands. Bicep declares desired state, compiles to an ARM template (the older JSON Azure Resource Manager understands), and ARM applies it through those same APIs. For the longer portal → API → CLI story, see History of Cloud Computing.

Week 6's Curious Geek tip still applies: add --debug to any az command and you see the HTTP call to management.azure.com.

https://gist.githack.com/lassebenni/4ac2c3e927477c176431a27e1639a217/raw/week_14__az_debug_arm_terminal.html

That is the same control plane the portal and Bicep use.

https://gist.githack.com/lassebenni/24073d42b2c9ca092b73266059876e38/raw/week_14__arm_control_plane_visual.html

Idempotency

Idempotency (same correct result whether you run once or many times) means running the same operation many times has the same effect as running it once. Deploy your template when nothing exists, and it creates everything. Deploy the same template again, and the desired state already matches reality, so Azure makes no meaningful change. Deploy it after someone deleted a resource by hand, and it recreates just that one.

<aside> 🤓 Curious Geek: idempotency comes from math

The word "idempotent" comes from mathematics: an operation is idempotent if applying it twice gives the same result as applying it once. Pressing a floor button in an elevator is idempotent; pressing it five times does not summon the elevator five times. Infrastructure tools borrowed the term because it captures exactly the property you want: a deploy you can run again and again without piling up duplicate resources or errors.

</aside>

This is the property that makes IaC safe to run repeatedly, which is exactly what you want in an automated pipeline. Individual az create commands are often idempotent too, almost by accident. The difference is that a declarative deploy is idempotent by design across the whole set of resources at once, and it can show you the diff first, so re-running is not just survivable but predictable.

<aside> 💡 Idempotency is why you can put an IaC deploy in CI and run it on every merge without fear. The deploy is a statement of intent ("this is what should exist"), not a one-shot action, so re-running it is always safe.

</aside>

You have already met a variation of idempotency in PostgreSQL: CREATE TABLE IF NOT EXISTS which only creates the table if it does not already exist. The tiny script below is the same idea in plain Python: a desired set of tables, and a function that makes reality match it.

desired = {"trips", "zones"}
existing: set[str] = set()  # start empty, like a fresh database

def ensure_tables(desired: set[str], existing: set[str]) -> set[str]:
    """Make reality match desired. Safe to call again."""
    for name in sorted(desired):
        if name not in existing:
            print(f"create {name}")
            existing.add(name)
        else:
            print(f"skip {name} (already exists)")
    return existing

ensure_tables(desired, existing)  # creates both
ensure_tables(desired, existing)  # skips both; that second run is idempotency

<aside> ⌨️ Hands on: Paste the script into a Python REPL (or a scratch .py file) and run it. You should see create trips, create zones, then two skip lines. Then delete "zones" from existing by hand (existing.remove("zones")) and call ensure_tables once more: you should see skip trips (already exists) and then create zones. Only the missing one gets recreated, and that is drift correction in miniature: the same pattern Bicep uses on real Azure resources.

</aside>

Drift and how IaC handles it

Drift is the gap when live infrastructure no longer matches your template, usually because someone changed something by hand. IaC gives you two tools against it:

https://gist.githack.com/lassebenni/ded049e9b2e475345add924aed5f27de/raw/week_14__drift_detect_correct_visual.html

The discipline that follows: make changes by editing the template and deploying, never by hand in the portal. The moment you edit in the portal, your template is outdated.

Environments from one template

Because a declarative template describes desired state, you can deploy the same template with different parameters (inputs supplied at deploy time). That produces different environments (separate deploys such as dev, staging, prod). One template, deployed with environment = 'dev', 'staging', or 'prod', gives you three setups that are identical except where you intended them to differ (smaller database in dev, larger in prod). This is the reproducibility benefit from Why use Infrastructure as Code, made concrete: identical environments are a parameter change, not an afternoon of careful clicking.

https://gist.githack.com/lassebenni/767981cfdcbfdbd25c4acbb09d3a10d2/raw/week_14__environments_one_template_visual.html

Where Bicep sits

Bicep is Azure's native IaC language. It is declarative, and it compiles to ARM templates: the JSON those APIs deploy. You write readable Bicep; the tooling turns it into that JSON.

For a team all-in on Azure, Bicep is the simplest choice: Azure-native, readable, and no separate state file to manage. That is why the Data Track uses it. The multi-cloud alternative (Terraform) lives on IaC tool landscape; advanced Bicep patterns live on Advanced Bicep. You do not need them for this week's assignment.

Knowledge Check

https://lasse.be/simple-hyf-teach-widget/mcq.html?bank=week_14_ch2_iac_concepts_quiz&embed=1

If declarative desired state still feels abstract, this beginner tutorial shows IaC and Bicep as describing what should exist instead of click-by-click steps.

https://www.youtube.com/watch?v=hksEWvk9p-0

Those same concepts are what make AI-drafted templates usable rather than dangerous.

<aside> 💡 Using AI to help: An LLM can draft a declarative Bicep template from a plain-English description, but the concepts in this chapter are what let you judge whether the draft is actually idempotent and drift-safe. The understanding is the part you cannot outsource. (⚠️ no real data, no PII)

</aside>

Ready for the next chapter when

The next chapter has you write and deploy a real Bicep template. You are ready when:

Extra reading


Next up: Azure Bicep, where you write your first template with resources, parameters, and outputs, and deploy it with the Azure CLI.