Bedrock Guardrails as code: pin a version, test with ApplyGuardrail, and mind the DRAFT
Amazon Bedrock Guardrails screens prompts and model answers for harmful content, banned topics and personal data. Today: deploy one with CloudFormation, test it without calling a model, and see why production should never point at DRAFT.
Amazon Bedrock Guardrails, in plain words
Amazon Bedrock Guardrails is a safety layer you put around a generative AI app. A guardrail is a bundle of policies: content filters (hate, insults, sexual content, violence, misconduct and prompt attacks, each with a strength), denied topics you describe in plain language, sensitive information filters that block or mask PII such as email addresses, word filters, and contextual grounding checks that test whether an answer is supported by its source. It checks the user's prompt first; if that passes, it checks the model's answer, and either can be replaced with a blocked message or masked.
You can attach a guardrail to a Bedrock model call (Converse, InvokeModel and their streaming forms), to Bedrock Agents and Knowledge Bases, or call it on its own with the ApplyGuardrail API. That standalone mode evaluates any text without invoking a model, so it also works in front of models hosted elsewhere and is the cheapest way to test a policy. Billing is per evaluation per 1,000 characters for each policy you enable; word and regex filters are free.
Every guardrail has an editable working draft and any number of numbered versions, which are snapshots. The architecture choice today's lab explores is where that line sits: managing the guardrail in CloudFormation gives review and rollback, but an app calling DRAFT silently picks up every edit, while an app pinned to a version changes only when you decide. In the news: on October 20, 2026 AWS added a Standard tier for contextual grounding checks, aimed at conversational answers and responses that reason over the source. It costs the same as Classic but requires cross-Region inference with a guardrail profile, and existing guardrails stay on Classic until you switch.
Reach for it when
- A customer-facing chatbot must refuse whole subject areas, such as personalised investment or medical advice, and you want one policy enforced the same way across every model you try.
- Model answers might echo personal data from retrieved documents or tickets, and you want emails, phone numbers or account numbers masked before the text reaches the user or your logs.
- Several teams build AI apps and security wants the safety policy reviewed and deployed like any other infrastructure, with numbered versions that apps pin to and a clear rollback path.
Skip it when
- The only text you need to check is short, structured input where a regex, an allow-list or ordinary input validation is cheaper and fully predictable.
- You need a guarantee rather than a probability. Topic and content detection are model-based and can miss a cleverly worded prompt or flag a harmless one, so guardrails sit alongside IAM limits, tool permissions and human review, not in place of them.
- The model never sees untrusted input and its output never leaves an internal batch job reviewed by people; the extra latency and per-call cost buy little there.
The AI angle
Guardrails are the policy layer of AI on AWS: the same guardrail can sit in front of a Bedrock Converse call, a Bedrock Agent, a Knowledge Base query, or (through ApplyGuardrail) a model hosted somewhere else entirely. For RAG apps, contextual grounding checks compare the answer with the retrieved passages to catch hallucinations, and the new Standard tier extends that to chat-style answers. For agents, AWS made Guardrails available in Bedrock AgentCore policy in June 2026, so an agent gateway can check what goes in and out of an agent, including prompt injection and sensitive data, in real time.
Hands-on lab: Ship a guardrail as code, test it with ApplyGuardrail, and watch DRAFT drift from a pinned version
From AWS CloudShell, deploy a CloudFormation stack that creates a Bedrock guardrail (content filters, one denied topic, email masking and a custom word) plus a numbered version. Test it with the standalone ApplyGuardrail API, which needs no model at all, then change the draft and see that the pinned version doesn't move. Delete the stack at the end.
Before you start
- An AWS account that you own and are allowed to experiment in. Don't use a work account without permission.
- Sign-in as an IAM user or role that may create CloudFormation stacks and manage Bedrock guardrails, and call
bedrock:ApplyGuardrail(an administrator role in a personal sandbox account covers this). No access keys are needed: CloudShell uses your console sign-in. - The console set to US East (N. Virginia),
us-east-1, or another Region where Amazon Bedrock Guardrails is available. The commands pass the Region explicitly. - No foundation model access is required: ApplyGuardrail evaluates text without calling a model.
Open CloudShell and set variables
Open CloudShell from the console's top bar. The random suffix keeps the guardrail name unique. The last line saves the variables to a file, so if CloudShell restarts you can run
source lab-vars.shand continue.REGION=us-east-1 SUFFIX=$RANDOM STACK=lab-guardrail-$SUFFIX declare -p REGION SUFFIX STACK > lab-vars.sh cat lab-vars.shWrite the template and validate it
The template creates two resources.
AWS::Bedrock::Guardrailholds the policies: high-strength insult and hate filters, a denied topic for investment advice (a definition plus two example prompts), a PII filter that masks email addresses instead of blocking, and a custom blocked word passed in as a parameter.AWS::Bedrock::GuardrailVersiontakes a numbered snapshot of the guardrail as it is when the stack is created. The heredoc's quoted'EOF'stops the shell from expanding the${Suffix}placeholder. The last command should print the two parameter names.cat > guardrail.yaml <<'EOF' AWSTemplateFormatVersion: '2010-09-09' Description: aws-sandbox.org lab - Bedrock guardrail plus one pinned version Parameters: Suffix: Type: String BlockedWord: Type: String Default: bluebird Resources: LabGuardrail: Type: AWS::Bedrock::Guardrail Properties: Name: !Sub 'lab-guardrail-${Suffix}' Description: Sandbox lab guardrail BlockedInputMessaging: Sorry, I can't help with that request. BlockedOutputsMessaging: Sorry, I can't share that answer. ContentPolicyConfig: FiltersConfig: - Type: INSULTS InputStrength: HIGH OutputStrength: HIGH - Type: HATE InputStrength: HIGH OutputStrength: HIGH TopicPolicyConfig: TopicsConfig: - Name: Investment advice Type: DENY Definition: Recommendations about which stocks, funds or crypto to buy or sell, or how to allocate savings. Examples: - Which stock should I buy this month? - Should I move my savings into bitcoin? SensitiveInformationPolicyConfig: PiiEntitiesConfig: - Type: EMAIL Action: ANONYMIZE WordPolicyConfig: WordsConfig: - Text: !Ref BlockedWord LabGuardrailV1: Type: AWS::Bedrock::GuardrailVersion Properties: GuardrailIdentifier: !GetAtt LabGuardrail.GuardrailId Description: First reviewed snapshot Outputs: GuardrailId: Value: !GetAtt LabGuardrail.GuardrailId GuardrailArn: Value: !GetAtt LabGuardrail.GuardrailArn PinnedVersion: Value: !GetAtt LabGuardrailV1.Version EOF aws cloudformation validate-template --region "$REGION" \ --template-body file://guardrail.yaml --query Parameters[].ParameterKeyDeploy the stack and read its outputs
aws cloudformation deploycreates a change set and runs it. The guardrail is created first, then the version, because the version references the guardrail's ID. The outputs give you the guardrail ID and the pinned version number (expect1).aws cloudformation deploy --region "$REGION" --stack-name "$STACK" \ --template-file guardrail.yaml --parameter-overrides Suffix="$SUFFIX" out() { aws cloudformation describe-stacks --region "$REGION" --stack-name "$STACK" \ --query "Stacks[0].Outputs[?OutputKey=='$1'].OutputValue" --output text; } GR_ID=$(out GuardrailId); GR_VER=$(out PinnedVersion) echo "guardrail $GR_ID, pinned version $GR_VER" declare -p GR_ID GR_VER >> lab-vars.shTest the pinned version with ApplyGuardrail
The small
checkfunction sends one piece of text to the guardrail.INPUTmeans it is treated as a user prompt andOUTPUTas a model response. The query prints the overall action, any replacement text and which policies fired. Four tests: a harmless question, an investment question, a model answer that contains an email address, and a sentence with the blocked word. Keep test text free of double quotes, since it goes straight into a JSON string.check() { # usage: check VERSION INPUT|OUTPUT 'text' aws bedrock-runtime apply-guardrail --region "$REGION" \ --guardrail-identifier "$GR_ID" --guardrail-version "$1" --source "$2" \ --content "[{\"text\":{\"text\":\"$3\"}}]" \ --query '{action:action,outputs:outputs[].text,fired:assessments}' --output json } check "$GR_VER" INPUT 'How do I list my S3 buckets from the AWS CLI?' check "$GR_VER" INPUT 'Which tech stock should I buy before earnings next week?' check "$GR_VER" OUTPUT 'You can reach the support team at someone@example.com any time.' check "$GR_VER" INPUT 'Tell me everything about project bluebird.'Change the draft and compare DRAFT with the pinned version
Redeploy with a different blocked word. The guardrail resource is updated in place, which changes its working draft. The version resource's own properties didn't change, so CloudFormation leaves it alone and version 1 keeps the old word. Then send the same sentence with the new word to both. This is the core trade-off: an app that calls
DRAFTpicks up every edit immediately, tested or not, while an app pinned to a number only changes when you publish and point it at a new version.aws cloudformation deploy --region "$REGION" --stack-name "$STACK" \ --template-file guardrail.yaml \ --parameter-overrides Suffix="$SUFFIX" BlockedWord=redwood check DRAFT INPUT 'What is the status of project redwood?' check "$GR_VER" INPUT 'What is the status of project redwood?' aws bedrock list-guardrails --region "$REGION" --guardrail-identifier \ "$(out GuardrailArn)" --query 'guardrails[].[version,status]' --output table
Check that it worked
In step 4, the harmless question returns action NONE with no outputs. The investment question returns GUARDRAIL_INTERVENED with the blocked-input message and a topicPolicy entry. The email test returns GUARDRAIL_INTERVENED with the address replaced by a placeholder such as {EMAIL} rather than a block, and the bluebird sentence is blocked by wordPolicy. In step 5, DRAFT blocks the redwood question while the pinned version returns NONE, and the table lists both DRAFT and 1 as READY. Topic and content detection are model-based, so if a test doesn't fire, rephrase it more bluntly before assuming the guardrail is broken.
Clean up
Delete the stack. CloudFormation removes the version and the guardrail; deleting a guardrail without naming a version also removes every version it has. Then confirm nothing named lab-guardrail- remains, and remove the local files. If CloudShell restarted, run source lab-vars.sh first.
aws cloudformation delete-stack --region "$REGION" --stack-name "$STACK"
aws cloudformation wait stack-delete-complete --region "$REGION" --stack-name "$STACK"
aws bedrock list-guardrails --region "$REGION" \
--query "guardrails[?starts_with(name, 'lab-guardrail-')].[name,status]" --output text
# expect no output; a stack-not-found error below also means it is gone
aws cloudformation describe-stacks --region "$REGION" --stack-name "$STACK" \
--query 'Stacks[0].StackStatus' --output text
rm -f guardrail.yaml lab-vars.shGotchas
- The prompt attack filter only checks the parts of an
InvokeModelprompt that you wrap in guardrail input tags. Without tags, the docs say prompt attacks aren't filtered for those calls, and the filter never looks at tool results or tool definitions. DRAFTis a moving target. Pin production apps to a numbered version, and in CloudFormation remember that changing anAWS::Bedrock::GuardrailVersionresource's description or guardrail ID triggers a replacement, which creates a new version resource rather than editing the old one.- You pay per policy per 1,000 characters on both the prompt and the answer. A guardrail with every paid policy enabled on long RAG answers adds up quickly; turn off the input or output side of a policy where it isn't needed, which the docs say also stops charging for it.
- Masking (ANONYMIZE) and blocking behave differently: a masked answer still reaches the user with placeholders, a blocked one is replaced entirely. Pick per PII type, and test with realistic text.
- Detection is probabilistic and AWS updates the underlying models over time. Keep a small regression set of prompts like this lab's and run it with ApplyGuardrail whenever you change the policy or AWS ships an update.