genaipros← The path
Line D · CloudC10 · Automation · IaC & CI/CD

Cloud for GenAI · Line D · C10 of the curriculum

Automation: Infrastructure as Code & CI/CD

If it isn't code, it doesn't exist. Three sessions on declarative desired state, drift, and pipelines that make change boring — across AWS, Azure, GCP, and the cross-cloud layer above all three.

Three claims this stage will make you believe

Read these first — they are the lens for everything in S1–S3

Every stage so far has taught you what to build. This one teaches you how the thing gets built, every time, the same way, by a machine, with a receipt. The enemy has a name.

Claim 01

If it's not code, it doesn't exist

A resource you created by clicking is a resource nobody can review, nobody can diff, and nobody can rebuild. It exists in the console and nowhere else. When the person who made it leaves, it becomes load-bearing folklore.

Claim 02

Declarative beats imperative

Don't write the steps. Write the destination. A desired-state tool computes the steps itself, notices when reality has diverged, and can converge again. You already trust this instinct — it's what a Kubernetes manifest is.

Claim 03

CI/CD makes change safe, not just fast

Pipelines are not a speed feature. They are a risk feature: automated tests, a plan you must read, a human gate, a staged rollout, and an automatic way back. The goal is boring, frequent, reversible deployments.

The villain: the snowflake and the drift it hides

Name the failure before you learn the fix

Snowflake server A resource that has been hand-tweaked so many times, by so many people, over so long, that nobody knows how to recreate it — and everybody is afraid to touch it. It is unique, fragile, and undocumented. Like a snowflake: one of a kind, and it melts if you breathe on it.

Config drift The gap between what your definition says the infrastructure should be and what the infrastructure actually is. Drift is created by every out-of-band change: a console click, an emergency kubectl edit, a support engineer's "quick fix." Drift is how a managed resource quietly becomes a snowflake.

Drift is not a hypothetical. It is the ordinary consequence of a 3 a.m. incident. Someone widens a firewall rule to restore service, the incident closes, and nobody puts the rule back — or into the code. Six weeks later a routine deploy silently reverts the fix, or a compliance scan finds an open port that "isn't in our Terraform." Both outcomes came from the same root: two sources of truth.

The progression: click-ops → IaC → pipeline-driven

Each rung removes one specific class of failure. Nothing more, nothing less.

RUNG 0Click-ops
A human opens a console and fills in forms. The knowledge of what was built lives in that human's head and in a screenshot in a wiki page from 2023.
Removes nothingNot reviewable. Not repeatable. Not auditable beyond raw API logs. Environments diverge by construction — a human cannot fill 22 forms identically three times.
RUNG 1Infrastructure as Code
The infrastructure is a file in Git. A tool reads the file and makes reality match it. You review changes as a diff, in a pull request, like any other code.
Removes: irreproducibilityYou can now rebuild an environment from scratch, diff dev against prod, roll back to last week's infrastructure, and prove to an auditor who changed what and when.
RUNG 2Pipeline-driven
No human runs the tool. A pipeline runs it, as a machine identity, only from the main branch, only after tests pass, only after a review — and rolls the change out gradually.
Removes: the unreviewed applyKills "it worked on my laptop with my admin credentials." Adds automated tests, a plan gate, staged rollout, and SLO-triggered rollback. Speed with safety.

A team on Rung 1 that still applies from laptops has removed irreproducibility but kept the human-error surface. A team on Rung 2 with no tests has automated the delivery of bugs. The rungs are cumulative and the order matters.

The map

Three sessions. Click a card to jump.

What you'll be able to do at the end

The success criteria for this stage, stated as verbs

  • Write declarative IaC and explain state, drift, and plan/apply to someone who has never seen them.
  • Choose native IaC vs Terraform/OpenTofu deliberately, and articulate the licence split without hand-waving.
  • Design a CI/CD pipeline with build, test, and gate stages, and pick a deployment strategy on evidence.
  • Wire an SLO-gated automatic rollback that uses the SLIs and error budgets from C7.
  • Translate a CloudFormation/CDK idea into Bicep, and into Cloud Build + Terraform.
  • Rebuild your imperative Vertex AI deploy script as declarative, reproducible IaC with a model-version canary.

Field notes — what changed recently

Three things have changed recently.

Correction 01 · AWS

It's §20 only, not 19–20

In AWS guidance's AWS architecture guidance, §19 is "Data Transfer Costs and Optimization" — a cost section with no IaC content. All of the Infrastructure-as-Code and CI/CD material is in §20, "Infrastructure as Code and CI/CD" (§20.1 IaC intro through §20.14 case studies). S1 is anchored there.

Correction 02 · GCP

§15 confirmed — exact title

GCP guidance's a leading GCP architecture reference, §15: "Agile, DevOps, SRE, SDLC, and Other Important GCP Topics." Covers Agile/SDLC, DevOps with GCP, SRE, release management, plus Well-Architected pillars and the PCA case studies.

Correction 03 · Patterns

No IaC section — pulled three patterns instead

Confirmed: the patterns catalog's the cloud application patterns catalog has no IaC or CI/CD section. I pulled Application Package (immutable build artifact), External Configuration (build once, deploy many), and its Dev/prod parity framing as light scaffolding for immutable infrastructure in S1. Everything else in S1 comes from AWS §20 plus first principles.

Confirmed · Azure

§15 as planned

Mastering Azure, §15: "DevOps and CI/CD" — Azure DevOps Services, CI pipelines, CD pipelines, IaC with ARM templates and Bicep, DevTest Labs, Azure Pipelines for deployments, GitHub Actions with Azure, and Azure Monitor / Application Insights for DevOps.

Freshness ledger: where the materials are already wrong

Certification guides age badly in this domain. Six things to unlearn before you start.

What a source saysWhat is true as of mid-2026Where it bites
GCP's native IaC tool is Deployment Manager Dead. Cloud Deployment Manager reached end of support on 31 March 2026. Successor is Infrastructure Manager, which is managed Terraform, not a new language. The GCP source teaches DM as current, and lists it under "Toil Reduction." Answering "Deployment Manager" on a 2026 exam or in a design review is wrong.
Use Cloud Source Repositories for Git on GCP End of sale 17 June 2024. If your org hadn't enabled the API before that date, you cannot use it at all. New work goes to GitHub/GitLab or Secure Source Manager. The GCP source's SDLC table lists CSR under both "Plan" and "Develop." That path doesn't exist for a new project.
Terraform is open source (MPL 2.0) Terraform has shipped under the Business Source License 1.1 since v1.6 (Aug 2023) — source-available, not OSI open source. OpenTofu is the MPL-2.0 fork, under the Linux Foundation, in the CNCF since April 2025. IBM acquired HashiCorp in 2025. Every source here calls Terraform "open source" without qualification. That sentence is now a procurement-review problem, not a footnote. Full treatment in S1.
Terraform's S3 backend needs a DynamoDB table for locking No longer. Terraform 1.10 added use_lockfile (S3 conditional writes); 1.11 made it GA and marked dynamodb_table deprecated. Nearly every tutorial you'll find still provisions a lock table. It works, but it's a deprecated moving part you can delete.
AWS CodeCommit is being retired Reversed. AWS closed it to new customers in July 2024, then returned it to full General Availability on 24 Nov 2025 after customer pushback. A good lesson in itself: vendor roadmaps reverse. Don't architect around a rumour, and don't trust a 2024 blog post about a 2026 decision.
Deploy Bicep with the azure/arm-deploy action Superseded by azure/bicep-deploy (first-party Deployment Stacks support); arm-deploy carries a deprecation notice. In Azure Pipelines the new first-party task is BicepDeploy@0. The Azure source's walkthrough uses a downloaded publish profile as a GitHub secret — a long-lived credential. S2 replaces it with OIDC federation.
Verified against vendor documentation and release notes, July 2026. Each claim is linked at its point of use in S1–S3.

What this stage assumes you already have

Stages 1–9 are the raw material. IaC codifies them; pipelines deploy onto them.

C1
Resource hierarchies. IaC provisions into them — an account/subscription/project is a variable in your config, not a thing you click.
C2
Identity. A pipeline runs as something: a deployment service account, or better, a short-lived OIDC-federated identity. Secrets are injected at run time, never committed.
Stages 3–5
Compute, network, storage. These are the nouns your IaC declares. Not re-taught — referenced as the things being coded.
C6
App architecture. CI/CD deploys those microservices. Immutable infrastructure is the operational face of the stateless design you already learned.
C7
Reliability. SLIs, SLOs, error budgets. In S3 these stop being dashboards and become the automatic trigger that aborts a bad rollout.
C9
Migration. A landing zone is only repeatable if it's code. IaC is how a migration destination gets stood up twice identically.

Why this session exists

Session 1 · the problem, in plain words

You need the same inference stack in three environments — dev, staging, production. You build it by clicking. Six weeks later, staging works and production has a mysterious 4-second cold start, and no living human can tell you what is different between them.

That is not a story about carelessness. It is arithmetic. Standing up one GPU inference environment by hand is roughly 22 discrete console operations: a VPC and subnet, three firewall rules, a cluster, a CPU node pool, an L4 GPU node pool, an artifact repository, a service account, three IAM bindings, a weights bucket, a secret, a load balancer and its certificate, a dashboard, three alert policies, a DNS record. Each one is a form with four to eight fields.

Do that three times and you have performed 66 operations and made roughly 300 individual field choices, from memory, on three different days, possibly by three different people. The probability that all three environments came out identical is not low — it is zero. And when they differ, you cannot diff them, because there is nothing to diff.

Click one of the three environmentsThe console produces three unrelated artifacts. Nothing connects them, so nothing can compare them.
DAY 1 DAY 30 DAY 90 👤 22 CLICKS ×3 DEV 22 operations · no record STAGING 22 operations · no record PROD 22 operations · no record e2-standard-4, open :22 e2-standard-8, alerts on + preview flag, ??? NO SHARED DEFINITION nothing to diff · nothing to review · nothing to rebuild
Divergence is the default state of hand-built infrastructure. Not because engineers are sloppy, but because there is no artifact that the three environments have in common. Everything in this session exists to create that artifact.
Built on Stages 1–9

The 22 operations above are your previous nine stages: the resource hierarchy from C1, identity from C2, compute/network/storage from Stages 3–5, the service topology from C6, the alert policies from C7. This session does not re-teach any of them. It teaches the one thing that turns them from a memory into an artifact.

Core concept 1 — Declarative desired state vs imperative scripts

Vendor-neutral · the idea underneath every tool in this stage

IaCInfrastructure as Code. Defining and provisioning infrastructure through machine-readable files that live in version control, instead of through manual console or CLI actions. The file is the source of truth; the running infrastructure is an effect of the file.

ImperativeYou specify how: an ordered sequence of operations. "Create the bucket. Then create the service account. Then bind the role. Then push the image."

DeclarativeYou specify what: the end state you want. "A bucket named X exists, versioned, in region Y." The tool works out which operations are needed to get there from wherever things currently are.

1Intuition

Imperative is turn-by-turn directions. Declarative is a destination address.

Directions — "left at the lights, third right, park behind the blue van" — work perfectly once, from one starting point. Change any assumption and they break silently: the van has moved, so you park somewhere wrong and never notice. Worse, if you are already halfway there, following the directions from the top takes you somewhere absurd.

An address works from anywhere. It survives roadworks. And critically — you can hand it to someone else and they will end up in the same place, which is exactly what "reproducible" means.

You already have both in your own toolkit. Your Vertex AI deploy script — build image, push image, create endpoint — is directions. Your Kubernetes and KServe manifests are addresses. You wrote replicas: 3, not "start two more pods"; the control loop worked out the rest. Terraform is that same instinct, applied to cloud resources instead of pods.

2Mechanism

A declarative tool runs a reconciliation loop: it reads what you asked for, reads what exists, computes the difference, and executes only the difference. The loop is the whole trick.

The artifact is the mechanism — a desired-state block
resource "google_container_node_pool" "gpu" {
  name       = "l4-inference"
  node_count = 2              # the WHAT. Never "add one node".
  node_config {
    machine_type = "g2-standard-8"
    guest_accelerator { type = "nvidia-l4", count = 1 }
  }
}

Nothing in that block says create. It says exists, with these properties. Run it against an empty project and it creates. Run it against a pool that already has two nodes and it does nothing. Change 2 to 4 and it adds two. Same file, three different behaviours, because the file describes an end state rather than an action.

Click any stage of the loopThis is the engine underneath Terraform, CloudFormation, ARM, Kubernetes controllers, and Argo CD. Learn it once.
1 · DESIRED Your config .tf / .yaml / .bicep 2 · ACTUAL Live cloud APIs refresh 3 · DIFF Compare property by property 4 · PLAN Dry run +2 ~1 −0 5 · APPLY Execute diff only the delta 6 · RECORD State / stack LOOP — runs again on every change, and on a schedule to catch drift HUMAN GATE HERE
Six stages, one loop. The loop is why declarative tools can be run repeatedly without fear — and why a stalled or stale loop is dangerous, as the AWS outage in the Reality Check below shows.

3Trade-offs, limits, and where it breaks

  • Declarative cannot express time. "Create the cluster, wait for the operator to become healthy, then apply the CRDs" is a sequence, not a state. Tools bolt on escape hatches — provisioner, local-exec, null_resource, CloudFormation custom resources — and every one of them is a small imperative script hiding inside your declarative config. They are not idempotent, they are not planned, and they are the usual cause of "it worked the first time."
  • Ordering is inferred, not written. The tool builds a dependency graph from references. If resource B needs A but never references A, the tool may create them in parallel and B fails. You then reach for depends_on, which is a manual patch to an inferred graph — use it, but treat each one as a smell.
  • Idempotency is a property of the provider, not a gift. The tool promises to compute the right delta; the underlying API has to behave when it applies it. Some APIs genuinely are one-shot — uploading a model artifact, running a database migration — and no declarative wrapper makes them replayable.
  • Coverage lags the cloud. A service launched last week may have no resource type. You end up with a hybrid: mostly declarative, plus a thin imperative edge. That is acceptable. Pretending the edge does not exist is not.
  • Replace is the sharp edge. Some properties are immutable in the API. Changing them means destroy-then-create, and the plan will tell you — in small text, next to a resource holding your data. This is why nobody applies without reading the plan.

Core concept 2 — State: the tool's memory

Vendor-neutral · the concept people skip and then get burned by

StateThe tool's record of the resources it manages: a mapping from your name for a thing (google_container_node_pool.gpu) to the cloud's identifier for it (projects/p/…/nodePools/l4-inference), plus the property values as of the last run.

BackendWhere that state is stored. Local means a file on your laptop — fine for a tutorial, fatal for a team. Remote means shared, versioned, encrypted, and lockable storage: an S3 bucket, an Azure Storage container, a GCS bucket, or a managed service.

1Intuition

State is the receipt. You told the shop what you wanted; the receipt records what you actually walked out with and its serial numbers. Without the receipt, you cannot return anything, because you cannot prove which of the identical items on the shelf is yours.

Concretely: you write "a bucket named acme-weights." The cloud creates it and hands back an ID. Next run, the tool needs to know whether that bucket is the one it made (so a name change means rename) or someone else's (so a name change means create a new one and leave theirs alone). Only the receipt can tell the difference.

2Mechanism

Once you have state, you have three pictures of your infrastructure, not two — and every operation in IaC is a comparison between two of them. Internalise this triangle and drift, plan, and import all stop being mysterious.

Click any edge of the triangleThree sources of truth. Three comparisons. Every IaC command you will ever run is one of these three.
CONFIG what you want STATE what it last saw REALITY what exists now your change config ↔ state DRIFT state ↔ reality the true plan config ↔ reality A plan you did not expect is almost always drift arriving through the right-hand edge.
Why terraform plan sometimes shows changes you never made. Refresh pulled in drift, and the plan is reporting the total gap between config and reality — not just your edit.

