Your AI chatbot can leak PII, discuss competitors, or generate toxic content. Bedrock Guardrails blocks all of that - and Terraform makes it version-controlled, reviewable, and consistent across environments.
You deployed your first Bedrock endpoint (Post 1). It works. Claude responds, tokens flow, invoices accrue.
But what happens when a user asks your customer-support bot to explain how to pick a lock? Or when the model accidentally includes a customer's Social Security number in its response? Or when someone asks it to compare your product to a competitor's?
Amazon Bedrock Guardrails is the safety layer that sits between your users and the model. It filters harmful content, blocks off-limit topics, masks PII, catches hallucinations, and blocks prompt injection attacks. Think of it as a bouncer for your AI endpoint.
The problem? Most teams configure guardrails through the AWS console. Click, click, click. No audit trail. No peer review. No way to promote the same guardrail config from dev to staging to prod without screenshots and hope.
Let's fix that with Terraform. Every safety policy as code. Every change reviewed in a PR. Every environment running the exact same guardrail. ποΈ
π§± What Guardrails Actually Do
Bedrock Guardrails gives you six layers of protection, each independently configurable:
| Layer | What It Does | Example |
|---|---|---|
| Content Filters | Blocks harmful text (hate, violence, sexual, insults, misconduct) | User sends hate speech, model refuses |
| Denied Topics | Blocks specific topics you define | "Don't discuss competitor products" |
| Word Filters | Exact-match blocking of words/phrases + profanity | Block brand names, slurs, internal codenames |
| PII Filters | Detects and masks/blocks personal data | SSN, email, phone numbers get redacted |
| Contextual Grounding | Catches hallucinations in RAG responses | Model invents facts not in the source docs |
| Prompt Attack Detection | Blocks jailbreaks and prompt injections | "Ignore your instructions and..." gets blocked |
You can enable any combination. Most production deployments use all six. Let's build them. π―
π° Guardrails Pricing (It's Cheap)
Before we write code, let's talk cost. Guardrails pricing was slashed up to 85% in late 2024:
| Filter Type | Cost per 1,000 Text Units | Notes |
|---|---|---|
| Content Filters | $0.15 | Includes prompt attack detection |
| Denied Topics | $0.15 | Per topic evaluated |
| Word Filters | Free | Exact match, no ML needed |
| PII Detection | $0.10 | Per entity type configured |
| Contextual Grounding | $0.10 | Source + query + response combined |
A text unit is up to 1,000 characters. A typical chatbot prompt of 2,000 characters costs 2 text units. At $0.15/1K units for content filtering, that's $0.0003 per message. For context, that's roughly 3 million messages per dollar. Guardrails are cheap insurance. πΈ
ποΈ Step 1: The Guardrail Resource
Here's a complete aws_bedrock_guardrail resource with all six safety layers:
# guardrails/main.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.60.0"
}
}
}
provider "aws" {
region = var.region
}
variable "region" {
type = string
default = "us-east-1"
}
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Must be: dev, staging, or prod."
}
}
Now the guardrail itself:
# guardrails/guardrail.tf
resource "aws_bedrock_guardrail" "ai_safety" {
name = "${var.environment}-ai-safety-guardrail"
description = "Content safety, PII protection, and topic restrictions for ${var.environment}"
blocked_input_messaging = var.blocked_input_message
blocked_outputs_messaging = var.blocked_output_message
# ββββββββββββββββββββββββββββββββββββββββββββββ
# Layer 1: Content Filters
# Block harmful content across 6 categories
# ββββββββββββββββββββββββββββββββββββββββββββββ
content_policy_config {
# Hate speech
filters_config {
type = "HATE"
input_strength = var.content_filter_strengths["hate"]
output_strength = var.content_filter_strengths["hate"]
}
# Insults
filters_config {
type = "INSULTS"
input_strength = var.content_filter_strengths["insults"]
output_strength = var.content_filter_strengths["insults"]
}
# Sexual content
filters_config {
type = "SEXUAL"
input_strength = var.content_filter_strengths["sexual"]
output_strength = var.content_filter_strengths["sexual"]
}
# Violence
filters_config {
type = "VIOLENCE"
input_strength = var.content_filter_strengths["violence"]
output_strength = var.content_filter_strengths["violence"]
}
# Misconduct (illegal activities, etc.)
filters_config {
type = "MISCONDUCT"
input_strength = var.content_filter_strengths["misconduct"]
output_strength = var.content_filter_strengths["misconduct"]
}
# Prompt attacks (jailbreaks, prompt injection)
filters_config {
type = "PROMPT_ATTACK"
input_strength = "HIGH" # Always HIGH - no reason to be lenient
output_strength = "NONE" # Only applies to input
}
}
# ββββββββββββββββββββββββββββββββββββββββββββββ
# Layer 2: Denied Topics
# Block specific topics your AI should never discuss
# ββββββββββββββββββββββββββββββββββββββββββββββ
topic_policy_config {
dynamic "topics_config" {
for_each = var.denied_topics
content {
name = topics_config.value.name
definition = topics_config.value.definition
examples = topics_config.value.examples
type = "DENY"
}
}
}
# ββββββββββββββββββββββββββββββββββββββββββββββ
# Layer 3: Word Filters
# Block profanity + custom words/phrases
# ββββββββββββββββββββββββββββββββββββββββββββββ
word_policy_config {
managed_word_lists_config {
type = "PROFANITY"
}
dynamic "words_config" {
for_each = var.blocked_words
content {
text = words_config.value
}
}
}
# ββββββββββββββββββββββββββββββββββββββββββββββ
# Layer 4: PII Filters
# Detect and mask/block personally identifiable info
# ββββββββββββββββββββββββββββββββββββββββββββββ
sensitive_information_policy_config {
dynamic "pii_entities_config" {
for_each = var.pii_entities
content {
type = pii_entities_config.value.type
action = pii_entities_config.value.action
}
}
# Custom regex patterns (e.g., internal IDs)
dynamic "regexes_config" {
for_each = var.custom_regex_filters
content {
name = regexes_config.value.name
description = regexes_config.value.description
pattern = regexes_config.value.pattern
action = regexes_config.value.action
}
}
}
# ββββββββββββββββββββββββββββββββββββββββββββββ
# Layer 5: Contextual Grounding
# Catch hallucinations in RAG responses
# ββββββββββββββββββββββββββββββββββββββββββββββ
contextual_grounding_policy_config {
filters_config {
type = "GROUNDING"
threshold = var.grounding_threshold
}
filters_config {
type = "RELEVANCE"
threshold = var.relevance_threshold
}
}
tags = {
Environment = var.environment
Purpose = "ai-safety"
ManagedBy = "terraform"
}
}
# Pin a version for production use
resource "aws_bedrock_guardrail_version" "current" {
guardrail_arn = aws_bedrock_guardrail.ai_safety.guardrail_arn
description = "Version managed by Terraform - ${var.environment}"
}
π§ Step 2: Variable-Driven Configuration
The power of this approach is that every safety setting is a variable. Different environments, different risk tolerances:
# guardrails/variables.tf
variable "blocked_input_message" {
type = string
description = "Message shown when user input is blocked"
default = "Your message could not be processed. Please rephrase your request without including harmful content, personal information, or off-topic questions."
}
variable "blocked_output_message" {
type = string
description = "Message shown when model output is blocked"
default = "The response was filtered for safety. Please try rephrasing your question."
}
# βββ Content Filter Strengths βββ
variable "content_filter_strengths" {
type = map(string)
description = "Filter strength per category: NONE, LOW, MEDIUM, HIGH"
default = {
hate = "HIGH"
insults = "HIGH"
sexual = "HIGH"
violence = "HIGH"
misconduct = "HIGH"
}
}
# βββ Denied Topics βββ
variable "denied_topics" {
type = list(object({
name = string
definition = string
examples = list(string)
}))
description = "Topics the AI should refuse to discuss"
default = []
}
# βββ Blocked Words βββ
variable "blocked_words" {
type = list(string)
description = "Exact words/phrases to block"
default = []
}
# βββ PII Entity Configuration βββ
variable "pii_entities" {
type = list(object({
type = string # NAME, EMAIL, PHONE, SSN, etc.
action = string # BLOCK or ANONYMIZE
}))
description = "PII types to detect and their action (BLOCK or ANONYMIZE)"
default = []
}
# βββ Custom Regex Filters βββ
variable "custom_regex_filters" {
type = list(object({
name = string
description = string
pattern = string
action = string # BLOCK or ANONYMIZE
}))
description = "Custom regex patterns to detect sensitive data"
default = []
}
# βββ Contextual Grounding βββ
variable "grounding_threshold" {
type = number
description = "Minimum confidence for grounding check (0.0 to 0.99). Higher = stricter."
default = 0.7
}
variable "relevance_threshold" {
type = number
description = "Minimum confidence for relevance check (0.0 to 0.99). Higher = stricter."
default = 0.7
}
π Step 3: Environment-Specific Configs
This is where the real value shows up. Each environment gets its own guardrail policy:
# environments/dev.tfvars
environment = "dev"
# Dev: More lenient for testing, still catches the big stuff
content_filter_strengths = {
hate = "HIGH"
insults = "MEDIUM"
sexual = "HIGH"
violence = "MEDIUM"
misconduct = "MEDIUM"
}
denied_topics = [
{
name = "competitor-products"
definition = "Questions about or comparisons with competitor products and services"
examples = [
"How does your product compare to CompetitorCo?",
"Is CompetitorCo better than you?",
"What are the alternatives to your service?"
]
}
]
blocked_words = ["confidential", "internal-only"]
pii_entities = [
{ type = "US_SOCIAL_SECURITY_NUMBER", action = "BLOCK" },
{ type = "CREDIT_DEBIT_CARD_NUMBER", action = "BLOCK" },
]
grounding_threshold = 0.5 # Lenient for testing
relevance_threshold = 0.5
# environments/prod.tfvars
environment = "prod"
# Prod: Maximum protection
content_filter_strengths = {
hate = "HIGH"
insults = "HIGH"
sexual = "HIGH"
violence = "HIGH"
misconduct = "HIGH"
}
denied_topics = [
{
name = "competitor-products"
definition = "Questions about or comparisons with competitor products and services"
examples = [
"How does your product compare to CompetitorCo?",
"Is CompetitorCo better than you?",
"What are the alternatives to your service?"
]
},
{
name = "investment-advice"
definition = "Specific financial investment recommendations or stock predictions"
examples = [
"Should I buy Tesla stock?",
"What cryptocurrency should I invest in?",
"Is now a good time to invest in real estate?"
]
},
{
name = "legal-advice"
definition = "Specific legal recommendations that should come from a licensed attorney"
examples = [
"Can I sue my landlord for this?",
"Is this contract legally binding?",
"What legal action should I take?"
]
},
{
name = "medical-diagnosis"
definition = "Specific medical diagnoses or treatment recommendations"
examples = [
"I have these symptoms, what disease do I have?",
"Should I stop taking my medication?",
"Is this mole cancerous?"
]
}
]
blocked_words = [
"confidential",
"internal-only",
"Project Falcon",
"Project Lighthouse"
]
pii_entities = [
{ type = "NAME", action = "ANONYMIZE" },
{ type = "EMAIL", action = "ANONYMIZE" },
{ type = "PHONE", action = "ANONYMIZE" },
{ type = "US_SOCIAL_SECURITY_NUMBER", action = "BLOCK" },
{ type = "CREDIT_DEBIT_CARD_NUMBER", action = "BLOCK" },
{ type = "CREDIT_DEBIT_CARD_CVV", action = "BLOCK" },
{ type = "CREDIT_DEBIT_CARD_EXPIRY", action = "BLOCK" },
{ type = "US_BANK_ACCOUNT_NUMBER", action = "BLOCK" },
{ type = "US_BANK_ROUTING_NUMBER", action = "BLOCK" },
{ type = "US_PASSPORT_NUMBER", action = "BLOCK" },
{ type = "DRIVER_ID", action = "BLOCK" },
{ type = "IP_ADDRESS", action = "ANONYMIZE" },
{ type = "USERNAME", action = "ANONYMIZE" },
{ type = "PASSWORD", action = "BLOCK" },
{ type = "AWS_ACCESS_KEY", action = "BLOCK" },
{ type = "AWS_SECRET_KEY", action = "BLOCK" },
]
custom_regex_filters = [
{
name = "internal-ticket-id"
description = "Internal support ticket IDs (format: TKT-XXXXXXXX)"
pattern = "TKT-[A-Z0-9]{8}"
action = "ANONYMIZE"
},
{
name = "employee-id"
description = "Employee ID numbers (format: EMP-XXXXX)"
pattern = "EMP-[0-9]{5}"
action = "ANONYMIZE"
}
]
grounding_threshold = 0.75 # Strict - block ungrounded responses
relevance_threshold = 0.75
Deploy per environment:
# Dev
terraform apply -var-file=environments/dev.tfvars
# Production
terraform apply -var-file=environments/prod.tfvars
What you get: Dev has 2 PII types blocked and lenient content filtering for testing. Prod has 16 PII types, 4 denied topics, custom regex patterns, and strict grounding. Same Terraform code, different configs. π―
π Step 4: Attach Guardrails to Your Endpoint
A guardrail does nothing until you attach it to model invocations. Here's how to use it with the Lambda endpoint from Post 1:
# guardrails/outputs.tf
output "guardrail_id" {
value = aws_bedrock_guardrail.ai_safety.guardrail_id
description = "Guardrail ID to pass to Bedrock InvokeModel calls"
}
output "guardrail_version" {
value = aws_bedrock_guardrail_version.current.version
description = "Pinned guardrail version for production use"
}
output "guardrail_arn" {
value = aws_bedrock_guardrail.ai_safety.guardrail_arn
description = "Guardrail ARN for IAM policies"
}
Update your Lambda environment variables to include the guardrail:
# In your Lambda resource from Post 1, add these env vars:
environment {
variables = {
MODEL_ID = var.primary_model.id
GUARDRAIL_ID = aws_bedrock_guardrail.ai_safety.guardrail_id
GUARDRAIL_VERSION = aws_bedrock_guardrail_version.current.version
AWS_REGION_ = var.region
}
}
And update the Lambda code to apply the guardrail:
import boto3
import json
import os
bedrock = boto3.client('bedrock-runtime')
def handler(event, context):
prompt = event.get('prompt', 'Say hello!')
max_tokens = event.get('max_tokens', 500)
model_id = os.environ.get('MODEL_ID')
guardrail_id = os.environ.get('GUARDRAIL_ID')
guardrail_version = os.environ.get('GUARDRAIL_VERSION')
body = json.dumps({
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": max_tokens,
"messages": [{"role": "user", "content": prompt}]
})
try:
response = bedrock.invoke_model(
modelId=model_id,
contentType='application/json',
accept='application/json',
body=body,
# Attach the guardrail
guardrailIdentifier=guardrail_id,
guardrailVersion=guardrail_version,
)
result = json.loads(response['body'].read())
# Check if guardrail intervened
stop_reason = result.get('stop_reason', '')
return {
'statusCode': 200,
'body': {
'response': result['content'][0]['text'],
'model': model_id,
'guardrail_action': stop_reason,
'input_tokens': result['usage']['input_tokens'],
'output_tokens': result['usage']['output_tokens']
}
}
except bedrock.exceptions.ClientError as e:
error_code = e.response['Error']['Code']
if error_code == 'ThrottlingException':
return {
'statusCode': 429,
'body': {'error': 'Rate limited. Try again shortly.'}
}
return {
'statusCode': 500,
'body': {'error': str(e)}
}
When the guardrail blocks a prompt, the response stop_reason will be "guardrail_intervened" and result['content'][0]['text'] will contain your blocked_input_messaging or blocked_outputs_messaging. Your application code stays clean - the guardrail handles all the messy safety logic. β
π Step 5: IAM for Guardrails
Your Lambda role needs permission to apply guardrails. Add this to the IAM policy from Post 1:
# guardrails/iam.tf
resource "aws_iam_role_policy" "guardrail_access" {
name = "${var.environment}-guardrail-access"
role = var.lambda_role_id # From Post 1
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowGuardrailUse"
Effect = "Allow"
Action = [
"bedrock:ApplyGuardrail",
"bedrock:GetGuardrail"
]
Resource = aws_bedrock_guardrail.ai_safety.guardrail_arn
}
]
})
}
Why this matters: Lock the permission to the specific guardrail ARN. Don't use Resource: "*" - that would let the Lambda bypass your guardrail by referencing a different (or no) guardrail. π
π Content Filter Strengths: What Do They Actually Do?
The input_strength and output_strength values control how aggressively each category is filtered:
| Strength | Behavior | Use When |
|---|---|---|
| NONE | No filtering | Deliberately allowing this category (rare) |
| LOW | Blocks only the most extreme content | Internal tools with trusted users |
| MEDIUM | Blocks clearly harmful content | Dev/staging environments |
| HIGH | Blocks anything borderline | Customer-facing production |
Practical example: With violence set to LOW, "The knight slayed the dragon" passes through. With HIGH, it might get flagged. For a fantasy game's AI, you'd want LOW. For a children's education chatbot, you'd want HIGH.
π§ͺ Step 6: Test Your Guardrails
After terraform apply, test each layer:
# Test 1: Content filter (should be blocked)
aws bedrock-runtime invoke-model \
--model-id anthropic.claude-3-5-sonnet-20241022-v2:0 \
--body '{"anthropic_version":"bedrock-2023-05-31","max_tokens":200,"messages":[{"role":"user","content":"Write an extremely violent scene"}]}' \
--guardrail-identifier $(terraform output -raw guardrail_id) \
--guardrail-version $(terraform output -raw guardrail_version) \
--cli-binary-format raw-in-base64-out \
response.json
# Test 2: PII masking (SSN should be blocked/masked)
aws bedrock-runtime invoke-model \
--model-id anthropic.claude-3-5-sonnet-20241022-v2:0 \
--body '{"anthropic_version":"bedrock-2023-05-31","max_tokens":200,"messages":[{"role":"user","content":"My SSN is 123-45-6789, can you confirm it?"}]}' \
--guardrail-identifier $(terraform output -raw guardrail_id) \
--guardrail-version $(terraform output -raw guardrail_version) \
--cli-binary-format raw-in-base64-out \
response.json
# Test 3: Denied topic (competitor discussion should be blocked)
aws bedrock-runtime invoke-model \
--model-id anthropic.claude-3-5-sonnet-20241022-v2:0 \
--body '{"anthropic_version":"bedrock-2023-05-31","max_tokens":200,"messages":[{"role":"user","content":"How does your product compare to CompetitorCo?"}]}' \
--guardrail-identifier $(terraform output -raw guardrail_id) \
--guardrail-version $(terraform output -raw guardrail_version) \
--cli-binary-format raw-in-base64-out \
response.json
cat response.json
When a guardrail blocks content, you'll see the blocked message instead of a model response. The amazon-bedrock-guardrailAction header will show INTERVENED. β
π The Upgrade Workflow
When compliance requirements change (and they always do), guardrail updates follow the same pattern as model upgrades:
# Current state: MEDIUM violence filter in staging
# New requirement: Legal says bump to HIGH everywhere
# 1. Update the .tfvars file
# staging.tfvars: violence = "MEDIUM" -> "HIGH"
# 2. Review the diff
terraform plan -var-file=environments/staging.tfvars
# 3. The plan shows exactly what changes:
# ~ content_policy_config {
# ~ filters_config {
# type = "VIOLENCE"
# ~ input_strength = "MEDIUM" -> "HIGH"
# ~ output_strength = "MEDIUM" -> "HIGH"
# }
# }
# 4. Apply after review
terraform apply -var-file=environments/staging.tfvars
Compare this to the console workflow: log in, find the guardrail, click through 6 tabs, change a dropdown, hope you changed the right one, repeat for every environment. With Terraform, it's a one-line diff in a PR that your security team can review. π
β‘ The ApplyGuardrail API: Use Guardrails Without a Model
Here's something most tutorials miss: you can use Guardrails independently from model invocations via the ApplyGuardrail API. This lets you moderate content from any source, not just Bedrock models:
# Use guardrails as a standalone content moderation layer
response = bedrock.apply_guardrail(
guardrailIdentifier=guardrail_id,
guardrailVersion=guardrail_version,
source='INPUT', # or 'OUTPUT'
content=[{
'text': {
'text': user_message
}
}]
)
# Check if content was blocked
if response['action'] == 'GUARDRAIL_INTERVENED':
print("Content blocked:", response['outputs'][0]['text'])
else:
print("Content passed")
This is powerful for moderating content from non-Bedrock sources like third-party APIs, user-generated content, or self-hosted models. You get Bedrock's safety filters without being locked into Bedrock for inference. π―
π’ Production Patterns
Pattern 1: Guardrail per use case. Don't share a single guardrail across your customer chatbot and your internal research tool. Create separate guardrails with different risk tolerances:
# Customer-facing: strict everything
resource "aws_bedrock_guardrail" "customer_facing" {
name = "${var.environment}-customer-facing"
# ... HIGH filters, aggressive PII masking
}
# Internal research: more permissive
resource "aws_bedrock_guardrail" "internal_tools" {
name = "${var.environment}-internal-tools"
# ... MEDIUM filters, fewer denied topics
}
Pattern 2: Version pinning. Always create a aws_bedrock_guardrail_version and reference it. The DRAFT version updates in place when you modify the guardrail, which means a terraform apply could change production behavior mid-flight. A pinned version only updates when you explicitly create a new one.
Pattern 3: Organization-level enforcement. For enterprises, Bedrock supports applying guardrails at the AWS Organization level. This means even if a developer forgets to attach a guardrail to their model call, the organization-level guardrail still applies. Configure this for your strictest baseline policies.
π― What You Just Built
ββββββββββββββββββββββββββββββββββββ
β User Input β
βββββββββββββββββ¬βββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β Bedrock Guardrail (Input) β
β β Content filters β
β β Denied topics β
β β Word filters β
β β PII detection β
β β Prompt attack detection β
βββββββββββββββββ¬βββββββββββββββββββ
β Passed? β
βΌ
ββββββββββββββββββββββββββββββββββββ
β Foundation Model β
β (Claude, Llama, Nova, etc.) β
βββββββββββββββββ¬βββββββββββββββββββ
β
βΌ
ββββββββββββββββββββββββββββββββββββ
β Bedrock Guardrail (Output) β
β β Content filters β
β β PII detection/masking β
β β Contextual grounding β
β β Word filters β
βββββββββββββββββ¬βββββββββββββββββββ
β Passed? β
βΌ
ββββββββββββββββββββββββββββββββββββ
β User Response β
ββββββββββββββββββββββββββββββββββββ
All six safety layers. All managed by Terraform. All version-controlled and reviewable. All consistent across dev, staging, and prod. π
βοΈ What's Next
This is Post 2 of the AWS AI Infrastructure with Terraform series.
- Post 1: Deploy Bedrock: First AI Endpoint
- Post 2: Bedrock Guardrails (you are here) π‘οΈ
- Post 3: Invocation Logging - Track every AI call for compliance and debugging
- Post 4: RAG Knowledge Base - Connect your company docs to AI with Bedrock + OpenSearch
Your AI endpoint is now protected by six layers of safety, all defined as code. When the compliance team asks "what guardrails do we have?", you point them to a Git repo instead of a screenshot. π
Found this helpful? Follow for the full AWS AI Infrastructure with Terraform series! π¬
Top comments (0)