Capstone Project
Why this matters
This is where the whole track converges. Everything you did across modules 00-07 in isolation — providers, state, variables, outputs, data sources, modules, remote state, and provisioning real networking and AKS+ACR — comes together into one coherent, multi-module Terraform configuration that stands up a complete environment and tears it back down with single commands. There is no new concept and no quiz here; the goal is to prove you can compose what you've learned into infrastructure a real team would recognize, and then destroy it cleanly and confirm nothing's left billing.
Treat this as a project, not a checklist of isolated exercises — the pieces depend on each other in the order you'd actually build them.
The project
Build a small but complete Azure environment entirely in Terraform, organized as a root module that calls at least two child modules, with its state in a remote Azure Storage backend. It reproduces — declaratively, and now integrated — the by-hand work of tracks 5 and 7.
Your configuration must provision:
- A remote-state backend (module 05): an Azure Storage account +
container, bootstrapped with
az, holding this project's state under its ownkey. Local state is not acceptable for the capstone. - A
networkmodule (modules 04-06): a VNet with at least two subnets and an NSG with sensible rules, associated to the appropriate subnet — the track 5 stack, packaged as a reusable module with clear inputs and outputs (at minimum, it must output the subnet IDs the cluster needs). - A
platform(oraks) module (modules 04-06): an ACR and an AKS cluster placed in the network module's subnet, with the AKS↔ACRAcrPullrole assignment wired up — the track 7 stack, as a module. - A root module that: configures the provider and remote backend, calls
both child modules, passes the network module's subnet output into the
platform module (so the wiring is a real reference, not a hard-coded
string), and is driven by variables (project name, environment,
region, address space, node size/count, ACR name) with a committed
terraform.tfvarsfor non-secret values. - Sensible outputs at the root: at least the resource group name, the
ACR login server, and the cluster's kubeconfig (marked
sensitive).
Then use it, prove it, and destroy it — see the acceptance checklist.
Acceptance checklist
Work top to bottom; each item depends on the previous ones actually working, not just existing.
- A remote backend exists: an
az-bootstrapped storage account + container, and your root config'sterraform { backend "azurerm" }points at it with a project-specifickey.terraform initsucceeds against it and a state blob appears in the container. - The configuration is genuinely multi-module: a root module calling
a
networkchild module and aplatform/akschild module, each in its own directory withvariables.tf/main.tf/outputs.tf. Neither child module configures aproviderblock (only the root does). - The
networkmodule creates a VNet with ≥2 subnets and an NSG associated to a subnet, and outputs the subnet ID(s) the cluster uses.terraform state listshows these undermodule.network.. - The
platformmodule creates an ACR and an AKS cluster placed in a subnet produced by the network module — the subnet ID is passed in as a module input, referenced (not hard-coded). - The AKS↔ACR attach is an explicit
azurerm_role_assignmentgranting the cluster's kubelet identityAcrPullon the ACR.az role assignment list --scope <acr-id>confirms it. - Everything is parameterized by variables with a
terraform.tfvarsfor non-secret values; no environment-specific names, regions, or CIDRs are hard-coded inside resource blocks. Deploying a second environment would be a different.tfvars, not edited resource code. - Root outputs expose at least the resource group name, the ACR
login server, and a
sensitivekubeconfig.terraform output -raw kube_configyields a working config:kubectl get nodesshows all nodesReady. - You read the plan before applying and it matched your expectation
(you can state the
N to addyou predicted and got). No surprise destroy-and-recreate slipped through. - An image pushed to the ACR is pullable by the cluster with no image pull secret (proving the AcrPull attach works end to end).
-
terraform destroytears the entire environment down in one command; afterwardaz aks list -o tableis empty, the workload resource group and the AKSMC_*group are gone, and you've separately removed the state-backend storage group.az group list -o tableshows nothing from this project remaining. - You can explain, for every resource, what it corresponds to in the by-hand track 5/7 work and why it's ordered where it is in the dependency graph. If you can't explain a piece, that's a signal to go back and understand it, not to leave a copy-pasted block that happened to apply.
Hints
- Bootstrap the backend first, separately. The state storage account
can't be created by the config that uses it (module 05's chicken-and-egg).
Create it with
azup front, note the exact account name andkey, then write your backend block. Getterraform init -migrate-state(or a clean init against the empty backend) working before you add the expensive resources. - Build in dependency order, applying as you go. Get the network module
applying on its own first, then add the platform module, then the AcrPull
wiring — don't write all three modules and debug a 20-resource plan in one
shot. Each
applythat works is a checkpoint you can build on. - Pass the subnet in as a reference, not a string. The whole point of
the multi-module design is that
module.platformreceivesmodule.network.subnet_ids[0](or similar) as an input — that reference is what makes Terraform order the network before the cluster. A hard-coded subnet ID would apply but defeats the exercise. - Read every plan's summary line. This capstone creates an AKS cluster;
a
count/for_eachslip on a node pool or subnet list can multiply billable resources. TheN to addline is your last defense — predict it, then confirm the plan matches before typingyes. - Keep the cluster small and destroy promptly. Two burstable nodes is plenty. This is real spend; don't leave it running while you write up your notes.
- Reuse names and patterns you already validated in modules 04-06 rather than inventing new configuration — the capstone is about integration, not fresh discovery.
Final cleanup
This is the end of the track's real-Azure-spend. Clean up deliberately.
- Confirm what you're about to delete:
terraform state list(everything Terraform manages) andaz resource list -g <your-workload-rg> -o table. - Destroy the whole environment:
terraform destroy— review the destroy plan, confirm it includes the AKS cluster, thenyes. - Remove the state-backend storage separately — it's a persistent resource
terraform destroywon't touch:az group delete -n <your-tfstate-rg> --yes --no-wait. - Final sweep across the subscription:
az aks list -o table(empty) andaz group list -o table(nothing from this project). An empty result from both is your signal you're no longer being billed for any of it.
Before you move on
Once everything is torn down, don't consider this finished yet. Wait a few
days, then — with no notes, none of the earlier modules open, and none of
the HCL in front of you — rebuild the entire capstone from memory: the
bootstrapped remote backend, the two child modules with clean inputs and
outputs, the root wiring the subnet from the network module into the AKS
cluster, the AcrPull attach, sensible variables and outputs, a working
kubectl get nodes, and a clean one-command destroy. Rebuilding the whole
structure cold — and noticing exactly where you stall (Was it the backend
bootstrap? The kubelet identity reference? The module output wiring?) — is
the truest retention check there is. Tear it all down again afterward and
confirm the subscription is clean.
Next
You've now taken the infrastructure you built by hand in tracks 5 and 7 and
made it reproducible, reviewable, and destroyable as code — the foundation
every later platform-engineering track assumes. The immediate next step is
10-cicd-and-gitops, which lifts
the terraform plan/apply you've been running by hand into a real
pipeline: a PR runs fmt/validate/plan and posts it for review, a merge
runs apply of that exact plan with an OIDC-federated identity no human
holds — exactly the pipeline shape you sketched in module 07, now built for
real and paired with GitOps for the application layer on top of the AKS
cluster this track taught you to provision.