3Trade-offs, limits, and where it breaks

  • State contains secrets in plaintext. A generated database password, a private key, a service account key — if the API returns it, it lands in state. State is therefore a secret. Encrypt the bucket, restrict read access as tightly as you restrict production, and never commit it to Git. OpenTofu additionally ships client-side state encryption (v1.7), which Terraform's open CLI does not have.
  • State is a shared mutable file, so it needs locking. Two concurrent applies against one state file corrupt it. For years the S3 backend needed a separate DynamoDB table for locks. Terraform 1.10 added use_lockfile using S3 conditional writes, and 1.11 made it GA and deprecated the dynamodb_table argument. Most tutorials you will find still provision the lock table — you can delete it.
  • State size is blast radius. One state file for the whole company means every apply risks everything and every plan takes ten minutes. Split by lifecycle and ownership: network in one, the GKE cluster in another, per-team workloads in their own. Cross-state reads via data sources or remote state outputs.
  • Losing state is recoverable but grim. The resources still exist; the tool has simply forgotten it owns them. Recovery means importing each one back. Versioned buckets make this a rollback instead of an archaeology project — turn on versioning before you need it.
  • Managed state is a genuine advantage of native tools. CloudFormation, ARM and Infrastructure Manager hold state for you: no bucket to bootstrap, no lock table, no chicken-and-egg. You trade control and portability for that. This is a real point in the native-vs-Terraform decision, not a detail.

Core concept 3 — Drift, plan/apply, and idempotency

Vendor-neutral · the daily working loop

1Intuition

Drift is an open window; the tool is a thermostat. You set 21°C — that is your config. The thermostat holds it. Someone opens a window. The room is now 17°C: reality has drifted from desired state. A good thermostat notices and acts. A bad one keeps insisting the setting is 21 while everyone freezes.

plan is asking the thermostat what it is about to do, before it does it. "I am going to run the heating for 40 minutes." You get to say "actually, close the window instead."

Idempotency is the promise that pressing the button twice does not make the room 42°C. Apply the same config ten times and you get one bucket, not ten.

2Mechanism

A plan is a diff with four verdicts per resource, and the symbols are worth memorising because every tool borrows them:

  + create            # resource is in config, not in reality
  ~ update in-place   # same resource, mutable field changed
-/+ replace           # immutable field changed → destroy THEN create
  - destroy           # in state, removed from config

Plan: 2 to add, 1 to change, 0 to destroy.

The line to fear is -/+ replace. It appears when you change a property the cloud API cannot mutate — a subnet CIDR, an instance boot disk image, a database engine version on some services. The plan is telling you, in the same neutral tone it uses for everything else, that it is about to delete a thing and build a new one. On a stateless inference pod that is fine. On the bucket holding your model weights it is a Tuesday you will remember.

IdempotencyAn operation you can repeat any number of times with the same end result as doing it once. apply is idempotent. curl -X POST /endpoints is not — run it twice, get two endpoints.

3Trade-offs, limits, and where it breaks

  • Not all drift is bad. If a cluster autoscaler changed node_count from 2 to 7 under load, reverting that is actively harmful — your next apply would scale production down mid-traffic. The fix is to tell the tool that field is not yours: lifecycle { ignore_changes = [node_count] }. Deciding which fields the platform owns and which you own is a real design decision, not a config detail.
  • Detection is not continuous by default. CloudFormation drift detection is an on-demand or scheduled check, not a live watch. Terraform only sees drift when someone runs a plan. If nobody plans for six weeks, you have six weeks of invisible divergence. Mature teams run a scheduled plan on a cron and alert on any non-empty diff.
  • Remediation is a choice with two right answers. Re-apply to revert the change (the config was right, the human was wrong), or update the config to adopt it (the human was right, the config was stale). Choosing "revert" reflexively is how you undo somebody's 3 a.m. incident fix at 9 a.m. on a Monday.
  • Prevention beats detection. The durable fix is IAM: production write permissions belong to the pipeline's identity, and humans get read-only plus a break-glass role that fires an alert when assumed. This is C2 material doing operational work.
  • Plans go stale. A plan computed at 09:00 and applied at 14:00 describes a world that may no longer exist. Good pipelines save the plan as an artifact and apply that exact plan, refusing if reality moved. Ignore this and you get the AWS outage in the Reality Check below — literally a stale plan overwriting a newer one.

Core concept 4 — Modules, and the CI/CD pipeline shape

Vendor-neutral · reuse, then delivery

ModuleA reusable, parameterised bundle of resources with inputs and outputs — a function, for infrastructure. You write "GPU inference environment" once with variables for size and region, then call it three times. The AWS analogues are nested stacks (composition) and StackSets (the same stack across many accounts and regions); Bicep calls them modules too.

CIContinuous Integration. Every commit is automatically merged into a shared branch, built, and tested. Its purpose is to find integration breakage in minutes rather than at the end of a quarter.

CD — two meaningsContinuous Delivery: every passing build is automatically made deployable and pushed to staging; a human clicks to release to production. Continuous Deployment: no human click — a passing build goes to production by itself. Most regulated teams do delivery. Saying "CD" without saying which one causes real arguments.

ArtifactThe immutable output of the build stage — a container image, a signed archive, a synthesised template. Built once, then promoted unchanged through every environment. Rebuilding per environment means production runs something staging never tested.

Stage / gateA stage is a phase of the pipeline (build, test, deploy-staging, deploy-prod). A gate is a condition between stages: tests green, plan reviewed, approver signed off, error budget not exhausted.

The shape below is universal. AWS, Azure and GCP each sell you the same seven boxes under different names, and S2 and S3 will simply relabel them.

01 · Commit — the only trigger

A push or a merged pull request starts everything. Nothing else does. The moment a human can trigger a production deploy by any other route, the pipeline has stopped being a control.

Two rules make this real: protected branches (nobody pushes straight to main) and required reviews. For IaC specifically, this is also where the plan gets posted as a PR comment so the reviewer approves an outcome, not a script.

02 · Build — produce the artifact once

Compile, containerise, or synthesise. For an inference service this is the Docker image with your vLLM server and its pinned dependencies. For IaC it is cdk synth, bicep build, or a saved terraform plan file.

The build must be hermetic — the same inputs give the same output, with no reliance on whatever happened to be installed on the runner. Pinned base images, locked dependency files, no latest.

03 · Test — fast feedback first

Ordered cheapest-and-fastest to slowest: lint and format, static analysis, unit tests, then integration tests. For IaC the equivalents are real and often skipped: validate, fmt, a security policy scan (Checkov, tfsec, CloudFormation Guard), and a plan against a scratch environment.

A pipeline whose first ten minutes are integration tests trains people to stop watching it.

04 · Publish — the immutable artifact

Push the image to a registry with an immutable tag — a digest or a commit SHA, never latest. Scan for vulnerabilities here, and sign it if you have a supply-chain requirement.

From this point on, nothing rebuilds. Every later stage refers to this exact digest. That single rule is what makes "we tested this" a true statement rather than a hopeful one.

05 · Deploy to staging — automatically

No approval needed. Staging exists to be broken. Deploy the artifact, run smoke tests and contract tests against the real deployed thing, and let it soak long enough for slow failures — memory growth, connection-pool exhaustion — to surface.

Staging is only useful to the degree it resembles production. This is the Cloud Patterns dev/prod parity idea: equivalent, separately provisioned, differing only by configuration injected at run time.

06 · Gate — the deliberate pause

The gate is what separates continuous delivery from continuous deployment. It can be a human approval, or it can be automated: tests green, plan reviewed, no open sev-1, change-freeze window respected, error budget not exhausted (C7 becoming a control here).

Manual gates cost lead time and should be reserved for changes that genuinely warrant judgement. A gate that everyone rubber-stamps within four seconds is theatre with a latency cost.

07 · Deploy to production — gradually

Never all at once. Choose blue-green, canary, or rolling based on the trade-offs below, shift traffic in steps, and watch a metric between steps. The rollout is not "run the deploy command"; it is "run the deploy command and then decide, repeatedly, whether to continue."

Every step must have a defined abort condition decided before the deploy, not improvised at 2 a.m.

08 · Observe — close the loop

Deployment is not done when the pipeline goes green; it is done when the SLIs have held for the bake period. Wire deploy markers into your dashboards so you can correlate a latency step-change with the exact release that caused it.

Feed the four DORA signals back: deployment frequency, lead time, change failure rate, failed-deployment recovery time. A pipeline you do not measure will slowly get worse and nobody will be able to prove it.

GitOpsAn operating model where Git is the single source of truth for the desired state of a system, and an agent running inside the target environment continuously pulls from Git and reconciles reality to match. It is the reconciliation loop from Concept 1, with Git as the input and no human in the apply path.

Push (classic CI/CD)

The pipeline reaches into the environment

CI holds credentials for production and calls its APIs from outside. Simple, works for anything, and it means your build system holds production write access — an attractive target, and a hard one to scope. Drift is invisible between runs, because nothing is watching.

Pull (GitOps)

The environment reaches out to Git

An in-cluster agent (Argo CD, Flux, Config Sync) polls the repo and applies changes itself. No outbound credentials to hold, and because the agent loops continuously, drift is corrected within minutes rather than discovered eventually. The cost: it fits Kubernetes-shaped resources far better than everything else.

You already have the mental model for pull-based GitOps: it is exactly what your KServe manifests do inside a cluster, extended to the cluster's own configuration. AWS ships a CloudFormation Template Sync Controller for Flux that applies the same idea to CloudFormation stacks, and CloudFormation Git sync does it natively.

Core concept 5 — The three deployment strategies

Vendor-neutral · this is the part that connects to C7

1Intuition

You are replacing the engines on a plane that is currently flying with passengers on board.

  • Rolling — swap one engine at a time. Cheap, no second plane needed, but for a while you are flying on a mix of old and new engines, and going back means swapping them all again, slowly.
  • Blue-green — build an entire second plane, move everyone across at once. Expensive (two planes) but reverting is instant: move everyone back.
  • Canary — move ten passengers to the new plane and watch them for twenty minutes before moving anyone else. Slowest, and the only one that finds problems that only appear with real passengers.

2Mechanism

Click a strategyAll three are traffic-routing decisions. What differs is how much capacity you pay for and how fast you can undo.
ROLLING replace in batches t0 t1 t2 COST ≈ 1.25x · ROLLBACK: SLOW Both versions live at once — they must be compatible. BLUE-GREEN flip 0% → 100% BLUE v1 100% → 0% GREEN v2 0% → 100% ROUTER — one switch COST ≈ 2x · ROLLBACK: SECONDS Every user hits v2 at once — a bad v2 hits 100% of them. CANARY 1% → 10% → 50% → 100% 1% · watch 15 min 10% · watch 15 min COST ≈ 1.1x · ROLLBACK: FAST + SMALL Slowest. Requires a metric and a threshold agreed beforehand.
A fourth exists and is worth naming: recreate — stop v1, start v2. Guaranteed downtime, zero version overlap. Correct for a batch job or a single-writer service where two versions must never run simultaneously.
The artifact is the mechanism — a canary traffic split
trafficSplit:
  - version: gemma-2b-v3   # incumbent
    weight: 90
  - version: gemma-2b-v4   # canary
    weight: 10
  abortOn: { sli: p95_latency_ms, threshold: 850, window: 15m }

3Trade-offs, limits, and where it breaks

DimensionRollingBlue-greenCanary
Peak capacity cost≈1.1–1.25× during overlap≈1.05–1.2×
Rollback speedSlow — roll forward againSeconds — flip the routerFast, and only the slice was exposed
Blast radius if v2 is badGrows with each batch100% instantlyBounded by the slice
Version overlapYes — must be compatibleBrief, or noneYes, for a long window
Needs a live metric + thresholdOptionalOptionalMandatory — otherwise it is just a slow deploy
Database schema changesHard — backward-compatible onlyHard — one DB, two coloursHardest — long overlap window
Best forStateless services, default choiceBig-bang cutovers, fast revert requiredHigh-risk changes; new model versions
The database row is the one people discover too late. None of these strategies solve schema migration — they all require the expand/contract pattern, because two versions of your code will be live at once.
  • Canary without a threshold is theatre. If nobody has agreed the abort condition in advance, the rollout becomes a group discussion at 2 a.m. and the change goes out anyway.
  • Canary can be blind by construction. A 1% slice is too small to move a global error rate. You must measure the canary's own SLI, not the blended one. S3 has a calculator that makes this uncomfortably concrete.
  • Blue-green doubles cost, and on GPU that is not a rounding error. Two full L4 fleets for an hour is a real invoice. Canary is usually the right answer for accelerated workloads for exactly this reason.

Core concept 6 — Immutable infrastructure and secret injection

Vendor-neutral · two short ideas that make the rest work

Immutable infrastructure

Never patch a running thing — replace it

A server is never upgraded in place. You build a new image, launch new instances from it, shift traffic, and destroy the old ones. Cattle, not pets.

This is what makes rollback trivial: the previous version still exists as an artifact, so going back is a deploy, not a repair. It also kills drift at the source — you cannot hand-tweak a thing you never log into.

the patterns catalog's the cloud application patterns catalog frames the same idea as the Application Package: one immutable build artifact deployed unchanged into every environment. If deployment recompiles anything, what runs in production is not what was tested.

External configuration & secret injection

One artifact, many environments

If the artifact is immutable but environments differ, the difference must live outside the artifact — injected at run time as environment variables or mounted secrets. Same pattern source calls this External Configuration.

Secrets never live in code, in the image, or in state. The pipeline authenticates as a workload identity — increasingly via OIDC federation, where the CI system presents a short-lived, cryptographically verifiable token instead of a stored long-lived key — and pulls the secret at deploy time from Secrets Manager, Key Vault, or Secret Manager.

This is C2 doing operational work. The single highest-value change most teams make to a pipeline is deleting a long-lived cloud credential from their CI secret store.

Decision: native IaC or Terraform/OpenTofu?

Follow "no ↓" until a "yes" exits right. The bottom-left box is the default.

Guard clauses · choosing an IaC tool for a new project

Answer honestly about the project you actually have, not the multi-cloud future you might have.

Guard 1Will this configuration ever need to provision resources in more than one cloud, or in a cloud plus a SaaS provider (Cloudflare, Datadog, GitHub, Snowflake)?
YES →
Terraform or OpenTofu. This is the case native tools genuinely cannot serve. One graph, one plan, one apply across providers. Nothing else does this.

no ↓

Guard 2Does your organisation already run a Terraform platform — module registry, state backends, policy-as-code, a platform team, people who know it?
YES →
Terraform/OpenTofu. Tool consistency beats per-cloud optimality. A second toolchain doubles your review surface, your CI wiring, and your on-call knowledge burden.

no ↓

Guard 3Do you need a brand-new service the week it launches, or a preview feature with no provider coverage yet?
YES →
Native. CloudFormation and ARM/Bicep track their own APIs at launch. Providers lag by weeks or months. Nothing is more frustrating than a resource type that does not exist yet.

no ↓

Guard 4Is OSI-approved open-source licensing a hard procurement or legal requirement — or do you intend to embed the tool in a product you sell?
YES →
OpenTofu specifically, not Terraform. MPL 2.0, Linux Foundation governance, CNCF project. Terraform since v1.6 is BUSL 1.1 — source-available, and not open source by OSI definition.

no ↓

Guard 5Is this a Kubernetes-centric platform where the team already lives in manifests and runs a GitOps agent?
YES →
Consider the K8s-native path — Config Connector / Config Controller on GCP, ACK on AWS, ASO on Azure. Cloud resources become CRDs your existing agent reconciles. Usually alongside Terraform for the cluster itself, not instead of it.

no ↓

Default — bottom left Use the cloud's native IaC. Single cloud, no existing Terraform platform, no licence constraint: CloudFormation/CDK, or Bicep, or Terraform-via-Infra-Manager on GCP. You get managed state with nothing to bootstrap, day-one API coverage, native IAM integration, and no third-party supply chain. "Multi-cloud someday" is not a reason to pay a portability tax today — and if that day comes, importing into Terraform is a real, well-trodden path.

