Module 4: Bedrock Guardrails And Defense In Depth

1. Introduction: the managed guardrail and the custom one

Description

ADR-001-llm-as-escalation-path.md (Module 1) has a row with this module's exact name: "M4 — Bedrock guardrails and defense in depth | Guards specifically the one path that touches a non-deterministic model; the default path needs no such guard". bedrock.tf (Module 3) left Andes Cargo's guardrail with two of five active policies — content with prompt-attack detection, sensitive information — and an explicit comment in its header: "Module 4 extends the guardrail with the remaining policies... this file does not build those yet". This module fulfills that promise, and adds something no terraform validate can give you on its own: two custom, deterministic checks that run regardless of whether Bedrock exists, responds, or has any guardrails configured at all.

Connection to the module

Module 3 was the guide's most executed module for a mechanical reason: declaring a new resource never needs network. This module inherits that same advantage for the three missing policies — terraform validate/plan are going to run, again, for real, on the complete guardrail — and adds a second column of real execution that doesn't depend on Terraform at all: guardrails/pre_invoke_checks.py and guardrails/post_invoke_checks.py, two Python scripts, run with pytest, that exist precisely because a declared guardrail — however complete — is never, on its own, the full answer to the question "is this safe to write to Shipments?"


Analogy: the bank doesn't rely only on the armored door

A bank with a state-of-the-art armored door — certified, tested, expensive — doesn't fire its guards or turn off its cameras. The armored door is a real, serious layer that stops the vast majority of obvious attempts. But a real bank also has a teller trained to notice unusual behavior, a daily withdrawal limit that doesn't depend on the door at all, and a silent alarm triggered for a completely different reason than "is the door locked?" None of those layers replaces the others. Each one covers something the others, by design, can't see.

Bedrock Guardrails — this module's armored door — is a managed mechanism, tested by AWS, that evaluates content against general rules: is this a prompt-attack attempt? Does this contain an email address? Does this touch a prohibited topic? It's a real, serious layer. But it knows nothing about the exact ShipmentFields contract Shipments expects to receive, nor whether a technically harmless response — no PII, no prompt attack, no denied topic — nonetheless has the correct shape to write into a real database. That's this module's own guard's responsibility: pre_invoke_checks.py and post_invoke_checks.py, additional layers, not substitutes, each seeing something the other — managed or custom — can't see on its own.


This module's map: the 8 lessons

   M4 -- BEDROCK GUARDRAILS AND DEFENSE IN DEPTH

   1  Introduction                             the managed guardrail and the custom one (this lesson)
   2  The six policies explained                conceptual, verified against AWS Docs
   3  Hands-on: the complete guardrail          all 6 policies in HCL, real validate/plan
   4  Why the managed guardrail isn't enough    defense in depth, what escapes it
   5  Hands-on: the PII scrubber                pre_invoke_checks.py, real pytest
   6  Hands-on: the schema validator             post_invoke_checks.py, real pytest
   7  The exact limit                           why real blocking can't be tested here
   8  Project: the complete guardrails layer    guardrail + 2 custom checks, integrated
#LessonWhat runs/gets practiced
1Introduction (this one)Why a managed guardrail and a custom one are layers, not substitutes
2Bedrock Guardrails' six policiesConceptual — verified against docs.aws.amazon.com/bedrock/ at time of writing
3Hands-on: the complete guardrail in HCLExecuted: the 6 policies (5 blocks) in aws_bedrock_guardrail, real validate/plan
4Why a managed guardrail isn't enough aloneDefense in depth — what Bedrock Guardrails covers, what escapes it
5Hands-on: the custom PII scrubberExecuted: pre_invoke_checks.py, regex, pytest — literal PASS/FAIL
6Hands-on: the schema validatorExecuted: post_invoke_checks.py, pytest — literal PASS/FAIL
7The exact limitRepresentative: why Bedrock Guardrails' real blocking can't be tested here
8Project: Andes Cargo's guardrails layerExecuted: guardrail HCL + two custom checks, integrated into a single deliverable

Notice something that sets this module apart from the previous three: this module's real execution doesn't depend on a single mechanism (the way Module 3 depended entirely on "validate/plan on a new resource never calls the API"). Here there are two sources of real execution, completely independent of each other — Terraform on one side, pure Python on the other — and that independence is, precisely, lesson 4's entire argument: if a managed guardrail ever failed to apply, or if Bedrock changed its rules without warning, pre_invoke_checks.py and post_invoke_checks.py would keep running exactly the same, because they never depended on the first one existing.


What this module inherits, without repeating

This module doesn't re-explain what terraform validate is, how a dynamic block works, or why a new resource never needs network — Module 3, lesson 1 already established that, precisely, and this module reuses it directly. It also doesn't re-explain pytest from scratch: Module 2, lesson 7 already built and ran a complete twelve-case deterministic suite on bedrock_cost_estimate.py, and this module's lessons 5 and 6 follow that exact same pattern — no random, no datetime.now(), same input, same output, always — applied to a different domain: security, not cost.

What's genuinely new in this module is four pieces, all in English like the rest of this guide's code:

  • Three new policies in modules/bedrock-guardrail/ (topic_policy_config, contextual_grounding_policy_config, word_policy_config), added to Module 3's module without rewriting its two already-existing blocks.
  • The manifest_extractor_guardrail module in bedrock.tf, now passing all six complete policies.
  • guardrails/pre_invoke_checks.py — the custom PII scrubber.
  • guardrails/post_invoke_checks.py — the ShipmentFields schema validator.

Common mistakes

