Project — Infrastructure as Code
Declare the infrastructure for your deployed Census Income service as code, so it is
reproducible and reviewable instead of click-ops. Today that infra exists because you (or
M8) clicked it into being in a console; here you replace those clicks with a config file
(Terraform, OpenTofu, or your provider’s equivalent) and run a real apply against your
own cloud account. The bar is reproducibility and reviewability: the .tf (or config)
files in the repo are the proof.
| Dataset | Adult / Census Income — the same dataset and repo you carry through every module’s project |
| Start from | Fork dutchengineer-org/phase2-starter — carries the working deployment you built across M5–M8, and this module’s file is its infra/main.tf. Already forked it in an earlier ship module? Keep building in that fork |
| Aim for | The package matches the ml-pipeline-starter reference — you declare the infra, you do not rebuild the service or change the package. No version bump: the infra config lives outside the library entirely — you are versioning where it runs, not the code itself, so the package’s version is untouched |
| Done when | It passes every line of the rubric below, then you push to GitHub |
Declare the infrastructure for your deployed Census Income service as code, so it is
reproducible and reviewable instead of click-ops. Today that infra exists because you (or
M8) clicked it into being in a console; here you replace those clicks with a config file
(Terraform, OpenTofu, or your provider’s equivalent) and run a real apply against your
own cloud account. The bar is reproducibility and reviewability: the .tf (or config)
files in the repo are the proof.
| Dataset | Adult / Census Income — the same dataset and repo you carry through every module’s project |
| Start from | Fork dutchengineer-org/phase2-starter — carries the working deployment you built across M5–M8, and this module’s file is its infra/main.tf. Already forked it in an earlier ship module? Keep building in that fork |
| Aim for | The package matches the ml-pipeline-starter reference — you declare the infra, you do not rebuild the service or change the package. No version bump: the infra config lives outside the library entirely — you are versioning where it runs, not the code itself, so the package’s version is untouched |
| Done when | It passes every line of the rubric below, then you push to GitHub |
Start here
- Continue in your Phase 2 fork →
dutchengineer-org/phase2-starter(or keep building in the one you already forked); this module’s file is itsinfra/main.tf - Work the tasks below in order — each maps to a lesson you just finished, and each runs a real
applyagainst your own cloud account - Check yourself against the rubric, then push and submit
By the end, someone could clone your repo, point the config at a fresh account, run one
apply, and stand up the same infrastructure your service runs on today.
The tasks
Do these in order; each maps to a lesson you just finished.
1. Declare one resource and apply it (from Lesson 1)
- In the Phase 2 repo, fill in the starter’s
infra/main.tfscaffold with a config for one real resource your service uses: a storage bucket, a container registry, a small VM, a DNS record, whatever the smallest real piece is. - Run
terraform initthenterraform applyagainst your own cloud account. The resource is created by the config, not by hand. - Commit the
.tffiles. The created resource and the config now match.
2. Replace the click-ops with code (from Lesson 2)
- Take a piece of the service’s infra that currently exists only because someone clicked
it (the thing M8 set up by hand) and express it as code in
infra/: region, sizing, names, and settings written down, not remembered. - Anything a reviewer would need to recreate it lives in the config. No “and then go to the console and toggle X” steps left implicit.
3. Separate application from infrastructure (from Lesson 3)
- Keep the two deploys distinct: the infrastructure (what the config provisions: the bucket, the registry, the host) versus the application (the model artifact / service image you ship onto it).
- The repo makes the split obvious:
infra/declares the platform; it does not bake a model version or app build step into a resource definition. Changing the app does not require an infraapply, and vice versa.
4. Read a plan before you apply (from Lesson 4)
- Change one thing in the config (rename a resource, alter a setting that forces
replacement) and run
terraform plan. Read the plan: identify whether it shows~ update in place,+ create, or the dangerous-/+ destroy and recreate. - Write a short note in the repo (
infra/PLAN_NOTES.mdor your README) explaining what one specific change would do and which line of the plan tells you it is destructive, then revert to the safe version. State is understood, drift is named, and no surprise destroy ships.
Hints
- Pick the smallest real resource for Task 1: a bucket or a DNS record applies in
seconds and costs nothing. You are proving the
init→plan→applyloop works on your account, not building a production estate. - The fastest way to find your click-ops (Task 2): list what your service touches in the console, then ask “if this account were wiped, what is written down that recreates it?” Everything that is not written down is your task.
- For Task 4, the destructive signal is the
-/+marker (or “must be replaced”) in the plan output; provoke it on purpose with a name change so you learn to spot it, then back the change out before applying.
Rubric — your project is done when
This is the standard the module holds you to (each bar maps to the lesson that taught it):
- Infra is version-controlled code, not click-ops — the service’s infrastructure is declared in committed config files; a wiped account could be rebuilt from the repo. (Lessons 1, 2)
- One real
applyis reproducible —initthenapplyagainst a clean account provisions the resource from the config alone, with no manual console steps. (Lesson 1) - Application and infrastructure are separated — the config provisions the platform; the model/app is shipped onto it separately. A reviewer can tell which is which. (Lesson 3)
- A reviewer can read intent from the code — from the config alone, a teammate can say what exists and what changing one thing would do, with no console login needed. (Lessons 2, 3)
- You can read a plan and catch a destroy — you can run
plan, distinguish update-in-place from destroy-and-recreate, and name a destructive change before applying it. (Lesson 4)
Run the plan self-check from Lesson 4 against your own config before you submit: change one setting, read the plan, and confirm you can predict whether it updates in place or destroys and recreates, then revert.
Submit
Use the branch workflow from the M1 git lesson, not commits straight to main. Branch
off main (git checkout -b m9-infra-as-code), build this module’s piece there, and open a
pull request to merge it back once it meets the rubric. main stays the last-good version
of the product you carry forward, so a half-finished module never breaks what later
modules build on.
When your repo meets every rubric line, merge your branch to main, push it to GitHub,
and submit the repository URL here. (Submission coming soon.)
Coming soon
This lesson is not published yet. Join the waitlist to hear when it ships.
Coming soon