The four dialects, side by side

Same ideas everywhere. Learn the row, then the column is just vocabulary.

ConceptAWS nativeAzure nativeGCP nativeTerraform / OpenTofu
IaC languageCloudFormation (YAML/JSON); CDK in TypeScript/Python/Java/Go, which synthesises to CloudFormationARM templates (JSON); Bicep, a DSL that transpiles to ARMTerraform HCL is the recommended path. Deployment Manager reached end of support 31 Mar 2026HCL, one language across every provider
Unit of deploymentStack; nested stacks to compose; StackSets across accounts/regionsDeployment; Deployment Stack tracks a managed set of resourcesDeployment (Infra Manager); Config Connector applies K8s-style manifestsRoot module + child modules; workspaces per environment
Where state livesServer-side, managed by CloudFormation. Nothing to bootstrapServer-side in Azure Resource Manager. Deployment Stacks add explicit ownership trackingServer-side in Infra Manager (which stores the Terraform state for you)You own it. Remote backend + locking. Power and responsibility
Dry runcreate-change-setwhat-ifterraform plan (via Infra Manager preview)terraform plan / tofu plan
Drift detectionBuilt-in stack drift detection, on demand or scheduledDeployment Stacks flag out-of-sync; Azure Policy for compliance driftPlan-based; Config Connector reconciles continuouslyPlan-based refresh. Run it on a cron and alert on non-empty diffs
Adopt existing resourcesIaC generator scans and generates templates; cdk migrateExport template from portal; bicep decompileterraform import; the old DM Convert tool targeted Terraformimport blocks and -generate-config-out
CI serviceCodeBuildAzure Pipelines or GitHub ActionsCloud BuildTool-agnostic — runs anywhere, including all three
CD / release serviceCodePipeline orchestrating CodeDeployAzure Pipelines stagesEnvironments with approvalsCloud Deploy — delivery pipelines, targets, canary, auto-rollbackNot a CD tool. Pair with the above, or Argo CD/Flux/Spinnaker
Artifact registryAmazon ECR; CodeArtifactAzure Container Registry; Azure ArtifactsArtifact Registryn/a — Terraform consumes registries, does not host them
Git sourceGitHub/GitLab, or CodeCommit (back to full GA Nov 2025)Azure Repos or GitHubGitHub/GitLab or Secure Source Manager. Cloud Source Repositories closed to new customers Jun 2024Any Git host
LicenceProprietary service; CDK is Apache 2.0Proprietary service; Bicep is MITProprietary serviceTerraform: BUSL 1.1 (source-available). OpenTofu: MPL 2.0 (OSI open source)
Lock-in profileDeep AWS coupling; day-one coverageDeep Azure coupling; day-one couplingGoogle chose the portable tool as its own recommendationPortable syntax. Resource definitions are still cloud-specific — you port the skill, not the config
The last row is the one people misread. Terraform does not make your infrastructure multi-cloud; an aws_instance does not become a google_compute_instance. What ports is the language, the workflow, the module patterns, and your team's expertise.

AWS: CloudFormation, CDK, and the Code* pipeline

AWS §20 · every service named with what it IS and which layer it sits in

AWS CloudFormation — the declarative IaC service (provisioning layer)

What it is: AWS's native declarative IaC service. You write a template in YAML or JSON describing resources; CloudFormation creates and manages them as a stack. State is held server-side — there is no state file for you to lose.

The vocabulary

  • Template — the file. Sections: Parameters (inputs), Mappings (lookup tables, typically per region), Conditions, Resources (the only required section), Outputs (values other stacks consume).
  • Stack — a deployed instance of a template. Delete the stack and every resource in it goes, which makes teardown genuinely one action.
  • Change set — CloudFormation's plan. Shows what will be created, modified, deleted, and critically whether anything requires replacement. The material's example is exactly right: a change set warning that an RDS update means replacement, and therefore downtime, is the difference between a Tuesday and an incident.
  • Nested stacks — modularity. A parent template references child templates via AWS::CloudFormation::Stack. Networking, security, and application layers become separately maintainable units.
  • StackSets — one template deployed and maintained across many accounts and regions in a single operation. This is the landing-zone tool from C9.
  • Stack policies — a guard on specific resources during updates. Stop a routine change accidentally replacing the production database.
  • Drift detection — compares live resource properties against the template, reporting each as in-sync, modified, or deleted. On demand or scheduled, not continuous.
  • Rollback — on a failed create or update, CloudFormation reverts the whole stack to its last known good state automatically.

Two things newer than the material

  • IaC generator — scans an account for resources created outside IaC and generates a template plus a managed stack for them. This is the supported escape route out of click-ops, and it is the answer to "we already have 200 hand-built resources."
  • Git sync — connect a stack to a Git repository and CloudFormation deploys template changes on commit, no pipeline required. Native GitOps for stacks.
AWS CDK — Cloud Development Kit (an abstraction above CloudFormation)

What it is: a framework for defining infrastructure in a real programming language — TypeScript, Python, Java, Go, C# — which synthesises down to a CloudFormation template. CDK is not a separate provisioning engine. The engine is still CloudFormation; CDK is a template generator with good ergonomics.

  • Construct — the basic building block, a cloud component. L1 constructs map one-to-one to CloudFormation resources. L2 add sensible defaults and helper methods (an L2 S3 bucket encrypts itself and gives you grantRead). L3, or patterns, assemble whole architectures — a load-balanced Fargate service in a handful of lines.
  • Stack — the deployment unit, becoming one CloudFormation stack. App — the root, containing one or more stacks.
  • cdk synth — generate the template. cdk deploy — synth then deploy. CDK Pipelines — a construct that builds a self-mutating CodePipeline for your CDK app.
  • cdk migrate — turn an existing template, deployed stack, or IaC-generator output into a CDK app.

The honest trade-off. CDK gives you loops, conditionals, type checking, unit tests, and IDE completion — genuine advantages over 900 lines of YAML. In return you accept a build step between your source and your infrastructure, a synthesised template you must still read, and the fact that a general-purpose language lets you write infrastructure code nobody can follow. Loops and abstraction are exactly the features that make a diff hard to review, and reviewability was the point.

Where CDK sits on the declarative/imperative line: you write imperative code that generates a declarative artifact. The apply is still fully declarative. This is a useful distinction to be able to state cleanly in an interview.

CodePipeline, CodeBuild, CodeDeploy — the delivery layer
  • AWS CodePipeline — the orchestrator. Defines the stages and their order, and calls other services to do the work. Handles source triggers, artifact passing between stages, and manual approval actions.
  • AWS CodeBuild — the build service. Runs your build and test commands in an isolated managed container from a buildspec.yml. This is where compilation, tests, container builds, and IaC validation run. It can also host managed self-hosted runners for GitHub Actions, which is a useful hybrid: GitHub Actions as the workflow language, CodeBuild as the compute inside your VPC.
  • AWS CodeDeploy — the deployment service. Puts the artifact onto EC2 instances, on-premises servers, Lambda, or ECS. Two strategies, in AWS's own vocabulary: in-place (AWS's name for rolling — update the existing instances in batches) and blue/green (provision a replacement fleet, shift traffic, keep the old one available for rollback).
  • Lifecycle hooks — CodeDeploy's extension points around each deployment step: BeforeInstall, AfterInstall, ApplicationStart, ValidateService. ValidateService is the one that matters — it is where your health check runs, and where a failure triggers automatic rollback.
  • Deployment groups — the set of targets a deployment applies to, so different environments can use different strategies.
  • Automatic rollback — configured per deployment group, triggered on deployment failure or on a CloudWatch alarm. That alarm is where your C7 SLIs plug in.
  • AWS CodeCommit — managed Git. AWS closed it to new customers in July 2024, then restored it to full GA on 24 November 2025. It is a viable choice again, though most teams here will use GitHub.

Naming discipline that pays off: CodePipeline orchestrates, CodeBuild builds, CodeDeploy deploys. Exam questions and design reviews both reward being precise about which of the three you mean.

Terraform vs OpenTofu — the licence split, and why an architect must know it

The timeline. Terraform was MPL 2.0 — standard open source — for nine years. In August 2023 HashiCorp relicensed it to the Business Source License 1.1, effective from v1.6. BUSL is source-available, not open source: you can read the code and run it free inside your own organisation, but you may not offer it in a service that competes with the vendor's commercial products. Each source file reverts to MPL four years after publication.

The fork. A coalition — Gruntwork, Spacelift, env0, Harness, Scalr, Cloud Posse and others — forked the last MPL version and created OpenTofu, now governed by the Linux Foundation and accepted into the CNCF in April 2025. In April 2024 HashiCorp sent a cease-and-desist alleging BUSL code had been incorporated; OpenTofu denied it, saying the code came from an MPL-licensed version. IBM acquired HashiCorp in 2025, which did not change the licence but did raise governance questions.

Where they stand now. OpenTofu is a drop-in for most workflows — same HCL, same provider protocol, same state model; migration is largely swapping the terraform binary for tofu. It has shipped features Terraform's open CLI lacks: client-side state encryption (1.7), early variable evaluation including backend config (1.8), provider for_each and the -exclude flag (1.9), OCI registry support (1.10), and ephemeral resources / write-only attributes (1.11).

What you should actually say when asked

  • Most internal teams: the licence changes nothing day to day. Either tool is fine.
  • If you sell a product that embeds it: BUSL is a real constraint. Get a commercial agreement, or use OpenTofu.
  • If procurement requires an OSI-approved licence: only OpenTofu qualifies. This surfaces in legal review far more often than in engineering.
  • If you want vendor-neutral governance and a predictable roadmap: OpenTofu is a Linux Foundation project with a multi-vendor committee; Terraform's roadmap is set by IBM.

The reason this belongs in an architecture curriculum rather than a legal one: the tool underneath your entire provisioning layer changed its licence overnight once already. "Which licence, and who governs the roadmap" is now a genuine architectural input, alongside features and cost.

Reality check

One worked example on paper, then three things that actually happened

Worked example: trace a plan/apply after somebody hand-edits a resource

This is the drift story, step by step. Managed resource: a security group allowing HTTPS from the internet to your inference load balancer. It is defined in Terraform, applied last week, everything is fine.

T0 · All three pictures agree

Config says port 443 from 0.0.0.0/0. State records the same, with the real security group ID. Reality matches. A plan right now returns "No changes." This is the only moment the triangle is closed.

CONFIG   ingress = [443/tcp from 0.0.0.0/0]
STATE    sg-0a1b2c3d → ingress [443/tcp from 0.0.0.0/0]
REALITY  sg-0a1b2c3d → ingress [443/tcp from 0.0.0.0/0]

No changes. Your infrastructure matches the configuration.

T1 · 03:14 — an incident, and a console click

Inference requests are failing. An on-call engineer diagnoses a health check being blocked, opens the console, and adds port 8080 from 10.0.0.0/8. Service recovers. Incident closed at 04:02. Everyone goes back to bed. Nobody touches the Terraform.

Note what just happened structurally: reality moved, and neither config nor state moved with it. Two sources of truth now exist. The infrastructure is correct and the code is wrong, and nothing in the system knows.

CONFIG   ingress = [443/tcp from 0.0.0.0/0]
STATE    sg-0a1b2c3d → ingress [443/tcp from 0.0.0.0/0]
REALITY  sg-0a1b2c3d → ingress [443/tcp from 0.0.0.0/0,
                                8080/tcp from 10.0.0.0/8]  ← DRIFT

T2 · Tuesday 09:30 — refresh discovers it

A colleague opens an unrelated PR adding a tag to the same security group. CI runs terraform plan. Step 2 of the reconciliation loop — refresh — calls the AWS API and reads the real security group.

This is the exact moment drift becomes visible, and it is visible to a person who had nothing to do with it, in a PR about something else entirely. That is the normal way drift is discovered, and it is why the plan output must be read rather than skimmed.

T3 · The plan, and the trap inside it

The plan compares config against reality — the bottom edge of the triangle. It therefore contains two unrelated things fused into one diff: the colleague's intended tag, and a silent removal of the incident fix.

  ~ resource "aws_security_group" "inference_lb" {
      + tags = { "cost-center" = "ml-platform" }

      - ingress {
      -     from_port   = 8080
      -     cidr_blocks = ["10.0.0.0/8"]
      -   }
    }

Plan: 0 to add, 1 to change, 0 to destroy.

"1 to change" is doing a lot of work there. A reviewer skimming the summary line approves a tag. A reviewer reading the diff sees that applying this PR will re-break production at 09:34 on a Tuesday, for reasons that have nothing to do with the PR.

T4 · The decision — two right answers, one wrong reflex

The wrong reflex is "IaC is the source of truth, so apply and let it revert." That undoes a fix that was made for a reason nobody has yet examined.

Adopt

The fix was correct — the health check genuinely needs 8080. Add the rule to the config, so the plan goes clean. Reality was right; the code was stale.

Revert

The fix was a blunt instrument — 8080 open to the whole /8 was overkill, and the real fix is a narrower source range. Write the correct rule and let apply converge. Neither the old code nor the console change was right.

Either way, the resolution is a commit. The one unacceptable outcome is leaving the drift in place, because then every future plan carries this landmine and reviewers learn to ignore red lines in diffs.

T5 · The durable fix is not technical

Detecting drift after the fact is useful. Preventing it is better, and it is an IAM and process problem, not a Terraform one:

  • Humans get read-only in production. Write access belongs to the pipeline identity. A break-glass role exists, and assuming it fires an alert.
  • Scheduled drift detection. A nightly plan across all environments; any non-empty diff opens a ticket automatically. Six weeks of invisible drift becomes twelve hours of visible drift.
  • An incident-fix rule. Emergency console changes are legitimate — service comes first. The rule is that the follow-up PR is part of the incident, not a nice-to-have after it. If the postmortem does not have a merged commit attached, the incident is not closed.

Notice the CloudFormation equivalent is identical in shape: drift detection reports in-sync, modified or deleted per resource, and you either update the template to adopt the change or re-apply to revert it. Same triangle, different vocabulary.

Counting what IaC actually removes from a three-environment setup

ActivityClick-opsIaC (module + 3 tfvars)What changed
Initial build, 3 envs66 console operations, ~300 field entries1 module + 3 variable files3 × applyHuman decisions drop from ~300 to ~15 (the values that genuinely differ)
Change instance size in all 33 × find the resource, edit, confirm. No record of who or whyEdit 1 line, open 1 PR, 3 appliesOne reviewable diff replaces three unreviewed edits
Prove dev matches prodNot possible. Compare screenshots, hopediff dev.tfvars prod.tfvarsAn unanswerable question becomes a one-line command
Rebuild after a region loss22 operations from memory, under pressure, at 3 a.m.Change the region variable, applyC9's landing zone stops being a document and becomes a command
Add a fourth environment+22 operations, +100 field entries+1 variable file (~15 lines)Marginal cost of an environment collapses. Ephemeral per-PR environments become viable
Tear it all down22 deletions per env, in dependency order, hoping you found everything. Orphans bill silentlyterraform destroyThe most underrated benefit: cheap teardown makes experimentation cheap
Audit "who changed the firewall"Trawl CloudTrail for an API callgit log -p — the diff, the material, the review, the reasonIntent is recorded, not just the action
The last row is the one that matters most in practice and gets mentioned least. CloudTrail tells you what happened. Git tells you what someone was trying to do.

Three things that actually happened

Postmortem · AWS, October 2025

A stale reconciliation plan took down US-EAST-1 for ~15 hours

DynamoDB manages hundreds of thousands of DNS records automatically, split into two components: a DNS Planner that generates plans, and redundant DNS Enactors that apply them. On 19–20 October 2025 a latent race condition struck: one Enactor became slow after retries and was still applying an old plan; a second, faster Enactor applied newer plans and then ran cleanup, deleting the older ones. The slow Enactor then overwrote current records with its stale plan, and cleanup deleted them — leaving an empty DNS record for dynamodb.us-east-1.amazonaws.com that the automation could not repair.

