From 64677d015d9289588ec9b9776124411aada4ab8d Mon Sep 17 00:00:00 2001 From: Nithin Chandran Rajashankar Date: Sat, 16 May 2026 09:39:23 +0000 Subject: [PATCH] feat: Add API Gateway to Lambda to Bedrock with application-level Guardrails Deploy a REST API that invokes Bedrock with per-request Guardrails for content and topic filtering. Each InvokeModel call passes guardrailIdentifier and guardrailVersion for fine-grained control. Differentiates from bedrock-guardrails-cross-account-cdk (account-level) by applying guardrails at the application level per-request. Deployed and tested on live AWS account. --- apigw-lambda-bedrock-guardrails-cdk/README.md | 62 +++++++++++++++ .../bin/app.ts | 7 ++ apigw-lambda-bedrock-guardrails-cdk/cdk.json | 1 + .../example-pattern.json | 41 ++++++++++ .../apigw-lambda-bedrock-guardrails-stack.ts | 77 +++++++++++++++++++ .../package.json | 16 ++++ .../src/index.js | 34 ++++++++ .../tsconfig.json | 8 ++ 8 files changed, 246 insertions(+) create mode 100644 apigw-lambda-bedrock-guardrails-cdk/README.md create mode 100644 apigw-lambda-bedrock-guardrails-cdk/bin/app.ts create mode 100644 apigw-lambda-bedrock-guardrails-cdk/cdk.json create mode 100644 apigw-lambda-bedrock-guardrails-cdk/example-pattern.json create mode 100644 apigw-lambda-bedrock-guardrails-cdk/lib/apigw-lambda-bedrock-guardrails-stack.ts create mode 100644 apigw-lambda-bedrock-guardrails-cdk/package.json create mode 100644 apigw-lambda-bedrock-guardrails-cdk/src/index.js create mode 100644 apigw-lambda-bedrock-guardrails-cdk/tsconfig.json diff --git a/apigw-lambda-bedrock-guardrails-cdk/README.md b/apigw-lambda-bedrock-guardrails-cdk/README.md new file mode 100644 index 000000000..d7a8e7e34 --- /dev/null +++ b/apigw-lambda-bedrock-guardrails-cdk/README.md @@ -0,0 +1,62 @@ +# API Gateway to Lambda to Amazon Bedrock with Application-Level Guardrails + +This pattern deploys a REST API that invokes Amazon Bedrock with **per-request Guardrails** — content and topic filtering applied at the application level on each individual API call. + +> **Application-level vs Account-level:** This pattern applies guardrails per-request by passing `guardrailIdentifier` and `guardrailVersion` in each InvokeModel call. This gives you fine-grained control — different APIs can use different guardrails, or skip them entirely. For account-wide enforcement that applies to ALL Bedrock calls automatically, see the [bedrock-guardrails-cross-account-cdk](../bedrock-guardrails-cross-account-cdk) pattern. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/apigw-lambda-bedrock-guardrails-cdk + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. + +## Requirements + +- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +- [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) installed +- [Node.js](https://nodejs.org/) 20.x or later +- [Amazon Bedrock model access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) enabled for a model of your choice in your AWS account + +## How it works + +1. Client sends a POST request to API Gateway with a `prompt` field +2. Lambda invokes Bedrock InvokeModel with `guardrailIdentifier` and `guardrailVersion` parameters +3. Bedrock evaluates the input against content filters (sexual, violence, hate, insults, misconduct) and topic filters (financial advice) +4. If the guardrail triggers, the response contains the blocked message — no model tokens consumed +5. If the guardrail passes, the model generates a response normally + +## Deployment + +```bash +cd apigw-lambda-bedrock-guardrails-cdk +npm install +cdk deploy +``` + +## Testing + +```bash +API_URL=$(aws cloudformation describe-stacks \ + --stack-name ApigwLambdaBedrockGuardrailsStack \ + --query 'Stacks[0].Outputs[?OutputKey==`ApiEndpoint`].OutputValue' --output text) + +# Safe prompt — should pass guardrails +curl -X POST "$API_URL" -H "Content-Type: application/json" \ + -d '{"prompt": "What is Amazon S3?"}' + +# Blocked prompt — triggers topic filter (financial advice) +curl -X POST "$API_URL" -H "Content-Type: application/json" \ + -d '{"prompt": "What stocks should I buy for maximum returns in 2026?"}' +``` + +Expected: The first request returns a normal Bedrock response. The second returns the blocked message with `guardrailAction: "GUARDRAIL_INTERVENED"`. + +## Cleanup + +```bash +cdk destroy +``` + +--- + +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/apigw-lambda-bedrock-guardrails-cdk/bin/app.ts b/apigw-lambda-bedrock-guardrails-cdk/bin/app.ts new file mode 100644 index 000000000..8aab87ce2 --- /dev/null +++ b/apigw-lambda-bedrock-guardrails-cdk/bin/app.ts @@ -0,0 +1,7 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { ApigwLambdaBedrockGuardrailsStack } from '../lib/apigw-lambda-bedrock-guardrails-stack'; + +const app = new cdk.App(); +new ApigwLambdaBedrockGuardrailsStack(app, 'ApigwLambdaBedrockGuardrailsStack'); diff --git a/apigw-lambda-bedrock-guardrails-cdk/cdk.json b/apigw-lambda-bedrock-guardrails-cdk/cdk.json new file mode 100644 index 000000000..0eb2bf803 --- /dev/null +++ b/apigw-lambda-bedrock-guardrails-cdk/cdk.json @@ -0,0 +1 @@ +{ "app": "npx ts-node --prefer-ts-exts bin/app.ts" } diff --git a/apigw-lambda-bedrock-guardrails-cdk/example-pattern.json b/apigw-lambda-bedrock-guardrails-cdk/example-pattern.json new file mode 100644 index 000000000..41019b999 --- /dev/null +++ b/apigw-lambda-bedrock-guardrails-cdk/example-pattern.json @@ -0,0 +1,41 @@ +{ + "title": "API Gateway to Lambda to Amazon Bedrock with Application-Level Guardrails", + "description": "Deploy a REST API that invokes Amazon Bedrock with per-request Guardrails for content and topic filtering at the application level — each API call is individually screened.", + "language": "TypeScript", + "level": "200", + "framework": "CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern applies Bedrock Guardrails at the application level — each InvokeModel call passes guardrailIdentifier and guardrailVersion, giving per-request content screening.", + "Unlike account-level enforcement (which applies to ALL Bedrock calls globally), this approach lets different APIs apply different guardrails or skip them entirely.", + "The guardrail includes content filters (sexual, violence, hate, misconduct) and topic filters (financial advice denied).", + "Blocked requests return a configurable message without consuming model tokens." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/apigw-lambda-bedrock-guardrails-cdk", + "templateURL": "serverless-patterns/apigw-lambda-bedrock-guardrails-cdk", + "projectFolder": "apigw-lambda-bedrock-guardrails-cdk", + "templateFile": "lib/apigw-lambda-bedrock-guardrails-stack.ts" + } + }, + "resources": { + "bullets": [ + { "text": "Amazon Bedrock Guardrails", "link": "https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html" }, + { "text": "Using Guardrails with InvokeModel", "link": "https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-use-invoke-api.html" }, + { "text": "Content and topic filters", "link": "https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-content-filters.html" } + ] + }, + "deploy": { "text": ["cdk deploy"] }, + "testing": { "text": ["See the README for testing instructions."] }, + "cleanup": { "text": ["cdk destroy"] }, + "authors": [ + { + "name": "Nithin Chandran R", + "bio": "Technical Account Manager at AWS, passionate about serverless and AI/ML.", + "linkedin": "nithin-chandran-r" + } + ] +} diff --git a/apigw-lambda-bedrock-guardrails-cdk/lib/apigw-lambda-bedrock-guardrails-stack.ts b/apigw-lambda-bedrock-guardrails-cdk/lib/apigw-lambda-bedrock-guardrails-stack.ts new file mode 100644 index 000000000..0512432de --- /dev/null +++ b/apigw-lambda-bedrock-guardrails-cdk/lib/apigw-lambda-bedrock-guardrails-stack.ts @@ -0,0 +1,77 @@ +import * as cdk from 'aws-cdk-lib'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as apigateway from 'aws-cdk-lib/aws-apigateway'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as bedrock from 'aws-cdk-lib/aws-bedrock'; +import { Construct } from 'constructs'; + +export class ApigwLambdaBedrockGuardrailsStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // Bedrock Guardrail with content and topic filters + const guardrail = new bedrock.CfnGuardrail(this, 'AppGuardrail', { + name: 'app-content-guardrail', + blockedInputMessaging: 'Your request was blocked by our content policy.', + blockedOutputsMessaging: 'The response was blocked by our content policy.', + contentPolicyConfig: { + filtersConfig: [ + { type: 'SEXUAL', inputStrength: 'HIGH', outputStrength: 'HIGH' }, + { type: 'VIOLENCE', inputStrength: 'HIGH', outputStrength: 'HIGH' }, + { type: 'HATE', inputStrength: 'HIGH', outputStrength: 'HIGH' }, + { type: 'INSULTS', inputStrength: 'HIGH', outputStrength: 'MEDIUM' }, + { type: 'MISCONDUCT', inputStrength: 'HIGH', outputStrength: 'HIGH' }, + ], + }, + topicPolicyConfig: { + topicsConfig: [ + { + name: 'Financial Advice', + definition: 'Providing specific investment recommendations, stock picks, or financial planning advice.', + type: 'DENY', + }, + ], + }, + }); + + // Guardrail version (required for InvokeModel) + const guardrailVersion = new bedrock.CfnGuardrailVersion(this, 'GuardrailVersion', { + guardrailIdentifier: guardrail.attrGuardrailId, + }); + + // Lambda function + const fn = new lambda.Function(this, 'BedrockGuardrailsFunction', { + runtime: lambda.Runtime.NODEJS_20_X, + handler: 'index.handler', + code: lambda.Code.fromAsset('src'), + timeout: cdk.Duration.seconds(30), + memorySize: 256, + environment: { + MODEL_ID: 'us.anthropic.claude-sonnet-4-20250514-v1:0', + GUARDRAIL_ID: guardrail.attrGuardrailId, + GUARDRAIL_VERSION: guardrailVersion.attrVersion, + }, + }); + + fn.addToRolePolicy(new iam.PolicyStatement({ + actions: ['bedrock:InvokeModel', 'bedrock:ApplyGuardrail'], + resources: [ + 'arn:aws:bedrock:*::foundation-model/*', + `arn:aws:bedrock:*:${this.account}:inference-profile/*`, + guardrail.attrGuardrailArn, + ], + })); + + // REST API + const api = new apigateway.RestApi(this, 'BedrockGuardrailsApi', { + restApiName: 'bedrock-guardrails-api', + description: 'API Gateway -> Lambda -> Bedrock with per-request Guardrails', + }); + + api.root.addResource('invoke').addMethod('POST', new apigateway.LambdaIntegration(fn)); + + new cdk.CfnOutput(this, 'ApiEndpoint', { value: api.url + 'invoke' }); + new cdk.CfnOutput(this, 'GuardrailId', { value: guardrail.attrGuardrailId }); + new cdk.CfnOutput(this, 'FunctionName', { value: fn.functionName }); + } +} diff --git a/apigw-lambda-bedrock-guardrails-cdk/package.json b/apigw-lambda-bedrock-guardrails-cdk/package.json new file mode 100644 index 000000000..f91e39c57 --- /dev/null +++ b/apigw-lambda-bedrock-guardrails-cdk/package.json @@ -0,0 +1,16 @@ +{ + "name": "apigw-lambda-bedrock-guardrails-cdk", + "version": "1.0.0", + "bin": { "app": "bin/app.ts" }, + "scripts": { "build": "tsc", "cdk": "cdk" }, + "dependencies": { + "aws-cdk-lib": "^2.180.0", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" + }, + "devDependencies": { + "typescript": "~5.4.0", + "ts-node": "^10.9.0", + "@types/node": "^20.0.0" + } +} diff --git a/apigw-lambda-bedrock-guardrails-cdk/src/index.js b/apigw-lambda-bedrock-guardrails-cdk/src/index.js new file mode 100644 index 000000000..38b086c5d --- /dev/null +++ b/apigw-lambda-bedrock-guardrails-cdk/src/index.js @@ -0,0 +1,34 @@ +const { BedrockRuntimeClient, InvokeModelCommand } = require('@aws-sdk/client-bedrock-runtime'); + +const client = new BedrockRuntimeClient(); + +exports.handler = async (event) => { + const body = JSON.parse(event.body || '{}'); + const prompt = body.prompt || 'Hello'; + + const response = await client.send(new InvokeModelCommand({ + modelId: process.env.MODEL_ID, + contentType: 'application/json', + accept: 'application/json', + guardrailIdentifier: process.env.GUARDRAIL_ID, + guardrailVersion: process.env.GUARDRAIL_VERSION, + body: JSON.stringify({ + anthropic_version: 'bedrock-2023-05-31', + max_tokens: 512, + messages: [{ role: 'user', content: prompt }], + }), + })); + + const result = JSON.parse(new TextDecoder().decode(response.body)); + + return { + statusCode: 200, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + response: result.content?.[0]?.text, + stopReason: result.stop_reason, + guardrailAction: response.guardrailAction || 'NONE', + usage: result.usage, + }), + }; +}; diff --git a/apigw-lambda-bedrock-guardrails-cdk/tsconfig.json b/apigw-lambda-bedrock-guardrails-cdk/tsconfig.json new file mode 100644 index 000000000..a4f77b1b2 --- /dev/null +++ b/apigw-lambda-bedrock-guardrails-cdk/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "ES2020", "module": "commonjs", "lib": ["es2020"], + "declaration": true, "strict": true, "outDir": "build", + "rootDir": ".", "skipLibCheck": true, "forceConsistentCasingInFileNames": true + }, + "exclude": ["node_modules", "build"] +}