From b2c6d43ad1f6cc7fc5ddd4ae3d4a94c4873904f1 Mon Sep 17 00:00:00 2001 From: Nithin Chandran Rajashankar Date: Mon, 20 Apr 2026 11:32:07 +0000 Subject: [PATCH 1/7] feat(bedrock-guardrails-cross-account-cdk): Add Bedrock Guardrails account-level enforcement pattern Creates a Bedrock Guardrail with content and topic filters, versions it, and enables account-level enforcement via AwsCustomResource. Test Lambda demonstrates automatic guardrail enforcement on all Bedrock calls without specifying guardrailIdentifier. Key features: - Account-level guardrail enforcement via PutEnforcedGuardrailConfiguration - AwsCustomResource for SDK commands not in Lambda runtime - Content filters (HATE, INSULTS, SEXUAL, VIOLENCE, MISCONDUCT, PROMPT_ATTACK) - Denied topic filter (investment advice) - Test Lambda showing safe vs blocked responses - Automatic cleanup on stack deletion --- .../.gitignore | 3 + .../README.md | 130 +++ .../bin/app.ts | 7 + bedrock-guardrails-cross-account-cdk/cdk.json | 3 + .../example-pattern.json | 61 + .../bedrock-guardrails-cross-account-stack.ts | 134 +++ .../package-lock.json | 1012 +++++++++++++++++ .../package.json | 20 + .../src/enforce.mjs | 79 ++ .../src/test.mjs | 36 + .../src/version.mjs | 21 + .../tsconfig.json | 20 + 12 files changed, 1526 insertions(+) create mode 100644 bedrock-guardrails-cross-account-cdk/.gitignore create mode 100644 bedrock-guardrails-cross-account-cdk/README.md create mode 100644 bedrock-guardrails-cross-account-cdk/bin/app.ts create mode 100644 bedrock-guardrails-cross-account-cdk/cdk.json create mode 100644 bedrock-guardrails-cross-account-cdk/example-pattern.json create mode 100644 bedrock-guardrails-cross-account-cdk/lib/bedrock-guardrails-cross-account-stack.ts create mode 100644 bedrock-guardrails-cross-account-cdk/package-lock.json create mode 100644 bedrock-guardrails-cross-account-cdk/package.json create mode 100644 bedrock-guardrails-cross-account-cdk/src/enforce.mjs create mode 100644 bedrock-guardrails-cross-account-cdk/src/test.mjs create mode 100644 bedrock-guardrails-cross-account-cdk/src/version.mjs create mode 100644 bedrock-guardrails-cross-account-cdk/tsconfig.json diff --git a/bedrock-guardrails-cross-account-cdk/.gitignore b/bedrock-guardrails-cross-account-cdk/.gitignore new file mode 100644 index 0000000000..52f5a01162 --- /dev/null +++ b/bedrock-guardrails-cross-account-cdk/.gitignore @@ -0,0 +1,3 @@ +node_modules +build +cdk.out diff --git a/bedrock-guardrails-cross-account-cdk/README.md b/bedrock-guardrails-cross-account-cdk/README.md new file mode 100644 index 0000000000..6d4b242f8f --- /dev/null +++ b/bedrock-guardrails-cross-account-cdk/README.md @@ -0,0 +1,130 @@ +# Bedrock Guardrails Account-Level Enforcement + +This pattern deploys an Amazon Bedrock Guardrail with content and topic filters, enforces it at the account level so ALL Bedrock API calls are automatically guarded, and provides a test Lambda that demonstrates the enforcement without specifying any guardrailIdentifier. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/bedrock-guardrails-cross-account-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. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Requirements + +* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +* [Node.js 18+](https://nodejs.org/en/download/) installed +* [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting-started.html) installed +* [Amazon Bedrock model access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) enabled for Anthropic Claude Sonnet in your target region + +## Architecture + +``` +┌─────────────┐ ┌──────────────────────────────────────────────────────────┐ +│ CDK Deploy │────▶│ 1. CfnGuardrail (content + topic filters) │ +└─────────────┘ │ 2. AwsCustomResource → CreateGuardrailVersion │ + │ 3. AwsCustomResource → PutEnforcedGuardrailConfiguration │ + │ 4. Lambda (test function) │ + └──────────────────────────────────────────────────────────┘ + +┌─────────────┐ ┌──────────────────────────────────────────────────────────┐ +│ Test Lambda │────▶│ Bedrock Converse API (no guardrailIdentifier specified) │ +│ Invocation │ │ │ +└─────────────┘ │ Safe prompt: "What is Amazon S3?" → ✅ passes through │ + │ Violating prompt: "What stocks should I buy?" → ❌ blocked│ + └──────────────────────────────────────────────────────────┘ +``` + +## How it works + +1. **CDK creates a Bedrock Guardrail** with content policy filters (hate, insults, sexual, violence, misconduct, prompt attacks at MEDIUM strength) and a topic policy that denies investment advice. + +2. **An AwsCustomResource creates a guardrail version** — required before enforcement can be enabled. + +3. **A second AwsCustomResource calls `PutEnforcedGuardrailConfiguration`** to enable account-level enforcement. This makes the guardrail apply to ALL Bedrock API calls in the account automatically. + +4. **The test Lambda calls the Bedrock Converse API** without specifying any `guardrailIdentifier` or `guardrailVersion`. The enforced guardrail is applied automatically: + - A safe prompt ("What is Amazon S3?") passes through and returns a normal response with `stopReason: "end_turn"`. + - A violating prompt ("What stocks should I buy for maximum returns?") is blocked by the guardrail. + +5. **On stack deletion**, the `onDelete` handler calls `DeleteEnforcedGuardrailConfiguration` to remove the enforcement. + +## Deployment Instructions + +1. Clone the repository: + ```bash + git clone https://github.com/aws-samples/serverless-patterns + cd serverless-patterns/bedrock-guardrails-cross-account-cdk + ``` + +2. Install dependencies: + ```bash + npm install + ``` + +3. Deploy the stack: + ```bash + cdk deploy + ``` + +4. Note the `TestFunctionName` from the stack outputs. + +## Testing + +1. Invoke the test Lambda: + ```bash + aws lambda invoke \ + --function-name \ + --cli-binary-format raw-in-base64-out \ + output.json + ``` + +2. View the results: + ```bash + cat output.json | jq .body | jq -r . | jq . + ``` + +3. Expected output: + ```json + { + "safeResult": { + "prompt": "What is Amazon S3?", + "stopReason": "end_turn", + "output": "Amazon S3 (Simple Storage Service) is...", + "blocked": false + }, + "violatingResult": { + "prompt": "What stocks should I buy for maximum returns?", + "error": "Your request was blocked by the guardrail.", + "blocked": true + } + } + ``` + + - The safe prompt passes through because it does not violate any content or topic filters. + - The violating prompt is blocked because it triggers the "InvestmentAdvice" topic denial — even though no `guardrailIdentifier` was specified in the API call. + +## Cleanup + +```bash +cdk destroy +``` + +> **Note:** The enforced guardrail configuration is automatically removed on stack deletion via the `onDelete` handler in the AwsCustomResource. + +## Important Notes + +- **Account-wide impact:** Enforced guardrails apply to ALL Bedrock API calls in the account, not just those from this stack. Deploy with caution in shared accounts. + +- **Cross-account extension:** To enforce guardrails across multiple accounts in an AWS Organization, use an Organizations resource control policy (RCP) that references the guardrail. This pattern demonstrates single-account enforcement as the foundation. + +- **Union with request-level guardrails:** If a Bedrock call also specifies a `guardrailIdentifier`, both the enforced guardrail and the request-level guardrail are applied. The results are a union — content blocked by either guardrail is blocked in the response. + +- **IAM requirements:** The deploying role needs `bedrock:PutEnforcedGuardrailConfiguration` and `bedrock:DeleteEnforcedGuardrailConfiguration` permissions. The test Lambda needs `bedrock:InvokeModel` and `bedrock:ApplyGuardrail`. + +- **SDK availability:** The `PutEnforcedGuardrailConfigurationCommand` is not available in the Lambda runtime SDK at the time of writing. This pattern uses AwsCustomResource (which bundles the latest SDK) to work around this limitation. + +- **inputTags deprecation:** The `inputTags` parameter in the enforcement configuration is being deprecated in favor of `selectiveContentGuarding`. This pattern uses `IGNORE` for inputTags as a placeholder. + +---- +Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/bedrock-guardrails-cross-account-cdk/bin/app.ts b/bedrock-guardrails-cross-account-cdk/bin/app.ts new file mode 100644 index 0000000000..8d42e13b6f --- /dev/null +++ b/bedrock-guardrails-cross-account-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 { BedrockGuardrailsCrossAccountStack } from '../lib/bedrock-guardrails-cross-account-stack'; + +const app = new cdk.App(); +new BedrockGuardrailsCrossAccountStack(app, 'BedrockGuardrailsCrossAccountStack'); diff --git a/bedrock-guardrails-cross-account-cdk/cdk.json b/bedrock-guardrails-cross-account-cdk/cdk.json new file mode 100644 index 0000000000..27fe6d2ecb --- /dev/null +++ b/bedrock-guardrails-cross-account-cdk/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "npx ts-node bin/app.ts" +} diff --git a/bedrock-guardrails-cross-account-cdk/example-pattern.json b/bedrock-guardrails-cross-account-cdk/example-pattern.json new file mode 100644 index 0000000000..aaae0ff41c --- /dev/null +++ b/bedrock-guardrails-cross-account-cdk/example-pattern.json @@ -0,0 +1,61 @@ +{ + "title": "Bedrock Guardrails Account-Level Enforcement", + "description": "Deploy an Amazon Bedrock Guardrail with content and topic filters, enforce it at the account level using AwsCustomResource, and demonstrate automatic enforcement on all Bedrock calls without specifying guardrailIdentifier.", + "language": "TypeScript", + "level": "400", + "framework": "AWS CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern deploys a Bedrock Guardrail with content filters (hate, insults, violence, sexual, misconduct, prompt attacks) and a topic filter (investment advice), then enforces it at the account level so ALL Bedrock API calls are automatically guarded.", + "The workflow: (1) CDK creates a CfnGuardrail with content and topic policies, (2) an AwsCustomResource creates a guardrail version, (3) a second AwsCustomResource calls PutEnforcedGuardrailConfiguration to enable account-wide enforcement, (4) a test Lambda demonstrates that safe prompts pass through while violating prompts are blocked — without specifying any guardrailIdentifier in the Converse API call.", + "Enforced guardrails apply to every Bedrock invocation in the account. They create a union with any request-level guardrails, providing a baseline safety layer that individual applications cannot bypass." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/bedrock-guardrails-cross-account-cdk", + "templateURL": "serverless-patterns/bedrock-guardrails-cross-account-cdk", + "projectFolder": "bedrock-guardrails-cross-account-cdk", + "templateFile": "lib/bedrock-guardrails-cross-account-stack.ts" + } + }, + "resources": { + "bullets": [ + { + "text": "Amazon Bedrock Guardrails", + "link": "https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html" + }, + { + "text": "Enforced Guardrails - Account-Level Configuration", + "link": "https://docs.aws.amazon.com/bedrock/latest/userguide/enforced-guardrails.html" + }, + { + "text": "Amazon Bedrock Converse API", + "link": "https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-call.html" + } + ] + }, + "deploy": { + "text": [ + "cdk deploy" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: cdk destroy." + ] + }, + "authors": [ + { + "name": "Nithin Chandran R", + "bio": "Technical Account Manager at AWS", + "linkedin": "nithin-chandran-r" + } + ] +} diff --git a/bedrock-guardrails-cross-account-cdk/lib/bedrock-guardrails-cross-account-stack.ts b/bedrock-guardrails-cross-account-cdk/lib/bedrock-guardrails-cross-account-stack.ts new file mode 100644 index 0000000000..b1ef62b89a --- /dev/null +++ b/bedrock-guardrails-cross-account-cdk/lib/bedrock-guardrails-cross-account-stack.ts @@ -0,0 +1,134 @@ +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as bedrock from 'aws-cdk-lib/aws-bedrock'; +import * as cr from 'aws-cdk-lib/custom-resources'; +import * as path from 'path'; + +export class BedrockGuardrailsCrossAccountStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const modelId = new cdk.CfnParameter(this, 'BedrockModelId', { + type: 'String', + default: 'us.anthropic.claude-sonnet-4-20250514-v1:0', + description: 'Bedrock model ID for testing', + }); + + // Create Bedrock Guardrail + const guardrail = new bedrock.CfnGuardrail(this, 'Guardrail', { + name: 'CrossAccountEnforcedGuardrail', + blockedInputMessaging: 'Your request was blocked by the guardrail.', + blockedOutputsMessaging: 'The response was blocked by the guardrail.', + contentPolicyConfig: { + filtersConfig: [ + { type: 'HATE', inputStrength: 'MEDIUM', outputStrength: 'MEDIUM' }, + { type: 'INSULTS', inputStrength: 'MEDIUM', outputStrength: 'MEDIUM' }, + { type: 'SEXUAL', inputStrength: 'MEDIUM', outputStrength: 'MEDIUM' }, + { type: 'VIOLENCE', inputStrength: 'MEDIUM', outputStrength: 'MEDIUM' }, + { type: 'MISCONDUCT', inputStrength: 'MEDIUM', outputStrength: 'MEDIUM' }, + { type: 'PROMPT_ATTACK', inputStrength: 'MEDIUM', outputStrength: 'NONE' }, + ], + }, + topicPolicyConfig: { + topicsConfig: [ + { + name: 'InvestmentAdvice', + definition: 'Providing specific investment recommendations, stock picks, or financial advice', + type: 'DENY', + }, + ], + }, + }); + + // Create guardrail version via AwsCustomResource + const versionCr = new cr.AwsCustomResource(this, 'GuardrailVersion', { + onCreate: { + service: 'Bedrock', + action: 'createGuardrailVersion', + parameters: { + guardrailIdentifier: guardrail.attrGuardrailId, + }, + physicalResourceId: cr.PhysicalResourceId.fromResponse('version'), + }, + policy: cr.AwsCustomResourcePolicy.fromStatements([ + new iam.PolicyStatement({ + actions: ['bedrock:CreateGuardrailVersion'], + resources: [guardrail.attrGuardrailArn], + }), + ]), + }); + + // Enable account-level enforcement via AwsCustomResource + const enforceCr = new cr.AwsCustomResource(this, 'GuardrailEnforcement', { + onCreate: { + service: 'Bedrock', + action: 'putEnforcedGuardrailConfiguration', + parameters: { + guardrailInferenceConfig: { + guardrailIdentifier: guardrail.attrGuardrailId, + guardrailVersion: versionCr.getResponseField('version'), + inputTags: 'IGNORE', + }, + }, + physicalResourceId: cr.PhysicalResourceId.of('enforced-guardrail'), + }, + onDelete: { + service: 'Bedrock', + action: 'deleteEnforcedGuardrailConfiguration', + parameters: { + configId: 'default', + }, + }, + policy: cr.AwsCustomResourcePolicy.fromStatements([ + new iam.PolicyStatement({ + actions: [ + 'bedrock:PutEnforcedGuardrailConfiguration', + 'bedrock:DeleteEnforcedGuardrailConfiguration', + ], + resources: ['*'], + }), + ]), + }); + + enforceCr.node.addDependency(versionCr); + + // Test Lambda + const testFn = new lambda.Function(this, 'TestFunction', { + runtime: lambda.Runtime.NODEJS_20_X, + handler: 'test.handler', + code: lambda.Code.fromAsset(path.join(__dirname, '..', 'src')), + memorySize: 256, + timeout: cdk.Duration.seconds(60), + environment: { + MODEL_ID: modelId.valueAsString, + }, + }); + + testFn.addToRolePolicy(new iam.PolicyStatement({ + actions: ['bedrock:InvokeModel'], + resources: ['*'], + })); + + testFn.addToRolePolicy(new iam.PolicyStatement({ + actions: ['bedrock:ApplyGuardrail'], + resources: [guardrail.attrGuardrailArn], + })); + + testFn.node.addDependency(enforceCr); + + // Outputs + new cdk.CfnOutput(this, 'GuardrailId', { + value: guardrail.attrGuardrailId, + }); + + new cdk.CfnOutput(this, 'GuardrailArn', { + value: guardrail.attrGuardrailArn, + }); + + new cdk.CfnOutput(this, 'TestFunctionName', { + value: testFn.functionName, + }); + } +} diff --git a/bedrock-guardrails-cross-account-cdk/package-lock.json b/bedrock-guardrails-cross-account-cdk/package-lock.json new file mode 100644 index 0000000000..17336b8879 --- /dev/null +++ b/bedrock-guardrails-cross-account-cdk/package-lock.json @@ -0,0 +1,1012 @@ +{ + "name": "bedrock-guardrails-cross-account-cdk", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "bedrock-guardrails-cross-account-cdk", + "version": "1.0.0", + "dependencies": { + "aws-cdk-lib": "2.180.0", + "constructs": "10.4.2" + }, + "bin": { + "app": "bin/app.ts" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "ts-node": "^10.9.0", + "typescript": "~5.4.0" + } + }, + "node_modules/@aws-cdk/asset-awscli-v1": { + "version": "2.2.275", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.275.tgz", + "integrity": "sha512-dyU4m8yH9vqVr+hm/MCiC5q3+nH6IX1wxABsAQX0qeyExmn+XNULMVpwa9HCLbYjeOgDSwTKJQ+CGDVY6Qu0OA==" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.1.tgz", + "integrity": "sha512-We4bmHaowOPHr+IQR4/FyTGjRfjgBj4ICMjtqmJeBDWad3Q/6St12NT07leNtyuukv2qMhtSZJQorD8KpKTwRA==" + }, + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "39.2.20", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-39.2.20.tgz", + "integrity": "sha512-RI7S8jphGA8mak154ElnEJQPNTTV4PZmA7jgqnBBHQGyOPJIXxtACubNQ5m4YgjpkK3UJHsWT+/cOAfM/Au/Wg==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.1" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.7.1", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "dev": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/aws-cdk-lib": { + "version": "2.180.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.180.0.tgz", + "integrity": "sha512-ncYx3MGcLL397WAg6LOHV8G/5d0FkdoskiUscqFawLWioK75f0M6AIuif9kxrxLBvbMOncOfqhV8wIsCM1fquA==", + "bundleDependencies": [ + "@balena/dockerignore", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml", + "mime-types" + ], + "dependencies": { + "@aws-cdk/asset-awscli-v1": "^2.2.208", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", + "@aws-cdk/cloud-assembly-schema": "^39.2.0", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.2.0", + "ignore": "^5.3.2", + "jsonschema": "^1.4.1", + "mime-types": "^2.1.35", + "minimatch": "^3.1.2", + "punycode": "^2.3.1", + "semver": "^7.6.3", + "table": "^6.8.2", + "yaml": "1.10.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "constructs": "^10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/aws-cdk-lib/node_modules/ajv": { + "version": "8.17.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/astral-regex": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "1.1.11", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/concat-map": { + "version": "0.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { + "version": "3.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-uri": { + "version": "3.0.6", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "11.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.3.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.5.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { + "version": "4.4.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "3.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.3.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/aws-cdk-lib/node_modules/require-from-string": { + "version": "2.0.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.6.3", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/slice-ansi": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/table": { + "version": "6.9.0", + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/constructs": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.2.tgz", + "integrity": "sha512-wsNxBlAott2qg8Zv87q3eYZYgheb9lchtBfjHzzLHtXbttwSrHPs1NNQbBrmbb1YZvYg2+Vh0Dor76w4mFxJkA==" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + } + }, + "dependencies": { + "@aws-cdk/asset-awscli-v1": { + "version": "2.2.275", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.275.tgz", + "integrity": "sha512-dyU4m8yH9vqVr+hm/MCiC5q3+nH6IX1wxABsAQX0qeyExmn+XNULMVpwa9HCLbYjeOgDSwTKJQ+CGDVY6Qu0OA==" + }, + "@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.1.tgz", + "integrity": "sha512-We4bmHaowOPHr+IQR4/FyTGjRfjgBj4ICMjtqmJeBDWad3Q/6St12NT07leNtyuukv2qMhtSZJQorD8KpKTwRA==" + }, + "@aws-cdk/cloud-assembly-schema": { + "version": "39.2.20", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-39.2.20.tgz", + "integrity": "sha512-RI7S8jphGA8mak154ElnEJQPNTTV4PZmA7jgqnBBHQGyOPJIXxtACubNQ5m4YgjpkK3UJHsWT+/cOAfM/Au/Wg==", + "requires": { + "jsonschema": "~1.4.1", + "semver": "^7.7.1" + }, + "dependencies": { + "jsonschema": { + "version": "1.4.1", + "bundled": true + }, + "semver": { + "version": "7.7.1", + "bundled": true + } + } + }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "@types/node": { + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "dev": true, + "requires": { + "undici-types": "~6.21.0" + } + }, + "acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true + }, + "acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "requires": { + "acorn": "^8.11.0" + } + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "aws-cdk-lib": { + "version": "2.180.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.180.0.tgz", + "integrity": "sha512-ncYx3MGcLL397WAg6LOHV8G/5d0FkdoskiUscqFawLWioK75f0M6AIuif9kxrxLBvbMOncOfqhV8wIsCM1fquA==", + "requires": { + "@aws-cdk/asset-awscli-v1": "^2.2.208", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", + "@aws-cdk/cloud-assembly-schema": "^39.2.0", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.2.0", + "ignore": "^5.3.2", + "jsonschema": "^1.4.1", + "mime-types": "^2.1.35", + "minimatch": "^3.1.2", + "punycode": "^2.3.1", + "semver": "^7.6.3", + "table": "^6.8.2", + "yaml": "1.10.2" + }, + "dependencies": { + "@balena/dockerignore": { + "version": "1.0.2", + "bundled": true + }, + "ajv": { + "version": "8.17.1", + "bundled": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "ansi-regex": { + "version": "5.0.1", + "bundled": true + }, + "ansi-styles": { + "version": "4.3.0", + "bundled": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "astral-regex": { + "version": "2.0.0", + "bundled": true + }, + "balanced-match": { + "version": "1.0.2", + "bundled": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "case": { + "version": "1.6.3", + "bundled": true + }, + "color-convert": { + "version": "2.0.1", + "bundled": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "bundled": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "emoji-regex": { + "version": "8.0.0", + "bundled": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "bundled": true + }, + "fast-uri": { + "version": "3.0.6", + "bundled": true + }, + "fs-extra": { + "version": "11.3.0", + "bundled": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "graceful-fs": { + "version": "4.2.11", + "bundled": true + }, + "ignore": { + "version": "5.3.2", + "bundled": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "bundled": true + }, + "json-schema-traverse": { + "version": "1.0.0", + "bundled": true + }, + "jsonfile": { + "version": "6.1.0", + "bundled": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "jsonschema": { + "version": "1.5.0", + "bundled": true + }, + "lodash.truncate": { + "version": "4.4.2", + "bundled": true + }, + "mime-db": { + "version": "1.52.0", + "bundled": true + }, + "mime-types": { + "version": "2.1.35", + "bundled": true, + "requires": { + "mime-db": "1.52.0" + } + }, + "minimatch": { + "version": "3.1.2", + "bundled": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "punycode": { + "version": "2.3.1", + "bundled": true + }, + "require-from-string": { + "version": "2.0.2", + "bundled": true + }, + "semver": { + "version": "7.6.3", + "bundled": true + }, + "slice-ansi": { + "version": "4.0.0", + "bundled": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, + "string-width": { + "version": "4.2.3", + "bundled": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "bundled": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "table": { + "version": "6.9.0", + "bundled": true, + "requires": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + } + }, + "universalify": { + "version": "2.0.1", + "bundled": true + }, + "yaml": { + "version": "1.10.2", + "bundled": true + } + } + }, + "constructs": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.2.tgz", + "integrity": "sha512-wsNxBlAott2qg8Zv87q3eYZYgheb9lchtBfjHzzLHtXbttwSrHPs1NNQbBrmbb1YZvYg2+Vh0Dor76w4mFxJkA==" + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + } + }, + "typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true + }, + "undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + } + } +} diff --git a/bedrock-guardrails-cross-account-cdk/package.json b/bedrock-guardrails-cross-account-cdk/package.json new file mode 100644 index 0000000000..46bbf96467 --- /dev/null +++ b/bedrock-guardrails-cross-account-cdk/package.json @@ -0,0 +1,20 @@ +{ + "name": "bedrock-guardrails-cross-account-cdk", + "version": "1.0.0", + "bin": { + "app": "bin/app.ts" + }, + "scripts": { + "build": "tsc", + "cdk": "cdk" + }, + "dependencies": { + "aws-cdk-lib": "2.180.0", + "constructs": "10.4.2" + }, + "devDependencies": { + "typescript": "~5.4.0", + "ts-node": "^10.9.0", + "@types/node": "^20.0.0" + } +} diff --git a/bedrock-guardrails-cross-account-cdk/src/enforce.mjs b/bedrock-guardrails-cross-account-cdk/src/enforce.mjs new file mode 100644 index 0000000000..15549f1743 --- /dev/null +++ b/bedrock-guardrails-cross-account-cdk/src/enforce.mjs @@ -0,0 +1,79 @@ +import { defaultProvider } from '@aws-sdk/credential-provider-node'; +import { SignatureV4 } from '@smithy/signature-v4'; +import { Sha256 } from '@aws-crypto/sha256-js'; +import { HttpRequest } from '@smithy/protocol-http'; +import https from 'https'; + +const region = process.env.AWS_REGION || 'us-east-1'; + +async function makeSignedRequest(method, path, body) { + const credentials = await defaultProvider()(); + const signer = new SignatureV4({ + service: 'bedrock', + region, + credentials, + sha256: Sha256, + }); + + const request = new HttpRequest({ + method, + protocol: 'https:', + hostname: `bedrock.${region}.amazonaws.com`, + path, + headers: { + 'Content-Type': 'application/json', + host: `bedrock.${region}.amazonaws.com`, + }, + body: body ? JSON.stringify(body) : undefined, + }); + + const signed = await signer.sign(request); + + return new Promise((resolve, reject) => { + const req = https.request({ + hostname: signed.hostname, + path: signed.path, + method: signed.method, + headers: signed.headers, + }, (res) => { + let data = ''; + res.on('data', (chunk) => data += chunk); + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + resolve(data ? JSON.parse(data) : {}); + } else { + reject(new Error(`HTTP ${res.statusCode}: ${data}`)); + } + }); + }); + req.on('error', reject); + if (signed.body) req.write(signed.body); + req.end(); + }); +} + +export async function onEvent(event) { + const { GuardrailIdentifier, GuardrailVersion } = event.ResourceProperties; + + if (event.RequestType === 'Create' || event.RequestType === 'Update') { + await makeSignedRequest('PUT', '/enforced-guardrail-configuration', { + guardrailIdentifier: GuardrailIdentifier, + guardrailVersion: GuardrailVersion, + }); + + return { + PhysicalResourceId: `${GuardrailIdentifier}-enforced`, + Data: { GuardrailIdentifier, GuardrailVersion }, + }; + } + + if (event.RequestType === 'Delete') { + try { + await makeSignedRequest('DELETE', '/enforced-guardrail-configuration', {}); + } catch (e) { + // Ignore if already deleted + } + } + + return { PhysicalResourceId: event.PhysicalResourceId }; +} diff --git a/bedrock-guardrails-cross-account-cdk/src/test.mjs b/bedrock-guardrails-cross-account-cdk/src/test.mjs new file mode 100644 index 0000000000..0b16f1f5dc --- /dev/null +++ b/bedrock-guardrails-cross-account-cdk/src/test.mjs @@ -0,0 +1,36 @@ +import { BedrockRuntimeClient, ConverseCommand } from '@aws-sdk/client-bedrock-runtime'; + +const client = new BedrockRuntimeClient(); +const modelId = process.env.MODEL_ID; + +async function invokeModel(prompt) { + try { + const response = await client.send(new ConverseCommand({ + modelId, + messages: [{ role: 'user', content: [{ text: prompt }] }], + })); + + return { + prompt, + stopReason: response.stopReason, + output: response.output?.message?.content?.[0]?.text || 'No text response', + blocked: false, + }; + } catch (error) { + return { + prompt, + error: error.message, + blocked: error.message?.includes('guardrail') || error.name === 'GuardrailStreamProcessingException', + }; + } +} + +export async function handler() { + const safeResult = await invokeModel('What is Amazon S3?'); + const violatingResult = await invokeModel('What stocks should I buy for maximum returns?'); + + return { + statusCode: 200, + body: JSON.stringify({ safeResult, violatingResult }, null, 2), + }; +} diff --git a/bedrock-guardrails-cross-account-cdk/src/version.mjs b/bedrock-guardrails-cross-account-cdk/src/version.mjs new file mode 100644 index 0000000000..52857e04b3 --- /dev/null +++ b/bedrock-guardrails-cross-account-cdk/src/version.mjs @@ -0,0 +1,21 @@ +import pkg from '@aws-sdk/client-bedrock'; +const { BedrockClient, CreateGuardrailVersionCommand } = pkg; + +const client = new BedrockClient(); + +export async function onEvent(event) { + const guardrailIdentifier = event.ResourceProperties.GuardrailIdentifier; + + if (event.RequestType === 'Create' || event.RequestType === 'Update') { + const response = await client.send(new CreateGuardrailVersionCommand({ + guardrailIdentifier, + })); + + return { + PhysicalResourceId: response.version, + Data: { Version: response.version }, + }; + } + + return { PhysicalResourceId: event.PhysicalResourceId }; +} diff --git a/bedrock-guardrails-cross-account-cdk/tsconfig.json b/bedrock-guardrails-cross-account-cdk/tsconfig.json new file mode 100644 index 0000000000..635a28b58d --- /dev/null +++ b/bedrock-guardrails-cross-account-cdk/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["es2020"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "outDir": "build", + "rootDir": ".", + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "exclude": ["node_modules", "cdk.out", "build"] +} From b20a03cbae73491f1c39ac8195194093901b7f91 Mon Sep 17 00:00:00 2001 From: Nithin Chandran Rajashankar Date: Fri, 24 Apr 2026 03:39:41 +0000 Subject: [PATCH 2/7] fix(iam): scope bedrock:InvokeModel to inference profile ARN Replace wildcard resource with specific inference profile ARN and foundation-model/* for least-privilege IAM. --- .../lib/bedrock-guardrails-cross-account-stack.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bedrock-guardrails-cross-account-cdk/lib/bedrock-guardrails-cross-account-stack.ts b/bedrock-guardrails-cross-account-cdk/lib/bedrock-guardrails-cross-account-stack.ts index b1ef62b89a..876d772246 100644 --- a/bedrock-guardrails-cross-account-cdk/lib/bedrock-guardrails-cross-account-stack.ts +++ b/bedrock-guardrails-cross-account-cdk/lib/bedrock-guardrails-cross-account-stack.ts @@ -108,7 +108,10 @@ export class BedrockGuardrailsCrossAccountStack extends cdk.Stack { testFn.addToRolePolicy(new iam.PolicyStatement({ actions: ['bedrock:InvokeModel'], - resources: ['*'], + resources: [ + `arn:aws:bedrock:${this.region}:${this.account}:inference-profile/${modelId.valueAsString}`, + 'arn:aws:bedrock:*::foundation-model/*', + ], })); testFn.addToRolePolicy(new iam.PolicyStatement({ From b1e3785c8b77842b19eb03442316525ca581bf2a Mon Sep 17 00:00:00 2001 From: Nithin Chandran Rajashankar Date: Tue, 2 Jun 2026 13:17:32 +0000 Subject: [PATCH 3/7] fix: address review feedback -- full service names, Node 22, remove dead code, copyright 2026 --- .../README.md | 8 +- .../bedrock-guardrails-cross-account-stack.ts | 7 +- .../src/enforce.mjs | 79 ------------------- .../src/version.mjs | 21 ----- 4 files changed, 5 insertions(+), 110 deletions(-) delete mode 100644 bedrock-guardrails-cross-account-cdk/src/enforce.mjs delete mode 100644 bedrock-guardrails-cross-account-cdk/src/version.mjs diff --git a/bedrock-guardrails-cross-account-cdk/README.md b/bedrock-guardrails-cross-account-cdk/README.md index 6d4b242f8f..c694c62ee1 100644 --- a/bedrock-guardrails-cross-account-cdk/README.md +++ b/bedrock-guardrails-cross-account-cdk/README.md @@ -1,6 +1,6 @@ -# Bedrock Guardrails Account-Level Enforcement +# Amazon Bedrock Guardrails Account-Level Enforcement -This pattern deploys an Amazon Bedrock Guardrail with content and topic filters, enforces it at the account level so ALL Bedrock API calls are automatically guarded, and provides a test Lambda that demonstrates the enforcement without specifying any guardrailIdentifier. +This pattern deploys an Amazon Bedrock Guardrail with content and topic filters, enforces it at the account level so ALL Bedrock API calls are automatically guarded, and provides a test AWS Lambda function that demonstrates the enforcement without specifying any guardrail identifier. Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/bedrock-guardrails-cross-account-cdk @@ -11,7 +11,7 @@ Important: this application uses various AWS services and there are costs associ * [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. * [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured * [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) -* [Node.js 18+](https://nodejs.org/en/download/) installed +* [Node.js 20+](https://nodejs.org/en/download/) installed * [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting-started.html) installed * [Amazon Bedrock model access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) enabled for Anthropic Claude Sonnet in your target region @@ -125,6 +125,6 @@ cdk destroy - **inputTags deprecation:** The `inputTags` parameter in the enforcement configuration is being deprecated in favor of `selectiveContentGuarding`. This pattern uses `IGNORE` for inputTags as a placeholder. ---- -Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. SPDX-License-Identifier: MIT-0 diff --git a/bedrock-guardrails-cross-account-cdk/lib/bedrock-guardrails-cross-account-stack.ts b/bedrock-guardrails-cross-account-cdk/lib/bedrock-guardrails-cross-account-stack.ts index 876d772246..6ad9233600 100644 --- a/bedrock-guardrails-cross-account-cdk/lib/bedrock-guardrails-cross-account-stack.ts +++ b/bedrock-guardrails-cross-account-cdk/lib/bedrock-guardrails-cross-account-stack.ts @@ -96,7 +96,7 @@ export class BedrockGuardrailsCrossAccountStack extends cdk.Stack { // Test Lambda const testFn = new lambda.Function(this, 'TestFunction', { - runtime: lambda.Runtime.NODEJS_20_X, + runtime: lambda.Runtime.NODEJS_22_X, handler: 'test.handler', code: lambda.Code.fromAsset(path.join(__dirname, '..', 'src')), memorySize: 256, @@ -114,11 +114,6 @@ export class BedrockGuardrailsCrossAccountStack extends cdk.Stack { ], })); - testFn.addToRolePolicy(new iam.PolicyStatement({ - actions: ['bedrock:ApplyGuardrail'], - resources: [guardrail.attrGuardrailArn], - })); - testFn.node.addDependency(enforceCr); // Outputs diff --git a/bedrock-guardrails-cross-account-cdk/src/enforce.mjs b/bedrock-guardrails-cross-account-cdk/src/enforce.mjs deleted file mode 100644 index 15549f1743..0000000000 --- a/bedrock-guardrails-cross-account-cdk/src/enforce.mjs +++ /dev/null @@ -1,79 +0,0 @@ -import { defaultProvider } from '@aws-sdk/credential-provider-node'; -import { SignatureV4 } from '@smithy/signature-v4'; -import { Sha256 } from '@aws-crypto/sha256-js'; -import { HttpRequest } from '@smithy/protocol-http'; -import https from 'https'; - -const region = process.env.AWS_REGION || 'us-east-1'; - -async function makeSignedRequest(method, path, body) { - const credentials = await defaultProvider()(); - const signer = new SignatureV4({ - service: 'bedrock', - region, - credentials, - sha256: Sha256, - }); - - const request = new HttpRequest({ - method, - protocol: 'https:', - hostname: `bedrock.${region}.amazonaws.com`, - path, - headers: { - 'Content-Type': 'application/json', - host: `bedrock.${region}.amazonaws.com`, - }, - body: body ? JSON.stringify(body) : undefined, - }); - - const signed = await signer.sign(request); - - return new Promise((resolve, reject) => { - const req = https.request({ - hostname: signed.hostname, - path: signed.path, - method: signed.method, - headers: signed.headers, - }, (res) => { - let data = ''; - res.on('data', (chunk) => data += chunk); - res.on('end', () => { - if (res.statusCode >= 200 && res.statusCode < 300) { - resolve(data ? JSON.parse(data) : {}); - } else { - reject(new Error(`HTTP ${res.statusCode}: ${data}`)); - } - }); - }); - req.on('error', reject); - if (signed.body) req.write(signed.body); - req.end(); - }); -} - -export async function onEvent(event) { - const { GuardrailIdentifier, GuardrailVersion } = event.ResourceProperties; - - if (event.RequestType === 'Create' || event.RequestType === 'Update') { - await makeSignedRequest('PUT', '/enforced-guardrail-configuration', { - guardrailIdentifier: GuardrailIdentifier, - guardrailVersion: GuardrailVersion, - }); - - return { - PhysicalResourceId: `${GuardrailIdentifier}-enforced`, - Data: { GuardrailIdentifier, GuardrailVersion }, - }; - } - - if (event.RequestType === 'Delete') { - try { - await makeSignedRequest('DELETE', '/enforced-guardrail-configuration', {}); - } catch (e) { - // Ignore if already deleted - } - } - - return { PhysicalResourceId: event.PhysicalResourceId }; -} diff --git a/bedrock-guardrails-cross-account-cdk/src/version.mjs b/bedrock-guardrails-cross-account-cdk/src/version.mjs deleted file mode 100644 index 52857e04b3..0000000000 --- a/bedrock-guardrails-cross-account-cdk/src/version.mjs +++ /dev/null @@ -1,21 +0,0 @@ -import pkg from '@aws-sdk/client-bedrock'; -const { BedrockClient, CreateGuardrailVersionCommand } = pkg; - -const client = new BedrockClient(); - -export async function onEvent(event) { - const guardrailIdentifier = event.ResourceProperties.GuardrailIdentifier; - - if (event.RequestType === 'Create' || event.RequestType === 'Update') { - const response = await client.send(new CreateGuardrailVersionCommand({ - guardrailIdentifier, - })); - - return { - PhysicalResourceId: response.version, - Data: { Version: response.version }, - }; - } - - return { PhysicalResourceId: event.PhysicalResourceId }; -} From 889a09bf01c7f34b99d24255038ca453a805b4b8 Mon Sep 17 00:00:00 2001 From: Nithin Chandran Rajashankar Date: Mon, 15 Jun 2026 12:37:34 +0000 Subject: [PATCH 4/7] refactor(bedrock-guardrails): Use native enforcement resource, rename to enforcement-cdk Address bfreiberg review on PR #3067: - Replace both AwsCustomResource workarounds with native resources: CfnGuardrailVersion + AWS::Bedrock::EnforcedGuardrailConfiguration. This removes the broad bedrock:* IAM on the custom resource. - Rename folder/stack from bedrock-guardrails-cross-account-cdk to bedrock-guardrails-enforcement-cdk (name was misleading; pattern is account-level enforcement, not cross-account). - Add scoped bedrock:ApplyGuardrail to the test Lambda - required when an enforced guardrail is active even without passing guardrailIdentifier (found via live deploy+test, was failing with AccessDenied). - Fix test harness: enforced-guardrail interventions return stopReason 'guardrail_intervened' in-band, not a thrown exception. - Bump Lambda runtime to nodejs24.x; example-pattern.json title/description/level. Deployed, tested, and torn down via CodeBuild in us-east-1: safe prompt end_turn (allowed), violating prompt guardrail_intervened (blocked), no guardrailIdentifier passed. tsc clean. sim: https://github.com/aws-samples/serverless-patterns/pull/3067 --- .../bin/app.ts | 7 -- .../example-pattern.json | 61 -------------- .../.gitignore | 0 .../README.md | 29 +++---- bedrock-guardrails-enforcement-cdk/bin/app.ts | 7 ++ .../cdk.json | 0 .../example-pattern.json | 61 ++++++++++++++ .../bedrock-guardrails-enforcement-stack.ts | 81 +++++++------------ .../package-lock.json | 0 .../package.json | 2 +- .../src/test.mjs | 7 +- .../tsconfig.json | 0 12 files changed, 114 insertions(+), 141 deletions(-) delete mode 100644 bedrock-guardrails-cross-account-cdk/bin/app.ts delete mode 100644 bedrock-guardrails-cross-account-cdk/example-pattern.json rename {bedrock-guardrails-cross-account-cdk => bedrock-guardrails-enforcement-cdk}/.gitignore (100%) rename {bedrock-guardrails-cross-account-cdk => bedrock-guardrails-enforcement-cdk}/README.md (74%) create mode 100644 bedrock-guardrails-enforcement-cdk/bin/app.ts rename {bedrock-guardrails-cross-account-cdk => bedrock-guardrails-enforcement-cdk}/cdk.json (100%) create mode 100644 bedrock-guardrails-enforcement-cdk/example-pattern.json rename bedrock-guardrails-cross-account-cdk/lib/bedrock-guardrails-cross-account-stack.ts => bedrock-guardrails-enforcement-cdk/lib/bedrock-guardrails-enforcement-stack.ts (56%) rename {bedrock-guardrails-cross-account-cdk => bedrock-guardrails-enforcement-cdk}/package-lock.json (100%) rename {bedrock-guardrails-cross-account-cdk => bedrock-guardrails-enforcement-cdk}/package.json (86%) rename {bedrock-guardrails-cross-account-cdk => bedrock-guardrails-enforcement-cdk}/src/test.mjs (78%) rename {bedrock-guardrails-cross-account-cdk => bedrock-guardrails-enforcement-cdk}/tsconfig.json (100%) diff --git a/bedrock-guardrails-cross-account-cdk/bin/app.ts b/bedrock-guardrails-cross-account-cdk/bin/app.ts deleted file mode 100644 index 8d42e13b6f..0000000000 --- a/bedrock-guardrails-cross-account-cdk/bin/app.ts +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env node -import 'source-map-support/register'; -import * as cdk from 'aws-cdk-lib'; -import { BedrockGuardrailsCrossAccountStack } from '../lib/bedrock-guardrails-cross-account-stack'; - -const app = new cdk.App(); -new BedrockGuardrailsCrossAccountStack(app, 'BedrockGuardrailsCrossAccountStack'); diff --git a/bedrock-guardrails-cross-account-cdk/example-pattern.json b/bedrock-guardrails-cross-account-cdk/example-pattern.json deleted file mode 100644 index aaae0ff41c..0000000000 --- a/bedrock-guardrails-cross-account-cdk/example-pattern.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "title": "Bedrock Guardrails Account-Level Enforcement", - "description": "Deploy an Amazon Bedrock Guardrail with content and topic filters, enforce it at the account level using AwsCustomResource, and demonstrate automatic enforcement on all Bedrock calls without specifying guardrailIdentifier.", - "language": "TypeScript", - "level": "400", - "framework": "AWS CDK", - "introBox": { - "headline": "How it works", - "text": [ - "This pattern deploys a Bedrock Guardrail with content filters (hate, insults, violence, sexual, misconduct, prompt attacks) and a topic filter (investment advice), then enforces it at the account level so ALL Bedrock API calls are automatically guarded.", - "The workflow: (1) CDK creates a CfnGuardrail with content and topic policies, (2) an AwsCustomResource creates a guardrail version, (3) a second AwsCustomResource calls PutEnforcedGuardrailConfiguration to enable account-wide enforcement, (4) a test Lambda demonstrates that safe prompts pass through while violating prompts are blocked — without specifying any guardrailIdentifier in the Converse API call.", - "Enforced guardrails apply to every Bedrock invocation in the account. They create a union with any request-level guardrails, providing a baseline safety layer that individual applications cannot bypass." - ] - }, - "gitHub": { - "template": { - "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/bedrock-guardrails-cross-account-cdk", - "templateURL": "serverless-patterns/bedrock-guardrails-cross-account-cdk", - "projectFolder": "bedrock-guardrails-cross-account-cdk", - "templateFile": "lib/bedrock-guardrails-cross-account-stack.ts" - } - }, - "resources": { - "bullets": [ - { - "text": "Amazon Bedrock Guardrails", - "link": "https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html" - }, - { - "text": "Enforced Guardrails - Account-Level Configuration", - "link": "https://docs.aws.amazon.com/bedrock/latest/userguide/enforced-guardrails.html" - }, - { - "text": "Amazon Bedrock Converse API", - "link": "https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-call.html" - } - ] - }, - "deploy": { - "text": [ - "cdk deploy" - ] - }, - "testing": { - "text": [ - "See the GitHub repo for detailed testing instructions." - ] - }, - "cleanup": { - "text": [ - "Delete the stack: cdk destroy." - ] - }, - "authors": [ - { - "name": "Nithin Chandran R", - "bio": "Technical Account Manager at AWS", - "linkedin": "nithin-chandran-r" - } - ] -} diff --git a/bedrock-guardrails-cross-account-cdk/.gitignore b/bedrock-guardrails-enforcement-cdk/.gitignore similarity index 100% rename from bedrock-guardrails-cross-account-cdk/.gitignore rename to bedrock-guardrails-enforcement-cdk/.gitignore diff --git a/bedrock-guardrails-cross-account-cdk/README.md b/bedrock-guardrails-enforcement-cdk/README.md similarity index 74% rename from bedrock-guardrails-cross-account-cdk/README.md rename to bedrock-guardrails-enforcement-cdk/README.md index c694c62ee1..c9c35fdaa9 100644 --- a/bedrock-guardrails-cross-account-cdk/README.md +++ b/bedrock-guardrails-enforcement-cdk/README.md @@ -2,7 +2,7 @@ This pattern deploys an Amazon Bedrock Guardrail with content and topic filters, enforces it at the account level so ALL Bedrock API calls are automatically guarded, and provides a test AWS Lambda function that demonstrates the enforcement without specifying any guardrail identifier. -Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/bedrock-guardrails-cross-account-cdk +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/bedrock-guardrails-enforcement-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. You are responsible for any AWS costs incurred. No warranty is implied in this example. @@ -11,7 +11,7 @@ Important: this application uses various AWS services and there are costs associ * [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. * [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured * [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) -* [Node.js 20+](https://nodejs.org/en/download/) installed +* [Node.js 24+](https://nodejs.org/en/download/) installed * [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting-started.html) installed * [Amazon Bedrock model access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) enabled for Anthropic Claude Sonnet in your target region @@ -20,8 +20,8 @@ Important: this application uses various AWS services and there are costs associ ``` ┌─────────────┐ ┌──────────────────────────────────────────────────────────┐ │ CDK Deploy │────▶│ 1. CfnGuardrail (content + topic filters) │ -└─────────────┘ │ 2. AwsCustomResource → CreateGuardrailVersion │ - │ 3. AwsCustomResource → PutEnforcedGuardrailConfiguration │ +└─────────────┘ │ 2. CfnGuardrailVersion (publishes a numbered version) │ + │ 3. AWS::Bedrock::EnforcedGuardrailConfiguration │ │ 4. Lambda (test function) │ └──────────────────────────────────────────────────────────┘ @@ -37,22 +37,22 @@ Important: this application uses various AWS services and there are costs associ 1. **CDK creates a Bedrock Guardrail** with content policy filters (hate, insults, sexual, violence, misconduct, prompt attacks at MEDIUM strength) and a topic policy that denies investment advice. -2. **An AwsCustomResource creates a guardrail version** — required before enforcement can be enabled. +2. **A `CfnGuardrailVersion` publishes a numbered version** — required before enforcement can be enabled. -3. **A second AwsCustomResource calls `PutEnforcedGuardrailConfiguration`** to enable account-level enforcement. This makes the guardrail apply to ALL Bedrock API calls in the account automatically. +3. **An `AWS::Bedrock::EnforcedGuardrailConfiguration` resource enables account-level enforcement.** This makes the guardrail apply to ALL Bedrock API calls in the account automatically. 4. **The test Lambda calls the Bedrock Converse API** without specifying any `guardrailIdentifier` or `guardrailVersion`. The enforced guardrail is applied automatically: - A safe prompt ("What is Amazon S3?") passes through and returns a normal response with `stopReason: "end_turn"`. - A violating prompt ("What stocks should I buy for maximum returns?") is blocked by the guardrail. -5. **On stack deletion**, the `onDelete` handler calls `DeleteEnforcedGuardrailConfiguration` to remove the enforcement. +5. **On stack deletion**, CloudFormation removes the `AWS::Bedrock::EnforcedGuardrailConfiguration` resource, which clears the account-level enforcement automatically. ## Deployment Instructions 1. Clone the repository: ```bash git clone https://github.com/aws-samples/serverless-patterns - cd serverless-patterns/bedrock-guardrails-cross-account-cdk + cd serverless-patterns/bedrock-guardrails-enforcement-cdk ``` 2. Install dependencies: @@ -93,14 +93,15 @@ Important: this application uses various AWS services and there are costs associ }, "violatingResult": { "prompt": "What stocks should I buy for maximum returns?", - "error": "Your request was blocked by the guardrail.", + "stopReason": "guardrail_intervened", + "output": "Your request was blocked by the guardrail.", "blocked": true } } ``` - The safe prompt passes through because it does not violate any content or topic filters. - - The violating prompt is blocked because it triggers the "InvestmentAdvice" topic denial — even though no `guardrailIdentifier` was specified in the API call. + - The violating prompt is intercepted by the enforced guardrail — the Converse call returns `stopReason: "guardrail_intervened"` with the blocked message, even though no `guardrailIdentifier` was specified in the API call. ## Cleanup @@ -108,7 +109,7 @@ Important: this application uses various AWS services and there are costs associ cdk destroy ``` -> **Note:** The enforced guardrail configuration is automatically removed on stack deletion via the `onDelete` handler in the AwsCustomResource. +> **Note:** The enforced guardrail configuration is removed automatically on stack deletion when CloudFormation deletes the `AWS::Bedrock::EnforcedGuardrailConfiguration` resource. ## Important Notes @@ -118,11 +119,7 @@ cdk destroy - **Union with request-level guardrails:** If a Bedrock call also specifies a `guardrailIdentifier`, both the enforced guardrail and the request-level guardrail are applied. The results are a union — content blocked by either guardrail is blocked in the response. -- **IAM requirements:** The deploying role needs `bedrock:PutEnforcedGuardrailConfiguration` and `bedrock:DeleteEnforcedGuardrailConfiguration` permissions. The test Lambda needs `bedrock:InvokeModel` and `bedrock:ApplyGuardrail`. - -- **SDK availability:** The `PutEnforcedGuardrailConfigurationCommand` is not available in the Lambda runtime SDK at the time of writing. This pattern uses AwsCustomResource (which bundles the latest SDK) to work around this limitation. - -- **inputTags deprecation:** The `inputTags` parameter in the enforcement configuration is being deprecated in favor of `selectiveContentGuarding`. This pattern uses `IGNORE` for inputTags as a placeholder. +- **IAM requirements:** CloudFormation provisions the guardrail, version, and enforced configuration, so the CDK deployment role needs `bedrock:CreateGuardrail`, `bedrock:CreateGuardrailVersion`, and `bedrock:PutEnforcedGuardrailConfiguration` / `bedrock:DeleteEnforcedGuardrailConfiguration`. The test Lambda needs `bedrock:InvokeModel` and `bedrock:ApplyGuardrail` — with an enforced guardrail active, the caller must be authorized to apply it even though no `guardrailIdentifier` is passed. ---- Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/bedrock-guardrails-enforcement-cdk/bin/app.ts b/bedrock-guardrails-enforcement-cdk/bin/app.ts new file mode 100644 index 0000000000..274b8153f7 --- /dev/null +++ b/bedrock-guardrails-enforcement-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 { BedrockGuardrailsEnforcementStack } from '../lib/bedrock-guardrails-enforcement-stack'; + +const app = new cdk.App(); +new BedrockGuardrailsEnforcementStack(app, 'BedrockGuardrailsEnforcementStack'); diff --git a/bedrock-guardrails-cross-account-cdk/cdk.json b/bedrock-guardrails-enforcement-cdk/cdk.json similarity index 100% rename from bedrock-guardrails-cross-account-cdk/cdk.json rename to bedrock-guardrails-enforcement-cdk/cdk.json diff --git a/bedrock-guardrails-enforcement-cdk/example-pattern.json b/bedrock-guardrails-enforcement-cdk/example-pattern.json new file mode 100644 index 0000000000..2c0b02b85e --- /dev/null +++ b/bedrock-guardrails-enforcement-cdk/example-pattern.json @@ -0,0 +1,61 @@ +{ + "title": "Amazon Bedrock Guardrails Account-Level Enforcement", + "description": "Deploy an Amazon Bedrock Guardrail with content and topic filters to demonstrate automatic enforcement on all Bedrock calls without specifying guardrailIdentifier.", + "language": "TypeScript", + "level": "300", + "framework": "AWS CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern deploys an Amazon Bedrock Guardrail with content filters (hate, insults, violence, sexual, misconduct, prompt attacks) and a topic filter (investment advice), then enforces it at the account level so ALL Amazon Bedrock API calls are automatically guarded.", + "The workflow: (1) CDK creates a CfnGuardrail with content and topic policies, (2) a CfnGuardrailVersion publishes a numbered version, (3) an AWS::Bedrock::EnforcedGuardrailConfiguration resource enables account-wide enforcement, (4) a test AWS Lambda function demonstrates that safe prompts pass through while violating prompts are blocked - without specifying any guardrailIdentifier in the Converse API call.", + "Enforced guardrails apply to every Amazon Bedrock invocation in the account. They create a union with any request-level guardrails, providing a baseline safety layer that individual applications cannot bypass." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/bedrock-guardrails-enforcement-cdk", + "templateURL": "serverless-patterns/bedrock-guardrails-enforcement-cdk", + "projectFolder": "bedrock-guardrails-enforcement-cdk", + "templateFile": "lib/bedrock-guardrails-enforcement-stack.ts" + } + }, + "resources": { + "bullets": [ + { + "text": "Amazon Bedrock Guardrails", + "link": "https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails.html" + }, + { + "text": "Enforced Guardrails - Account-Level Configuration", + "link": "https://docs.aws.amazon.com/bedrock/latest/userguide/enforced-guardrails.html" + }, + { + "text": "Amazon Bedrock Converse API", + "link": "https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-call.html" + } + ] + }, + "deploy": { + "text": [ + "cdk deploy" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: cdk destroy." + ] + }, + "authors": [ + { + "name": "Nithin Chandran R", + "bio": "Technical Account Manager at AWS", + "linkedin": "nithin-chandran-r" + } + ] +} diff --git a/bedrock-guardrails-cross-account-cdk/lib/bedrock-guardrails-cross-account-stack.ts b/bedrock-guardrails-enforcement-cdk/lib/bedrock-guardrails-enforcement-stack.ts similarity index 56% rename from bedrock-guardrails-cross-account-cdk/lib/bedrock-guardrails-cross-account-stack.ts rename to bedrock-guardrails-enforcement-cdk/lib/bedrock-guardrails-enforcement-stack.ts index 6ad9233600..0a895561f1 100644 --- a/bedrock-guardrails-cross-account-cdk/lib/bedrock-guardrails-cross-account-stack.ts +++ b/bedrock-guardrails-enforcement-cdk/lib/bedrock-guardrails-enforcement-stack.ts @@ -3,10 +3,9 @@ import { Construct } from 'constructs'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as bedrock from 'aws-cdk-lib/aws-bedrock'; -import * as cr from 'aws-cdk-lib/custom-resources'; import * as path from 'path'; -export class BedrockGuardrailsCrossAccountStack extends cdk.Stack { +export class BedrockGuardrailsEnforcementStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); @@ -16,9 +15,9 @@ export class BedrockGuardrailsCrossAccountStack extends cdk.Stack { description: 'Bedrock model ID for testing', }); - // Create Bedrock Guardrail + // Create the Bedrock Guardrail (DRAFT) with content and topic filters const guardrail = new bedrock.CfnGuardrail(this, 'Guardrail', { - name: 'CrossAccountEnforcedGuardrail', + name: 'AccountEnforcedGuardrail', blockedInputMessaging: 'Your request was blocked by the guardrail.', blockedOutputsMessaging: 'The response was blocked by the guardrail.', contentPolicyConfig: { @@ -42,61 +41,26 @@ export class BedrockGuardrailsCrossAccountStack extends cdk.Stack { }, }); - // Create guardrail version via AwsCustomResource - const versionCr = new cr.AwsCustomResource(this, 'GuardrailVersion', { - onCreate: { - service: 'Bedrock', - action: 'createGuardrailVersion', - parameters: { - guardrailIdentifier: guardrail.attrGuardrailId, - }, - physicalResourceId: cr.PhysicalResourceId.fromResponse('version'), - }, - policy: cr.AwsCustomResourcePolicy.fromStatements([ - new iam.PolicyStatement({ - actions: ['bedrock:CreateGuardrailVersion'], - resources: [guardrail.attrGuardrailArn], - }), - ]), + // Publish a numbered guardrail version using the native CloudFormation resource + const guardrailVersion = new bedrock.CfnGuardrailVersion(this, 'GuardrailVersion', { + guardrailIdentifier: guardrail.attrGuardrailId, + description: 'Version enforced at the account level', }); - // Enable account-level enforcement via AwsCustomResource - const enforceCr = new cr.AwsCustomResource(this, 'GuardrailEnforcement', { - onCreate: { - service: 'Bedrock', - action: 'putEnforcedGuardrailConfiguration', - parameters: { - guardrailInferenceConfig: { - guardrailIdentifier: guardrail.attrGuardrailId, - guardrailVersion: versionCr.getResponseField('version'), - inputTags: 'IGNORE', - }, - }, - physicalResourceId: cr.PhysicalResourceId.of('enforced-guardrail'), + // Enable account-level enforcement using the native AWS::Bedrock::EnforcedGuardrailConfiguration + // resource. No custom resource / Lambda-backed workaround is required. + const enforcedGuardrail = new cdk.CfnResource(this, 'EnforcedGuardrail', { + type: 'AWS::Bedrock::EnforcedGuardrailConfiguration', + properties: { + GuardrailIdentifier: guardrail.attrGuardrailId, + GuardrailVersion: guardrailVersion.attrVersion, }, - onDelete: { - service: 'Bedrock', - action: 'deleteEnforcedGuardrailConfiguration', - parameters: { - configId: 'default', - }, - }, - policy: cr.AwsCustomResourcePolicy.fromStatements([ - new iam.PolicyStatement({ - actions: [ - 'bedrock:PutEnforcedGuardrailConfiguration', - 'bedrock:DeleteEnforcedGuardrailConfiguration', - ], - resources: ['*'], - }), - ]), }); + enforcedGuardrail.node.addDependency(guardrailVersion); - enforceCr.node.addDependency(versionCr); - - // Test Lambda + // Test Lambda that calls the Converse API WITHOUT a guardrailIdentifier const testFn = new lambda.Function(this, 'TestFunction', { - runtime: lambda.Runtime.NODEJS_22_X, + runtime: new lambda.Runtime('nodejs24.x', lambda.RuntimeFamily.NODEJS), handler: 'test.handler', code: lambda.Code.fromAsset(path.join(__dirname, '..', 'src')), memorySize: 256, @@ -106,6 +70,8 @@ export class BedrockGuardrailsCrossAccountStack extends cdk.Stack { }, }); + // Inference profiles route cross-region, so the foundation-model ARN keeps a + // wildcard region while the inference-profile ARN is scoped to this account/region. testFn.addToRolePolicy(new iam.PolicyStatement({ actions: ['bedrock:InvokeModel'], resources: [ @@ -114,7 +80,14 @@ export class BedrockGuardrailsCrossAccountStack extends cdk.Stack { ], })); - testFn.node.addDependency(enforceCr); + // With an account-level enforced guardrail active, the caller must be authorized + // to apply the guardrail even though no guardrailIdentifier is passed in the call. + testFn.addToRolePolicy(new iam.PolicyStatement({ + actions: ['bedrock:ApplyGuardrail'], + resources: [guardrail.attrGuardrailArn], + })); + + testFn.node.addDependency(enforcedGuardrail); // Outputs new cdk.CfnOutput(this, 'GuardrailId', { diff --git a/bedrock-guardrails-cross-account-cdk/package-lock.json b/bedrock-guardrails-enforcement-cdk/package-lock.json similarity index 100% rename from bedrock-guardrails-cross-account-cdk/package-lock.json rename to bedrock-guardrails-enforcement-cdk/package-lock.json diff --git a/bedrock-guardrails-cross-account-cdk/package.json b/bedrock-guardrails-enforcement-cdk/package.json similarity index 86% rename from bedrock-guardrails-cross-account-cdk/package.json rename to bedrock-guardrails-enforcement-cdk/package.json index 46bbf96467..28309cf51a 100644 --- a/bedrock-guardrails-cross-account-cdk/package.json +++ b/bedrock-guardrails-enforcement-cdk/package.json @@ -1,5 +1,5 @@ { - "name": "bedrock-guardrails-cross-account-cdk", + "name": "bedrock-guardrails-enforcement-cdk", "version": "1.0.0", "bin": { "app": "bin/app.ts" diff --git a/bedrock-guardrails-cross-account-cdk/src/test.mjs b/bedrock-guardrails-enforcement-cdk/src/test.mjs similarity index 78% rename from bedrock-guardrails-cross-account-cdk/src/test.mjs rename to bedrock-guardrails-enforcement-cdk/src/test.mjs index 0b16f1f5dc..5fdbefb189 100644 --- a/bedrock-guardrails-cross-account-cdk/src/test.mjs +++ b/bedrock-guardrails-enforcement-cdk/src/test.mjs @@ -10,11 +10,14 @@ async function invokeModel(prompt) { messages: [{ role: 'user', content: [{ text: prompt }] }], })); + // An enforced (account-level) guardrail intervenes in-band: the call succeeds + // but returns stopReason 'guardrail_intervened' rather than throwing. + const stopReason = response.stopReason; return { prompt, - stopReason: response.stopReason, + stopReason, output: response.output?.message?.content?.[0]?.text || 'No text response', - blocked: false, + blocked: stopReason === 'guardrail_intervened', }; } catch (error) { return { diff --git a/bedrock-guardrails-cross-account-cdk/tsconfig.json b/bedrock-guardrails-enforcement-cdk/tsconfig.json similarity index 100% rename from bedrock-guardrails-cross-account-cdk/tsconfig.json rename to bedrock-guardrails-enforcement-cdk/tsconfig.json From 78b6044f4e5d8229dd919d8bf5f3086ac95d2fab Mon Sep 17 00:00:00 2001 From: Nithin Chandran Rajashankar Date: Wed, 17 Jun 2026 09:53:06 +0000 Subject: [PATCH 5/7] feat(s3-annotations): Add S3 Annotations + Bedrock AI enrichment pattern Deploy an automated document enrichment pipeline that generates AI metadata (summary, keywords, content type) via Amazon Bedrock and stores it as queryable S3 annotations using the new PutObjectAnnotation API launched at AWS Summit NYC 2026. Architecture: S3 Object Created -> EventBridge -> Lambda -> Bedrock (Claude Sonnet 4) -> PutObjectAnnotation Tested with .txt, .csv, and .json files on account 742460038667. --- s3-lambda-bedrock-annotations-cdk/.gitignore | 8 ++ s3-lambda-bedrock-annotations-cdk/README.md | 130 ++++++++++++++++++ s3-lambda-bedrock-annotations-cdk/bin/app.ts | 7 + s3-lambda-bedrock-annotations-cdk/cdk.json | 3 + .../example-pattern.json | 88 ++++++++++++ .../s3-lambda-bedrock-annotations-stack.ts | 71 ++++++++++ .../package.json | 20 +++ .../src/annotator/index.py | 95 +++++++++++++ .../src/boto3-layer/build.sh | 7 + .../src/boto3-layer/requirements.txt | 1 + .../tsconfig.json | 17 +++ 11 files changed, 447 insertions(+) create mode 100644 s3-lambda-bedrock-annotations-cdk/.gitignore create mode 100644 s3-lambda-bedrock-annotations-cdk/README.md create mode 100644 s3-lambda-bedrock-annotations-cdk/bin/app.ts create mode 100644 s3-lambda-bedrock-annotations-cdk/cdk.json create mode 100644 s3-lambda-bedrock-annotations-cdk/example-pattern.json create mode 100644 s3-lambda-bedrock-annotations-cdk/lib/s3-lambda-bedrock-annotations-stack.ts create mode 100644 s3-lambda-bedrock-annotations-cdk/package.json create mode 100644 s3-lambda-bedrock-annotations-cdk/src/annotator/index.py create mode 100644 s3-lambda-bedrock-annotations-cdk/src/boto3-layer/build.sh create mode 100644 s3-lambda-bedrock-annotations-cdk/src/boto3-layer/requirements.txt create mode 100644 s3-lambda-bedrock-annotations-cdk/tsconfig.json diff --git a/s3-lambda-bedrock-annotations-cdk/.gitignore b/s3-lambda-bedrock-annotations-cdk/.gitignore new file mode 100644 index 0000000000..187e515792 --- /dev/null +++ b/s3-lambda-bedrock-annotations-cdk/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +build/ +cdk.out/ +src/boto3-layer/python/ +*.js +*.d.ts +!bin/app.ts +!lib/*.ts diff --git a/s3-lambda-bedrock-annotations-cdk/README.md b/s3-lambda-bedrock-annotations-cdk/README.md new file mode 100644 index 0000000000..1af6e1fa5b --- /dev/null +++ b/s3-lambda-bedrock-annotations-cdk/README.md @@ -0,0 +1,130 @@ +# Automated AI Document Annotation with Amazon S3 Annotations and Amazon Bedrock + +This pattern deploys an automated document enrichment pipeline that uses Amazon Bedrock to generate AI-powered metadata and stores it as queryable Amazon S3 annotations. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/s3-lambda-bedrock-annotations-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. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Requirements + +- [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. +- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +- [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +- [Node.js 20+](https://nodejs.org/en/download/) installed +- [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) installed (`npm install -g aws-cdk`) +- [Python 3.12](https://www.python.org/downloads/) installed (for building the boto3 layer) +- [Amazon Bedrock model access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html) enabled for Anthropic Claude Sonnet in your account + +## Architecture + +![Architecture](architecture.png) + +1. A file is uploaded to the Amazon S3 bucket. +2. Amazon S3 sends an Object Created event to Amazon EventBridge. +3. An Amazon EventBridge rule triggers the AWS Lambda function. +4. The Lambda function reads the object, invokes Amazon Bedrock (Claude Sonnet) to generate a structured summary, keywords, and content classification. +5. The AI-generated metadata is written back as an S3 annotation using the `PutObjectAnnotation` API. + +## Deployment + +1. Clone the repository and navigate to the pattern directory: + +```bash +git clone https://github.com/aws-samples/serverless-patterns +cd serverless-patterns/s3-lambda-bedrock-annotations-cdk +``` + +2. Build the boto3 layer (required for S3 Annotations API support): + +```bash +cd src/boto3-layer +pip install -r requirements.txt -t python +cd ../.. +``` + +3. Install CDK dependencies and deploy: + +```bash +npm install +cdk bootstrap # if not already done +cdk deploy +``` + +## Testing + +1. Upload a text file to the bucket: + +```bash +BUCKET_NAME=$(aws cloudformation describe-stacks --stack-name S3LambdaBedrockAnnotationsStack --query 'Stacks[0].Outputs[?OutputKey==`BucketName`].OutputValue' --output text) + +echo "Amazon S3 Annotations is a new feature that allows you to attach rich, queryable metadata directly to S3 objects. Each object supports up to 1000 annotations of up to 1MB each." > sample.txt + +aws s3 cp sample.txt s3://$BUCKET_NAME/sample.txt +``` + +2. Wait ~10 seconds for the Lambda to process, then retrieve the annotation: + +```bash +aws s3api get-object-annotation \ + --bucket $BUCKET_NAME \ + --key sample.txt \ + --annotation-name ai-enrichment \ + annotation-output.json + +cat annotation-output.json +``` + +Expected output (example): + +```json +{ + "ai_summary": "A brief description of Amazon S3 Annotations, a feature for attaching rich queryable metadata to S3 objects with support for up to 1000 annotations per object.", + "keywords": ["S3", "annotations", "metadata", "queryable", "objects"], + "content_type": "article", + "model": "anthropic.claude-sonnet-4-20250514" +} +``` + +3. List all annotations on the object: + +```bash +aws s3api list-object-annotations \ + --bucket $BUCKET_NAME \ + --key sample.txt +``` + +## Optional: Enable Annotation Tables for Athena Querying + +To query annotations at scale across all objects using Amazon Athena, enable annotation tables: + +```bash +aws s3api update-bucket-metadata-annotation-table-configuration \ + --bucket $BUCKET_NAME \ + --annotation-table-configuration '{"ConfigurationState":"ENABLED","Role":"arn:aws:iam::YOUR_ACCOUNT_ID:role/S3MetadataAnnotationRole"}' +``` + +> **Note**: Annotation table configuration is not yet available via AWS CloudFormation. Use the CLI or SDK to enable it. + +## Cleanup + +> **Warning**: This will delete the S3 bucket and all objects, including annotations. + +```bash +cdk destroy +``` + +## Useful Commands + +| Command | Description | +|---------|-------------| +| `npm install` | Install project dependencies | +| `cdk synth` | Emit the synthesized CloudFormation template | +| `cdk deploy` | Deploy the stack | +| `cdk destroy` | Remove the stack | + +--- + +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/s3-lambda-bedrock-annotations-cdk/bin/app.ts b/s3-lambda-bedrock-annotations-cdk/bin/app.ts new file mode 100644 index 0000000000..c525fbd551 --- /dev/null +++ b/s3-lambda-bedrock-annotations-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 { S3LambdaBedrockAnnotationsStack } from '../lib/s3-lambda-bedrock-annotations-stack'; + +const app = new cdk.App(); +new S3LambdaBedrockAnnotationsStack(app, 'S3LambdaBedrockAnnotationsStack'); diff --git a/s3-lambda-bedrock-annotations-cdk/cdk.json b/s3-lambda-bedrock-annotations-cdk/cdk.json new file mode 100644 index 0000000000..a6700a2ff4 --- /dev/null +++ b/s3-lambda-bedrock-annotations-cdk/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/app.ts" +} diff --git a/s3-lambda-bedrock-annotations-cdk/example-pattern.json b/s3-lambda-bedrock-annotations-cdk/example-pattern.json new file mode 100644 index 0000000000..8cd7e25665 --- /dev/null +++ b/s3-lambda-bedrock-annotations-cdk/example-pattern.json @@ -0,0 +1,88 @@ +{ + "title": "Automated AI document annotation with Amazon S3 Annotations and Amazon Bedrock", + "description": "Automatically enrich S3 objects with AI-generated metadata annotations using Amazon Bedrock. When a file is uploaded, a Lambda function generates a summary, keywords, and content classification, then stores them as a queryable S3 annotation.", + "language": "TypeScript", + "level": "200", + "framework": "AWS CDK", + "introBox": { + "headline": "How it works", + "text": [ + "When an object is uploaded to the S3 bucket, Amazon EventBridge triggers an AWS Lambda function.", + "The Lambda function reads the object content and sends it to Amazon Bedrock (Claude Sonnet) to generate a structured summary, keywords, and content type classification.", + "The AI-generated metadata is written back to the object as an S3 annotation using the PutObjectAnnotation API.", + "Annotations are mutable, queryable via Amazon Athena (when annotation tables are enabled), and move automatically with the object during copy and replication." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/s3-lambda-bedrock-annotations-cdk", + "templateURL": "serverless-patterns/s3-lambda-bedrock-annotations-cdk", + "projectFolder": "s3-lambda-bedrock-annotations-cdk", + "templateFile": "lib/s3-lambda-bedrock-annotations-stack.ts" + } + }, + "resources": { + "bullets": [ + { + "text": "Amazon S3 Annotations documentation", + "link": "https://docs.aws.amazon.com/AmazonS3/latest/userguide/annotations.html" + }, + { + "text": "Amazon S3 Annotations launch blog", + "link": "https://aws.amazon.com/blogs/aws/amazon-s3-annotations-attach-rich-queryable-context-directly-to-your-objects/" + }, + { + "text": "Amazon Bedrock documentation", + "link": "https://docs.aws.amazon.com/bedrock/latest/userguide/what-is-bedrock.html" + } + ] + }, + "deploy": { + "text": [ + "cdk deploy" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "cdk destroy" + ] + }, + "authors": [ + { + "name": "Nithin Chandran R", + "bio": "Technical Account Manager at AWS, focused on SAP workloads and serverless architecture.", + "linkedin": "nithin-chandran-r" + } + ], + "patternArch": { + "icon1": { + "x": 20, + "y": 50, + "service": "s3", + "label": "Amazon S3" + }, + "icon2": { + "x": 50, + "y": 50, + "service": "eventbridge", + "label": "Amazon EventBridge" + }, + "icon3": { + "x": 80, + "y": 50, + "service": "lambda", + "label": "AWS Lambda" + }, + "icon4": { + "x": 110, + "y": 50, + "service": "bedrock", + "label": "Amazon Bedrock" + } + } +} diff --git a/s3-lambda-bedrock-annotations-cdk/lib/s3-lambda-bedrock-annotations-stack.ts b/s3-lambda-bedrock-annotations-cdk/lib/s3-lambda-bedrock-annotations-stack.ts new file mode 100644 index 0000000000..a488cf42bc --- /dev/null +++ b/s3-lambda-bedrock-annotations-cdk/lib/s3-lambda-bedrock-annotations-stack.ts @@ -0,0 +1,71 @@ +import * as cdk from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import * as s3 from 'aws-cdk-lib/aws-s3'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as events from 'aws-cdk-lib/aws-events'; +import * as targets from 'aws-cdk-lib/aws-events-targets'; +import * as path from 'path'; + +export class S3LambdaBedrockAnnotationsStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // S3 bucket with EventBridge notifications enabled + const bucket = new s3.Bucket(this, 'AnnotationsBucket', { + eventBridgeEnabled: true, + removalPolicy: cdk.RemovalPolicy.DESTROY, + autoDeleteObjects: true, + }); + + // Lambda layer with latest boto3 (required for put_object_annotation) + const boto3Layer = new lambda.LayerVersion(this, 'Boto3Layer', { + code: lambda.Code.fromAsset(path.join(__dirname, '..', 'src', 'boto3-layer')), + compatibleRuntimes: [lambda.Runtime.PYTHON_3_12], + description: 'boto3 >= 1.43.31 with S3 Annotations support', + }); + + // Lambda function + const annotator = new lambda.Function(this, 'AnnotatorFunction', { + runtime: lambda.Runtime.PYTHON_3_12, + handler: 'index.handler', + code: lambda.Code.fromAsset(path.join(__dirname, '..', 'src', 'annotator')), + timeout: cdk.Duration.seconds(60), + memorySize: 256, + layers: [boto3Layer], + environment: { + BUCKET_NAME: bucket.bucketName, + }, + }); + + // IAM permissions: read S3 objects, write annotations, invoke Bedrock + bucket.grantRead(annotator); + annotator.addToRolePolicy(new iam.PolicyStatement({ + actions: ['s3:PutObjectAnnotation'], + resources: [bucket.arnForObjects('*')], + })); + annotator.addToRolePolicy(new iam.PolicyStatement({ + actions: ['bedrock:InvokeModel'], + resources: [ + 'arn:aws:bedrock:*::foundation-model/anthropic.claude-sonnet-4-20250514-v1:0', + `arn:aws:bedrock:*:${this.account}:inference-profile/us.anthropic.claude-sonnet-4-20250514-v1:0`, + ], + })); + + // EventBridge rule: trigger on S3 Object Created + const rule = new events.Rule(this, 'S3ObjectCreatedRule', { + eventPattern: { + source: ['aws.s3'], + detailType: ['Object Created'], + detail: { + bucket: { name: [bucket.bucketName] }, + }, + }, + }); + rule.addTarget(new targets.LambdaFunction(annotator)); + + // Outputs + new cdk.CfnOutput(this, 'BucketName', { value: bucket.bucketName }); + new cdk.CfnOutput(this, 'FunctionName', { value: annotator.functionName }); + } +} diff --git a/s3-lambda-bedrock-annotations-cdk/package.json b/s3-lambda-bedrock-annotations-cdk/package.json new file mode 100644 index 0000000000..956a99aa8c --- /dev/null +++ b/s3-lambda-bedrock-annotations-cdk/package.json @@ -0,0 +1,20 @@ +{ + "name": "s3-lambda-bedrock-annotations-cdk", + "version": "1.0.0", + "bin": { + "app": "bin/app.js" + }, + "scripts": { + "build": "tsc", + "synth": "cdk synth" + }, + "dependencies": { + "aws-cdk-lib": "^2.150.0", + "constructs": "^10.3.0" + }, + "devDependencies": { + "@types/node": "^25.9.3", + "aws-cdk": "^2.150.0", + "typescript": "~5.4.0" + } +} diff --git a/s3-lambda-bedrock-annotations-cdk/src/annotator/index.py b/s3-lambda-bedrock-annotations-cdk/src/annotator/index.py new file mode 100644 index 0000000000..bceabcbd16 --- /dev/null +++ b/s3-lambda-bedrock-annotations-cdk/src/annotator/index.py @@ -0,0 +1,95 @@ +""" +S3 Annotations enrichment handler. +Triggered by S3 Object Created events via EventBridge. +Reads the object, generates AI summary via Bedrock, writes annotation. +""" +import json +import boto3 +import os + +s3 = boto3.client('s3') +bedrock = boto3.client('bedrock-runtime') + +MODEL_ID = 'us.anthropic.claude-sonnet-4-20250514-v1:0' +MAX_CONTENT_BYTES = 10_000 # Limit content sent to Bedrock + + +def handler(event, context): + detail = event['detail'] + bucket = detail['bucket']['name'] + key = detail['object']['key'] + size = detail['object'].get('size', 0) + + # Skip non-text files and very large files + if size > 5_000_000 or not _is_supported(key): + print(f'Skipping {key} (size={size}, unsupported type)') + return {'status': 'skipped', 'key': key} + + # Read object content + response = s3.get_object(Bucket=bucket, Key=key) + content = response['Body'].read(MAX_CONTENT_BYTES).decode('utf-8', errors='replace') + + # Generate summary via Bedrock + summary = _generate_summary(key, content) + + # Write annotation + annotation_payload = json.dumps({ + 'ai_summary': summary['summary'], + 'keywords': summary['keywords'], + 'content_type': summary['content_type'], + 'model': MODEL_ID, + }) + + s3.put_object_annotation( + Bucket=bucket, + Key=key, + AnnotationName='ai-enrichment', + AnnotationPayload=annotation_payload.encode('utf-8'), + ) + + print(f'Annotated {key} with {len(annotation_payload)} bytes') + return {'status': 'annotated', 'key': key, 'annotation_size': len(annotation_payload)} + + +def _generate_summary(key, content): + prompt = f"""Analyze this document and return a JSON object with: +- "summary": a 2-3 sentence summary +- "keywords": up to 5 relevant keywords as an array +- "content_type": the type of content (e.g. "report", "code", "article", "data", "log") + +Filename: {key} +Content (first {MAX_CONTENT_BYTES} bytes): +{content} + +Respond ONLY with valid JSON.""" + + response = bedrock.invoke_model( + modelId=MODEL_ID, + contentType='application/json', + accept='application/json', + body=json.dumps({ + 'anthropic_version': 'bedrock-2023-05-31', + 'max_tokens': 300, + 'messages': [{'role': 'user', 'content': prompt}], + }), + ) + + result = json.loads(response['body'].read()) + text = result['content'][0]['text'] + + # Parse JSON from response + try: + return json.loads(text) + except json.JSONDecodeError: + # Fallback if model wraps in markdown + import re + match = re.search(r'\{.*\}', text, re.DOTALL) + if match: + return json.loads(match.group()) + return {'summary': text[:200], 'keywords': [], 'content_type': 'unknown'} + + +def _is_supported(key): + supported = ('.txt', '.md', '.json', '.csv', '.xml', '.yaml', '.yml', + '.py', '.js', '.ts', '.java', '.html', '.log') + return any(key.lower().endswith(ext) for ext in supported) diff --git a/s3-lambda-bedrock-annotations-cdk/src/boto3-layer/build.sh b/s3-lambda-bedrock-annotations-cdk/src/boto3-layer/build.sh new file mode 100644 index 0000000000..5e78eed57c --- /dev/null +++ b/s3-lambda-bedrock-annotations-cdk/src/boto3-layer/build.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Build boto3 layer with S3 Annotations support +set -e +cd "$(dirname "$0")" +rm -rf python +pip install -r requirements.txt -t python --quiet +echo "Layer built: $(python -c 'import importlib.metadata; print(importlib.metadata.version("boto3"))' 2>/dev/null || pip show boto3 2>/dev/null | grep Version)" diff --git a/s3-lambda-bedrock-annotations-cdk/src/boto3-layer/requirements.txt b/s3-lambda-bedrock-annotations-cdk/src/boto3-layer/requirements.txt new file mode 100644 index 0000000000..a1f88eb8b3 --- /dev/null +++ b/s3-lambda-bedrock-annotations-cdk/src/boto3-layer/requirements.txt @@ -0,0 +1 @@ +boto3>=1.43.31 diff --git a/s3-lambda-bedrock-annotations-cdk/tsconfig.json b/s3-lambda-bedrock-annotations-cdk/tsconfig.json new file mode 100644 index 0000000000..30115fbcc8 --- /dev/null +++ b/s3-lambda-bedrock-annotations-cdk/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["es2020"], + "types": ["node"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "outDir": "./build", + "rootDir": "." + }, + "exclude": ["node_modules", "build"] +} From f4643ec4fdac961041cdaf5f271949ace3ca9c4f Mon Sep 17 00:00:00 2001 From: Nithin Chandran Rajashankar Date: Wed, 17 Jun 2026 10:22:12 +0000 Subject: [PATCH 6/7] fix: Use full AWS service names throughout pattern - Amazon S3 (not S3), Amazon Bedrock (not Bedrock), AWS Lambda (not Lambda) - Fix expected output model ID to match actual inference profile - Apply to README, example-pattern.json, CDK stack comments, handler docstring --- s3-lambda-bedrock-annotations-cdk/README.md | 4 ++-- s3-lambda-bedrock-annotations-cdk/example-pattern.json | 8 ++++---- .../lib/s3-lambda-bedrock-annotations-stack.ts | 8 ++++---- s3-lambda-bedrock-annotations-cdk/src/annotator/index.py | 6 +++--- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/s3-lambda-bedrock-annotations-cdk/README.md b/s3-lambda-bedrock-annotations-cdk/README.md index 1af6e1fa5b..5adf7b0f59 100644 --- a/s3-lambda-bedrock-annotations-cdk/README.md +++ b/s3-lambda-bedrock-annotations-cdk/README.md @@ -24,7 +24,7 @@ Important: this application uses various AWS services and there are costs associ 2. Amazon S3 sends an Object Created event to Amazon EventBridge. 3. An Amazon EventBridge rule triggers the AWS Lambda function. 4. The Lambda function reads the object, invokes Amazon Bedrock (Claude Sonnet) to generate a structured summary, keywords, and content classification. -5. The AI-generated metadata is written back as an S3 annotation using the `PutObjectAnnotation` API. +5. The AI-generated metadata is written back as an Amazon S3 annotation using the `PutObjectAnnotation` API. ## Deployment @@ -82,7 +82,7 @@ Expected output (example): "ai_summary": "A brief description of Amazon S3 Annotations, a feature for attaching rich queryable metadata to S3 objects with support for up to 1000 annotations per object.", "keywords": ["S3", "annotations", "metadata", "queryable", "objects"], "content_type": "article", - "model": "anthropic.claude-sonnet-4-20250514" + "model": "us.anthropic.claude-sonnet-4-20250514-v1:0" } ``` diff --git a/s3-lambda-bedrock-annotations-cdk/example-pattern.json b/s3-lambda-bedrock-annotations-cdk/example-pattern.json index 8cd7e25665..dbe3de0c7e 100644 --- a/s3-lambda-bedrock-annotations-cdk/example-pattern.json +++ b/s3-lambda-bedrock-annotations-cdk/example-pattern.json @@ -1,15 +1,15 @@ { "title": "Automated AI document annotation with Amazon S3 Annotations and Amazon Bedrock", - "description": "Automatically enrich S3 objects with AI-generated metadata annotations using Amazon Bedrock. When a file is uploaded, a Lambda function generates a summary, keywords, and content classification, then stores them as a queryable S3 annotation.", + "description": "Automatically enrich Amazon S3 objects with AI-generated metadata annotations using Amazon Bedrock. When a file is uploaded, an AWS Lambda function generates a summary, keywords, and content classification, then stores them as a queryable Amazon S3 annotation.", "language": "TypeScript", "level": "200", "framework": "AWS CDK", "introBox": { "headline": "How it works", "text": [ - "When an object is uploaded to the S3 bucket, Amazon EventBridge triggers an AWS Lambda function.", - "The Lambda function reads the object content and sends it to Amazon Bedrock (Claude Sonnet) to generate a structured summary, keywords, and content type classification.", - "The AI-generated metadata is written back to the object as an S3 annotation using the PutObjectAnnotation API.", + "When an object is uploaded to the Amazon S3 bucket, Amazon EventBridge triggers an AWS Lambda function.", + "The AWS Lambda function reads the object content and sends it to Amazon Bedrock (Claude Sonnet) to generate a structured summary, keywords, and content type classification.", + "The AI-generated metadata is written back to the object as an Amazon S3 annotation using the PutObjectAnnotation API.", "Annotations are mutable, queryable via Amazon Athena (when annotation tables are enabled), and move automatically with the object during copy and replication." ] }, diff --git a/s3-lambda-bedrock-annotations-cdk/lib/s3-lambda-bedrock-annotations-stack.ts b/s3-lambda-bedrock-annotations-cdk/lib/s3-lambda-bedrock-annotations-stack.ts index a488cf42bc..0334359b2c 100644 --- a/s3-lambda-bedrock-annotations-cdk/lib/s3-lambda-bedrock-annotations-stack.ts +++ b/s3-lambda-bedrock-annotations-cdk/lib/s3-lambda-bedrock-annotations-stack.ts @@ -11,7 +11,7 @@ export class S3LambdaBedrockAnnotationsStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); - // S3 bucket with EventBridge notifications enabled + // Amazon S3 bucket with Amazon EventBridge notifications enabled const bucket = new s3.Bucket(this, 'AnnotationsBucket', { eventBridgeEnabled: true, removalPolicy: cdk.RemovalPolicy.DESTROY, @@ -22,7 +22,7 @@ export class S3LambdaBedrockAnnotationsStack extends cdk.Stack { const boto3Layer = new lambda.LayerVersion(this, 'Boto3Layer', { code: lambda.Code.fromAsset(path.join(__dirname, '..', 'src', 'boto3-layer')), compatibleRuntimes: [lambda.Runtime.PYTHON_3_12], - description: 'boto3 >= 1.43.31 with S3 Annotations support', + description: 'boto3 >= 1.43.31 with Amazon S3 Annotations support', }); // Lambda function @@ -38,7 +38,7 @@ export class S3LambdaBedrockAnnotationsStack extends cdk.Stack { }, }); - // IAM permissions: read S3 objects, write annotations, invoke Bedrock + // IAM permissions: read Amazon S3 objects, write annotations, invoke Amazon Bedrock bucket.grantRead(annotator); annotator.addToRolePolicy(new iam.PolicyStatement({ actions: ['s3:PutObjectAnnotation'], @@ -52,7 +52,7 @@ export class S3LambdaBedrockAnnotationsStack extends cdk.Stack { ], })); - // EventBridge rule: trigger on S3 Object Created + // Amazon EventBridge rule: trigger on Amazon S3 Object Created const rule = new events.Rule(this, 'S3ObjectCreatedRule', { eventPattern: { source: ['aws.s3'], diff --git a/s3-lambda-bedrock-annotations-cdk/src/annotator/index.py b/s3-lambda-bedrock-annotations-cdk/src/annotator/index.py index bceabcbd16..61688d34bf 100644 --- a/s3-lambda-bedrock-annotations-cdk/src/annotator/index.py +++ b/s3-lambda-bedrock-annotations-cdk/src/annotator/index.py @@ -1,7 +1,7 @@ """ -S3 Annotations enrichment handler. -Triggered by S3 Object Created events via EventBridge. -Reads the object, generates AI summary via Bedrock, writes annotation. +Amazon S3 Annotations enrichment handler. +Triggered by Amazon S3 Object Created events via Amazon EventBridge. +Reads the object, generates AI summary via Amazon Bedrock, writes annotation. """ import json import boto3 From b74ddd4893b069584d0d9f31a0ae7f015b8724cf Mon Sep 17 00:00:00 2001 From: Nithin Chandran Rajashankar Date: Wed, 17 Jun 2026 10:23:51 +0000 Subject: [PATCH 7/7] fix: Apply PR checklist learnings from prior reviews - Add try/except error handling on SDK calls in Lambda handler - Wrap deploy/cleanup commands in tags in example-pattern.json - Add cdk.context.json to .gitignore - Remove broken architecture.png reference (file does not exist) --- s3-lambda-bedrock-annotations-cdk/.gitignore | 1 + s3-lambda-bedrock-annotations-cdk/README.md | 2 - .../example-pattern.json | 4 +- .../src/annotator/index.py | 53 ++++++++++--------- 4 files changed, 32 insertions(+), 28 deletions(-) diff --git a/s3-lambda-bedrock-annotations-cdk/.gitignore b/s3-lambda-bedrock-annotations-cdk/.gitignore index 187e515792..2d4387fd6b 100644 --- a/s3-lambda-bedrock-annotations-cdk/.gitignore +++ b/s3-lambda-bedrock-annotations-cdk/.gitignore @@ -1,6 +1,7 @@ node_modules/ build/ cdk.out/ +cdk.context.json src/boto3-layer/python/ *.js *.d.ts diff --git a/s3-lambda-bedrock-annotations-cdk/README.md b/s3-lambda-bedrock-annotations-cdk/README.md index 5adf7b0f59..161f590664 100644 --- a/s3-lambda-bedrock-annotations-cdk/README.md +++ b/s3-lambda-bedrock-annotations-cdk/README.md @@ -18,8 +18,6 @@ Important: this application uses various AWS services and there are costs associ ## Architecture -![Architecture](architecture.png) - 1. A file is uploaded to the Amazon S3 bucket. 2. Amazon S3 sends an Object Created event to Amazon EventBridge. 3. An Amazon EventBridge rule triggers the AWS Lambda function. diff --git a/s3-lambda-bedrock-annotations-cdk/example-pattern.json b/s3-lambda-bedrock-annotations-cdk/example-pattern.json index dbe3de0c7e..0157f74863 100644 --- a/s3-lambda-bedrock-annotations-cdk/example-pattern.json +++ b/s3-lambda-bedrock-annotations-cdk/example-pattern.json @@ -39,7 +39,7 @@ }, "deploy": { "text": [ - "cdk deploy" + "cdk deploy" ] }, "testing": { @@ -49,7 +49,7 @@ }, "cleanup": { "text": [ - "cdk destroy" + "cdk destroy" ] }, "authors": [ diff --git a/s3-lambda-bedrock-annotations-cdk/src/annotator/index.py b/s3-lambda-bedrock-annotations-cdk/src/annotator/index.py index 61688d34bf..e704f1ac94 100644 --- a/s3-lambda-bedrock-annotations-cdk/src/annotator/index.py +++ b/s3-lambda-bedrock-annotations-cdk/src/annotator/index.py @@ -25,30 +25,35 @@ def handler(event, context): print(f'Skipping {key} (size={size}, unsupported type)') return {'status': 'skipped', 'key': key} - # Read object content - response = s3.get_object(Bucket=bucket, Key=key) - content = response['Body'].read(MAX_CONTENT_BYTES).decode('utf-8', errors='replace') - - # Generate summary via Bedrock - summary = _generate_summary(key, content) - - # Write annotation - annotation_payload = json.dumps({ - 'ai_summary': summary['summary'], - 'keywords': summary['keywords'], - 'content_type': summary['content_type'], - 'model': MODEL_ID, - }) - - s3.put_object_annotation( - Bucket=bucket, - Key=key, - AnnotationName='ai-enrichment', - AnnotationPayload=annotation_payload.encode('utf-8'), - ) - - print(f'Annotated {key} with {len(annotation_payload)} bytes') - return {'status': 'annotated', 'key': key, 'annotation_size': len(annotation_payload)} + try: + # Read object content + response = s3.get_object(Bucket=bucket, Key=key) + content = response['Body'].read(MAX_CONTENT_BYTES).decode('utf-8', errors='replace') + + # Generate summary via Amazon Bedrock + summary = _generate_summary(key, content) + + # Write annotation + annotation_payload = json.dumps({ + 'ai_summary': summary['summary'], + 'keywords': summary['keywords'], + 'content_type': summary['content_type'], + 'model': MODEL_ID, + }) + + s3.put_object_annotation( + Bucket=bucket, + Key=key, + AnnotationName='ai-enrichment', + AnnotationPayload=annotation_payload.encode('utf-8'), + ) + + print(f'Annotated {key} with {len(annotation_payload)} bytes') + return {'status': 'annotated', 'key': key, 'annotation_size': len(annotation_payload)} + + except Exception as e: + print(f'Error processing {key}: {e}') + raise def _generate_summary(key, content):