Because DynamoDB backs EC2, Lambda and IAM internals, the failure cascaded across 140+ services; independent measurement put 20–30% of internet-facing services in disruption. AWS disabled the Planner and Enactor automation worldwide pending fixes.

Why it belongs in this session: this is the reconciliation loop from Concept 1, at planetary scale, failing in exactly the way the loop can fail — a plan computed against one reality, applied against a different one. AWS's own remediations read like this session's checklist: validate plan freshness before applying, add version checks and checksums, and never let cleanup delete a plan whose status has not been re-verified. It is also the strongest possible argument for the pipeline pattern of saving the plan as an artifact and refusing to apply it if reality has moved.

Postmortem · CrowdStrike, July 2024

No staged rollout: 8.5 million machines, in about an hour

A malformed Rapid Response Content update passed a buggy Content Validator and hit an out-of-bounds read in a kernel-mode driver. Roughly 8.5 million Windows hosts blue-screened, grounding airlines and pushing hospitals to paper. The technical bug is not the lesson.

The lesson is in CrowdStrike's own Preliminary Post Incident Report, under "how do we prevent this from happening again": implement a staggered deployment strategy in which updates are gradually deployed to larger portions of the sensor base, starting with a canary deployment, plus monitoring during rollout to guide the phases, and customer control over when updates land. Their sensor releases already went through internal dogfooding and early adopters — content updates did not.

Why it belongs here: every remediation on that list is something you learned three sections ago. This is the clearest available demonstration that deployment strategy is not a nicety for large teams — it is the difference between a bug affecting a canary slice and a bug affecting 8.5 million machines. Note also which two things failed together: the test that should have caught it and the rollout that should have contained it. Defence in depth applies to pipelines too.

Industry data · DORA, 2025

Throughput is up. So is instability — and that is a pipeline problem

The 2025 DORA report found AI adoption improves throughput by an estimated 2–18% while correlating with higher change failure rates, more rework, and longer recovery. DORA added a fifth metric, rework rate, and reframed mean-time-to-restore as failed deployment recovery time. The distribution is sobering: only 16.2% of organisations deploy on demand, 23.9% deploy less than monthly, only 9.4% have lead times under an hour, and only 8.5% hit the 0–2% change-failure benchmark while 39.5% are above 16%.

Why it belongs here, and to you specifically: the mechanism is volume. AI generates changes faster than review and deployment infrastructure can absorb them, and the bottleneck moves downstream into testing and release. The 2025 framing — throughput and stability as separate categories — is the same argument this session makes: deployment frequency without change failure rate is a vanity metric. If you are shipping model and adapter changes at AI speed, the gates and canaries in this stage are not bureaucracy, they are the thing keeping the second number down.

Roadmap reversal · AWS CodeCommit

Deprecated, then un-deprecated fourteen months later

In July 2024 AWS closed new customer access to CodeCommit as part of a broader trim; guidance pointed at GitHub and GitLab, and vendors ran migration webinars. Then on 24 November 2025 AWS returned CodeCommit to full General Availability, saying customer feedback was clear that an AWS-managed repository was essential — particularly for regulated teams valuing its IAM integration, VPC endpoints and CloudTrail logging — and apologising to customers who had spent effort migrating away.

Why it belongs here: partly as a correction (sources written in 2024–25 have this backwards), but mostly as an architectural lesson. Vendor roadmaps reverse. The teams least hurt by both announcements were the ones whose repository choice was a variable in their IaC rather than a hard assumption threaded through every pipeline. Portability is not only about clouds.

Apply it — your context, and one lab

Session 1 · from your existing scripts and manifests to declarative IaC

You already have all three postures in your own work

Already declarative

Your K8s / KServe manifests

replicas: 3 is a desired state. A controller runs the reconciliation loop from Concept 1 continuously — kill a pod and it comes back, because the manifest is the truth and the cluster is an effect of it.

This is your bridge. Terraform is that loop pointed at cloud APIs instead of the API server, with one difference worth naming: Kubernetes reconciles continuously, Terraform reconciles when you run it. That difference is precisely why drift detection is a feature in one and not the other.

Declarative-flavoured

Your Modal deployment

Modal's Python decorators — @app.function(gpu="A10G", image=…) — are a genuine IaC dialect: the config lives in version-controlled code, and Modal reconciles its side to match. Same philosophy as CDK: a real language producing a declaration.

What it does not give you is a readable plan before it acts, or a drift report. Worth noticing which properties of IaC you already have from Modal, and which you have been getting by without.

Imperative

Your Vertex AI deploy script

Build image → push → create endpoint → deploy model. Turn-by-turn directions. It works, and it is the single best thing in your portfolio to rewrite this stage, because every weakness in imperative deployment automation is visible in it.

Critique your Vertex script against declarative IaC

PropertyYour script todayDeclarative equivalent
Run it twiceTwo endpoints, or a crash on "already exists." Not idempotent.Second run is a no-op. The config describes one endpoint, so one endpoint exists.
Preview the changeNone. You find out what it does by letting it do it.plan shows adds, changes and — critically — replacements, before anything happens.
Someone edits the endpoint in the consoleThe script cannot tell. Nothing records what it built.Refresh surfaces it as drift on the next plan.
It fails at step 3 of 5Half-built state. Re-running may compound it. You clean up by hand.State records what succeeded; the next apply resumes from the true gap.
Tear it downWrite a second script, and remember every resource. Orphans bill silently.destroy. One command, dependency-ordered, complete.
Review itA reviewer reads procedure and simulates it mentally.A reviewer reads a diff of intent: two lines changed, here is the resulting plan.
Stand up a second regionParameterise the script, hope nothing was hardcoded.New variable file. The module already generalised.
The honest exception: model upload and endpoint deployment are partly one-shot operations, and Terraform's coverage there is weaker than for plumbing. The right target is a hybrid — Terraform for the durable resources (registry, endpoint, service accounts, buckets, monitoring, node pools) and the SDK or Cloud Deploy for the model-version rollout. Say so explicitly rather than pretending it is all declarative; S3 builds exactly this split.
The same intent, declarative — sketch, not the lab
resource "google_vertex_ai_endpoint" "inference" {
  name         = "gemma-inference"
  display_name = "gemma-inference"
  location     = var.region
  labels       = { env = var.env, owner = "ml-platform" }
}
# Durable plumbing: declarative, planned, drift-detected, destroyable.
# Model version rollout + traffic split: stays with the SDK / Cloud Deploy — see S3.
Where a GPU / GenAI workload stresses this specifically
  • Quota is not a resource. Accelerator quota is an account property, not something IaC creates. Your apply will fail with a quota error that looks like a bug. Request L4/A100 quota before you write the config, and document it as a prerequisite in the module README.
  • Node pool changes are frequently replace, not update. Changing accelerator type or machine type on a GPU node pool usually means destroy-and-recreate. On a pool holding warm model weights that is a very expensive line in a plan you skimmed.
  • Scale-to-zero is the cost control that matters. min_node_count = 0 on the GPU pool, with a separate always-on CPU pool for system workloads. An idle L4 node costs real money for producing nothing; an idle e2-medium does not.
  • Image pull time is your cold start. A vLLM image with CUDA is multi-gigabyte. Bake weights into the image or pre-warm them onto a disk, and declare that choice in IaC so every environment makes it identically — otherwise staging is fast and production is not, and nobody can see why.
  • Model weights are not infrastructure. Terraform declares the bucket. It should not manage 40 GB of weights. Keep the artifact and the artifact store in different tools.

Optional lab — make drift, see it, then fix it (~40 minutes)

The point of this lab is not to provision anything impressive. It is to produce drift on purpose and watch the plan catch it, so the triangle stops being a diagram. Everything here is free-tier or fractions of a cent.

Before you start — cost guardrails

  • Set a budget alert first. AWS Budgets, $1 threshold, email notification. Two minutes, and it turns an unpleasant surprise into an email.
  • Nothing in this lab provisions compute. S3 (free tier: 5 GB), a security group (free), and state objects measured in kilobytes. No EC2, no NAT gateway — the two things that quietly bill.
  • Use OpenTofu if you want the open-source licence; every command below works identically with tofu in place of terraform.

1 · Bootstrap the backend, the modern way

Create a versioned, encrypted S3 bucket by hand — once, this is the chicken-and-egg exception — then point the backend at it. No DynamoDB table. Locking is native now.

aws s3api create-bucket --bucket tf-lab-<your-initials>-state --region us-east-1
aws s3api put-bucket-versioning --bucket tf-lab-<your-initials>-state \
  --versioning-configuration Status=Enabled
aws s3api put-bucket-encryption --bucket tf-lab-<your-initials>-state \
  --server-side-encryption-configuration \
  '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'
# backend.tf
terraform {
  required_version = ">= 1.11"
  backend "s3" {
    bucket       = "tf-lab-<your-initials>-state"
    key          = "lab/terraform.tfstate"
    region       = "us-east-1"
    encrypt      = true
    use_lockfile = true   # S3-native locking. 1.11+. No lock table.
  }
}

2 · Declare two resources

# main.tf
provider "aws" { region = "us-east-1" }

resource "aws_s3_bucket" "weights" {
  bucket = "tf-lab-<your-initials>-weights"
  tags   = { env = "lab", purpose = "drift-demo" }
}

resource "aws_security_group" "inference" {
  name        = "tf-lab-inference"
  description = "Drift demo - not attached to anything"
  ingress { from_port = 443, to_port = 443, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }
}
terraform init
terraform plan     # READ IT. Two creates. Note the + symbols.
terraform apply

3 · Look at what you just made

  • terraform state list — the receipt: your two addresses.
  • terraform show — every recorded property, including ones you never wrote (the cloud filled in defaults). Those defaults are now yours to manage.
  • Open the state object in S3 and read the JSON. It is not magic. Note how casually it would leak a secret if one of these resources had produced one.
  • terraform plan again → No changes. The triangle is closed.

4 · Cause drift on purpose — this is the actual exercise

aws ec2 authorize-security-group-ingress \
  --group-name tf-lab-inference \
  --protocol tcp --port 8080 --cidr 10.0.0.0/8

aws s3api put-bucket-tagging --bucket tf-lab-<your-initials>-weights \
  --tagging 'TagSet=[{Key=env,Value=lab},{Key=purpose,Value=drift-demo},{Key=hotfix,Value=true}]'

You are now the 3 a.m. engineer. Two out-of-band changes, both plausible, neither in code.

5 · Catch it

terraform plan

Read this plan properly. You should see the ingress rule marked for removal and the hotfix tag marked for removal. Neither change was requested by you. That is drift arriving through the right-hand edge of the triangle — and this is the exact experience a colleague has when your incident fix shows up inside their unrelated PR.

6 · Practise both resolutions

  • Adopt: add the 8080 ingress block to main.tf, plan again, watch that line disappear from the diff. Code caught up to reality.
  • Revert: leave the tag out of the config and apply — Terraform removes it. Reality caught up to code.
  • Now try lifecycle { ignore_changes = [tags["hotfix"]] } and re-plan: you have declared that field as not-yours. This is how you coexist with an autoscaler.

7 · Stretch, if you have appetite

  • Turn the two resources into a module with a name_prefix variable; call it twice with different prefixes. That is the three-environment pattern, in miniature.
  • Run terraform plan -out=tfplan, then apply that file. Change something in the console in between and watch the apply refuse. This is the AWS-outage lesson as a two-command exercise.
Teardown — do this, it is one command and it is the point

terraform destroy — confirm the plan shows 2 to destroy and nothing survives. Then delete the state bucket manually (it was created outside IaC, so IaC will not remove it):

aws s3 rm s3://tf-lab-<your-initials>-state --recursive && aws s3api delete-bucket --bucket tf-lab-<your-initials>-state

Cheap, complete teardown is the property that makes everything else in this stage affordable. It is why you can afford a per-PR environment, and why you should never again leave an experiment running because deleting it looked like work.

Next →

Session 2 goes to Azure, where the interesting twist is that you have been writing IaC all along without knowing it — every action you have ever taken in the Azure portal was already an ARM template deployment. We will look at what Azure gives you for free by making that true, what it costs you, and why Deployment Stacks is the piece the reference texts have not caught up with.

Why this session exists

Session 2 · the problem, in plain words

Here is a fact from the Azure source that most readers skim past and that reframes the whole platform: every deployment you have ever done through the Azure portal was already an ARM template deployment. The portal is a form that builds a template. You can export it, before or after the fact.

That is a genuinely different architecture from AWS or GCP. On Azure there is exactly one control plane — Azure Resource Manager — and everything goes through it: the portal, the CLI, PowerShell, the SDKs, GitHub Actions, and Terraform. There is no back door. Which means Azure gets something for free that the others have to build: the platform always knows the current configuration of every resource, so there is no state file for you to own, lose, or leak.

But the free thing comes with a specific hole, and it is the hole this session is really about. Ask the question that state answers on AWS and GCP:

The question ARM historically could not answer

"Which resources does this template own?"

You deploy a template with five resources. Later you delete one from the template and redeploy. On a classic ARM deployment, the fifth resource stays alive. ARM deployed what you asked for; it was never told to remove anything. Repeat over two years and you accumulate a graveyard of orphans that no template mentions, nobody can attribute, and everybody keeps paying for. Meanwhile terraform destroy takes one command, because Terraform kept a receipt.

The modern answer is Deployment Stacks — a first-class Azure resource that tracks a managed set and defines what happens when something leaves the template. The Azure source predates it entirely. It is the most important gap between what you will read and what you should build.

Click any clientEverything on Azure is a template deployment. Portal clicks are not a different mechanism — they are a generated template you never see.
Azure portal CLI / PowerShell Bicep Terraform / SDKs AZURE RESOURCE MANAGER One control plane auth · RBAC · policy · quotas templates · locks · tags Compute RP Storage RP Network RP RESOURCE PROVIDERS DEPLOYMENT STACK tracks ownership No back door. Which is why Azure needs no state file — and why it needed Deployment Stacks anyway.
One control plane, four front doors. The architectural consequence: on Azure, "adopt existing resources into IaC" is export the template, not reverse-engineer and import.

Core concept 1 — ARM templates and Bicep

Azure §15 · the same declarative idea, in two syntaxes

ARMAzure Resource Manager. Azure's deployment and management control plane. It is the layer that receives every request, applies RBAC and policy, and dispatches to resource providers. "ARM template" means a JSON document ARM executes.

BicepA domain-specific language for authoring Azure infrastructure that transpiles to ARM JSON. Same capability, dramatically less syntax. MIT-licensed, maintained by Microsoft, and now the default recommendation for new work.

Resource groupAzure's deployment container and the usual scope for a template. Templates can also target subscription, management group, or tenant scope — which is how you deploy the resource groups themselves.

1Intuition

ARM JSON is the machine format. Bicep is the human format. The relationship is TypeScript to JavaScript: you write the pleasant one, a compiler emits the one the runtime actually consumes, and you can always read the output when you need to.

The difference matters more than it sounds, because ARM JSON is genuinely hostile to write by hand. Expressions live inside strings — "[concat(parameters('prefix'), '-vm')]" — so you are writing a programming language inside a data format with no syntax highlighting to help you. A modest VM deployment runs to a few hundred lines of JSON with quoting rules that punish typos.

Bicep removes the quoting, adds real types, and produces a file you can review in a pull request without losing the will to live. Same deployment, roughly a third of the lines.

2Mechanism

The template has the same skeleton in both syntaxes: parameters (inputs, typed, optionally with allowed values), variables (computed locals), resources (the declarations), outputs (values other templates or pipelines consume), plus modules in Bicep for composition.

