Module 3: Infrastructure As Code For An Ai Endpoint
2. What Terraform resources exist for Bedrock
Description
Lesson 1 confirmed why Terraform can declare Bedrock with no need for a real API. This lesson confirms what, exactly, there is to declare — not from memory, not from a generic search, but extracted directly from the hashicorp/aws provider's schema, already installed in this environment, with the command designed exactly for this: terraform providers schema -json. That extraction's result is the literal source for the rest of this module and all of Module 4.
Connection to the module
The bedrock-guardrail module lesson 3 builds, and lesson 4's least-privilege role, use only arguments that appear in the schema extracted here. When Module 4 declares the guardrail's full six policies, it's going to cite this same source, extended — never an invented argument, nor one copied from an earlier version of the documentation.
Analogy: the manufacturer's catalog, not the sales brochure
A furniture manufacturer's sales brochure tells you, in general terms, "this cabinet has adjustable compartments." The manufacturer's technical catalog — the one the installer uses — tells you exactly how many screws, of what size, in what position, with what tolerance. terraform providers schema -json is the hashicorp/aws provider's technical catalog: not a conceptual description of "Bedrock has guardrails," but the exact name of every argument, whether it's required or optional, and how each block nests — the only source a real main.tf can use without guessing.
Step 1 — The full landscape: how many Bedrock resources exist in the provider
Before focusing on the three resources this module uses, it's worth seeing the catalog's real scale. The following command extracts the already-installed provider's complete schema (hashicorp/aws v6.60.0, confirmed since Module 2) and counts how many resources have bedrock in their name:
terraform providers schema -json > schema.json
python3 -c "
import json
data = json.load(open('schema.json'))
aws = data['provider_schemas']['registry.terraform.io/hashicorp/aws']
bedrock = sorted(k for k in aws['resource_schemas'] if 'bedrock' in k)
print(len(bedrock), 'resources with \'bedrock\' in the name')
for r in bedrock[:10]:
print(' -', r)
print('...')
"
What to expect (literal — extracted from the real provider installed in this environment):
39 resources with 'bedrock' in the name
- aws_bedrock_custom_model
- aws_bedrock_evaluation_job
- aws_bedrock_foundation_model_agreement
- aws_bedrock_guardrail
- aws_bedrock_guardrail_version
- aws_bedrock_inference_profile
- aws_bedrock_model_invocation_logging_configuration
- aws_bedrock_provisioned_model_throughput
- aws_bedrock_use_case_for_model_access
...
Thirty-nine resources — the complete catalog ranges from resources for fine-tuning custom models (aws_bedrock_custom_model) to the entire aws_bedrockagent_* and aws_bedrockagentcore_* family for building agents with Bedrock AgentCore. This guide uses almost none of those 39 — the boundary with AI Engineering (Module 1, lesson 4) is exactly why: agents, knowledge bases, custom models belong to the territory of building the AI application, not operating its infrastructure. The three that do matter here are the ones governing a guardrail and a managed inference capacity — the infrastructure around the model, not the model or the application itself.
Step 2 — aws_bedrock_guardrail: this module's central resource
Extracted from the same schema.json, with the complete block (not summarized — every argument name, every level of nesting, exactly as the provider exposes it):
aws_bedrock_guardrail
======================
blocked_input_messaging: string [required]
blocked_outputs_messaging: string [required]
name: string [required]
description: string [optional, computed]
kms_key_arn: string [optional]
tags: map(string) [optional]
created_at: string [computed]
guardrail_arn: string [computed]
guardrail_id: string [computed]
region: string [optional, computed]
status: string [computed]
tags_all: map(string) [computed]
updated_at: string [computed]
version: string [computed]
content_policy_config { } (block, nesting=list)
tier_config: list(object) [optional, computed]
filters_config { } (block, nesting=set)
type: string [required] -- e.g. PROMPT_ATTACK, HATE, SEXUAL
input_strength: string [required] -- NONE | LOW | MEDIUM | HIGH
output_strength: string [required]
input_action, output_action: string [optional]
input_enabled, output_enabled: bool [optional]
input_modalities, output_modalities: set(string) [optional]
sensitive_information_policy_config { } (block, nesting=list)
pii_entities_config { } (block, nesting=list)
type: string [required] -- e.g. EMAIL, PHONE, NAME
action: string [required] -- BLOCK | ANONYMIZE
input_action, output_action: string [optional, computed]
input_enabled, output_enabled: bool [optional, computed]
regexes_config { } (block, nesting=list)
name: string [required]
pattern: string [required]
action: string [required]
description: string [optional, computed]
input_action, output_action: string [optional, computed]
input_enabled, output_enabled: bool [optional, computed]
topic_policy_config { } (block, nesting=list)
tier_config: list(object) [optional, computed]
topics_config { } (block, nesting=list)
name: string [required]
definition: string [required]
type: string [required] -- DENY
examples: list(string) [optional, computed]
contextual_grounding_policy_config { } (block, nesting=list)
filters_config { } (block, nesting=list)
type: string [required] -- GROUNDING | RELEVANCE
threshold: number [required]
word_policy_config { } (block, nesting=list)
managed_word_lists_config { } (block, nesting=list)
type: string [required] -- e.g. PROFANITY
input_action, output_action: string [optional]
input_enabled, output_enabled: bool [optional]
words_config { } (block, nesting=list)
text: string [required]
input_action, output_action: string [optional]
input_enabled, output_enabled: bool [optional]
cross_region_config { } (block, nesting=list)
guardrail_profile_identifier: string [required]
timeouts { } (block, nesting=single)
create, update, delete: string [optional]
Five policies — content, sensitive information, topics, contextual grounding, words — plus a cross-region block, all as nested blocks of a single resource. Notice something important for Module 4: almost every argument inside the policies is optional, not required — the only structural requirement is that, if you declare a policy block (say content_policy_config), the fields inside it (type, input_strength, output_strength in filters_config) are indeed mandatory. This is what lets this module (lesson 3) declare only two of the five policies — content and sensitive information — with no complaint from Terraform about the other three missing: the entire block of a policy you don't declare simply doesn't exist in the plan.
Step 3 — The other two: aws_bedrock_guardrail_version and aws_bedrock_provisioned_model_throughput
aws_bedrock_guardrail_version
==============================
guardrail_arn: string [required]
description: string [optional]
skip_destroy: bool [optional]
region: string [optional, computed]
version: string [computed]
timeouts { } (block, nesting=single)
create, delete: string [optional]
This resource publishes an immutable version of an already-declared guardrail — the difference between editing the guardrail "live" (the draft, DRAFT, which changes every time you apply aws_bedrock_guardrail) and fixing a specific numbered version for a production application to reference, so a future change to the guardrail doesn't affect it without warning. This guide doesn't declare it — Andes Cargo's guardrail stays in DRAFT throughout this entire lab, an honest decision for a system that doesn't have a real invocation to version yet —, but it's part of the provider's complete landscape, and the pattern (guardrail_arn as input, version as a computed output) is the same as any resource versioning flow already seen in terraform-and-iac-guide.
aws_bedrock_provisioned_model_throughput
==========================================
model_arn: string [required]
model_units: number [required]
provisioned_model_name: string [required]
commitment_duration: string [optional] -- NONE | ONE_MONTH | SIX_MONTHS
tags: map(string) [optional]
region: string [optional, computed]
provisioned_model_arn: string [computed]
tags_all: map(string) [computed]
timeouts { } (block, nesting=single)
create: string [optional]
This is the resource that would declare Provisioned Throughput — reserved inference capacity, billed per hour regardless of usage, the alternative to on-demand Module 2, lesson 2 already named and Module 6, lesson 7 picks back up with real numbers. Notice model_units, an integer: it's AWS's reserved-capacity unit, not tokens or invocations — reserving more model units increases guaranteed throughput, at a fixed cost per hour, exactly the "on = costs money" model Module 2, lesson 1 contrasted against on-demand's per-token cost. This guide doesn't declare it either: Andes Cargo, at the volume GENAI-COST-PROFILE.md projects ($0.00–$0.42/month), has no business case for reserved capacity — Module 6, lesson 7 explains exactly the volume point where that decision would change.
How this schema was extracted, so you can repeat it
The complete command, at the root of andes-cargo-infra/ (with terraform init already run, so the provider is installed):
terraform providers schema -json > schema.json
What to expect (literal): a single JSON file of about 19 MB — the complete schema for every provider used in the project (hashicorp/aws and hashicorp/archive), with all of their resources, not just Bedrock. The size is proof of the provider's real scale: thousands of resources and data sources, of which this guide uses, in total — counting the seven sibling guides —, fewer than twenty.
import json
data = json.load(open("schema.json"))
aws = data["provider_schemas"]["registry.terraform.io/hashicorp/aws"]
block = aws["resource_schemas"]["aws_bedrock_guardrail"]["block"]
print(sorted(block["block_types"].keys()))
What to expect (literal):
['content_policy_config', 'contextual_grounding_policy_config', 'cross_region_config', 'sensitive_information_policy_config', 'timeouts', 'topic_policy_config', 'word_policy_config']
Seven top-level nested blocks — the same complete list Step 2 showed, extracted here in three lines of Python against the same file. This is the exact method that produced every table in this lesson: never a copy from the web documentation (which can fall out of date relative to the provider actually installed), always the schema.json for the environment where the HCL is going to be validated.
Common mistakes
Copying an HCL example from an online tutorial without confirming the argument exists in the installed schema (unverified-source mistake). What happens: a tutorial — or an earlier version of AWS's documentation — uses an argument name that changed, or that never existed in the hashicorp/aws provider, but in a fork or a different version. How to spot it: terraform validate fails with Unsupported argument or Blocks of type "X" are not expected here. How to fix it: against the schema.json extracted in this lesson's Step 3, grep the exact block or argument name — if it doesn't show up there, it doesn't exist in the provider version you have installed, no matter how many tutorials mention it.
Assuming aws_bedrock_guardrail's five policies are mutually exclusive, or that all five must be declared (incomplete-schema-reading mistake). What happens: someone, looking at Step 2, concludes a valid guardrail needs all five policies present. How to spot it: if you hesitate to declare a guardrail with only two policies, thinking Terraform is going to reject it. How to fix it: each of the five policy blocks is optional at the resource's top level — the schema doesn't mark any of them as required — declaring only content_policy_config and sensitive_information_policy_config, as lesson 3's module does, is a perfectly valid guardrail. Module 4 adds the remaining three because Andes Cargo's case needs them, not because the schema demands it.
Confusing aws_bedrock_guardrail_version with "the provider's version" or "the guardrail's version in the sense of a Git change history" (ambiguity-of-the-word-"version" mistake). What happens: someone reads the resource's name and assumes it manages something related to code version control. How to spot it: if your expectation for aws_bedrock_guardrail_version is that it records a Git-like history. How to fix it: it's a Bedrock-API-specific concept — it freezes the current DRAFT guardrail into an immutable version number (1, 2, 3...) an application can reference stably, so a later change to the DRAFT doesn't silently affect something already in production. It has no relation to the Terraform code's own versioning (Git handles that, outside this resource).
Exercises
Exercise 1 — Locate, without looking at this lesson, whether aws_bedrock_guardrail accepts an argument called guardrail_name. Using this lesson's Step 3 pattern (terraform providers schema -json + a Python or grep filter), confirm yourself whether that argument name exists.
See solution
It doesn't exist. The correct argument, confirmed in this lesson's Step 2 and verifiable with python3 -c "import json; d=json.load(open('schema.json')); print(sorted(d['provider_schemas']['registry.terraform.io/hashicorp/aws']['resource_schemas']['aws_bedrock_guardrail']['block']['attributes'].keys()))", is simply name, not guardrail_name. This is exactly the kind of error an outdated tutorial, or a mental comparison with another resource (aws_dynamodb_table does use name for the table, but other AWS resources sometimes use a prefix with the service name) can introduce unintentionally.
Exercise 2 — Explain why aws_bedrock_provisioned_model_throughput has model_units as a number, not free text. What does this tell you about how AWS models a model's reserved capacity, compared to how Bedrock models on-demand cost (per token)?
See solution
model_units is an integer because it represents a discrete quantity of reserved capacity — how many "units" of guaranteed throughput you're buying per hour, a fixed-provisioning concept, structurally the same as "how many EC2 instances" or "how much reserved read/write capacity in DynamoDB." Contrast this with Bedrock's on-demand model, where there's no "unit" to reserve upfront: cost emerges from real input and output tokens, measured after the fact, with no number to declare in Terraform. model_units's very existence as a numeric argument confirms, at the schema level, the distinction all of Module 2 developed in prose: on-demand has no "quantity" to provision, Provisioned Throughput does.
Exercise 3 — Predict what would happen if you tried declaring content_policy_config with no filters_config inside. Based on Step 2's schema (filters_config as a nesting=set block inside content_policy_config), would it be a valid plan, or would Terraform reject it?
See solution
It would be a valid plan — an empty content_policy_config {}, with no filters_config inside, is syntactically correct HCL: the block itself has no required arguments at its own level (tier_config is optional, computed), and filters_config is a repeatable block (nesting=set), not a required argument of the parent block. The practical result would be a guardrail with a content policy declared but no real filter actually active inside it — probably not what was intended, but not a Terraform error. This is exactly the kind of case where terraform validate (which only confirms types and structure) doesn't substitute for a human review of whether the declared content makes sense for the real use case.
Summary and next step
This lesson extracted, with terraform providers schema -json against the already-installed hashicorp/aws v6.60.0 provider, the complete landscape of Bedrock resources: 39 total, of which this guide uses three. You saw aws_bedrock_guardrail's complete schema — five policies, all optional at the resource level, each with its own required fields inside —, and the other two named resources (aws_bedrock_guardrail_version, aws_bedrock_provisioned_model_throughput), neither of which this guide declares, for reasons explicit in each case.
Before moving on you should be able to: name the three Bedrock resources this guide uses, without looking at the lesson; explain why none of aws_bedrock_guardrail's five policies is required at the resource level; and repeat, yourself, the schema-extraction command against any installed provider.
Lesson 3 puts this schema to work for the first time: modules/bedrock-guardrail/, the real mold, validated in isolation, exactly as terraform-and-iac-guide already built modules/s3-bucket/ and modules/iam-role/.
Resources
- Terraform Registry —
aws_bedrock_guardrail— official documentation for this module's central resource. - Terraform Registry —
aws_bedrock_guardrail_version— official documentation. - Terraform Registry —
aws_bedrock_provisioned_model_throughput— official documentation. - Terraform Docs — Command: providers schema — official reference for the command used in this lesson to extract the real schema.
terraform-and-iac-guide, Module 5 — the module convention (modules/s3-bucket/,modules/iam-role/) lesson 3 follows.