aws-sandbox.orgOne AWS service and one hands-on lab a day. Independent; not affiliated with AWS.
Wednesday, October 28, 2026ProfessionalAI and ML

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.

TIMEAbout 20 minutes (stack create and delete take a minute or two each)
COSTA few cents at most, and in practice well under one cent. Guardrails are billed per evaluation, per 1,000 text units (one text unit is up to 1,000 characters). The Amazon Bedrock pricing page lists $0.15 per 1,000 text units for content filters and for denied topics, $0.10 for PII filters, and word filters free. This lab makes about ten short calls, each one text unit per policy, so roughly $0.004 in total. Charges apply to the policies you configure, not to the guardrail sitting idle, and CloudFormation itself adds nothing for these resources.
FREE TIER / CREDITSAmazon Bedrock Guardrails has no always-free allowance; the pricing page lists only per-use prices (with word filters and regex filters free). Accounts on the credit-based AWS Free Tier plans can usually cover a lab like this from their credits; check your plan's terms.

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.
  1. 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.sh and continue.

    REGION=us-east-1
    SUFFIX=$RANDOM
    STACK=lab-guardrail-$SUFFIX
    declare -p REGION SUFFIX STACK > lab-vars.sh
    cat lab-vars.sh
  2. Write the template and validate it

    The template creates two resources. AWS::Bedrock::Guardrail holds 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::GuardrailVersion takes 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[].ParameterKey
  3. Deploy the stack and read its outputs

    aws cloudformation deploy creates 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 (expect 1).

    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.sh
  4. Test the pinned version with ApplyGuardrail

    The small check function sends one piece of text to the guardrail. INPUT means it is treated as a user prompt and OUTPUT as 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.'
  5. 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 DRAFT picks 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.sh

Gotchas

  • The prompt attack filter only checks the parts of an InvokeModel prompt 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.
  • DRAFT is a moving target. Pin production apps to a numbered version, and in CloudFormation remember that changing an AWS::Bedrock::GuardrailVersion resource'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.