The artifact is the mechanism — the same resource, both syntaxes
// Bicep — dependency inferred from the symbolic reference
param location string = resourceGroup.location
resource plan 'Microsoft.Web/serverfarms@2023-01-01' = {
  name: 'asp-inference'
  location: location
  sku: { name: 'F1' }        // free tier
}
resource site 'Microsoft.Web/sites@2023-01-01' = {
  name: 'app-inference-01'
  location: location
  properties: { serverFarmId: plan.id }   // ← implicit dependsOn
}

Note plan.id. In Bicep, referring to another resource by its symbolic name creates the dependency automatically — the same graph inference Terraform does. The ARM JSON equivalent requires an explicit "dependsOn" array and a resourceId string expression, which is exactly the kind of hand-maintained bookkeeping that rots.

Dry run: what-if

Azure's equivalent of plan and change sets. It reports Create, Modify, Delete, Deploy, Ignore, NoChange per resource, with a property-level diff for modifications. It is available on the CLI (az deployment group what-if), in the azure/bicep-deploy GitHub Action, and in the BicepDeploy@0 Azure Pipelines task.

Resource and property changes are indicated with these symbols:
  + Create
  ~ Modify
  - Delete

~ Microsoft.Web/sites/app-inference-01
  ~ properties.siteConfig.alwaysOn: false => true

Resource changes: 1 to modify.

Post this into the pull request. That single habit — reviewer approves the what-if output, not the Bicep diff — is the highest-value practice in this session, and it is what Microsoft's own reference implementation does.

3Trade-offs, limits, and where it breaks

  • what-if is less reliable than terraform plan, and you must know this. It works by asking the ARM API to evaluate the change, and coverage varies by resource provider. Some properties report as changing when they will not (noisy false positives on defaults the provider fills in); occasionally something genuinely changes and is not reported. Treat it as a strong signal, not a contract. Terraform's plan, backed by a state file it fully controls, is more precise here — a real point for Terraform on Azure.
  • Classic deployments never delete. Default mode is Incremental: resources in the template are created or updated, and anything not mentioned is left alone. Complete mode does delete unmentioned resources in the target resource group — which is a blunt and frankly dangerous instrument, because it acts on the whole group rather than on what your template owns. Deployment Stacks exist precisely to give you the sharp version of this.
  • API versions are pinned per resource and they age. @2023-01-01 in the snippet above is part of the resource type. Templates written three years ago pin old API versions and silently miss newer properties. There is no global upgrade; you bump them resource by resource.
  • Bicep is Azure-only, by design. That is not a criticism — it is the trade. You get day-one coverage of every new Azure resource because Bicep targets the same ARM schema, and you get nothing at all for AWS or GCP.
  • Loops and conditions exist but are constrained. Bicep has for loops, if conditions, and functions, but it is not a general-purpose language. Coming from CDK this feels limiting; coming from ARM JSON it feels like liberation. Both reactions are correct.

Core concept 2 — Deployment Stacks: Azure's answer to state

Not in the material · the most important thing in this session

Deployment stackA first-class Azure resource that owns a collection of resources as a single managed unit. It records which resources its template produced, so it can act when one leaves the template — and it can lock those resources against out-of-band modification.

actionOnUnmanageThe setting that answers "what happens when a resource is no longer in the template?" — delete (remove it) or detach (leave it running but stop tracking it). Set separately for resources, resource groups, and management groups.

Deny settingsA lock the stack applies to its own resources: denyDelete, denyWriteAndDelete, or none. It blocks changes even from users who have RBAC permission — which makes it a genuine anti-drift control rather than a suggestion.

1Intuition

A classic ARM deployment is a shopping trip: you hand over a list, you get those items, and nobody remembers the trip afterwards. Cross something off next week's list and last week's purchase does not vanish from your kitchen.

A deployment stack is a subscription box. The provider knows exactly what is in your box. Remove an item from your preferences and next month's box does not contain it — and, if you have asked them to, they will come and take the old one away. They will also stop you rummaging in the box and swapping things yourself.

That is state's job — knowing what you own — delivered as a platform feature instead of a file you have to store and protect.

2Mechanism

Click each scenarioThe same template change, handled three different ways depending on your actionOnUnmanage setting.
TEMPLATE v1 — 3 RESOURCES A B C → deployed. Then you delete C from the template and redeploy. INCREMENTAL (default) A ✓ B ✓ C ⚠ C survives, orphaned Still running. Still billing. No template mentions it. COMPLETE MODE A ✓ B ✓ C ✕ Z ✕ C deleted — and so is Z Z belonged to another team. Scope is the group, not the template. DEPLOYMENT STACK A 🔒 B 🔒 C ✕ Z — C removed. Z untouched. A and B locked against edits. Delete the stack → all of it goes.
Deployment Stacks give Azure the two things state buys you — knowing what you own, and clean teardown — without asking you to store, encrypt, lock, and back up a file.
The artifact is the mechanism — a stack deployment
az stack group create \
  --name inference-lab --resource-group rg-inference \
  --template-file main.bicep --parameters main.bicepparam \
  --action-on-unmanage deleteAll \
  --deny-settings-mode denyWriteAndDelete

3Trade-offs, limits, and where it breaks

  • Deny settings can lock out things you did not expect. denyWriteAndDelete blocks writes from everyone, including automation that legitimately needs to touch a resource — an autoscaler adjusting capacity, a certificate renewal job, an operator writing status. There is an exclusion list for principals and actions; you will need it, and you will discover which entries you need by breaking something first. Start with denyDelete, which is safer and still prevents the worst outcome.
  • detach is the humane default while adopting stacks. Pointing a stack with deleteAll at an existing production resource group and getting the template subtly wrong is a bad afternoon. Adopt with detach, verify the stack's managed-resources list matches your expectation, then tighten.
  • Stacks are scoped. Resource group, subscription, or management group. Cross-scope architectures need more than one stack and a deliberate decision about which owns what — the same "how do I split state" question Terraform users argue about, wearing a different hat.
  • Still no plan-quality diff. Stacks fix ownership; they do not fix what-if fidelity. You still get a good-but-imperfect preview.
  • Tooling has caught up recently and unevenly. The azure/bicep-deploy action supports stacks first-class, as does BicepDeploy@0 in Azure Pipelines. Older material — including the material — predates all of it, so most tutorials you find will show plain deployments.

Core concept 3 — Azure Pipelines and GitHub Actions

Azure §15 · two products, one company, genuinely different shapes

Azure DevOps ServicesMicrosoft's SaaS suite covering the whole lifecycle: Azure Repos (Git, plus legacy TFVC), Azure Pipelines (CI/CD), Azure Boards (work items), Azure Artifacts (package feeds — NuGet, npm, Maven, Python, Universal), and Azure Test Plans. Distinct from the self-hosted Azure DevOps Server.

Pipeline anatomyStagejobstep. A stage is a phase (build, test, deploy-prod). A job is a group of steps that run together on one agent. A step is one task or script. Agents are Microsoft-hosted (clean VM per job) or self-hosted (your infrastructure, your network, your GPU).

TriggerWhat starts a pipeline: a push to a branch, a pull request, a schedule, the completion of another pipeline, or a manual run.

EnvironmentA named deployment target in Azure Pipelines (or GitHub) carrying approvals, checks, and deployment history. This is where the gate lives, and it is the object that turns continuous delivery into a governed process.

The organising insight: Azure Pipelines and GitHub Actions solve the same problem from opposite directions. Azure Pipelines was built for enterprise release management and grew CI. GitHub Actions was built for repository automation and grew deployment. That history shows in every trade-off below.

Azure Pipelines

Release-management heritage

  • Environments with rich checks — approvals, business hours windows, invoke-REST-API gates, query-Azure-Monitor-alerts gates, exclusive locks. The most mature gating model of the two by some distance.
  • Deployment jobs with built-in runOnce, rolling, canary and blueGreen strategies, plus preDeploy/deploy/routeTraffic/postRouteTraffic/on: failure lifecycle hooks.
  • Service connections — a governed, RBAC-controlled credential object rather than a bag of repository secrets. Supports workload identity federation.
  • First-party BicepDeploy@0 task — deploys .bicep and .bicepparam directly without pre-compiling to JSON, caches the Bicep CLI, runs validate and what-if, and supports Deployment Stacks including deny settings.
  • Multi-cloud by design — the material stresses this, correctly: Azure Pipelines deploys happily to AWS, GCP and on-premises. It is a general CI/CD product that happens to be sold by Microsoft.
GitHub Actions

