From d3998177b227e635d97e84b5979cc8c824e34233 Mon Sep 17 00:00:00 2001
From: Ben Freiberg <9841563+bfreiberg@users.noreply.github.com>
Date: Fri, 6 Mar 2026 13:09:57 +0100
Subject: [PATCH 1/3] feat(lambda-durable-bedrock-async-invoke-cdk) new pattern
---
.../.gitignore | 8 +
.../.npmignore | 6 +
.../README.md | 140 ++++++++++
.../bin/cdk-bedrock-async-invoke.ts | 20 ++
.../cdk.json | 103 ++++++++
.../example-pattern.json | 71 +++++
.../jest.config.js | 8 +
.../lib/cdk-bedrock-async-invoke-stack.ts | 102 ++++++++
.../lib/lambda/retry-strategies.ts | 30 +++
.../lib/lambda/types.ts | 30 +++
.../lib/lambda/video-generator.test.ts | 100 +++++++
.../lib/lambda/video-generator.ts | 245 ++++++++++++++++++
.../package.json | 29 +++
.../test/cdk-bedrock-async-invoke.test.ts | 47 ++++
.../tsconfig.json | 32 +++
15 files changed, 971 insertions(+)
create mode 100644 lambda-durable-bedrock-async-invoke-cdk/.gitignore
create mode 100644 lambda-durable-bedrock-async-invoke-cdk/.npmignore
create mode 100644 lambda-durable-bedrock-async-invoke-cdk/README.md
create mode 100644 lambda-durable-bedrock-async-invoke-cdk/bin/cdk-bedrock-async-invoke.ts
create mode 100644 lambda-durable-bedrock-async-invoke-cdk/cdk.json
create mode 100644 lambda-durable-bedrock-async-invoke-cdk/example-pattern.json
create mode 100644 lambda-durable-bedrock-async-invoke-cdk/jest.config.js
create mode 100644 lambda-durable-bedrock-async-invoke-cdk/lib/cdk-bedrock-async-invoke-stack.ts
create mode 100644 lambda-durable-bedrock-async-invoke-cdk/lib/lambda/retry-strategies.ts
create mode 100644 lambda-durable-bedrock-async-invoke-cdk/lib/lambda/types.ts
create mode 100644 lambda-durable-bedrock-async-invoke-cdk/lib/lambda/video-generator.test.ts
create mode 100644 lambda-durable-bedrock-async-invoke-cdk/lib/lambda/video-generator.ts
create mode 100644 lambda-durable-bedrock-async-invoke-cdk/package.json
create mode 100644 lambda-durable-bedrock-async-invoke-cdk/test/cdk-bedrock-async-invoke.test.ts
create mode 100644 lambda-durable-bedrock-async-invoke-cdk/tsconfig.json
diff --git a/lambda-durable-bedrock-async-invoke-cdk/.gitignore b/lambda-durable-bedrock-async-invoke-cdk/.gitignore
new file mode 100644
index 0000000000..f60797b6a9
--- /dev/null
+++ b/lambda-durable-bedrock-async-invoke-cdk/.gitignore
@@ -0,0 +1,8 @@
+*.js
+!jest.config.js
+*.d.ts
+node_modules
+
+# CDK asset staging directory
+.cdk.staging
+cdk.out
diff --git a/lambda-durable-bedrock-async-invoke-cdk/.npmignore b/lambda-durable-bedrock-async-invoke-cdk/.npmignore
new file mode 100644
index 0000000000..c1d6d45dcf
--- /dev/null
+++ b/lambda-durable-bedrock-async-invoke-cdk/.npmignore
@@ -0,0 +1,6 @@
+*.ts
+!*.d.ts
+
+# CDK asset staging directory
+.cdk.staging
+cdk.out
diff --git a/lambda-durable-bedrock-async-invoke-cdk/README.md b/lambda-durable-bedrock-async-invoke-cdk/README.md
new file mode 100644
index 0000000000..c1e43e1d06
--- /dev/null
+++ b/lambda-durable-bedrock-async-invoke-cdk/README.md
@@ -0,0 +1,140 @@
+# Amazon Bedrock Async Invoke with AWS Lambda durable functions
+
+This pattern shows how to use AWS Lambda durable functions to orchestrate [Amazon Bedrock Async Invoke](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_StartAsyncInvoke.html) for AI video generation. The durable function starts an Amazon Nova Reel video generation job, then polls for completion using `waitForCondition` with exponential backoff. During each polling interval the function suspends execution entirely, incurring zero compute charges while Bedrock processes the video.
+
+Without durable functions this pattern would require a separate polling mechanism such as Step Functions, EventBridge rules, or a cron-based poller. Here the entire workflow is a single, linear function that reads top-to-bottom.
+
+Learn more about this pattern at Serverless Land Patterns: [https://serverlessland.com/patterns/lambda-durable-bedrock-async-invoke](https://serverlessland.com/patterns/lambda-durable-bedrock-async-invoke)
+
+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 v2](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) (latest available version) installed and configured
+* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
+* [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html) (version 2.232.1 or later) installed and configured
+* [Node.js 22.x](https://nodejs.org/) installed
+* Amazon Bedrock model access enabled for **Amazon Nova Reel** (`amazon.nova-reel-v1:1`) in your target region
+
+## Deployment Instructions
+
+1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository:
+
+ ```bash
+ git clone https://github.com/aws-samples/serverless-patterns
+ ```
+
+1. Change directory to the pattern directory:
+
+ ```bash
+ cd lambda-durable-bedrock-async-invoke
+ ```
+
+1. Install the project dependencies:
+
+ ```bash
+ npm install
+ ```
+
+1. Deploy the CDK stack:
+
+ ```bash
+ npx cdk deploy
+ ```
+
+ The stack creates:
+ - An S3 bucket for video output (auto-deleted on stack destroy, 7-day lifecycle)
+ - A durable Lambda function with 30-minute execution timeout
+ - IAM permissions for Bedrock and S3 access
+ - A CloudWatch log group with 1-week retention
+
+1. Note the outputs from the CDK deployment process. These contain the resource names and ARNs used for testing.
+
+## How it works
+
+The durable function performs three logical phases:
+
+1. **`start-video-generation` step** — calls `StartAsyncInvoke` with a `clientRequestToken` for Bedrock-level idempotency. Because this runs inside a durable step, the Bedrock invocation ARN is checkpointed and will not be re-executed on replay.
+
+2. **`wait-for-video-ready` waitForCondition** — polls `GetAsyncInvoke` with exponential backoff (30 s → 60 s cap). The function suspends during each wait interval, consuming no compute time while the video is being generated.
+
+3. **`build-result` step** — assembles the final response with the S3 output location and metadata, or throws an error if the generation failed.
+
+```
+Client ──► Lambda (durable) ──► Bedrock StartAsyncInvoke ──► S3 (video output)
+ │ │
+ │ ◄── waitForCondition ──► │
+ │ (poll with │
+ │ exponential │
+ │ backoff) │
+ │ │
+ └── GetAsyncInvoke ─────────┘
+```
+
+Key concepts:
+
+| Concept | How it is used |
+|---|---|
+| `step` | Wraps the `StartAsyncInvoke` call so it is checkpointed and never re-executed on replay |
+| `waitForCondition` | Polls `GetAsyncInvoke` with exponential backoff; the function suspends between polls |
+| `clientRequestToken` | Bedrock idempotency token generated inside a step, ensuring replays cannot create duplicate invocations |
+| `context.logger` | Replay-aware structured logging throughout the workflow |
+| S3 output | Bedrock writes the generated video directly to an S3 bucket provisioned by CDK |
+
+## Testing
+
+After deployment, invoke the durable function using the AWS CLI.
+
+Because the durable execution timeout is 30 minutes (exceeding Lambda's 15-minute synchronous limit), you must invoke the function **asynchronously**. Use `--durable-execution-name` for idempotency at the Lambda level.
+
+### Invoke the durable function
+
+```bash
+aws lambda invoke \
+ --function-name 'video-generator-durable:$LATEST' \
+ --invocation-type Event \
+ --durable-execution-name "my-beach-video-001" \
+ --payload '{"prompt":"A golden retriever playing fetch on a sunny beach","durationSeconds":6}' \
+ --cli-binary-format raw-in-base64-out \
+ response.json
+```
+
+Repeat the same command with the same `--durable-execution-name` to safely retry without creating a duplicate execution.
+
+### Check execution status
+
+```bash
+aws lambda get-durable-execution \
+ --function-name 'video-generator-durable:$LATEST' \
+ --durable-execution-name "my-beach-video-001"
+```
+
+Once the status shows `SUCCEEDED`, the result will contain the S3 URI where the video was written.
+
+### Run unit tests
+
+```bash
+npm test
+```
+
+This runs both CDK infrastructure tests and durable handler tests (with mocked Bedrock calls).
+
+## Cleanup
+
+1. Delete the stack:
+
+ ```bash
+ npx cdk destroy
+ ```
+
+1. Confirm the stack has been deleted by checking the AWS CloudFormation console or running:
+
+ ```bash
+ aws cloudformation list-stacks --stack-status-filter DELETE_COMPLETE
+ ```
+
+----
+Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+SPDX-License-Identifier: MIT-0
diff --git a/lambda-durable-bedrock-async-invoke-cdk/bin/cdk-bedrock-async-invoke.ts b/lambda-durable-bedrock-async-invoke-cdk/bin/cdk-bedrock-async-invoke.ts
new file mode 100644
index 0000000000..65f02a355d
--- /dev/null
+++ b/lambda-durable-bedrock-async-invoke-cdk/bin/cdk-bedrock-async-invoke.ts
@@ -0,0 +1,20 @@
+#!/opt/homebrew/opt/node/bin/node
+import * as cdk from 'aws-cdk-lib/core';
+import { CdkBedrockAsyncInvokeStack } from '../lib/cdk-bedrock-async-invoke-stack';
+
+const app = new cdk.App();
+new CdkBedrockAsyncInvokeStack(app, 'CdkBedrockAsyncInvokeStack', {
+ /* If you don't specify 'env', this stack will be environment-agnostic.
+ * Account/Region-dependent features and context lookups will not work,
+ * but a single synthesized template can be deployed anywhere. */
+
+ /* Uncomment the next line to specialize this stack for the AWS Account
+ * and Region that are implied by the current CLI configuration. */
+ // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
+
+ /* Uncomment the next line if you know exactly what Account and Region you
+ * want to deploy the stack to. */
+ // env: { account: '123456789012', region: 'us-east-1' },
+
+ /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
+});
diff --git a/lambda-durable-bedrock-async-invoke-cdk/cdk.json b/lambda-durable-bedrock-async-invoke-cdk/cdk.json
new file mode 100644
index 0000000000..70ed193ff9
--- /dev/null
+++ b/lambda-durable-bedrock-async-invoke-cdk/cdk.json
@@ -0,0 +1,103 @@
+{
+ "app": "npx ts-node --prefer-ts-exts bin/cdk-bedrock-async-invoke.ts",
+ "watch": {
+ "include": [
+ "**"
+ ],
+ "exclude": [
+ "README.md",
+ "cdk*.json",
+ "**/*.d.ts",
+ "**/*.js",
+ "tsconfig.json",
+ "package*.json",
+ "yarn.lock",
+ "node_modules",
+ "test"
+ ]
+ },
+ "context": {
+ "@aws-cdk/aws-signer:signingProfileNamePassedToCfn": true,
+ "@aws-cdk/aws-ecs-patterns:secGroupsDisablesImplicitOpenListener": true,
+ "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
+ "@aws-cdk/core:checkSecretUsage": true,
+ "@aws-cdk/core:target-partitions": [
+ "aws",
+ "aws-cn"
+ ],
+ "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
+ "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
+ "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
+ "@aws-cdk/aws-iam:minimizePolicies": true,
+ "@aws-cdk/core:validateSnapshotRemovalPolicy": true,
+ "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
+ "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
+ "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
+ "@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
+ "@aws-cdk/core:enablePartitionLiterals": true,
+ "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
+ "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
+ "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
+ "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
+ "@aws-cdk/aws-route53-patters:useCertificate": true,
+ "@aws-cdk/customresources:installLatestAwsSdkDefault": false,
+ "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
+ "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
+ "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
+ "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
+ "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
+ "@aws-cdk/aws-redshift:columnId": true,
+ "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
+ "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
+ "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
+ "@aws-cdk/aws-kms:aliasNameRef": true,
+ "@aws-cdk/aws-kms:applyImportedAliasPermissionsToPrincipal": true,
+ "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
+ "@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
+ "@aws-cdk/aws-efs:denyAnonymousAccess": true,
+ "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
+ "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
+ "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
+ "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
+ "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
+ "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
+ "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true,
+ "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true,
+ "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true,
+ "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true,
+ "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true,
+ "@aws-cdk/aws-eks:nodegroupNameAttribute": true,
+ "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
+ "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true,
+ "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false,
+ "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false,
+ "@aws-cdk/core:explicitStackTags": true,
+ "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false,
+ "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true,
+ "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true,
+ "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true,
+ "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true,
+ "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true,
+ "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true,
+ "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true,
+ "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true,
+ "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true,
+ "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true,
+ "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true,
+ "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true,
+ "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true,
+ "@aws-cdk/core:enableAdditionalMetadataCollection": true,
+ "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": false,
+ "@aws-cdk/aws-s3:setUniqueReplicationRoleName": true,
+ "@aws-cdk/aws-events:requireEventBusPolicySid": true,
+ "@aws-cdk/core:aspectPrioritiesMutating": true,
+ "@aws-cdk/aws-dynamodb:retainTableReplica": true,
+ "@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2": true,
+ "@aws-cdk/s3-notifications:addS3TrustKeyPolicyForSnsSubscriptions": true,
+ "@aws-cdk/aws-ec2:requirePrivateSubnetsForEgressOnlyInternetGateway": true,
+ "@aws-cdk/aws-s3:publicAccessBlockedByDefault": true,
+ "@aws-cdk/aws-lambda:useCdkManagedLogGroup": true,
+ "@aws-cdk/aws-elasticloadbalancingv2:networkLoadBalancerWithSecurityGroupByDefault": true,
+ "@aws-cdk/aws-ecs-patterns:uniqueTargetGroupId": true
+ }
+}
diff --git a/lambda-durable-bedrock-async-invoke-cdk/example-pattern.json b/lambda-durable-bedrock-async-invoke-cdk/example-pattern.json
new file mode 100644
index 0000000000..30f79b0565
--- /dev/null
+++ b/lambda-durable-bedrock-async-invoke-cdk/example-pattern.json
@@ -0,0 +1,71 @@
+{
+ "title": "Amazon Bedrock Async Invoke with AWS Lambda durable functions",
+ "description": "Orchestrate long-running Amazon Bedrock video generation jobs using AWS Lambda durable functions with waitForCondition polling and zero-cost waits",
+ "language": "TypeScript",
+ "level": "300",
+ "framework": "AWS CDK",
+ "introBox": {
+ "headline": "How it works",
+ "text": [
+ "This pattern deploys an AWS Lambda durable function that orchestrates Amazon Bedrock Async Invoke to generate AI videos using Amazon Nova Reel.",
+ "The durable function starts a video generation job with StartAsyncInvoke, then polls for completion using waitForCondition with exponential backoff. During each polling interval the function suspends entirely, incurring zero compute charges while Bedrock processes the video.",
+ "An S3 bucket is provisioned for video output. The durable execution SDK checkpoints progress at each step, so the workflow is fault-tolerant and idempotent without requiring external orchestration."
+ ]
+ },
+ "gitHub": {
+ "template": {
+ "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/lambda-durable-bedrock-async-invoke",
+ "templateURL": "serverless-patterns/lambda-durable-bedrock-async-invoke",
+ "projectFolder": "lambda-durable-bedrock-async-invoke",
+ "templateFile": "lib/cdk-bedrock-async-invoke-stack.ts"
+ }
+ },
+ "resources": {
+ "bullets": [
+ {
+ "text": "AWS Lambda durable functions documentation",
+ "link": "https://docs.aws.amazon.com/lambda/latest/dg/durable-basic-concepts.html"
+ },
+ {
+ "text": "Durable Execution SDK",
+ "link": "https://docs.aws.amazon.com/lambda/latest/dg/durable-execution-sdk.html"
+ },
+ {
+ "text": "Amazon Bedrock Async Invoke API reference",
+ "link": "https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_StartAsyncInvoke.html"
+ },
+ {
+ "text": "AWS CDK Developer Guide",
+ "link": "https://docs.aws.amazon.com/cdk/latest/guide/"
+ }
+ ]
+ },
+ "deploy": {
+ "text": [
+ "npm install",
+ "npx cdk deploy"
+ ]
+ },
+ "testing": {
+ "text": [
+ "See the GitHub repo for detailed testing instructions."
+ ]
+ },
+ "cleanup": {
+ "text": [
+ "Delete the stack: npx cdk destroy."
+ ]
+ },
+ "authors": [
+ {
+ "name": "Ben Freiberg",
+ "image": "https://serverlessland.com/assets/images/resources/contributors/ben-freiberg.jpg",
+ "bio": "Ben is a Senior Solutions Architect at Amazon Web Services (AWS) based in Frankfurt, Germany.",
+ "linkedin": "benfreiberg"
+ },{
+ "name": "Michael Gasch",
+ "bio": "Michael is a Senior Product Manager at Amazon Web Services (AWS) based in Germany.",
+ "linkedin": "michael-gasch"
+ }
+ ]
+}
diff --git a/lambda-durable-bedrock-async-invoke-cdk/jest.config.js b/lambda-durable-bedrock-async-invoke-cdk/jest.config.js
new file mode 100644
index 0000000000..c0e833bce2
--- /dev/null
+++ b/lambda-durable-bedrock-async-invoke-cdk/jest.config.js
@@ -0,0 +1,8 @@
+module.exports = {
+ testEnvironment: 'node',
+ roots: ['/test', '/lib'],
+ testMatch: ['**/*.test.ts'],
+ transform: {
+ '^.+\\.tsx?$': 'ts-jest'
+ },
+};
\ No newline at end of file
diff --git a/lambda-durable-bedrock-async-invoke-cdk/lib/cdk-bedrock-async-invoke-stack.ts b/lambda-durable-bedrock-async-invoke-cdk/lib/cdk-bedrock-async-invoke-stack.ts
new file mode 100644
index 0000000000..0a6ab0affa
--- /dev/null
+++ b/lambda-durable-bedrock-async-invoke-cdk/lib/cdk-bedrock-async-invoke-stack.ts
@@ -0,0 +1,102 @@
+import * as iam from 'aws-cdk-lib/aws-iam';
+import * as lambda from 'aws-cdk-lib/aws-lambda';
+import * as nodejs from 'aws-cdk-lib/aws-lambda-nodejs';
+import * as logs from 'aws-cdk-lib/aws-logs';
+import * as s3 from 'aws-cdk-lib/aws-s3';
+import * as cdk from 'aws-cdk-lib/core';
+import { Construct } from 'constructs';
+import * as path from 'path';
+
+export class CdkBedrockAsyncInvokeStack extends cdk.Stack {
+ constructor(scope: Construct, id: string, props?: cdk.StackProps) {
+ super(scope, id, props);
+
+ // S3 bucket where Bedrock writes the generated video output
+ const outputBucket = new s3.Bucket(this, 'VideoOutputBucket', {
+ removalPolicy: cdk.RemovalPolicy.DESTROY,
+ autoDeleteObjects: true,
+ lifecycleRules: [
+ {
+ expiration: cdk.Duration.days(7),
+ id: 'ExpireVideosAfter7Days',
+ },
+ ],
+ });
+
+ // Explicit log group with cleanup on stack destroy
+ const logGroup = new logs.LogGroup(this, 'VideoGeneratorLogGroup', {
+ logGroupName: '/aws/lambda/video-generator-durable',
+ retention: logs.RetentionDays.ONE_WEEK,
+ removalPolicy: cdk.RemovalPolicy.DESTROY,
+ });
+
+ // Durable Lambda function for the video generation workflow
+ const videoGeneratorFunction = new nodejs.NodejsFunction(this, 'VideoGeneratorFunction', {
+ functionName: 'video-generator-durable',
+ description:
+ 'Durable function demonstrating Bedrock async invoke for AI video generation',
+ runtime: lambda.Runtime.NODEJS_22_X,
+ handler: 'handler',
+ entry: path.join(__dirname, 'lambda', 'video-generator.ts'),
+ timeout: cdk.Duration.minutes(1),
+ memorySize: 256,
+ durableConfig: {
+ executionTimeout: cdk.Duration.minutes(30),
+ retentionPeriod: cdk.Duration.days(1),
+ },
+ bundling: {
+ minify: true,
+ sourceMap: true,
+ externalModules: [],
+ },
+ environment: {
+ NODE_OPTIONS: '--enable-source-maps',
+ OUTPUT_BUCKET_NAME: outputBucket.bucketName,
+ BEDROCK_MODEL_ID: 'amazon.nova-reel-v1:1',
+ BEDROCK_REGION: 'us-east-1',
+ },
+ logGroup: logGroup,
+ });
+
+ // Grant the function permission to write to the output bucket.
+ // Bedrock writes the video output directly, but the function also
+ // needs s3:PutObject so that Bedrock can use the function's role
+ // when writing to the bucket via the async invocation.
+ outputBucket.grantReadWrite(videoGeneratorFunction);
+
+ // Grant Bedrock invocation permissions
+ videoGeneratorFunction.addToRolePolicy(
+ new iam.PolicyStatement({
+ actions: [
+ 'bedrock:InvokeModel',
+ 'bedrock:GetAsyncInvoke',
+ 'bedrock:StartAsyncInvoke',
+ ],
+ resources: ['*'],
+ }),
+ );
+
+ // Add durable execution managed policy (required when using explicit log groups)
+ videoGeneratorFunction.role?.addManagedPolicy(
+ iam.ManagedPolicy.fromAwsManagedPolicyName(
+ 'service-role/AWSLambdaBasicDurableExecutionRolePolicy',
+ ),
+ );
+
+ // Stack outputs
+ new cdk.CfnOutput(this, 'VideoGeneratorFunctionArn', {
+ value: videoGeneratorFunction.functionArn,
+ description: 'ARN of the Video Generator Durable Function',
+ });
+
+ new cdk.CfnOutput(this, 'VideoGeneratorFunctionName', {
+ value: videoGeneratorFunction.functionName,
+ description: 'Name of the Video Generator Durable Function',
+ });
+
+ new cdk.CfnOutput(this, 'VideoOutputBucketName', {
+ value: outputBucket.bucketName,
+ description: 'S3 bucket where generated videos are stored',
+ });
+ }
+}
\ No newline at end of file
diff --git a/lambda-durable-bedrock-async-invoke-cdk/lib/lambda/retry-strategies.ts b/lambda-durable-bedrock-async-invoke-cdk/lib/lambda/retry-strategies.ts
new file mode 100644
index 0000000000..823581ffcc
--- /dev/null
+++ b/lambda-durable-bedrock-async-invoke-cdk/lib/lambda/retry-strategies.ts
@@ -0,0 +1,30 @@
+/**
+ * Retry strategy for Bedrock API calls.
+ * Skips retries on validation errors (wrong model ID, bad input, etc.)
+ * and uses exponential backoff for transient failures.
+ */
+export function bedrockRetryStrategy(error: any, attemptCount: number) {
+ if (error?.name === 'ValidationException' || error?.message?.includes('ValidationException')) {
+ return { shouldRetry: false };
+ }
+ if (attemptCount >= 3) {
+ return { shouldRetry: false };
+ }
+ return {
+ shouldRetry: true,
+ delay: { seconds: Math.pow(2, attemptCount) },
+ };
+}
+
+/**
+ * Default retry strategy with exponential backoff, max 3 attempts.
+ */
+export function defaultRetryStrategy(_error: any, attemptCount: number) {
+ if (attemptCount >= 3) {
+ return { shouldRetry: false };
+ }
+ return {
+ shouldRetry: true,
+ delay: { seconds: Math.pow(2, attemptCount) },
+ };
+}
\ No newline at end of file
diff --git a/lambda-durable-bedrock-async-invoke-cdk/lib/lambda/types.ts b/lambda-durable-bedrock-async-invoke-cdk/lib/lambda/types.ts
new file mode 100644
index 0000000000..c61ad1e328
--- /dev/null
+++ b/lambda-durable-bedrock-async-invoke-cdk/lib/lambda/types.ts
@@ -0,0 +1,30 @@
+/**
+ * Input for the video generation workflow
+ */
+export interface VideoGenerationInput {
+ prompt: string;
+ durationSeconds?: number;
+ seed?: number;
+}
+
+/**
+ * State tracked across polling iterations
+ */
+export interface AsyncInvokeState {
+ invocationArn: string;
+ status: 'InProgress' | 'Completed' | 'Failed';
+ checkCount: number;
+ failureMessage?: string;
+}
+
+/**
+ * Final result returned by the handler
+ */
+export interface VideoGenerationResult {
+ invocationArn: string;
+ status: string;
+ outputS3Uri: string;
+ totalChecks: number;
+ prompt: string;
+ completedAt: string;
+}
\ No newline at end of file
diff --git a/lambda-durable-bedrock-async-invoke-cdk/lib/lambda/video-generator.test.ts b/lambda-durable-bedrock-async-invoke-cdk/lib/lambda/video-generator.test.ts
new file mode 100644
index 0000000000..c0973180ba
--- /dev/null
+++ b/lambda-durable-bedrock-async-invoke-cdk/lib/lambda/video-generator.test.ts
@@ -0,0 +1,100 @@
+import { LocalDurableTestRunner, OperationType, OperationStatus } from '@aws/durable-execution-sdk-js-testing';
+import { handler } from './video-generator';
+
+// Mock the Bedrock Runtime client
+jest.mock('@aws-sdk/client-bedrock-runtime', () => {
+ const MOCK_INVOCATION_ARN =
+ 'arn:aws:bedrock:us-east-1:123456789012:async-invoke/abc123def456';
+
+ let callCount = 0;
+
+ return {
+ BedrockRuntimeClient: jest.fn().mockImplementation(() => ({
+ send: jest.fn().mockImplementation((command: any) => {
+ if (command.constructor.name === 'StartAsyncInvokeCommand') {
+ return Promise.resolve({
+ invocationArn: MOCK_INVOCATION_ARN,
+ });
+ }
+ if (command.constructor.name === 'GetAsyncInvokeCommand') {
+ callCount++;
+ // Simulate: first two checks return InProgress, third returns Completed
+ if (callCount >= 3) {
+ return Promise.resolve({
+ invocationArn: MOCK_INVOCATION_ARN,
+ status: 'Completed',
+ outputDataConfig: {
+ s3OutputDataConfig: {
+ s3Uri: 's3://test-bucket/videos/output/',
+ },
+ },
+ });
+ }
+ return Promise.resolve({
+ invocationArn: MOCK_INVOCATION_ARN,
+ status: 'InProgress',
+ });
+ }
+ return Promise.reject(new Error('Unknown command'));
+ }),
+ })),
+ StartAsyncInvokeCommand: jest.fn().mockImplementation((input: any) => ({
+ constructor: { name: 'StartAsyncInvokeCommand' },
+ input,
+ })),
+ GetAsyncInvokeCommand: jest.fn().mockImplementation((input: any) => ({
+ constructor: { name: 'GetAsyncInvokeCommand' },
+ input,
+ })),
+ };
+});
+
+describe('Video Generator - Bedrock Async Invoke', () => {
+ beforeAll(() => LocalDurableTestRunner.setupTestEnvironment({ skipTime: true }));
+ afterAll(() => LocalDurableTestRunner.teardownTestEnvironment());
+
+ it('should start async invocation and poll until completion', async () => {
+ const runner = new LocalDurableTestRunner({
+ handlerFunction: handler,
+ });
+
+ const execution = await runner.run({
+ payload: {
+ prompt: 'A golden retriever playing fetch on a sunny beach',
+ durationSeconds: 6,
+ },
+ });
+
+ const result = execution.getResult() as any;
+
+ // Verify the workflow completed successfully
+ expect(result).toBeDefined();
+ expect(result.status).toBe('Completed');
+ expect(result.prompt).toBe('A golden retriever playing fetch on a sunny beach');
+ expect(result.invocationArn).toContain('async-invoke');
+ expect(result.totalChecks).toBeGreaterThanOrEqual(1);
+ expect(result.completedAt).toBeDefined();
+
+ // Verify the idempotency token step ran
+ const tokenStep = runner.getOperation('generate-idempotency-token');
+ expect(tokenStep).toBeDefined();
+ expect(tokenStep.getType()).toBe(OperationType.STEP);
+ expect(tokenStep.getStatus()).toBe(OperationStatus.SUCCEEDED);
+
+ // Verify the start-video-generation step ran
+ const startStep = runner.getOperation('start-video-generation');
+ expect(startStep).toBeDefined();
+ expect(startStep.getType()).toBe(OperationType.STEP);
+ expect(startStep.getStatus()).toBe(OperationStatus.SUCCEEDED);
+
+ // Verify the waitForCondition polling ran
+ const waitOp = runner.getOperation('wait-for-video-ready');
+ expect(waitOp).toBeDefined();
+
+ // Verify the build-result step ran
+ const resultStep = runner.getOperation('build-result');
+ expect(resultStep).toBeDefined();
+ expect(resultStep.getType()).toBe(OperationType.STEP);
+ expect(resultStep.getStatus()).toBe(OperationStatus.SUCCEEDED);
+ });
+});
\ No newline at end of file
diff --git a/lambda-durable-bedrock-async-invoke-cdk/lib/lambda/video-generator.ts b/lambda-durable-bedrock-async-invoke-cdk/lib/lambda/video-generator.ts
new file mode 100644
index 0000000000..b4b1869557
--- /dev/null
+++ b/lambda-durable-bedrock-async-invoke-cdk/lib/lambda/video-generator.ts
@@ -0,0 +1,245 @@
+import { DurableContext, withDurableExecution } from '@aws/durable-execution-sdk-js';
+import {
+ BedrockRuntimeClient,
+ StartAsyncInvokeCommand,
+ GetAsyncInvokeCommand,
+} from '@aws-sdk/client-bedrock-runtime';
+
+import { VideoGenerationInput, AsyncInvokeState, VideoGenerationResult } from './types';
+import { bedrockRetryStrategy, defaultRetryStrategy } from './retry-strategies';
+
+const BEDROCK_MODEL_ID = process.env.BEDROCK_MODEL_ID ?? 'amazon.nova-reel-v1:1';
+const BEDROCK_REGION = process.env.BEDROCK_REGION ?? 'us-east-1';
+const OUTPUT_BUCKET = process.env.OUTPUT_BUCKET_NAME ?? '';
+
+// Maximum number of polling iterations before treating the invocation as timed out
+const MAX_POLL_CHECKS = 10;
+
+const bedrockClient = new BedrockRuntimeClient({ region: BEDROCK_REGION });
+
+/**
+ * Start a Bedrock async invocation for video generation.
+ */
+async function startVideoGeneration(
+ client: BedrockRuntimeClient,
+ event: VideoGenerationInput,
+ idempotencyToken: string,
+ stepCtx: { logger: { info: (msg: string, data?: any) => void } },
+): Promise {
+ const s3OutputUri = `s3://${OUTPUT_BUCKET}/videos/${idempotencyToken}/`;
+
+ stepCtx.logger.info('Starting Bedrock async invocation', {
+ model: BEDROCK_MODEL_ID,
+ outputUri: s3OutputUri,
+ });
+
+ const response = await client.send(
+ new StartAsyncInvokeCommand({
+ modelId: BEDROCK_MODEL_ID,
+ clientRequestToken: idempotencyToken,
+ modelInput: {
+ taskType: 'TEXT_VIDEO',
+ textToVideoParams: {
+ text: event.prompt,
+ },
+ videoGenerationConfig: {
+ durationSeconds: event.durationSeconds ?? 6,
+ fps: 24,
+ dimension: '1280x720',
+ seed: event.seed ?? 0,
+ },
+ },
+ outputDataConfig: {
+ s3OutputDataConfig: {
+ s3Uri: s3OutputUri,
+ },
+ },
+ }),
+ );
+
+ const arn = response.invocationArn!;
+ stepCtx.logger.info('Bedrock async invocation started', { invocationArn: arn });
+ return arn;
+}
+
+/**
+ * Check the current status of a Bedrock async invocation.
+ */
+async function checkInvocationStatus(
+ client: BedrockRuntimeClient,
+ currentState: AsyncInvokeState,
+ ctx: { logger: { info: (msg: string, data?: any) => void } },
+): Promise {
+ ctx.logger.info('Checking Bedrock async invocation status', {
+ invocationArn: currentState.invocationArn,
+ checkNumber: currentState.checkCount + 1,
+ });
+
+ const response = await client.send(
+ new GetAsyncInvokeCommand({
+ invocationArn: currentState.invocationArn,
+ }),
+ );
+
+ const status = (response.status as AsyncInvokeState['status']) ?? 'InProgress';
+
+ ctx.logger.info('Bedrock invocation status retrieved', {
+ invocationArn: currentState.invocationArn,
+ status,
+ failureMessage: response.failureMessage,
+ });
+
+ return {
+ invocationArn: currentState.invocationArn,
+ status,
+ checkCount: currentState.checkCount + 1,
+ failureMessage: response.failureMessage,
+ };
+}
+
+/**
+ * Build the final result from the completed polling state.
+ * Throws if the generation failed or timed out.
+ */
+function buildResult(
+ finalState: AsyncInvokeState,
+ event: VideoGenerationInput,
+ idempotencyToken: string,
+ stepCtx: { logger: { info: (msg: string, data?: any) => void; error: (msg: string, data?: any) => void } },
+): VideoGenerationResult {
+ if (finalState.status === 'Failed') {
+ stepCtx.logger.error('Video generation failed', {
+ invocationArn: finalState.invocationArn,
+ failureMessage: finalState.failureMessage,
+ });
+ throw new Error(
+ `Video generation failed: ${finalState.failureMessage ?? 'Unknown error'}`,
+ );
+ }
+
+ if (finalState.status !== 'Completed') {
+ stepCtx.logger.error('Video generation timed out', {
+ invocationArn: finalState.invocationArn,
+ checkCount: finalState.checkCount,
+ });
+ throw new Error(
+ `Video generation timed out after ${finalState.checkCount} polling attempts (status: ${finalState.status})`,
+ );
+ }
+
+ const outputUri = `s3://${OUTPUT_BUCKET}/videos/${idempotencyToken}/`;
+
+ stepCtx.logger.info('Video generation completed', {
+ invocationArn: finalState.invocationArn,
+ totalChecks: finalState.checkCount,
+ outputUri,
+ });
+
+ return {
+ invocationArn: finalState.invocationArn,
+ status: finalState.status,
+ outputS3Uri: outputUri,
+ totalChecks: finalState.checkCount,
+ prompt: event.prompt,
+ completedAt: new Date().toISOString(),
+ };
+}
+
+/**
+ * AI Video Generation Pipeline — Demonstrates Bedrock Async Invoke with durable functions
+ *
+ * This durable function orchestrates Amazon Nova Reel video generation:
+ * 1. Generates a deterministic idempotency token (checkpointed for replay safety)
+ * 2. Starts a Bedrock async invocation (video output written to S3)
+ * 3. Polls GetAsyncInvoke with exponential backoff using waitForCondition
+ * 4. Returns the S3 location of the generated video
+ *
+ * Without durable functions you would need a separate polling mechanism
+ * (Step Functions, EventBridge, or a cron-based poller). Here the entire
+ * flow is a single, linear function with automatic state persistence and
+ * zero compute cost during waits.
+ */
+export const handler = withDurableExecution(
+ async (event: VideoGenerationInput, context: DurableContext): Promise => {
+ context.logger.info('Starting video generation workflow', {
+ prompt: event.prompt,
+ durationSeconds: event.durationSeconds ?? 6,
+ });
+
+ try {
+ // Step 1: Generate idempotency token
+ // This is in its own step so the token is checkpointed. If the
+ // subsequent Bedrock call fails and retries, the same token is
+ // reused and the request stays idempotent.
+ const idempotencyToken = await context.step(
+ 'generate-idempotency-token',
+ async (): Promise => {
+ return `video-${Date.now()}-${Math.random().toString(36).substring(2, 10)}`;
+ },
+ );
+
+ // Step 2: Start the async Bedrock invocation
+ const invocationArn = await context.step(
+ 'start-video-generation',
+ async (stepCtx) => startVideoGeneration(bedrockClient, event, idempotencyToken, stepCtx),
+ { retryStrategy: bedrockRetryStrategy },
+ );
+
+ // Step 3: Poll for completion using waitForCondition with exponential backoff
+ // The durable function suspends during each wait interval, incurring zero
+ // compute charges while the video is being generated.
+ const finalState = await context.waitForCondition(
+ 'wait-for-video-ready',
+ async (currentState: AsyncInvokeState, ctx) =>
+ checkInvocationStatus(bedrockClient, currentState, ctx),
+ {
+ initialState: {
+ invocationArn,
+ status: 'InProgress' as const,
+ checkCount: 0,
+ },
+ waitStrategy: (state: AsyncInvokeState, attempt: number) => {
+ if (state.status === 'Completed' || state.status === 'Failed') {
+ return { shouldContinue: false };
+ }
+
+ // Guard against infinite polling
+ if (state.checkCount >= MAX_POLL_CHECKS) {
+ return { shouldContinue: false };
+ }
+
+ // Exponential backoff starting at 30s: 30s, 60s, 60s, ... capped at 60s.
+ // Nova Reel generation takes minutes so a higher initial delay avoids
+ // unnecessary API calls.
+ const delaySeconds = Math.min(30 * Math.pow(2, attempt - 1), 60);
+
+ return {
+ shouldContinue: true,
+ delay: { seconds: delaySeconds },
+ };
+ },
+ },
+ );
+
+ // Step 4: Build the final result
+ const result = await context.step(
+ 'build-result',
+ async (stepCtx) => buildResult(finalState, event, idempotencyToken, stepCtx),
+ { retryStrategy: defaultRetryStrategy },
+ );
+
+ context.logger.info('Video generation workflow completed', {
+ invocationArn: result.invocationArn,
+ status: result.status,
+ });
+
+ return result;
+ } catch (error: any) {
+ context.logger.error('Video generation workflow failed', {
+ error: error.message,
+ prompt: event.prompt,
+ });
+ throw error;
+ }
+ },
+);
diff --git a/lambda-durable-bedrock-async-invoke-cdk/package.json b/lambda-durable-bedrock-async-invoke-cdk/package.json
new file mode 100644
index 0000000000..6b2077fde2
--- /dev/null
+++ b/lambda-durable-bedrock-async-invoke-cdk/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "cdk-bedrock-async-invoke",
+ "version": "0.1.0",
+ "bin": {
+ "cdk-bedrock-async-invoke": "bin/cdk-bedrock-async-invoke.js"
+ },
+ "scripts": {
+ "build": "tsc",
+ "watch": "tsc -w",
+ "test": "jest",
+ "cdk": "cdk"
+ },
+ "devDependencies": {
+ "@aws/durable-execution-sdk-js-testing": "^1.0.0",
+ "@types/jest": "^30",
+ "@types/node": "^24.10.1",
+ "jest": "^30",
+ "ts-jest": "^29",
+ "aws-cdk": "2.1100.3",
+ "ts-node": "^10.9.2",
+ "typescript": "~5.9.3"
+ },
+ "dependencies": {
+ "@aws/durable-execution-sdk-js": "^1.0.0",
+ "@aws-sdk/client-bedrock-runtime": "^3.0.0",
+ "aws-cdk-lib": "^2.232.2",
+ "constructs": "^10.0.0"
+ }
+}
diff --git a/lambda-durable-bedrock-async-invoke-cdk/test/cdk-bedrock-async-invoke.test.ts b/lambda-durable-bedrock-async-invoke-cdk/test/cdk-bedrock-async-invoke.test.ts
new file mode 100644
index 0000000000..181c147923
--- /dev/null
+++ b/lambda-durable-bedrock-async-invoke-cdk/test/cdk-bedrock-async-invoke.test.ts
@@ -0,0 +1,47 @@
+import * as cdk from 'aws-cdk-lib/core';
+import { Template, Match } from 'aws-cdk-lib/assertions';
+import { CdkBedrockAsyncInvokeStack } from '../lib/cdk-bedrock-async-invoke-stack';
+
+describe('CdkBedrockAsyncInvokeStack', () => {
+ const app = new cdk.App();
+ const stack = new CdkBedrockAsyncInvokeStack(app, 'TestStack');
+ const template = Template.fromStack(stack);
+
+ test('creates an S3 bucket for video output', () => {
+ template.hasResource('AWS::S3::Bucket', {
+ DeletionPolicy: 'Delete',
+ });
+ });
+
+ test('creates a durable Lambda function', () => {
+ template.hasResourceProperties('AWS::Lambda::Function', {
+ FunctionName: 'video-generator-durable',
+ Runtime: 'nodejs22.x',
+ });
+ });
+
+ test('grants Bedrock permissions', () => {
+ template.hasResourceProperties('AWS::IAM::Policy', {
+ PolicyDocument: {
+ Statement: Match.arrayWith([
+ Match.objectLike({
+ Action: Match.arrayWith([
+ 'bedrock:InvokeModel',
+ 'bedrock:GetAsyncInvoke',
+ 'bedrock:StartAsyncInvoke',
+ ]),
+ }),
+ ]),
+ },
+ });
+ });
+
+ test('creates CloudWatch log group with DESTROY removal', () => {
+ template.hasResource('AWS::Logs::LogGroup', {
+ DeletionPolicy: 'Delete',
+ Properties: {
+ LogGroupName: '/aws/lambda/video-generator-durable',
+ },
+ });
+ });
+});
\ No newline at end of file
diff --git a/lambda-durable-bedrock-async-invoke-cdk/tsconfig.json b/lambda-durable-bedrock-async-invoke-cdk/tsconfig.json
new file mode 100644
index 0000000000..bfc61bf833
--- /dev/null
+++ b/lambda-durable-bedrock-async-invoke-cdk/tsconfig.json
@@ -0,0 +1,32 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "lib": [
+ "es2022"
+ ],
+ "declaration": true,
+ "strict": true,
+ "noImplicitAny": true,
+ "strictNullChecks": true,
+ "noImplicitThis": true,
+ "alwaysStrict": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": false,
+ "inlineSourceMap": true,
+ "inlineSources": true,
+ "experimentalDecorators": true,
+ "strictPropertyInitialization": false,
+ "skipLibCheck": true,
+ "typeRoots": [
+ "./node_modules/@types"
+ ]
+ },
+ "exclude": [
+ "node_modules",
+ "cdk.out"
+ ]
+}
From 4124a131c8ae6855e6ccc930499646be2a73c06a Mon Sep 17 00:00:00 2001
From: Ben Freiberg <9841563+bfreiberg@users.noreply.github.com>
Date: Mon, 16 Mar 2026 09:53:10 +0100
Subject: [PATCH 2/3] docs(lambda-durable-bedrock-async-invoke-cdk): Update
README with configuration and testing guidance
- Add Configuration section documenting environment variables (BEDROCK_MODEL_ID, BEDROCK_REGION, OUTPUT_BUCKET_NAME)
- Include table with variable defaults and descriptions for clarity
- Move unit test instructions earlier in deployment workflow (before CDK deploy)
- Clarify that tests verify both CDK infrastructure and durable handler functionality
- Update pattern description to better explain polling alternatives and cost benefits
---
.../README.md | 36 ++++++++++++-------
1 file changed, 23 insertions(+), 13 deletions(-)
diff --git a/lambda-durable-bedrock-async-invoke-cdk/README.md b/lambda-durable-bedrock-async-invoke-cdk/README.md
index c1e43e1d06..48e2993548 100644
--- a/lambda-durable-bedrock-async-invoke-cdk/README.md
+++ b/lambda-durable-bedrock-async-invoke-cdk/README.md
@@ -2,7 +2,7 @@
This pattern shows how to use AWS Lambda durable functions to orchestrate [Amazon Bedrock Async Invoke](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_StartAsyncInvoke.html) for AI video generation. The durable function starts an Amazon Nova Reel video generation job, then polls for completion using `waitForCondition` with exponential backoff. During each polling interval the function suspends execution entirely, incurring zero compute charges while Bedrock processes the video.
-Without durable functions this pattern would require a separate polling mechanism such as Step Functions, EventBridge rules, or a cron-based poller. Here the entire workflow is a single, linear function that reads top-to-bottom.
+Without durable functions this pattern would require separate polling mechanism such as in-process waiting (charging for idle time) or additional infrastructure for orchestration or cron schedules. Here the entire workflow is a single, linear function that reads top-to-bottom.
Learn more about this pattern at Serverless Land Patterns: [https://serverlessland.com/patterns/lambda-durable-bedrock-async-invoke](https://serverlessland.com/patterns/lambda-durable-bedrock-async-invoke)
@@ -17,6 +17,18 @@ Important: this application uses various AWS services and there are costs associ
* [Node.js 22.x](https://nodejs.org/) installed
* Amazon Bedrock model access enabled for **Amazon Nova Reel** (`amazon.nova-reel-v1:1`) in your target region
+## Configuration
+
+The Lambda function reads the following environment variables. The CDK stack sets `OUTPUT_BUCKET_NAME` automatically; the others can be overridden if needed.
+
+| Variable | Default | Description |
+|---|---|---|
+| `BEDROCK_MODEL_ID` | `amazon.nova-reel-v1:1` | Bedrock model identifier for video generation. Change this if you want to use a different model. |
+| `BEDROCK_REGION` | `us-east-1` | AWS region where the Bedrock model is available. Override this if your model access is enabled in a different region (e.g. `us-west-2`). |
+| `OUTPUT_BUCKET_NAME` | *(set by CDK)* | S3 bucket where Bedrock writes the generated video. Provisioned and configured automatically by the CDK stack. |
+
+To override `BEDROCK_MODEL_ID` or `BEDROCK_REGION`, update the Lambda environment variables in `lib/cdk-bedrock-async-invoke-stack.ts` or pass them as CDK context values before deploying.
+
## Deployment Instructions
1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository:
@@ -37,6 +49,12 @@ Important: this application uses various AWS services and there are costs associ
npm install
```
+1. Run unit tests to verify everything is working:
+
+ ```bash
+ npm test
+ ```
+
1. Deploy the CDK stack:
```bash
@@ -44,10 +62,10 @@ Important: this application uses various AWS services and there are costs associ
```
The stack creates:
- - An S3 bucket for video output (auto-deleted on stack destroy, 7-day lifecycle)
- - A durable Lambda function with 30-minute execution timeout
- - IAM permissions for Bedrock and S3 access
- - A CloudWatch log group with 1-week retention
+ * An S3 bucket for video output (auto-deleted on stack destroy, 7-day lifecycle)
+ * A durable Lambda function with 30-minute execution timeout
+ * IAM permissions for Bedrock and S3 access
+ * A CloudWatch log group with 1-week retention
1. Note the outputs from the CDK deployment process. These contain the resource names and ARNs used for testing.
@@ -112,14 +130,6 @@ aws lambda get-durable-execution \
Once the status shows `SUCCEEDED`, the result will contain the S3 URI where the video was written.
-### Run unit tests
-
-```bash
-npm test
-```
-
-This runs both CDK infrastructure tests and durable handler tests (with mocked Bedrock calls).
-
## Cleanup
1. Delete the stack:
From cfb396b81d3b3f29c6f655a25f59c2216dedeab2 Mon Sep 17 00:00:00 2001
From: Ben Freiberg <9841563+bfreiberg@users.noreply.github.com>
Date: Fri, 20 Mar 2026 21:49:24 +0100
Subject: [PATCH 3/3] feat(lambda-durable-bedrock-async-invoke-cdk): Scope
Bedrock permissions and update paths
- Add log file exclusions to .gitignore (error.log, combined.log)
- Update README documentation links and directory paths to reflect -cdk suffix
- Update example-pattern.json repository and project folder references to -cdk variant
- Refactor IAM policy statements to scope Bedrock permissions to Nova Reel model ARN
- Split GetAsyncInvoke permission into separate policy statement scoped to invocation ARNs
- Update test to verify scoped Bedrock permissions are correctly applied
- Improves security posture by following principle of least privilege for Bedrock access
---
.../.gitignore | 4 ++++
.../README.md | 4 ++--
.../example-pattern.json | 6 +++---
.../lib/cdk-bedrock-async-invoke-stack.ts | 16 +++++++++++++---
.../test/cdk-bedrock-async-invoke.test.ts | 7 +++++--
5 files changed, 27 insertions(+), 10 deletions(-)
diff --git a/lambda-durable-bedrock-async-invoke-cdk/.gitignore b/lambda-durable-bedrock-async-invoke-cdk/.gitignore
index f60797b6a9..c8ac2b1e9e 100644
--- a/lambda-durable-bedrock-async-invoke-cdk/.gitignore
+++ b/lambda-durable-bedrock-async-invoke-cdk/.gitignore
@@ -6,3 +6,7 @@ node_modules
# CDK asset staging directory
.cdk.staging
cdk.out
+
+# Log files
+error.log
+combined.log
diff --git a/lambda-durable-bedrock-async-invoke-cdk/README.md b/lambda-durable-bedrock-async-invoke-cdk/README.md
index 48e2993548..828716f0ca 100644
--- a/lambda-durable-bedrock-async-invoke-cdk/README.md
+++ b/lambda-durable-bedrock-async-invoke-cdk/README.md
@@ -4,7 +4,7 @@ This pattern shows how to use AWS Lambda durable functions to orchestrate [Amazo
Without durable functions this pattern would require separate polling mechanism such as in-process waiting (charging for idle time) or additional infrastructure for orchestration or cron schedules. Here the entire workflow is a single, linear function that reads top-to-bottom.
-Learn more about this pattern at Serverless Land Patterns: [https://serverlessland.com/patterns/lambda-durable-bedrock-async-invoke](https://serverlessland.com/patterns/lambda-durable-bedrock-async-invoke)
+Learn more about this pattern at Serverless Land Patterns: [https://serverlessland.com/patterns/lambda-durable-bedrock-async-invoke-cdk](https://serverlessland.com/patterns/lambda-durable-bedrock-async-invoke-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.
@@ -40,7 +40,7 @@ To override `BEDROCK_MODEL_ID` or `BEDROCK_REGION`, update the Lambda environmen
1. Change directory to the pattern directory:
```bash
- cd lambda-durable-bedrock-async-invoke
+ cd lambda-durable-bedrock-async-invoke-cdk
```
1. Install the project dependencies:
diff --git a/lambda-durable-bedrock-async-invoke-cdk/example-pattern.json b/lambda-durable-bedrock-async-invoke-cdk/example-pattern.json
index 30f79b0565..e18c80df81 100644
--- a/lambda-durable-bedrock-async-invoke-cdk/example-pattern.json
+++ b/lambda-durable-bedrock-async-invoke-cdk/example-pattern.json
@@ -14,9 +14,9 @@
},
"gitHub": {
"template": {
- "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/lambda-durable-bedrock-async-invoke",
- "templateURL": "serverless-patterns/lambda-durable-bedrock-async-invoke",
- "projectFolder": "lambda-durable-bedrock-async-invoke",
+ "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/lambda-durable-bedrock-async-invoke-cdk",
+ "templateURL": "serverless-patterns/lambda-durable-bedrock-async-invoke-cdk",
+ "projectFolder": "lambda-durable-bedrock-async-invoke-cdk",
"templateFile": "lib/cdk-bedrock-async-invoke-stack.ts"
}
},
diff --git a/lambda-durable-bedrock-async-invoke-cdk/lib/cdk-bedrock-async-invoke-stack.ts b/lambda-durable-bedrock-async-invoke-cdk/lib/cdk-bedrock-async-invoke-stack.ts
index 0a6ab0affa..f566801b79 100644
--- a/lambda-durable-bedrock-async-invoke-cdk/lib/cdk-bedrock-async-invoke-stack.ts
+++ b/lambda-durable-bedrock-async-invoke-cdk/lib/cdk-bedrock-async-invoke-stack.ts
@@ -64,15 +64,25 @@ export class CdkBedrockAsyncInvokeStack extends cdk.Stack {
// when writing to the bucket via the async invocation.
outputBucket.grantReadWrite(videoGeneratorFunction);
- // Grant Bedrock invocation permissions
+ // Grant Bedrock invocation permissions scoped to the Nova Reel model
+ const bedrockRegion = 'us-east-1';
+ const modelArn = `arn:aws:bedrock:${bedrockRegion}::foundation-model/amazon.nova-reel-v1:*`;
+
videoGeneratorFunction.addToRolePolicy(
new iam.PolicyStatement({
actions: [
'bedrock:InvokeModel',
- 'bedrock:GetAsyncInvoke',
'bedrock:StartAsyncInvoke',
],
- resources: ['*'],
+ resources: [modelArn],
+ }),
+ );
+
+ // GetAsyncInvoke operates on invocation ARNs, not model ARNs
+ videoGeneratorFunction.addToRolePolicy(
+ new iam.PolicyStatement({
+ actions: ['bedrock:GetAsyncInvoke'],
+ resources: [`arn:aws:bedrock:${bedrockRegion}:${this.account}:async-invoke/*`],
}),
);
diff --git a/lambda-durable-bedrock-async-invoke-cdk/test/cdk-bedrock-async-invoke.test.ts b/lambda-durable-bedrock-async-invoke-cdk/test/cdk-bedrock-async-invoke.test.ts
index 181c147923..84c764fe45 100644
--- a/lambda-durable-bedrock-async-invoke-cdk/test/cdk-bedrock-async-invoke.test.ts
+++ b/lambda-durable-bedrock-async-invoke-cdk/test/cdk-bedrock-async-invoke.test.ts
@@ -20,16 +20,19 @@ describe('CdkBedrockAsyncInvokeStack', () => {
});
});
- test('grants Bedrock permissions', () => {
+ test('grants scoped Bedrock permissions', () => {
template.hasResourceProperties('AWS::IAM::Policy', {
PolicyDocument: {
Statement: Match.arrayWith([
Match.objectLike({
Action: Match.arrayWith([
'bedrock:InvokeModel',
- 'bedrock:GetAsyncInvoke',
'bedrock:StartAsyncInvoke',
]),
+ Resource: 'arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-reel-v1:*',
+ }),
+ Match.objectLike({
+ Action: 'bedrock:GetAsyncInvoke',
}),
]),
},