diff --git a/lambda-durable-bedrock-cdk/.gitignore b/lambda-durable-bedrock-cdk/.gitignore new file mode 100644 index 000000000..2303e9b95 --- /dev/null +++ b/lambda-durable-bedrock-cdk/.gitignore @@ -0,0 +1,6 @@ +node_modules +cdk.out +*.js +!src/**/*.js +*.d.ts +package-lock.json diff --git a/lambda-durable-bedrock-cdk/README.md b/lambda-durable-bedrock-cdk/README.md new file mode 100644 index 000000000..5f49c8726 --- /dev/null +++ b/lambda-durable-bedrock-cdk/README.md @@ -0,0 +1,114 @@ +# AWS Lambda durable functions with Amazon Bedrock + +This pattern deploys a Lambda durable function that orchestrates a multi-step AI content pipeline using Amazon Bedrock. Each step is automatically checkpointed, so the workflow resumes from the last completed step after any interruption — without re-invoking Bedrock. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/lambda-durable-bedrock-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 + +``` +┌─────────────┐ ┌──────────────────────────────────────────────────┐ +│ Invoke │────▶│ Lambda Durable Function │ +│ (CLI/SDK) │ │ │ +└─────────────┘ │ Step 1: Generate Outline ──▶ Bedrock (Claude) │ + │ ✓ checkpoint │ + │ Wait: 5s (simulate review) │ + │ ✓ checkpoint │ + │ Step 2: Expand Draft ──▶ Bedrock (Claude) │ + │ ✓ checkpoint │ + │ Step 3: Summarize ──▶ Bedrock (Claude) │ + │ ✓ checkpoint │ + └──────────────────────────────────────────────────┘ +``` + +## How it works + +1. The Lambda durable function receives a topic as input. +2. **Step 1** calls Amazon Bedrock (Claude Sonnet) to generate a blog outline from the topic. The result is checkpointed. +3. The function **waits** 5 seconds (simulating an editorial review pause). During the wait, no compute charges are incurred. +4. **Step 2** calls Bedrock to expand the outline into a full blog draft. Checkpointed. +5. **Step 3** calls Bedrock to generate a concise summary. Checkpointed. +6. If the function is interrupted at any point, it replays from the last checkpoint — completed Bedrock calls are not re-executed. + +## Deployment Instructions + +1. Clone the repository: + ```bash + git clone https://github.com/aws-samples/serverless-patterns + cd serverless-patterns/lambda-durable-bedrock-cdk + ``` + +2. Install dependencies: + ```bash + npm install + ``` + +3. Deploy the stack: + ```bash + cdk deploy + ``` + +4. Note the outputs printed after deployment. You will need `FunctionName` and `FunctionVersion` for testing. + + Example output: + ``` + Outputs: + LambdaDurableBedrockStack.FunctionName = LambdaDurableBedrockStack-DurableBedrockFn3CE0D50D-AbCdEfGh + LambdaDurableBedrockStack.FunctionVersion = 1 + ``` + +## Testing + +1. Invoke the durable function. Replace `` with the `FunctionName` value from the deploy output, and `` with the `FunctionVersion` value: + ```bash + aws lambda invoke \ + --function-name \ + --qualifier \ + --payload '{"topic": "Serverless AI workflows with Lambda durable functions"}' \ + --cli-binary-format raw-in-base64-out \ + output.json + ``` + + For example, if your deploy output showed `FunctionName = MyStack-DurableBedrockFn-AbCdEfGh` and `FunctionVersion = 1`: + ```bash + aws lambda invoke \ + --function-name MyStack-DurableBedrockFn-AbCdEfGh \ + --qualifier 1 \ + --payload '{"topic": "Serverless AI workflows with Lambda durable functions"}' \ + --cli-binary-format raw-in-base64-out \ + output.json + ``` + +2. The function includes a durable wait, so the initial invocation returns quickly with a `"PENDING"` status. Check the durable execution status using the same `` and ``: + ```bash + aws lambda list-durable-executions \ + --function-name \ + --qualifier + ``` + +3. Once the execution status shows `"SUCCEEDED"`, view the result: + ```bash + cat output.json | jq . + ``` + +## Cleanup + +```bash +cdk destroy +``` + +---- +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/lambda-durable-bedrock-cdk/bin/app.ts b/lambda-durable-bedrock-cdk/bin/app.ts new file mode 100644 index 000000000..e35ff8486 --- /dev/null +++ b/lambda-durable-bedrock-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 { LambdaDurableBedrockStack } from "../lib/lambda-durable-bedrock-stack"; + +const app = new cdk.App(); +new LambdaDurableBedrockStack(app, "LambdaDurableBedrockStack"); diff --git a/lambda-durable-bedrock-cdk/cdk.json b/lambda-durable-bedrock-cdk/cdk.json new file mode 100644 index 000000000..822400f59 --- /dev/null +++ b/lambda-durable-bedrock-cdk/cdk.json @@ -0,0 +1,11 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/app.ts", + "watch": { + "include": ["**"], + "exclude": ["README.md", "cdk*.json", "**/*.d.ts", "**/*.js", "node_modules", "src"] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true + } +} diff --git a/lambda-durable-bedrock-cdk/example-pattern.json b/lambda-durable-bedrock-cdk/example-pattern.json new file mode 100644 index 000000000..10a6080b6 --- /dev/null +++ b/lambda-durable-bedrock-cdk/example-pattern.json @@ -0,0 +1,61 @@ +{ + "title": "AWS Lambda durable functions with Amazon Bedrock", + "description": "AWS Lambda durable functions with Amazon Bedrock for a multi-step AI pipeline with automatic checkpointing and failure recovery.", + "language": "TypeScript", + "level": "300", + "framework": "AWS CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern deploys a Lambda durable function that orchestrates a multi-step AI content pipeline using Amazon Bedrock. The function uses the Durable Execution SDK to automatically checkpoint progress at each step.", + "The workflow: (1) generates a blog outline from a topic using Claude, (2) waits 5 seconds to simulate editorial review, (3) expands the outline into a full draft, (4) generates a summary of the draft. Each step is checkpointed, so if the function is interrupted, it resumes from the last completed step without re-invoking Bedrock.", + "This pattern demonstrates how durable functions eliminate the need for Step Functions in tightly-coupled AI workflows while providing built-in resilience." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/lambda-durable-bedrock-cdk", + "templateURL": "serverless-patterns/lambda-durable-bedrock-cdk", + "projectFolder": "lambda-durable-bedrock-cdk", + "templateFile": "lib/lambda-durable-bedrock-stack.ts" + } + }, + "resources": { + "bullets": [ + { + "text": "Lambda Durable Functions Documentation", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" + }, + { + "text": "Build multi-step applications and AI workflows with Lambda durable functions", + "link": "https://aws.amazon.com/blogs/aws/build-multi-step-applications-and-ai-workflows-with-aws-lambda-durable-functions/" + }, + { + "text": "Amazon Bedrock", + "link": "https://aws.amazon.com/bedrock/" + } + ] + }, + "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" + } + ] +} \ No newline at end of file diff --git a/lambda-durable-bedrock-cdk/lib/lambda-durable-bedrock-stack.ts b/lambda-durable-bedrock-cdk/lib/lambda-durable-bedrock-stack.ts new file mode 100644 index 000000000..bd32d93a1 --- /dev/null +++ b/lambda-durable-bedrock-cdk/lib/lambda-durable-bedrock-stack.ts @@ -0,0 +1,70 @@ +import * as cdk from "aws-cdk-lib"; +import * as lambda from "aws-cdk-lib/aws-lambda"; +import * as iam from "aws-cdk-lib/aws-iam"; +import { Construct } from "constructs"; + +export class LambdaDurableBedrockStack 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 (inference profile) to use", + }); + + // Lambda function with durable execution enabled + const fn = new lambda.Function(this, "DurableBedrockFn", { + runtime: lambda.Runtime.NODEJS_20_X, + handler: "index.handler", + code: lambda.Code.fromAsset("src"), + timeout: cdk.Duration.minutes(15), + memorySize: 256, + environment: { + MODEL_ID: modelId.valueAsString, + }, + }); + + // Enable durable execution via CfnFunction escape hatch + const cfnFn = fn.node.defaultChild as lambda.CfnFunction; + cfnFn.addOverride("Properties.Runtime", "nodejs24.x"); + cfnFn.addOverride("Properties.DurableConfig", { + ExecutionTimeout: 900, // 15 minutes max durable execution + RetentionPeriodInDays: 14, + }); + + // Bedrock InvokeModel — scoped to the specific inference profile and its + // underlying foundation model rather than a wildcard resource. + fn.addToRolePolicy( + new iam.PolicyStatement({ + actions: ["bedrock:InvokeModel"], + resources: [ + `arn:aws:bedrock:${this.region}:${this.account}:inference-profile/${modelId.valueAsString}`, + "arn:aws:bedrock:*::foundation-model/*", + ], + }) + ); + + // Durable execution + CloudWatch Logs permissions via AWS managed policy + // https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AWSLambdaBasicDurableExecutionRolePolicy.html + fn.role!.addManagedPolicy( + iam.ManagedPolicy.fromAwsManagedPolicyName( + "service-role/AWSLambdaBasicDurableExecutionRolePolicy" + ) + ); + + // Publish a version via L1 — fn.currentVersion doesn't recognise the + // DurableConfig escape-hatch property on CDK 2.180. + const cfnVersion = new lambda.CfnVersion(this, "DurableBedrockFnVersion", { + functionName: fn.functionName, + description: "Durable execution version", + }); + + new cdk.CfnOutput(this, "FunctionName", { value: fn.functionName }); + new cdk.CfnOutput(this, "FunctionArn", { value: fn.functionArn }); + new cdk.CfnOutput(this, "FunctionVersion", { + value: cfnVersion.attrVersion, + description: "Published version number — use as --qualifier value", + }); + } +} diff --git a/lambda-durable-bedrock-cdk/package.json b/lambda-durable-bedrock-cdk/package.json new file mode 100644 index 000000000..2b3e56193 --- /dev/null +++ b/lambda-durable-bedrock-cdk/package.json @@ -0,0 +1,20 @@ +{ + "name": "lambda-durable-bedrock-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": { + "@types/node": "^22.0.0", + "ts-node": "^10.9.0", + "typescript": "~5.7.0" + } +} diff --git a/lambda-durable-bedrock-cdk/src/index.js b/lambda-durable-bedrock-cdk/src/index.js new file mode 100644 index 000000000..fe8993a11 --- /dev/null +++ b/lambda-durable-bedrock-cdk/src/index.js @@ -0,0 +1,60 @@ +const { + withDurableExecution, +} = require("@aws/durable-execution-sdk-js"); +const { + BedrockRuntimeClient, + InvokeModelCommand, +} = require("@aws-sdk/client-bedrock-runtime"); + +const client = new BedrockRuntimeClient(); +const MODEL_ID = process.env.MODEL_ID; + +async function callBedrock(prompt) { + const res = await client.send( + new InvokeModelCommand({ + modelId: MODEL_ID, + contentType: "application/json", + accept: "application/json", + body: JSON.stringify({ + anthropic_version: "bedrock-2023-05-31", + max_tokens: 1024, + messages: [{ role: "user", content: prompt }], + }), + }) + ); + const body = JSON.parse(new TextDecoder().decode(res.body)); + return body.content[0].text; +} + +exports.handler = withDurableExecution(async (event, context) => { + const topic = event.topic || "Serverless computing"; + + // Step 1: Generate blog outline + const outline = await context.step(async (stepCtx) => { + stepCtx.logger.info(`Generating outline for: ${topic}`); + return callBedrock( + `Create a concise blog post outline (5 sections max) about: ${topic}. Return only the outline.` + ); + }); + + // Wait — simulate editorial review pause + await context.wait({ seconds: 5 }); + + // Step 2: Expand outline into draft + const draft = await context.step(async (stepCtx) => { + stepCtx.logger.info("Expanding outline into draft"); + return callBedrock( + `Expand this outline into a short blog draft (under 500 words):\n\n${outline}` + ); + }); + + // Step 3: Generate summary + const summary = await context.step(async (stepCtx) => { + stepCtx.logger.info("Generating summary"); + return callBedrock( + `Summarize this blog post in 2-3 sentences:\n\n${draft}` + ); + }); + + return { topic, outline, draft, summary }; +}); diff --git a/lambda-durable-bedrock-cdk/tsconfig.json b/lambda-durable-bedrock-cdk/tsconfig.json new file mode 100644 index 000000000..15e54de36 --- /dev/null +++ b/lambda-durable-bedrock-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, + "outDir": "build", + "rootDir": ".", + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "esModuleInterop": true + }, + "exclude": ["node_modules", "build", "src"] +}