Thinking "defense in depth" means "the custom guardrail checks the same thing as the managed one, just in case" (misunderstood-redundancy mistake). What happens: someone assumes pre_invoke_checks.py exists only for the case where Bedrock Guardrails "gets it wrong" detecting an email address, as a second chance to catch exactly the same kind of error. How to spot it: if your explanation of why the custom check exists is limited to "in case the managed guardrail fails." How to fix it: this module's lesson 4 develops this precisely — the custom check doesn't repeat the managed one's work, it covers territory the managed one structurally cannot see (the exact ShipmentFields contract, Andes Cargo's specific business context). They're complementary layers, not a backup copy of each other.

Confusing "the six policies" with "six HCL blocks" (imprecise-counting mistake). What happens: someone, writing about this module, counts six distinct dynamic blocks in modules/bedrock-guardrail/main.tf and gets confused when they only find five. How to spot it: if your count of HCL blocks doesn't match "six mechanisms" at some point in this module. How to fix it: lesson 2 clarifies this precisely, cited against AWS Docs — general content filters (HATE, INSULTS, SEXUAL, VIOLENCE, MISCONDUCT) and prompt-attack detection are two distinct mechanisms, but AWS models them inside the same block (content_policy_config), not in separate blocks. Six mechanisms, five policy blocks — it isn't a counting error in this guide, it's how the provider's real schema structures it.

Jumping ahead to write real Bedrock invocation code before lesson 7 (out-of-order-anticipation mistake). What happens: someone, excited by this module's lessons 5 and 6, tries connecting pre_invoke_checks.py and post_invoke_checks.py to a real bedrock-runtime invoke-model call before the guide gets to that point. How to spot it: if your code, at this point in the module, is already trying to import a boto3 client for Bedrock. How to fix it: this guide, declared since Module 1, never invokes a real model, in any module — this module's lesson 7 explains exactly why, with the same honesty Module 3, lesson 6 already applied to apply. This module's two scripts are deliberately independent of any real call; connecting them to one would be valid work on your own Bedrock account, but it isn't part of what this $0 lab can demonstrate.


Exercises

Exercise 1 — Without looking at the rest of the module, write from memory the names of the two Python scripts this module builds, and what each one evaluates. Then, check your answer against the lesson map above.

See solution

pre_invoke_checks.py evaluates the manifest's raw text before extract-shipment-manifest-fields invokes Bedrock — it looks for known PII (emails, phone numbers) with regular expressions. post_invoke_checks.py evaluates the response after the invocation (real or representative) — it confirms it has exactly ShipmentFields's shape before any code tries writing it to Shipments. The names themselves encode the order: pre_ before the call to the model, post_ after.

Exercise 2 — Explain, in one sentence, why the independence between "Terraform" and "pure Python" as this module's two sources of real execution matters more here than in Module 3. Think about what would happen if Bedrock, as a service, stopped existing tomorrow.

See solution

It matters more here because, if Bedrock as a service disappeared — or if LocalStack pulled Ultimate support, or if this guide ran in an environment with no Terraform access at all —, pre_invoke_checks.py and post_invoke_checks.py would still be valid Python code, running with pytest, with no change: they never depended on aws_bedrock_guardrail, on a provider, or on an API. In Module 3, by contrast, practically all the real execution depended on a single mechanism (validate/plan on a new resource). This module spreads its real execution across two systems sharing no dependency with each other — the very definition of defense in depth, applied this time to the guide's own architecture.

Exercise 3 — Predict which of the six policies (lesson 2) is the only one Bedrock evaluates exclusively against the model's OUTPUT, never against the input alone. Based on this lesson's analogy (layers that see different things), what kind of problem would make sense to be detectable only after the model has already responded?

See solution

Contextual grounding (contextual_grounding_policy_config). It makes sense that it's evaluated only against the output because its question — "is this response grounded in the source I gave it, or did the model make something up?" — doesn't exist yet when there's only an input: a hallucination is, by definition, something that shows up in the response, compared against a source. This module's lesson 2 confirms this with AWS Docs's exact quote, and lesson 6's post_invoke_checks.py design applies the same underlying logic — though to a different problem (schema shape, not truthfulness) — to the same point in the flow: after the response, before writing.


Summary and next step

This lesson installed the entire module's central thesis: a managed guardrail and a custom one aren't substitutes, they're layers — each sees something the other, by design, can't see. You saw the complete map of the 8 lessons, with two completely independent sources of real execution (Terraform for the guardrail's policies, pure Python for the two custom checks), and confirmed what this module inherits without repeating (validate/plan, the pytest discipline) against the four genuinely new pieces it builds.

Before moving on you should be able to: explain, without hesitating, why "defense in depth" doesn't mean redundancy; name this module's four new pieces; and anticipate why the independence between Terraform and Python, as two sources of real execution, is a design decision, not a coincidence.

Lesson 2 puts Bedrock Guardrails' six policies under the microscope, one by one, verified against AWS's official documentation at the time of writing this guide — the conceptual foundation lesson 3 is going to declare real HCL on top of.

Resources

  1. ADR-001-llm-as-escalation-path.md (Module 1, lesson 8 of this guide) — the row assigning this module its exact responsibility within the guide's complete arc.
  2. bedrock.tf (Module 3, lesson 8 of this guide) — the file this module extends, with the verbatim comment that already anticipated this work.
  3. AWS — Amazon Bedrock Guardrails — official landscape for the six policies lesson 2 develops in detail.
  4. This guide's Module 2, lesson 7 (07-hands-on-the-cost-per-token-calculator.md) — the source of the deterministic pytest pattern this module's lessons 5 and 6 reapply.