Repository-automation heritage

  • Workflows in .github/workflows/*.yml, triggered by any repository event — push, PR, issue, comment, release, schedule, manual dispatch. Far broader event surface.
  • The marketplace is the decisive advantage. Thousands of prebuilt actions, including Microsoft's own azure/login and azure/bicep-deploy. Its flip side is a supply-chain surface: pin third-party actions to a commit SHA, not a tag.
  • Matrix builds — run the same job across many versions or configurations in parallel with a few lines.
  • OIDC-native. azure/login with permissions: id-token: write federates a short-lived token to a Microsoft Entra app registration. No stored secret at all.
  • Environments and required reviewers exist, and are good — but the checks vocabulary is thinner than Azure Pipelines'.
  • It is where the developers already are. That is not a small factor; a pipeline nobody looks at is a pipeline nobody fixes.
Correction to the material's walkthrough — this one is a security fix, not a preference

The Azure section's GitHub Actions walkthrough downloads a publish profile from the portal and stores it as the repository secret AZURE_WEBAPP_PUBLISH_PROFILE. That is a long-lived credential in a shared secret store. It does not expire on any useful schedule, it is hard to scope, it does not rotate itself, and anyone who can run a workflow can use it.

Use OIDC workload identity federation instead. GitHub mints a short-lived token describing the workflow; Entra ID trusts that token for a specific repository, branch or environment, and issues credentials scoped by RBAC. Nothing long-lived is ever stored. It is roughly ten minutes of setup and it deletes an entire class of incident. Also note the material's actions/checkout@v2 and setup-node@v2 are several major versions behind.

permissions:
  id-token: write        # mint the OIDC token
  contents: read
steps:
  - uses: azure/login@v2
    with:
      client-id:       ${{ secrets.AZURE_CLIENT_ID }}       # IDs, not secrets
      tenant-id:       ${{ secrets.AZURE_TENANT_ID }}
      subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

Those three values are identifiers, not credentials — leaking them grants nothing without the federated trust relationship. Compare that with a leaked publish profile, which is a working key. This is C2's OIDC federation doing concrete work.

Where the deployment strategies live on Azure compute

TargetWhat it isNative strategy mechanismRollback
App ServiceManaged PaaS web/API hostingDeployment slots. Deploy to staging, warm it, then swap — this is blue-green as a first-class feature, plus percentage traffic routing for canarySwap back. Seconds.
Azure Container AppsServerless containers on managed KubernetesRevisions with weighted traffic splitting — canary by percentage, nativelyShift weight to the previous revision
AKSManaged KubernetesKubernetes rolling updates by default; canary and blue-green via ingress or a service mesh. Your KServe manifests land here unchanged.kubectl rollout undo, or shift ingress weights
Azure FunctionsEvent-driven serverlessDeployment slots, same swap model as App ServiceSwap back
Virtual Machine Scale SetsManaged identical VM fleetsRolling upgrade policy with batch size, pause between batches, and automatic OS image upgradesRoll forward to the previous image version
App Service slots are the cleanest blue-green implementation on any of the three clouds — a platform primitive rather than a pattern you assemble. Worth knowing even if you never use App Service, because it is the reference example.
One more Azure-specific piece from the section

Azure DevTest Labs is a service for managing development and test environments with cost controls built in: VM templates (formulas), post-creation artifacts, per-user and per-lab spend policies, and — the genuinely useful bit — automatic shutdown and startup schedules. For a GPU-backed dev environment that only needs to exist during working hours, auto-shutdown is not a nice-to-have; it is the difference between a plausible and an implausible monthly bill. Think of it as guardrails for environments that IaC does not otherwise give you.

Decision: Azure Pipelines, GitHub Actions, or Terraform inside either?

Follow "no ↓" until a "yes" exits right. The bottom-left box is the default.

Guard clauses · choosing a pipeline for an Azure workload

Note that the IaC language and the pipeline are independent choices — you can run Bicep from GitHub Actions or Terraform from Azure Pipelines. Decide them separately.

Guard 1Does the code already live in Azure Repos, with Boards work items linked to commits and a team that lives in that UI?
YES →
Azure Pipelines. The integration between Repos, Boards and Pipelines is the actual product. Splitting CI to another tool loses traceability from work item to deployment for no gain.

no ↓

Guard 2Do you need rich release gates — approvals plus business-hours windows, exclusive locks, or an automated gate that queries Azure Monitor alerts before promoting?
YES →
Azure Pipelines. Its Environments checks model is materially more capable than GitHub's. This is where a regulated deployment process actually gets expressed.

no ↓

Guard 3Is this pipeline deploying to more than one cloud from a single workflow?
YES →
Either tool, but Terraform/OpenTofu as the IaC layer. Both pipeline products deploy anywhere; the constraint is the language. Bicep cannot describe an AWS resource. Choose the pipeline on team preference, then use Terraform inside it.

no ↓

Guard 4Are you deploying only Azure resources, with a team that wants day-one coverage of new services and no state to manage?
YES →
Bicep with Deployment Stacks, run from whichever pipeline you prefer. Managed state, no bootstrap, native what-if, and deny settings as a real anti-drift control. Terraform gives you a better plan and an ecosystem you may not need here.

no ↓

Guard 5Do you need self-hosted runners inside a private network, or on GPU hardware, for builds that cannot leave your perimeter?
YES →
Both support it — Azure Pipelines self-hosted agents, GitHub Actions self-hosted runners. Decide on operational cost: who patches the runner fleet, and how you stop a fork from running arbitrary code on it.

no ↓

Default — bottom left GitHub Actions with Bicep, deployed as a Deployment Stack, authenticating via OIDC. For a new Azure project with no Azure DevOps history, this is where the ecosystem, the documentation and Microsoft's own reference implementations now point. The developers are already in GitHub, the marketplace covers the boring parts, azure/bicep-deploy is first-party with stacks support, and OIDC means no stored credentials. Move to Azure Pipelines when a gate requirement genuinely exceeds what GitHub Environments can express — that is a real reason, and it does happen.

The four dialects — Azure column in focus

Same table shape as S1, read down the Azure column

CapabilityAWS nativeAzure nativeGCP nativeTerraform / OpenTofu
Authoring languageCloudFormation YAML/JSON; CDKBicep (recommended); ARM JSON underneathTerraform HCL is the recommendationHCL
Ownership trackingStack (server-side)Deployment StackactionOnUnmanage + deny settingsInfra Manager deployment (stores TF state for you)State file you own
Dry runChange setwhat-if — good, not exact; provider coverage variesterraform planplan — the most precise of the four
Drift preventionStack policies; IAMDeny settings block writes even for permitted users — strongest of the fourConfig Connector reconciles continuously; org policyNone natively — prevention is an IAM design problem
Multi-account / multi-region fan-outStackSetsStacks at management-group scope; Azure Policy deployIfNotExistsInfra Manager per project; org policy aboveWorkspaces, or a module called per target
CICodeBuildAzure Pipelines or GitHub ActionsCloud BuildRuns in any of them
Gates and approvalsCodePipeline manual approval actionsEnvironments + checks — approvals, time windows, REST-API gates, Monitor-alert gates, exclusive locksCloud Deploy approvals + deploy policiesInherits whatever the pipeline provides
Blue-green primitiveCodeDeploy blue/green (EC2, ECS, Lambda)App Service / Functions deployment slots — swap. Container Apps revisions for weighted canaryCloud Run revisions; Cloud Deploy strategiesDescribes the resources; does not orchestrate the rollout
Pipeline auth to the cloudIAM role for the pipeline; OIDC from GitHubWorkload identity federation (OIDC), or a service connection. Not a publish profileWorkload Identity Federation; service accountsWhatever the host pipeline supplies
Two Azure-specific standouts worth remembering for design reviews: deny settings are the only mechanism here that prevents drift rather than detecting it after the fact, and App Service slots are the only blue-green implementation that is a platform primitive rather than an assembled pattern.

Reality check

One worked example on paper, then three current things the material cannot know

Worked example: the same change, traced through what-if and then through a stack

Scenario: an inference API on App Service. You are making two changes in one pull request — enabling alwaysOn to kill cold starts, and removing a Redis cache the team stopped using last quarter.

01 · Two changes, one PR

// main.bicep — diff
  resource site 'Microsoft.Web/sites@2023-01-01' = {
    properties: {
      serverFarmId: plan.id
+     siteConfig: { alwaysOn: true }
    }
  }
- resource cache 'Microsoft.Cache/redis@2023-08-01' = { ... }

Both changes are correct and intentional. Watch what the platform does with each.

02 what-if reports one of them

az deployment group what-if -g rg-inference -f main.bicep

Resource and property changes are indicated with these symbols:
  ~ Modify

~ Microsoft.Web/sites/app-inference-01
  ~ properties.siteConfig.alwaysOn: false => true

Resource changes: 1 to modify.

The alwaysOn change is reported precisely, down to the property and its before/after values. This is what-if doing exactly what you want, and it is genuinely good output to put in a PR comment.

03 · The Redis cache is not mentioned. At all.

Read the output again: "1 to modify." Nothing about Redis. Not a delete, not a warning, not a note.

This is incremental mode behaving exactly as designed. The template no longer mentions the cache, so ARM has no instruction concerning it, so nothing happens to it. The cache keeps running. The cache keeps billing. Six months later somebody in a cost review asks what redis-inference-prod is for, and the honest answer is that nobody knows, and everyone is now slightly afraid to delete it.

This is the exact moment a snowflake is born

Not through carelessness. Through a correct template change, a correct deployment, and a mechanism that has no concept of ownership. Every orphan in every Azure estate was created by someone doing the right thing.

04 · The same change, as a Deployment Stack

az stack group create --name inference --resource-group rg-inference \
  --template-file main.bicep --action-on-unmanage deleteAll \
  --deny-settings-mode denyWriteAndDelete

The following resources will be DELETED (no longer managed by the stack):
  - Microsoft.Cache/redis/redis-inference-prod
Confirm? (y/n):

The stack knew it created the cache. The cache left the template. actionOnUnmanage: deleteAll therefore means delete — and, crucially, you are told and asked before it happens.

Had you set detach instead, the cache would survive but be removed from the stack's managed set — the "I will deal with this manually" option, recorded rather than accidental. Either way the ambiguity is gone: the platform has an opinion and states it.

05 · Now somebody tries to drift it

An engineer opens the portal, navigates to the App Service, and switches alwaysOn back off to save a little money. They have Contributor on the resource group — RBAC says yes.

Operation failed. The resource is protected by a deployment stack
with denySettingsMode = denyWriteAndDelete.
Stack: /subscriptions/.../deploymentStacks/inference

Compare this with the S1 drift trace. There, the change succeeded, went undetected for four days, and surfaced inside somebody else's unrelated pull request. Here the change is refused at the moment of attempt, with an error naming the stack that owns the resource and, implicitly, the repository they should open a PR against.

That is the difference between detecting drift and preventing it — and it is the single strongest argument for Azure-native IaC over Terraform on Azure. Terraform has no equivalent; on Terraform, prevention is an IAM design problem you solve yourself.

The catch, restated: that same lock will refuse a legitimate automated write. Configure the exclusion list deliberately, and start with denyDelete before reaching for denyWriteAndDelete.

Three current things

Tooling shift · 2025–2026

azure/arm-deploy is out; azure/bicep-deploy is in

Microsoft replaced the long-standing GitHub Action for ARM deployments. The azure/arm-deploy repository now carries a deprecation notice pointing at azure/bicep-deploy, which handles both ARM and Bicep, adds first-party Deployment Stacks support, and exposes validate, whatIf and create as explicit operations. Its type input switches between deployment and deploymentStack; for stacks you also set action-on-unmanage-resources and deny-settings-mode. Microsoft's guidance is that Deployment Stacks are the recommended approach going forward.

The Azure Pipelines side caught up more recently with the first-party BicepDeploy@0 task. Microsoft Learn now states plainly that it is the recommended option for new pipelines: it deploys .bicep and .bicepparam directly without pre-compiling to JSON, downloads and caches the Bicep CLI itself, works at resource-group, subscription, management-group and tenant scope, and supports stacks including deny settings and unmanaged-resource actions.

Why it matters to you: almost every tutorial older than about eighteen months shows you the deprecated path, usually with a publish profile attached. If you copy a 2024 workflow you will get long-lived credentials, a pre-compile step you do not need, and no ownership tracking.

Reference implementation

Microsoft's own Bicep + GitHub Actions workflow is the pattern to copy

The Azure-Samples/bicep-github-actions repository is a reference implementation of exactly the shape this session argues for, and it is worth reading before you write your own:

  • Work happens on a branch; a pull request opens against main.
  • A PR workflow runs unit tests on the infrastructure code — formatting, internal consistency, and security checks on what the template would produce.
  • The same workflow runs what-if and posts the preview, so the reviewer approves an outcome rather than reading Bicep and imagining one.
  • Only after review does the merge to main trigger the workflow that actually deploys.

Every element of the pipeline stepper from S1 is present, and nothing extra is. Note especially that the plan-in-the-PR step is treated as non-optional; that is the practice that turns IaC from a scripting convenience into a review process.

Where the material is thin

Observability wired into the pipeline, not bolted beside it

The section covers Azure Monitor and Application Insights well as tools — KQL queries, dashboards, alert rules, action groups — and offers genuinely useful cost advice that most teams learn the expensive way: enable adaptive sampling, set daily ingestion caps in Log Analytics, use Basic Logs for non-critical data, and filter noisy telemetry. Verbose logging at production request volumes produces bills people do not anticipate.

What it does not do is connect any of that to the release. The connection is the point of this stage: an Azure Monitor alert rule is exactly what an Azure Pipelines gate can query before promoting a stage, and an App Service slot swap is exactly what you undo when it fires. That is an SLO-gated rollback assembled from services the section already describes separately — and it is the C7 material becoming a control rather than a dashboard. S3 builds the fully wired version.

Apply it — your context, and one lab

Session 2 · working Azure literacy for a GCP/AWS-first engineer

Translating what you already know

You already knowAzure equivalentThe difference that will trip you up
Your KServe / K8s manifestsDeploy unchanged to AKSNothing at the manifest layer. The Azure-specific parts are the GPU node pool, the ingress, and Entra Workload ID for pod-level identity.
terraform planwhat-ifFidelity. what-if is a good preview, not a contract. Do not build automation that assumes an empty diff means no change.
terraform destroyaz stack group deleteOnly if you deployed as a stack. A plain deployment has no teardown command — you delete the resource group and hope nothing else lived there.
Terraform state fileNothing to manage — ARM holds itLiberating until you want state's other benefits. No state list, no state mv, no import/export of your own bookkeeping.
Vertex AI endpoint + traffic splitAzure ML managed online endpoints with weighted deploymentsVery close conceptually: one endpoint, multiple deployments, traffic percentages. Your S3 canary design ports over almost directly.
Cloud Run revisionsContainer Apps revisionsAlso very close — weighted traffic across revisions. The most natural Azure home for a containerised inference service that does not need a full cluster.
Modal's Python decoratorsNo direct analogueBicep is a DSL, not an embedded language. Azure's ergonomic win is conciseness and tooling, not being in your application language.
Where a GPU / GenAI workload stresses this specifically
  • GPU SKUs are quota-gated and regionally scarce. NC- and ND-series capacity varies a lot by region, and a Bicep deployment for an unavailable SKU fails at apply, not at what-if. Check availability and request quota before writing the template.
  • Deny settings and GPU autoscalers interact badly. denyWriteAndDelete on an AKS cluster will block the cluster autoscaler from adjusting the node pool. Either exclude the autoscaler's principal or scope the deny to the resources that genuinely should never change.
  • Spot / low-priority GPU nodes for training, standard for serving. Declare this split in the template — a spot node pool for batch and fine-tuning, an on-demand pool for inference — so every environment inherits the same cost posture rather than each one being decided ad hoc.
  • DevTest Labs auto-shutdown is a real GPU cost control. A GPU dev box running nights and weekends for no reason is a meaningful monthly line item. Schedule it off.
  • Deployment slots do not help a GPU service much. Slots are App Service; a GPU inference workload lives on AKS or Container Apps. Your blue-green story there is ingress weights or revision traffic, and blue-green on GPU means paying for two fleets — which is why canary usually wins.

Optional lab — Bicep, what-if in a PR, and a stack that deletes cleanly (~50 minutes)

Builds the S2 pattern end to end on free-tier resources only: an F1 App Service plan (free), a storage account (pennies), and a Deployment Stack (free). No GPU, no VMs, no gateways.

Before you start — cost guardrails

  • Create a budget first. Azure Cost Management → Budgets → $1, alert at 80%. Two minutes.
  • F1 App Service plan is genuinely free but limited to one per region per subscription. Use --sku F1 and nothing larger.
  • Everything lives in one resource group so cleanup has a backstop.

1 · Set up OIDC — do this instead of a publish profile

az ad app create --display-name gh-bicep-lab
# note the appId, then create a service principal and a federated credential
az ad sp create --id <appId>
az role assignment create --assignee <appId> --role Contributor \
  --scope /subscriptions/<subId>/resourceGroups/rg-bicep-lab

# federated credential: trust THIS repo on THIS branch — no secret anywhere
az ad app federated-credential create --id <appId> --parameters '{
  "name":"gh-main",
  "issuer":"https://token.actions.githubusercontent.com",
  "subject":"repo:<you>/<repo>:ref:refs/heads/main",
  "audiences":["api://AzureADTokenExchange"]
}'

Store AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID as repository secrets. All three are identifiers, not credentials. That is the whole point.

2 · Write the Bicep

// main.bicep
param location string = resourceGroup.location
param prefix   string

resource plan 'Microsoft.Web/serverfarms@2023-01-01' = {
  name: 'asp-${prefix}'
  location: location
  sku: { name: 'F1' }          // free tier — do not change
}
resource site 'Microsoft.Web/sites@2023-01-01' = {
  name: 'app-${prefix}'
  location: location
  properties: { serverFarmId: plan.id, httpsOnly: true }
}
resource sa 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: 'st${prefix}'
  location: location
  sku: { name: 'Standard_LRS' }
  kind: 'StorageV2'
}
output siteUrl string = 'https://${site.properties.defaultHostName}'

3 · The PR workflow — what-if as the review artifact

// .github/workflows/infra.yml
on:
  pull_request: { paths: ['**.bicep'] }
  push:         { branches: [main] }
permissions: { id-token: write, contents: read, pull-requests: write }

jobs:
  preview:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: azure/login@v2
        with:
          client-id:       ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id:       ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      - uses: azure/bicep-deploy@v2
        with:
          type: deploymentStack
          operation: validate          # what-if / preview — changes nothing
          name: bicep-lab
          scope: resourceGroup
          resource-group-name: rg-bicep-lab
          template-file: ./main.bicep
          parameters: '{"prefix":"labdemo01"}'

4 · The deploy job — stack, on main only

  deploy:
    if: github.ref == 'refs/heads/main'
    environment: production        # ← put a required reviewer on this
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: azure/login@v2
        with: { client-id: ..., tenant-id: ..., subscription-id: ... }
      - uses: azure/bicep-deploy@v2
        with:
          type: deploymentStack
          operation: create
          name: bicep-lab
          scope: resourceGroup
          resource-group-name: rg-bicep-lab
          template-file: ./main.bicep
          parameters: '{"prefix":"labdemo01"}'
          action-on-unmanage-resources: delete
          deny-settings-mode: denyDelete   # start here, not denyWriteAndDelete

5 · Now actually exercise the two ideas

  • Prove ownership tracking. Delete the storage account resource from main.bicep, open a PR, and read the what-if preview. Merge, and watch the stack remove it. Then compare: run a plain az deployment group create with the same template and confirm a removed resource simply survives.
  • Prove drift prevention. Go to the portal and try to delete the App Service. It will be refused, and the error will name the stack. This is the moment the concept lands.
  • See what what-if gets wrong. Add a property the provider defaults, re-run, and look for noise in the diff. Knowing where the preview is imprecise is more useful than assuming it is exact.
  • Stretch: add a required reviewer to the production environment and watch the deploy job pause. That pause is the gate from S1, in about four lines of YAML.
Teardown — one command, and it works because you used a stack

az stack group delete --name bicep-lab --resource-group rg-bicep-lab --action-on-unmanage deleteAll --yes

Then remove the container: az group delete --name rg-bicep-lab --yes --no-wait, and delete the app registration: az ad app delete --id <appId>.

Note carefully what just happened: the stack made teardown a single, complete, reliable command — exactly the property a plain ARM deployment does not give you, and exactly the property that made terraform destroy feel so good in S1. Same benefit, delivered by the platform rather than by a file you maintain.

Next →

Session 3 goes to GCP, the cloud that retired its own IaC language and told everyone to use Terraform — an unusual and instructive decision. It is also where the deployment strategies stop being a menu and become arithmetic: Google's SRE framing turns your C7 error budget into the trigger that aborts a rollout automatically, and there is a calculator to prove your instinct about canary sizing is wrong.

Why this session exists

Session 3 · the problem, in plain words

Google did something none of the other hyperscalers did: it retired its own infrastructure-as-code language and told customers to use a third-party tool instead. Cloud Deployment Manager reached end of support on 31 March 2026. Its replacement, Infrastructure Manager, does not introduce a new syntax — it runs Terraform for you.

That decision is worth pausing on, because it inverts the incentive you would expect. AWS and Microsoft both keep proprietary IaC languages that only describe their own cloud. Google looked at an ecosystem where the portable tool had already won and decided that competing with it was worth less than being excellent at running it.

The practical consequence for you is that the S1 Terraform material is the GCP material. There is no third dialect to learn. What is genuinely GCP-specific — and genuinely worth this session — is the layer above provisioning: how releases are engineered, and how Google's SRE discipline turns the reliability numbers from C7 into a mechanism that aborts a bad rollout without a human.

Correction to the material — the most consequential one in this stage

The GCP section lists Deployment Manager as "GCP's native IaC tool, using YAML or Jinja2 templates," and again under SRE toil reduction. As of 31 March 2026 it reached end of support — the service is gone and its APIs no longer function. Google's own migration guidance is explicit about the difference: Deployment Manager used YAML with Jinja or Python templates; Infra Manager leverages Terraform to create infrastructure deployments.

The section also routes source control through Cloud Source Repositories in both the "Plan" and "Develop" rows of its SDLC table. CSR reached end of sale on 17 June 2024: if your organisation had not already enabled the API, you cannot use it at all, and new projects outside an existing organisation cannot enable it. New work goes to GitHub, GitLab, or Google's Secure Source Manager.

Everything else in the section — Cloud Build, Artifact Registry, Cloud Deploy, the SRE material, the release strategies — is current and good. Two stale rows, not a stale section.

Core concept 1 — IaC on GCP: three paths, one language

GCP §15 (corrected) · what to actually use

Infra ManagerInfrastructure Manager. A managed Google Cloud service that executes your Terraform configurations and stores the state for you. You write standard HCL; Google runs plan and apply, keeps the state, and integrates with Cloud IAM and Cloud Build.

Config ConnectorA Kubernetes add-on that lets you manage Google Cloud resources as Kubernetes custom resources. A Cloud SQL instance becomes a YAML manifest you kubectl apply, and a controller reconciles it continuously — the same loop as any other Kubernetes object.

Config ControllerA hosted, Google-managed control plane bundling Config Connector, Policy Controller and Config Sync. Config Connector without you operating the cluster it runs in.

1Intuition

Think of it as three ways to run the same engine, differing only in who holds the keys and how often it runs.

  • Terraform yourself — you own the binary, the state bucket, the CI wiring. Maximum control and maximum portability, because nothing about it is GCP-specific.
  • Infra Manager — Google runs the same binary and holds the state. You give up some control and delete a whole category of chores: no bucket to bootstrap, no locking to configure, no state to encrypt and back up.
  • Config Connector — a fundamentally different rhythm. Not "run a tool when I say" but "a controller watches, continuously, forever." Cloud resources become objects your existing GitOps agent reconciles alongside your workloads.

This third one should feel familiar. You already write declarative manifests that a controller reconciles — that is what your KServe deployments are. Config Connector extends that loop past the cluster boundary so a Cloud Storage bucket and an inference service are managed by the same agent, in the same repository, with the same review process.

2Mechanism

The difference that matters is reconciliation cadence, and it changes what drift even means.

PathRuns whenState livesDrift behaviourBest for
Terraform in CIYou trigger itYour GCS bucketDetected on next plan. Invisible until then.Multi-cloud, existing Terraform platform, full control
Infra ManagerYou trigger itGoogle-managedSame as Terraform — plan-basedGCP-only teams who want Terraform without the plumbing
Config ConnectorContinuouslyIn the cluster, as object statusCorrected automatically, within minutesKubernetes-centric platforms already running GitOps
That middle column is the real decision. Continuous reconciliation means drift is self-healing — and also that a legitimate manual change gets silently reverted by a controller you may have forgotten is running.
The artifact is the mechanism — a GCP resource as a Kubernetes object
apiVersion: storage.cnrm.cloud.google.com/v1beta1
kind: StorageBucket            # a GCS bucket, as a K8s CRD
metadata:
  name: gemma-weights-prod
spec:
  location: US-CENTRAL1
  uniformBucketLevelAccess: true
  # kubectl apply this. A controller reconciles it. Forever.

Same file shape as your KServe InferenceService. Same kubectl apply. Same controller pattern. The only new thing is that the object on the other end lives outside the cluster.

3Trade-offs, limits, and where it breaks

  • Infra Manager is Terraform, so it inherits Terraform's licence question. You are consuming it as a managed service rather than distributing it, so BUSL is not a practical constraint here — but note the asymmetry: if you later move that configuration off GCP to run yourself, the licence question arrives with it. This is one of the few places where "which binary" and "which cloud" are genuinely coupled.
  • Config Connector fights anything else that writes. Two controllers with opinions about one resource is a loop that never settles. Pick one owner per resource and be strict about it. This is also why "Config Connector for everything" is usually wrong — the cluster itself has to be created by something else.
  • Continuous reconciliation reverts your incident fix. The S1 drift story ends differently here: your 3 a.m. console change is undone within minutes, possibly mid-incident, by a controller nobody thought about. That is either the best or the worst property of this model depending on whether you knew it was running.
  • Terraform on GCP has the same state chores as anywhere. A GCS backend with versioning and object-level locking, split by lifecycle, encrypted, access-restricted. Infra Manager exists to delete that list.
  • Migration off Deployment Manager was a one-way door. Google shipped DM Convert to translate configurations to Terraform, with terraform import for adopting resources without redeploying. If you meet an estate still on DM in 2026, it is already broken, not about to break.

Core concept 2 — Cloud Build and Cloud Deploy

GCP §15 · the CI half and the CD half, kept properly separate

Cloud BuildGCP's serverless CI service. You define steps in a cloudbuild.yaml; each step runs as a container, and steps share a workspace. Triggered by pushes, pull requests, tags, Pub/Sub messages, or schedules. Its output is an artifact.

Artifact RegistryThe store for build outputs — container images, Maven, npm, Python, Go, OS packages. Handles versioning, vulnerability scanning, and access control. The successor to Container Registry.

Cloud DeployGCP's managed continuous delivery service. It does not build anything. It takes an existing artifact and promotes it through a defined sequence of environments, with approvals, verification, progressive rollout strategies, and rollback.

Why the separation matters: most teams conflate "my pipeline" into one YAML file that builds and deploys. GCP splits them deliberately, and the split is architecturally correct — the build produces an immutable artifact once, and the delivery pipeline moves that exact artifact through environments without rebuilding. It is the S1 "build once, promote many" rule enforced by product boundaries.

Cloud Build · CI

Every step is a container

This is Cloud Build's defining idea and it is genuinely elegant: a build step is just an image plus arguments. Need Node? Use a Node image. Need Terraform? Use a Terraform image. There is no plugin ecosystem to learn because the container ecosystem is the plugin ecosystem.

steps:
  - name: 'gcr.io/cloud-builders/npm'
    args: ['install']
  - name: 'gcr.io/cloud-builders/npm'
    args: ['test']

Worth knowing: private pools run builds inside your VPC with VPC Service Controls, which is how you build against private GKE clusters or internal package mirrors without exposing them. Repositories (2nd gen) connect Cloud Build directly to GitHub and GitLab, which is the current path now that Cloud Source Repositories is closed to new customers.

Cloud Deploy · CD

A pipeline is a resource, not a script

  • Delivery pipeline — the ordered sequence of targets. Declared in YAML, and itself a Google Cloud resource you can manage with Terraform.
  • Target — a deployment destination: a GKE cluster, a Cloud Run service, or a custom target. Each can carry requireApproval: true.
  • Release — an immutable snapshot: this artifact, these manifests, this pipeline. Created once and promoted.
  • Rollout — one release arriving at one target. Rollouts have phases, which is how canary percentages are expressed.
  • Verify — a container Cloud Deploy runs after deploying to check the deployment actually works, before the phase is allowed to advance.
  • Automation rulespromote-release advances between targets automatically; advance-rollout advances between canary phases automatically. Both went GA in 2024. This is what turns continuous delivery into continuous deployment.
  • Deploy policies — block rollouts inside a defined time window. Change freezes as configuration.
  • Rollback — one action, from console, CLI or API. Plus automatic retry of failed rollouts and automatic rollback to the last successful one.
  • Skaffold underneath renders manifests per target.
The artifact is the mechanism — canary phases in a delivery pipeline
strategy:
  canary:
    runtimeConfig: { kubernetes: { gatewayServiceMesh: { ... } } }
    canaryDeployment:
      percentages: [5, 25, 50]     # then 100% automatically
      verify: true                 # run the verify container at each phase
# Each percentage is a PHASE. advance-rollout automation moves between them.
Where the material is right and it is worth repeating

The section's rollback table is accurate and worth memorising because the mechanism differs per service: App Engine — revert to the previous version. Cloud Run — reassign traffic to the previous revision. GKEkubectl rollout undo deployment. And its exam-shaped heuristics hold up: no-downtime release with fast rollback → blue-green on App Engine or Cloud Run; gradual rollout with monitoring → canary with Cloud Deploy; container deployment across environments → Cloud Deploy with targets.

Core concept 3 — SRE as a release mechanism, not a philosophy

GCP §15 · where C7 stops being a dashboard

1Intuition

You know the C7 definitions already: an SLI is a measured signal, an SLO is the target for it, an SLA is the contractual version with penalties, and the error budget is the unreliability the SLO permits — 99.9% availability over 30 days allows roughly 43 minutes of failure.

What this session adds is one move: stop treating the error budget as a report and start treating it as a spending account with a card attached.

Budget remaining? Ship. That is what it is for — an unspent error budget means you are over-investing in stability and under-investing in features. Budget exhausted? The pipeline stops accepting risky changes until reliability work restores it. No negotiation, no escalation, no judgement call at 4 p.m. on a Friday. It is the same rule for everyone, and it converts an argument about "are we moving too fast" into arithmetic.

Google's other two principles support this. Toil — manual, repetitive, automatable work with no lasting value — is capped at under 50% of an SRE's time, which is a budget for building automation rather than a hope. Blameless postmortems mean the goal is learning, not fault, because a team that fears blame hides the near-misses you most need to see.

2Mechanism

Here is the wiring. Every box is a service the GCP section already describes — the contribution of this stage is connecting them into a loop that closes without a human in it.

Click any componentAn SLO-gated automatic rollback. Nothing here is exotic; the discipline is in agreeing the threshold before the deploy, not during the incident.
1 · RELEASE Artifact manifests 2 · CANARY 5% Shift traffic start bake timer 3 · MEASURE Canary-scoped SLI p95 latency · error rate 4 · COMPARE Threshold agreed BEFORE the deploy 5a · HELD advance → 25% 5b · BREACHED automatic rollback 6 · BUDGET outer gate Every box is a service the section already describes. The contribution of this stage is the arrows.
The loop closes without a human. The judgement happens once, in a pull request, when the threshold is agreed — not at 2 a.m. when everyone is tired and the deploy is half-done.

3Trade-offs, limits, and where it breaks

  • The blended-SLI trap is the big one, and it is counter-intuitive. A small canary cannot move a global error rate enough to trip a global alert. You can run a completely broken model version at 1% and watch the SLO dashboard stay green for the entire bake window. The calculator below makes this concrete; internalise it before designing your first rollout.
  • Bake windows fight statistics. A 5% canary on a low-traffic service may not see enough requests in fifteen minutes to distinguish a real regression from noise. Either lengthen the window, widen the slice, or accept that you are not measuring anything. Compute the request count before choosing the numbers.
  • Automatic rollback needs a clean rollback path. Stateless services roll back trivially. Anything that has written a schema change, migrated data, or emitted events consumed by others does not. Expand/contract migrations are the prerequisite, not an optimisation.
  • Quality regressions do not show up in latency or errors. This is the GenAI-specific failure. A new adapter that responds fast, returns HTTP 200 every time, and produces worse answers passes every conventional SLI. You need a quality SLI — an eval score on sampled traffic, a refusal rate, a response-length distribution — or the canary is checking that the server is up, not that the model is good.
  • Error-budget policy is organisational, not technical. The hardest part is not the query; it is the prior agreement that an exhausted budget genuinely stops feature work. Without that agreement it is a dashboard everyone glances at and overrides.

Decision: Cloud Deploy, GitHub Actions, or a GitOps agent?

Follow "no ↓" until a "yes" exits right. The bottom-left box is the default.

Guard clauses · choosing a delivery mechanism on GCP

Cloud Build is the CI answer in almost every branch — this tree is about the delivery half.

Guard 1Do you already run a GitOps agent — Argo CD, Flux, or Config Sync — that your team trusts and operates?
YES →
Keep it, and add Cloud Build for CI. Do not run two reconcilers against one cluster. Progressive delivery comes from Argo Rollouts or Flagger rather than Cloud Deploy.

no ↓

Guard 2Do you need progressive delivery with automatic rollback across several environments, on GKE or Cloud Run, without building the machinery yourself?
YES →
Cloud Deploy. This is precisely its job — canary percentages, verify containers, approvals, deploy policies, one-action rollback, and automation rules that advance phases unattended. Assembling this in raw GitHub Actions is weeks of work you will maintain forever.

no ↓

Guard 3Does this pipeline also deploy to AWS or Azure from the same workflow?
YES →
GitHub Actions or GitLab CI as the orchestrator, with Terraform/OpenTofu for provisioning. Cloud Deploy targets GKE, Cloud Run and custom targets — it is not a multi-cloud release manager.

no ↓

Guard 4Are your deployment targets not GKE and not Cloud Run — Vertex AI endpoints, BigQuery objects, Cloud Functions, or something bespoke?
YES →
Cloud Build with explicit deploy steps, or Cloud Deploy custom targets if you want its promotion model. Be honest about the extra work custom targets involve before choosing them for a single service.

no ↓

Guard 5Is the whole thing one service, one environment, with a small team and no promotion story?
YES →
Cloud Build alone. Build, test, deploy in one cloudbuild.yaml. A delivery pipeline resource for a single target is ceremony without benefit — add it when the second environment appears.

no ↓

Default — bottom left Cloud Build for CI, Cloud Deploy for CD, Terraform (via Infra Manager or your own CI) for provisioning. This is Google's own recommended shape and it maps cleanly onto the S1 pipeline: Cloud Build owns commit through publish, Cloud Deploy owns staging through production with the gate in between, and Terraform owns the resources both of them run on. Three tools with three clean boundaries beats one tool doing everything badly — and it is the combination the PCA exam expects you to name.

The four dialects — release and delivery layer

The comparison from S1 and S2, now at the CD layer where GCP is strongest

CapabilityAWS nativeAzure nativeGCP nativeTerraform / OpenTofu
CI serviceCodeBuildAzure Pipelines / GitHub ActionsCloud Build — every step is a containerNot a CI tool; runs inside one
Release orchestrationCodePipelineAzure Pipelines stages + EnvironmentsCloud Deploy — pipeline, target, release, rollout as first-class resourcesn/a
Canary as configurationCodeDeploy traffic shifting (Lambda, ECS)Container Apps revision weights; Pipelines canary strategyPercentages + phases in the pipeline YAML, with verify per phaseDeclares resources, not rollouts
Automatic phase advanceVia CodePipeline stages + alarmsVia gates on Environmentsadvance-rollout and promote-release automation rules (GA 2024)n/a
Post-deploy verificationCodeDeploy ValidateService lifecycle hookPipelines postRouteTraffic hook; Monitor gatesverify: true — a container run per phasen/a
Automatic rollback triggerCloudWatch alarm on the deployment groupMonitor alert gate; slot swap backFailed verify, or an alerting policy; rollback to last successful rolloutn/a
Change-freeze windowsCustom, via pipeline conditionsBusiness-hours check on EnvironmentsDeploy policies — a first-class resourcen/a
Continuous drift correctionFlux + CFN Template Sync ControllerDeny settings prevent rather than correctConfig Connector / Config Sync — reconciles continuouslyPlan-based only
Artifact storeAmazon ECRAzure Container RegistryArtifact RegistryConsumes, does not host
SLO tooling in the loopCloudWatch alarmsAzure Monitor alert-query gatesCloud Monitoring SLO objects — SLIs, targets and burn rates as native resourcesCan provision the SLO objects themselves
Read the bottom two rows together. GCP is the only one of the three where an SLO is a first-class monitoring resource rather than an alarm you assemble — which is why the SLO-gated rollback pattern is least DIY here. That is not an accident; it is Google productising its own SRE practice.

Reality check

Design a canary on paper, prove the blind spot with arithmetic, then three cited things

Worked example: designing a model-version canary and its abort threshold

Concrete scenario, using your numbers. A Gemma inference endpoint serving 10 million requests per month — about 3,858 per minute. C7 gave it a 99.9% availability SLO over 30 days, which is an error budget of 10,000 failed requests, or roughly 43 minutes of total failure. You are rolling out a new fine-tuned adapter.

Canary blind-spot calculator

Move the sliders. The question is whether your global SLO alert would ever notice the canary is broken. The standard fast-burn page fires at a 14.4× burn rate over one hour.

1%
10%
99.9%
Blended error rate0.10%what a global SLI sees
Global burn rate1.0×vs 14.4× page threshold
Canary-scoped burn100×if you measure per version
Budget exhausted in30dat the blended rate
Invisible A canary at this size and error rate produces a global burn rate below the 14.4× page threshold. Your global SLO alert will never fire. The dashboard stays green for the whole bake window while every request the canary touches fails. Measure the canary version's own SLI, not the blended one.

Try this sequence, in order. Start at the defaults — 1% traffic, 10% errors — and note the global burn rate is exactly 1.0×, precisely at budget, completely invisible. Now push the error rate to 100%: a totally broken canary at 1% still only reaches 10× burn, still under the page threshold. Now leave errors at 10% and widen the slice to 20%: the global alert finally fires — but you have exposed twenty times as many users to get there. That is the trade the calculator exists to make visible.

The conclusion, stated plainly

Canary size and detectability are in direct tension, and you cannot resolve it by watching global metrics. A slice small enough to be safe is too small to move a blended number. The only way out is to measure the canary version separately — label your metrics by version and alert on the canary's own error rate against the same threshold. Then a 1% canary at 10% errors reads as a 100× burn rate and aborts within a minute, having harmed 1% of traffic instead of all of it.

This is the single most useful thing in this session, and it is not in any of your four sources.

The resulting rollout design

PhaseTrafficBakeRequests in windowAbort if (canary-scoped)
1 · Smoke0% (deployed, no traffic)Synthetic onlyverify container fails — health, model loads, one known-good prompt returns sane output
2 · Canary5%15 min≈2,900Canary error rate > 1%, or canary p95 latency > 850 ms, or eval score > 5% below incumbent
3 · Expand25%30 min≈29,000Same thresholds, now with enough volume for the latency distribution to be trustworthy
4 · Majority50%60 min≈116,000Same thresholds, plus a cost check — GPU utilisation per request against the incumbent
5 · Full100%24 h watchAllStandard SLO alerting resumes. Keep the previous revision deployable for the whole window.
Note phase 2's request count: about 2,900 requests. That is enough to detect a 1% error-rate regression with reasonable confidence, and not enough to trust a p95 latency figure to the millisecond. Sizing the window against the statistics you actually need is the step most rollout designs skip.
The GenAI-specific threshold nobody else will tell you to add

Three of the five abort conditions above are conventional — errors, latency, cost. The fourth is not, and for a model rollout it is the one that matters most: a quality SLI.

A new adapter can be fast, return HTTP 200 on every request, use less GPU than the incumbent, and be worse. Refusals climb. Answers get shorter and blander. A regression in one domain hides inside an aggregate that looks fine. Every conventional SLI passes and the canary promotes itself to 100% on schedule.

Practical options, cheapest first: run a fixed eval set through the canary during each phase and compare scores; track refusal rate and response-length distribution as cheap proxies; sample live traffic into an LLM-as-judge scorer; watch a downstream behavioural signal like thumbs-down rate or retry rate. Any of these beats not having one. A model canary without a quality signal is checking that the server is up.

Three cited things

Deprecation · Google Cloud

Deployment Manager: end of support, 31 March 2026

Google's deprecation notice is unambiguous: support for Cloud Deployment Manager is discontinued, all related APIs and functions are no longer supported, and customers were told to migrate to Infrastructure Manager or an alternative before that date. Google's stated reasoning was that it had spent years modernising the Google Cloud deployment experience through Infrastructure Manager.

The comparison in Google's own migration document is the part worth quoting the shape of: Deployment Manager defined infrastructure declaratively in YAML with Jinja or Python templates; Infra Manager leverages Terraform, so you write Terraform configuration files which Infra Manager deploys and manages on Google Cloud. Google also shipped DM Convert to translate existing configurations, and pointed users at terraform import to adopt already-deployed resources without recreating them.

Why it matters beyond the correction: this is a hyperscaler concluding that owning the IaC language was not worth it. When you argue "native versus Terraform" in a design review, that is a data point on the Terraform side that costs you nothing to cite.

Capability · Cloud Deploy

The automation that turns delivery into deployment went GA in 2024

Cloud Deploy's release notes track the progression precisely: canary strategy support reached general availability, parallel deployment to multiple targets reached GA, and delivery pipeline automation became generally available in March 2024. The two rules are the ones that matter for this session — promote-release automatically advances a release between targets, and advance-rollout automatically advances a canary between its percentage phases as each one succeeds. Every automation run produces an automationRun resource, so you get an audit trail of what advanced, when, and why.

Two further capabilities arrived alongside: automatic retry of failed rollouts and automatic rollback to the most recent successful rollout, and deploy policies that block rollouts during a specified time window.

Why it matters: this is the complete SLO-gated rollback loop available as configuration rather than as a bespoke system. Two years ago you would have written this yourself with Cloud Functions and alerting webhooks — the material's own suggestion. Now you declare it.

Industry data · DORA 2025, revisited

The 2025 report restructured itself around exactly this session's argument

DORA's 2025 research reorganised its metrics into throughput and stability as separate categories, added rework rate as a fifth metric — how often teams must ship unplanned fixes for user-facing defects — and redefined mean-time-to-restore as failed deployment recovery time. It also replaced the old low/medium/high/elite tiers with seven team archetypes, because a single performance axis stopped explaining the data.

The finding underneath the restructure: AI adoption raises throughput while correlating with higher instability — more change failures, more rework, longer recovery. One analysis of the same period found PRs merged per person up 98% while incidents per PR rose 242.7%. DORA's framing is that AI is an amplifier: it makes strong delivery systems faster and weak ones more chaotic.

Why it matters to this session specifically: the report's own conclusion is that reliability sits in its own category, assessed through SLOs and SLIs reflecting user experience. That is precisely the mechanism in the wiring diagram above. If you are shipping model changes at AI speed — and you are — the canary, the quality SLI, and the error-budget gate are not process overhead. They are the things keeping the second number down while the first one climbs.

Release engineering · the practices behind the tools

Four properties that make a release trustworthy

  • Hermetic builds. Same source in, same artifact out, regardless of which machine built it. Pin base images by digest, lock dependencies, never resolve latest at build time. Without this, "we tested this artifact" is not a true statement.
  • Build once, promote many. One artifact travels dev → staging → prod unchanged. Cloud Deploy enforces this structurally: a release is an immutable snapshot, and promotion moves it rather than rebuilding it.
  • Policy at the gate, not in a wiki. Binary Authorization can require that an image was built by your pipeline and passed its checks before GKE or Cloud Run will run it. That converts "we always scan images" from an aspiration into an admission-control decision.
  • Toil budget as a real budget. Google's under-50% rule is what pays for all of the above. If nobody is allowed time to automate the release, the release stays manual, and every point in this stage stays theoretical.

Apply it — rebuild your Vertex deployment properly

Session 3 · the end goal of this stage, on your own workload

This is the thing the whole stage was pointing at. Your Vertex AI deploy script builds an image, pushes it, and creates an endpoint — imperative, non-idempotent, unreviewable, with no teardown. Here is the honest target architecture, including the part that should not be Terraform.

Layer 1 · Terraform

The durable plumbing

Artifact Registry repository, the Vertex AI endpoint itself, service accounts and IAM bindings, the weights bucket, the GKE cluster and node pools if you serve there, monitoring dashboards, alert policies, and the Cloud Deploy delivery pipeline resource.

Everything here is long-lived, changes rarely, benefits from plan review, and should be destroyable in one command. This is the layer your script never had.

Layer 2 · Cloud Build

The artifact

Build the serving container, run tests, push to Artifact Registry with an immutable digest tag, scan it. Triggered by a merge to main. Produces exactly one thing and then stops.

The model weights are not in this layer's git history — they live in the bucket Terraform declared, versioned separately. Keep artifacts and artifact stores in different tools.

Layer 3 · SDK or Cloud Deploy

The model-version rollout

Upload the model version, deploy it to the endpoint alongside the incumbent at 0% traffic, then shift the traffic split in steps while watching the canary-scoped SLIs.

This is genuinely imperative and that is correct. Model upload is a one-shot operation; a traffic split is a sequence of timed decisions. Do not contort it into a declarative resource — declare the endpoint, orchestrate the rollout.

Layer 1 sketch — the plumbing your script never declared
resource "google_vertex_ai_endpoint" "gemma" {
  name         = "gemma-inference-${var.env}"
  display_name = "gemma-inference-${var.env}"
  location     = var.region
  labels       = { env = var.env, managed_by = "terraform" }
}

resource "google_container_node_pool" "l4" {
  cluster            = google_container_cluster.serving.id
  initial_node_count = 0
  autoscaling { min_node_count = 0, max_node_count = 3 }   # scale to zero
  node_config {
    machine_type     = "g2-standard-8"
    guest_accelerator { type = "nvidia-l4", count = 1 }
    spot             = var.env != "prod"            # spot everywhere but prod
  }
  lifecycle { ignore_changes = [node_count] }        # the autoscaler owns this
}

Read the last two lines of each block. min_node_count = 0 is the difference between an idle L4 costing money overnight and costing nothing. ignore_changes = [node_count] is the S1 drift lesson applied: the autoscaler owns that field, so Terraform must be told not to fight it. Both are one line, and both are the kind of thing an imperative script has no place to express.

Where a GPU / GenAI workload stresses this specifically
  • Two versions on one endpoint means two sets of replicas. A canary is not free on GPU — during the overlap you are paying for incumbent capacity plus canary capacity. Give the canary min_replica_count = 1 and let it autoscale; do not mirror production sizing for a 5% slice.
  • Model load time dominates your rollout clock. A multi-gigabyte model takes minutes to load before it serves a single request. Deploy at 0% traffic first, wait for readiness, then start shifting. Skipping the 0% phase means your first canary requests hit a cold replica and your latency SLI aborts a perfectly good rollout.
  • Accelerator duty cycle is an SLI you actually have. Vertex exposes autoscaling targets on accelerator duty cycle, not just CPU. A new adapter that quietly needs 40% more GPU per token shows up here long before it shows up in the invoice.
  • Keep the previous version deployed at 0% during the watch window. Rollback then costs one traffic-split update rather than a full redeploy and reload. On GPU that is the difference between seconds and ten minutes of degraded service.
  • Prediction logging is a real cost line. Sampling at 10% into BigQuery is manageable; 100% scales with request volume and surprises people. Declare the sampling rate in IaC so it is a reviewed decision rather than a default.

Optional lab — Terraform a scale-to-zero endpoint, then canary a version (~60 minutes)

The most expensive lab in this stage, so read the guardrails first. It is still under a dollar if you follow them, and it is the one that most directly rebuilds the thing you already wrote imperatively.

Before you start — cost guardrails, and these are not optional

  • Set a budget alert with a $5 threshold before you create anything. Billing → Budgets & alerts. GPU resources bill by the minute and mistakes here cost real money, unlike S1 and S2.
  • Use CPU, not GPU, for the lab itself. The mechanics of endpoints, traffic splits and canaries are identical on n1-standard-2 and cost a fraction. Prove the pattern on CPU; apply it to L4 later with the numbers you already trust.
  • Set min_replica_count = 1 and delete the endpoint the same day. Vertex endpoints bill for provisioned replicas whether or not anything calls them. This is the single most common surprise-bill mechanism on Vertex.
  • If you insist on a GPU, request L4 quota first and expect the apply to fail with a quota error otherwise — that failure is a quota issue, not a bug in your config.

1 · Provision the plumbing declaratively

# backend + provider omitted — use a GCS bucket with versioning
resource "google_artifact_registry_repository" "models" {
  location      = var.region
  repository_id = "inference-lab"
  format        = "DOCKER"
}
resource "google_vertex_ai_endpoint" "lab" {
  name         = "lab-endpoint"
  display_name = "lab-endpoint"
  location     = var.region
  labels       = { env = "lab", delete_by = "today" }
}
terraform init && terraform plan   # read it properly
terraform apply

2 · Deploy two model versions to one endpoint

Use the SDK — this is layer 3, and it is imperative on purpose. Deploy v1 at 100%, then deploy v2 at 0% traffic and wait for it to become ready before shifting anything.

gcloud ai endpoints deploy-model $ENDPOINT_ID --region=$REGION \
  --model=$MODEL_V2 --display-name=v2-canary \
  --machine-type=n1-standard-2 --min-replica-count=1 \
  --traffic-split=0=100          # v2 deployed, receiving nothing

3 · Run the canary by hand, once, to feel the rhythm

# shift 5% to the canary
gcloud ai endpoints update $ENDPOINT_ID --region=$REGION \
  --traffic-split=$V1_ID=95,$V2_ID=5

# ... send traffic, watch, wait the bake window ...

# then 25, then 50, then 100 — or roll back in one command
gcloud ai endpoints update $ENDPOINT_ID --region=$REGION \
  --traffic-split=$V1_ID=100

That last command is the rollback. One call, seconds, no rebuild, no reload — because the incumbent was never undeployed. Notice how much cheaper that is than anything your original script could offer.

4 · Now make it measurable — this is the actual lesson

  • In Cloud Monitoring, build a chart of prediction error rate filtered by deployed_model_id. Confirm you can see the two versions separately.
  • Deliberately deploy a broken v2 — a container that returns 500 on one in ten requests. Set the split to 5%.
  • Watch the blended endpoint error rate. It will sit around 0.5%. Then look at the per-version chart: the canary is at 10%.
  • That gap, on your own screen with your own traffic, is the calculator made real. Everything else in this lab is setup for this one observation.

5 · Stretch, if you have time

  • Create a Cloud Monitoring SLO object on the endpoint and look at the burn-rate chart. GCP models this natively; it is worth seeing.
  • Write the traffic-shift loop as a script that reads the canary-scoped SLI between steps and aborts on breach. That script is a hand-rolled Cloud Deploy automation rule — and writing it once is the best way to understand what the managed service is doing for you.
  • Move the endpoint into a module with an env variable and instantiate it twice. Three environments, one definition — the S1 arithmetic, on your own workload.
Teardown — do this today, not tomorrow

Undeploy both models first, since Terraform does not own them: gcloud ai endpoints undeploy-model $ENDPOINT_ID --region=$REGION --deployed-model-id=$ID for each.

Then terraform destroy and confirm the plan shows every resource going. Finally check the billing page for anything still running — an undeployed model still occupies Model Registry, and a forgotten replica is the one thing here that bills overnight.

The asymmetry is the lesson: Terraform tore down its layer in one command; the SDK-managed layer needed manual cleanup. That is exactly the trade you accepted in the three-layer design, and it is why the boundary between the layers should be a deliberate decision rather than an accident.

Stage close — what you can now do

Check yourself against the end goals from Tab 0

You can explain, without notes

  • Why declarative desired state beats imperative scripts, using the reconciliation loop and your own Vertex script as the counter-example.
  • The config/state/reality triangle, and which comparison produces a plan, drift, and an import.
  • Why Terraform is not open source, what OpenTofu is, and when the distinction changes a decision.
  • The three deployment strategies with their cost, rollback-speed and blast-radius trade-offs.
  • Why a 1% canary is invisible to a global SLO alert, with the arithmetic.

You can design and build

  • A pipeline with build, test, gate and staged-rollout stages, and defend each gate.
  • An SLO-gated automatic rollback wired from a canary-scoped SLI to a traffic-split reversion.
  • The same intent in CloudFormation/CDK, in Bicep with a Deployment Stack, and in Terraform on GCP — and say why you would choose each.
  • A three-layer split for a model deployment: Terraform for plumbing, CI for the artifact, an orchestrator for the version rollout.
  • A rollout whose abort thresholds — including a quality SLI — were agreed before the deploy started.

The one habit worth carrying out of this stage above all others: put the plan in the pull request. Not the code — the plan. A reviewer who approves an outcome catches the -/+ replace on the weights bucket, the silently removed incident fix, and the resource that quietly became an orphan. A reviewer who approves a script catches typos.

C10 ends

Everything from Stages 1–9 is now expressible as code, reviewable as a diff, and deployable through a pipeline that can undo itself. The natural next question is governance at scale: policy as code, cost controls that enforce rather than report, and how an organisation keeps hundreds of these pipelines coherent without slowing any of them down.

Web grounding (verified July 2026): Deployment Manager deprecation — Google Cloud Cloud Source Repositories end of sale Cloud Deploy release notes Cloud Deploy pipeline automation GA The Future of AWS CodeCommit CrowdStrike Falcon Content Update Preliminary PIR AWS DynamoDB DNS race-condition postmortem coverage Bicep with Azure Pipelines (BicepDeploy@0) azure/bicep-deploy action Microsoft's Bicep + GitHub Actions reference implementation OpenTofu manifesto CloudFormation Template Sync Controller for Flux. DORA 2025 State of DevOps figures as reported across DORA and secondary analyses.

C10 of a Cloud for GenAI · Line D curriculum. Teaching material written from the sources above, in the original own words; no source text or figures reproduced.

← C9The path
Next stage · ↺ 08 →genaipros · C10 · Automation · IaC & CI/CDAI for Everyone ↗