Module 3: Infrastructure As Code For An Ai Endpoint
1. Introduction: what Terraform knows how to declare without needing to invoke anything
Description
ADR-001-llm-as-escalation-path.md (Module 1) has a row bearing this module's name: "M3 — IaC for an AI endpoint | Declares only the escalation path's infrastructure (bedrock.tf); process-shipment-manifest is not touched". GENAI-COST-PROFILE.md (Module 2) closed with the exact sentence that opens this module: "Module 3 opens... declaring the AI workload's complete infrastructure as real code, actually validated and planned, with no need for LocalStack or an AWS account to do it — the module with the most executed weight in this entire guide". This lesson explains why that sentence isn't a marketing promise: it's a direct consequence of how Terraform works, verifiable on any machine, right now, with no service running behind it.
Connection to the module
This entire module depends on a single technical fact, and this lesson installs it before touching a single new line of HCL: terraform validate and terraform plan, on a resource that doesn't exist yet in state, never need a network call to the real API. Lessons 2 through 5 build on that fact — the provider's landscape, the bedrock-guardrail module, the least-privilege role, the complete plan. Lessons 6 and 7 mark, with the same precision, exactly where that fact stops applying: the moment apply does need to talk to something real.
This module's map: the 8 lessons
M3 -- INFRASTRUCTURE AS CODE FOR AN AI ENDPOINT
1 Introduction why validate/plan don't call the real API (this lesson)
2 What resources exist for Bedrock aws_bedrock_guardrail and two more, real schema cited
3 Hands-on: bedrock-guardrail the new module, isolated fmt + validate
4 Hands-on: the least-privilege role BedrockManifestExtractorRole, InvokeModel scoped
5 Hands-on: the complete plan bedrock.tf + the module, real N to add
6 The exact limit why apply doesn't run against LocalStack Hobby
7 Hands-on: what does apply the exact boundary, within the same .tf file
8 Project: declared infrastructure bedrock.tf + module, the honesty ledger
| # | Lesson | What runs/gets practiced |
|---|---|---|
| 1 | Introduction (this one) | The mechanism that makes the rest of the module possible, explained and cited |
| 2 | What resources exist for Bedrock | Provider landscape, real schema extracted from terraform providers schema -json |
| 3 | Hands-on: modules/bedrock-guardrail/ | Executed: new module, isolated terraform fmt + terraform validate |
| 4 | Hands-on: BedrockManifestExtractorRole | Executed: aws_iam_role + policy scoped to a model ARN, real terraform validate |
| 5 | Hands-on: complete terraform plan | Executed: bedrock.tf + the module, real output with N to add |
| 6 | The exact limit | Representative: why apply doesn't run against LocalStack Hobby, official citation |
| 7 | Hands-on: what does apply | The exact boundary between what's real and what's representative, within the same .tf |
| 8 | Project: declared infrastructure | Executed: the complete repository, with this module's honesty ledger |
Notice this table's shape: five of eight lessons are marked Executed, with no qualifier — more than any other module in this guide. That's not a design coincidence; it's the direct consequence of the fact this lesson explains now.
Analogy: reviewing the blueprints, not laying a brick
Before a construction company lays a single brick, an architect reviews a house's blueprints: do the measurements add up? does the staircase fit in the allotted space? is the second floor's load supported by the right columns? That review is thorough, it's real, and it doesn't need the lot, doesn't need the materials, doesn't need the construction crew present. It happens on paper, against the rules of structural engineering, and produces a complete, reliable list of "this is going to work" or "this doesn't add up" — before a single physical wall exists to compare it against.
terraform validate is exactly that blueprint review: it confirms the HCL you wrote is syntactically correct and that every argument you gave each resource matches what the provider's schema expects — correct types, required fields present, nothing badly nested. terraform plan, one step further, is as if the architect also calculated, pen in hand, exactly which materials would need to be ordered and in what order to place them, without having called the materials supplier yet: for a house that doesn't exist yet, everything that needs ordering is deduced from the blueprint itself, not from a site visit. None of this replaces the real construction — that's apply, and lesson 6 marks exactly where that step stops in this lab —, but it confirms, with real, verifiable certainty, that the blueprint itself has no errors.
The exact mechanism, explained with no shortcuts
terraform validate and terraform plan do different jobs, and it's worth separating them precisely before moving on:
WHAT EACH COMMAND DOES, AND WHAT IT NEEDS TO DO IT
terraform validate
│
│ Reads the HCL. Compares it against the provider's *schema*
│ already installed locally (the same one lesson 2 extracts
│ with `terraform providers schema -json`). Confirms types,
│ required fields, properly nested blocks.
│
│ NEEDS: the HCL on disk + the provider already downloaded
│ (`terraform init`). Does NOT need network access to any
│ AWS endpoint, valid credentials, or prior `state`.
▼
Success! -- or a list of syntax/type errors, no network
terraform plan
│
│ Compares the declared HCL against the current `state` --
│ the file (or backend) recording which resources exist
│ ALREADY, and with what attributes, as of the last time
│ Terraform read or created them.
│
│ For a NEW resource -- one the `state` doesn't know about
│ yet -- there's nothing to read from reality: the complete
│ diff ("this is going to be created, with these attributes")
│ gets built 100% from the HCL you wrote. Zero network calls
│ to the resource itself.
│
│ For a resource that ALREADY EXISTS in the `state`, plan
│ DOES need to refresh it against the real API -- to detect
│ drift, a change made outside Terraform -- and that DOES
│ need real connectivity.
▼
Plan: N to add, 0 to change, 0 to destroy -- no network, if N
resources are ALL new (exactly this module's case)
This entire module lives in the left half of that second block: everything bedrock.tf and modules/bedrock-guardrail/ declare, at this point in the guide, is new — none of it exists yet in any state, in any lab, in any account. That's why terraform plan on this infrastructure can run, for real, with no LocalStack running and no real AWS account behind it — not because Terraform "simulates" something, but because, literally, there's nothing yet that needs to be read from reality.
The evidence this has already been proven, isn't being promised here for the first time. This same guide's Module 2, lesson 5 already ran, for real, exactly this sequence against bedrock.tf's first draft:
terraform init -input=false
terraform validate
terraform plan -out=tfplan -input=false
What to expect (literal — already executed and documented in this guide's Module 2, lesson 5):
Terraform has been successfully initialized!
Success! The configuration is valid.
Plan: 15 to add, 0 to change, 0 to destroy.
Fifteen resources — all of andes-cargo-infra/ up to that point, including the first draft aws_bedrock_guardrail —, planned end to end, with no port 4566 listening anywhere. This module repeats exactly that sequence (lesson 5), now with the complete infrastructure — the module and the IAM role lessons 3 and 4 build —, and with a higher resource count.
The gap this does NOT fill, stated up front
This guide's own Module 1, lesson 7 already anticipated, in its common mistakes section, the question this lesson could unintentionally raise: "if Terraform can validate/plan a Bedrock resource without LocalStack or an AWS account, why couldn't awslocal bedrock list-foundation-models do the same?" The answer, already given there and repeated here because it's the foundation of this entire module: these are fundamentally different operations. terraform plan on a resource that doesn't exist yet never needs to read anything real. awslocal bedrock list-foundation-models is the opposite — a call that does need a real API, running, responding, with a license plan that includes it. This entire module lives off the first kind of operation. Lesson 6 confirms, with the same honesty, that apply — actually creating the resource — belongs to the second kind, and that's why it stops exactly where Module 1 already predicted it would.
Common mistakes
Thinking terraform plan "simulates" the infrastructure, as if it were some kind of internal LocalStack (mechanism-confusion mistake). What happens: someone concludes Terraform has its own built-in AWS emulation, and that's why plan "works without LocalStack." How to spot it: if your explanation of why plan runs with no network mentions some form of AWS simulation on Terraform's part. How to fix it: Terraform doesn't simulate anything — a new resource's plan is, literally, the direct translation of the HCL you wrote into "this is what I'm going to ask the API when you run apply," without having made that request yet. There's no fake AWS running anywhere during plan; there's simply nothing yet that needs consulting.
Assuming this applies to ANY terraform plan, not just new resources (over-generalization mistake). What happens: someone concludes that, since this module's plan ran with no network, no Terraform plan ever needs real connectivity. How to spot it: if your mental rule is "terraform plan never needs network," without the "for a new resource" condition. How to fix it: a resource that already exists in the state does need to refresh against the real API during plan — to detect whether someone changed it manually outside Terraform, the drift problem terraform-and-iac-guide already covered. This module's case is the favorable case, not the general one: everything new here is, precisely, new.
Expecting this lesson, on its own, to already settle whether apply runs or not in this lab (forward-looking expectation mistake). What happens: someone finishes this lesson and assumes that, since validate/plan run with no network, apply is also going to run just as cleanly. How to spot it: if your plan is to jump straight to a terraform apply on bedrock.tf before lesson 6. How to fix it: apply is a completely different operation — it does need to talk to something real, whether LocalStack or AWS —, and this guide already confirmed, in Module 1, lesson 7, that Bedrock specifically isn't available on LocalStack's free plan. This module's lesson 6 documents that exact limit, with the same honesty as the rest of the guide.
Exercises
Exercise 1 — Explain, in your own words, the difference between what terraform validate needs and what terraform plan needs for a new resource. A colleague who's never used Terraform asks why one command ("validate") never needs network, while the other ("plan") sometimes does, sometimes doesn't. Explain the difference to them using this lesson's example.
See solution
terraform validate only compares your HCL against the provider's schema — which arguments exist, of what type, which are required — already downloaded locally by terraform init; it never needs network, for any resource, in any case, because it never looks beyond your own file and the installed schema. terraform plan, on the other hand, does need to compare against reality — but only for resources that already exist in the state. For a completely new resource, there's no prior reality to consult: the complete plan gets built by reading your HCL, exactly like validate. That's why, in this module, where everything is new, plan behaves — as far as needing network or not — just like validate.
Exercise 2 — Predict what would happen if, after this module, someone ran terraform plan again on bedrock.tf, AFTER having applied the infrastructure against a real AWS account. Based on this lesson's "WHAT EACH COMMAND DOES" diagram, would that second plan still run with no network?
See solution
No. Once aws_bedrock_guardrail.this (inside the module) actually exists in the state — because someone ran apply against a real account —, any later plan on that same resource stops being the "new resource" case and enters the "existing resource" case: Terraform needs to refresh it against the real Bedrock API before calculating the diff, to detect whether something changed outside Terraform since last time. This lesson's exact condition — zero network needed — applies only while the resource still doesn't exist in any state, which is, precisely, the state this entire module works in.
Exercise 3 — Connect this lesson to the mistake Module 1, lesson 7 already anticipated. Without rereading that whole lesson, explain why "Terraform can plan Bedrock without LocalStack" and "awslocal couldn't list Bedrock models without LocalStack" aren't a contradiction.
See solution
It isn't a contradiction because these are two completely different types of operation, not two different results for the same question. terraform plan on a new resource never needs to read anything from reality — the diff is calculated solely from the declared HCL. awslocal bedrock list-foundation-models is, by nature, a query that does need a real API responding with real data — there's no HCL from which the response could be derived. That one works with no network and the other doesn't isn't an inconsistency in this guide: it's the exact difference between "declaring an intent" (what Terraform does) and "querying a real state" (what awslocal always does).
Summary and next step
This lesson explained the exact mechanism that makes this module the most executed one in the entire guide: terraform validate never needs network, for any resource; terraform plan, for a resource that doesn't exist yet in any state, doesn't need it either, because the complete diff gets built from the declared HCL, with nothing real to consult yet. You confirmed this with already-real evidence from Module 2, lesson 5 (Success! and Plan: 15 to add, with no LocalStack running), and you saw, ahead of time, where that same mechanism stops applying — apply, lesson 6 — without needing to reread all of Module 1.
Before moving on you should be able to: explain unaided why validate never needs network; explain under exactly what condition plan doesn't need it either; and anticipate, without anyone telling you, that apply is a different kind of operation, one for which the same argument doesn't apply.
Lesson 2 puts this mechanism to work over the complete landscape of Bedrock resources the hashicorp/aws provider exposes — extracted from the real schema, not memorized from an earlier version of the documentation.
Resources
- Terraform Docs — Command: validate — official reference, including confirmation that it requires no access to remote data providers.
- Terraform Docs — Command: plan — official reference for the
refresh→diff→planlifecycle. - This guide's Module 1, lesson 7 (
07-hands-on-the-honest-attempt-against-bedrock-on-localstack.md) — the source of the question this lesson answers directly in its common mistakes section. - This guide's Module 2, lesson 5 (
05-hands-on-the-honest-attempt-of-infracost-scan-on-bedrock-tf.md) — the source of the real evidence (Success!,Plan: 15 to add) cited in this lesson.