diff --git a/eventbridge-sqs-fair-queue-lambda-cdk/.gitignore b/eventbridge-sqs-fair-queue-lambda-cdk/.gitignore new file mode 100644 index 000000000..2303e9b95 --- /dev/null +++ b/eventbridge-sqs-fair-queue-lambda-cdk/.gitignore @@ -0,0 +1,6 @@ +node_modules +cdk.out +*.js +!src/**/*.js +*.d.ts +package-lock.json diff --git a/eventbridge-sqs-fair-queue-lambda-cdk/README.md b/eventbridge-sqs-fair-queue-lambda-cdk/README.md new file mode 100644 index 000000000..9e8766d49 --- /dev/null +++ b/eventbridge-sqs-fair-queue-lambda-cdk/README.md @@ -0,0 +1,102 @@ +# EventBridge to SQS Fair Queue with Lambda Consumer + +This pattern deploys an EventBridge rule that routes events to an SQS fair queue, consumed by a Lambda function. Fair queues ensure equitable processing across tenants — no single tenant can monopolize the consumer. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/eventbridge-sqs-fair-queue-lambda-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. + +> **⚠️ Important Note:** As of April 2026, `FairQueueConfiguration` is **API-only** and not yet supported in the CloudFormation schema. This means the fair queue configuration cannot be deployed via CDK/CloudFormation. This pattern deploys the standard SQS queue, EventBridge rule, Lambda consumer, and DLQ via CDK, but the fair queue enablement must be done separately via the AWS CLI or SDK after deployment: +> +> ```bash +> aws sqs set-queue-attributes \ +> --queue-url \ +> --attributes '{"FairQueueConfiguration":"{\"MessageGroupIdFieldPath\":\"$.detail.tenantId\"}"}' +> ``` +> +> Once CloudFormation adds `FairQueueConfiguration` support, this pattern will be updated to deploy fully via CDK. + +## 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. +* [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 + +## Architecture + +``` +┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────┐ +│ EventBridge │────▶│ EventBridge │────▶│ SQS Fair Queue │────▶│ Lambda │ +│ PutEvents │ │ Rule │ │ (per-tenant │ │ Consumer │ +│ (tenant-id) │ │ (source match) │ │ fair sharing) │ │ │ +└──────────────┘ └──────────────────┘ └──────────────────┘ └──────────────┘ +``` + +## How it works + +1. Events are published to EventBridge with a `detail.tenantId` field identifying the tenant. +2. An EventBridge rule matches events with `source: "com.myapp.orders"` and routes them to an SQS fair queue. +3. The fair queue uses `tenantId` as the message group ID, ensuring equitable consumption across tenants. +4. A Lambda function processes messages from the fair queue. Even if Tenant A sends 1000 messages and Tenant B sends 10, both tenants get fair processing time. +5. A dead-letter queue captures failed messages after 3 retries. + +## Deployment Instructions + +1. Clone the repository: + ```bash + git clone https://github.com/aws-samples/serverless-patterns + cd serverless-patterns/eventbridge-sqs-fair-queue-lambda-cdk + ``` + +2. Install dependencies: + ```bash + npm install + ``` + +3. Deploy the stack: + ```bash + cdk deploy + ``` + +4. Note the EventBridge bus name and queue URL from the stack outputs. + +## Testing + +1. Send events for multiple tenants: + ```bash + # Tenant A — burst of events + for i in $(seq 1 5); do + aws events put-events --entries '[{ + "Source": "com.myapp.orders", + "DetailType": "OrderCreated", + "Detail": "{\"tenantId\": \"tenant-a\", \"orderId\": \"order-a-'$i'\", \"amount\": 99.99}", + "EventBusName": "" + }]' + done + + # Tenant B — single event + aws events put-events --entries '[{ + "Source": "com.myapp.orders", + "DetailType": "OrderCreated", + "Detail": "{\"tenantId\": \"tenant-b\", \"orderId\": \"order-b-1\", \"amount\": 49.99}", + "EventBusName": "" + }]' + ``` + +2. Check Lambda logs to verify fair processing — Tenant B's message should not be starved by Tenant A's burst: + ```bash + aws logs tail /aws/lambda/ --follow + ``` + +## Cleanup + +```bash +cdk destroy +``` + +---- +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/eventbridge-sqs-fair-queue-lambda-cdk/bin/app.ts b/eventbridge-sqs-fair-queue-lambda-cdk/bin/app.ts new file mode 100644 index 000000000..f4e802bd2 --- /dev/null +++ b/eventbridge-sqs-fair-queue-lambda-cdk/bin/app.ts @@ -0,0 +1,10 @@ +#!/usr/bin/env node +import "source-map-support/register"; +import * as cdk from "aws-cdk-lib"; +import { EventBridgeSqsFairQueueLambdaStack } from "../lib/eventbridge-sqs-fair-queue-lambda-stack"; + +const app = new cdk.App(); +new EventBridgeSqsFairQueueLambdaStack( + app, + "EventBridgeSqsFairQueueLambdaStack" +); diff --git a/eventbridge-sqs-fair-queue-lambda-cdk/cdk.json b/eventbridge-sqs-fair-queue-lambda-cdk/cdk.json new file mode 100644 index 000000000..822400f59 --- /dev/null +++ b/eventbridge-sqs-fair-queue-lambda-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/eventbridge-sqs-fair-queue-lambda-cdk/example-pattern.json b/eventbridge-sqs-fair-queue-lambda-cdk/example-pattern.json new file mode 100644 index 000000000..cb188a16c --- /dev/null +++ b/eventbridge-sqs-fair-queue-lambda-cdk/example-pattern.json @@ -0,0 +1,61 @@ +{ + "title": "EventBridge to SQS Fair Queue with Lambda Consumer", + "description": "Route events from Amazon EventBridge to an SQS fair queue for equitable multi-tenant processing with Lambda.", + "language": "TypeScript", + "level": "300", + "framework": "AWS CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern deploys an EventBridge rule that routes events to an SQS fair queue, consumed by a Lambda function. Fair queues ensure equitable processing across tenants by preventing any single tenant from monopolizing the consumer.", + "Events are published to EventBridge with a tenant ID. The EventBridge rule matches events and forwards them to an SQS fair queue with the tenant ID as the message group. SQS fair queues automatically balance consumption across groups, ensuring no single tenant starves others.", + "This pattern is ideal for multi-tenant SaaS applications where you need fair processing guarantees without building custom throttling logic." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/eventbridge-sqs-fair-queue-lambda-cdk", + "templateURL": "serverless-patterns/eventbridge-sqs-fair-queue-lambda-cdk", + "projectFolder": "eventbridge-sqs-fair-queue-lambda-cdk", + "templateFile": "lib/eventbridge-sqs-fair-queue-lambda-stack.ts" + } + }, + "resources": { + "bullets": [ + { + "text": "Amazon EventBridge targets SQS fair queues", + "link": "https://aws.amazon.com/about-aws/whats-new/2025/11/amazon-eventbridge-sqs-fair-queue-targets/" + }, + { + "text": "Amazon SQS fair queues", + "link": "https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/fair-queues.html" + }, + { + "text": "Amazon EventBridge", + "link": "https://aws.amazon.com/eventbridge/" + } + ] + }, + "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/eventbridge-sqs-fair-queue-lambda-cdk/lib/eventbridge-sqs-fair-queue-lambda-stack.ts b/eventbridge-sqs-fair-queue-lambda-cdk/lib/eventbridge-sqs-fair-queue-lambda-stack.ts new file mode 100644 index 000000000..64e0f38f9 --- /dev/null +++ b/eventbridge-sqs-fair-queue-lambda-cdk/lib/eventbridge-sqs-fair-queue-lambda-stack.ts @@ -0,0 +1,113 @@ +import * as cdk from "aws-cdk-lib"; +import * as sqs from "aws-cdk-lib/aws-sqs"; +import * as lambda from "aws-cdk-lib/aws-lambda"; +import * as events from "aws-cdk-lib/aws-events"; +import * as iam from "aws-cdk-lib/aws-iam"; +import * as logs from "aws-cdk-lib/aws-logs"; +import { Construct } from "constructs"; + +export class EventBridgeSqsFairQueueLambdaStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // Dead-letter queue for failed messages + const dlq = new sqs.Queue(this, "DLQ", { + queueName: "fair-queue-dlq", + retentionPeriod: cdk.Duration.days(14), + }); + + // SQS Fair Queue (uses CfnQueue — fair queues are a new queue type) + const cfnFairQueue = new sqs.CfnQueue(this, "FairQueue", { + queueName: "tenant-fair-queue", + visibilityTimeout: 60, + messageRetentionPeriod: 345600, // 4 days + redrivePolicy: { + deadLetterTargetArn: dlq.queueArn, + maxReceiveCount: 3, + }, + }); + + // Enable fair queue configuration via property override + (cfnFairQueue as any).addPropertyOverride("FairQueueConfiguration", { + NumberOfMessageGroups: 100, + }); + + const fairQueueArn = cfnFairQueue.attrArn; + const fairQueueUrl = cfnFairQueue.ref; + + // Import the fair queue as an IQueue for event source mapping + const fairQueue = sqs.Queue.fromQueueAttributes(this, "FairQueueImport", { + queueArn: fairQueueArn, + queueUrl: fairQueueUrl, + }); + + // Custom EventBridge bus + const bus = new events.EventBus(this, "OrdersBus", { + eventBusName: "orders-bus", + }); + + // EventBridge rule — match order events and route to fair queue + // Uses CfnRule because L2 SqsQueue target rejects messageGroupId on non-FIFO queues + const rule = new events.CfnRule(this, "OrderRule", { + eventBusName: bus.eventBusName, + eventPattern: { + source: ["com.myapp.orders"], + }, + targets: [ + { + id: "FairQueueTarget", + arn: fairQueueArn, + sqsParameters: { + messageGroupId: "$.detail.tenantId", + }, + }, + ], + }); + + // Grant EventBridge permission to send to the fair queue + const sendPolicy = new iam.PolicyStatement({ + actions: ["sqs:SendMessage"], + resources: [fairQueueArn], + principals: [new iam.ServicePrincipal("events.amazonaws.com")], + }); + const queuePolicy = new sqs.CfnQueuePolicy(this, "FairQueuePolicy", { + queues: [fairQueueUrl], + policyDocument: { + Statement: [ + { + Effect: "Allow", + Principal: { Service: "events.amazonaws.com" }, + Action: "sqs:SendMessage", + Resource: fairQueueArn, + Condition: { + ArnEquals: { "aws:SourceArn": rule.attrArn }, + }, + }, + ], + }, + }); + + // Lambda consumer + const fn = new lambda.Function(this, "ConsumerFn", { + runtime: lambda.Runtime.NODEJS_20_X, + handler: "index.handler", + code: lambda.Code.fromAsset("src"), + timeout: cdk.Duration.seconds(30), + memorySize: 128, + logRetention: logs.RetentionDays.ONE_WEEK, + }); + + // SQS event source mapping + fn.addEventSource( + new (require("aws-cdk-lib/aws-lambda-event-sources").SqsEventSource)( + fairQueue, + { batchSize: 10, maxBatchingWindow: cdk.Duration.seconds(5) } + ) + ); + + new cdk.CfnOutput(this, "EventBusName", { value: bus.eventBusName }); + new cdk.CfnOutput(this, "FairQueueUrl", { value: fairQueueUrl }); + new cdk.CfnOutput(this, "FunctionName", { value: fn.functionName }); + new cdk.CfnOutput(this, "DLQUrl", { value: dlq.queueUrl }); + } +} diff --git a/eventbridge-sqs-fair-queue-lambda-cdk/package.json b/eventbridge-sqs-fair-queue-lambda-cdk/package.json new file mode 100644 index 000000000..fb29b7916 --- /dev/null +++ b/eventbridge-sqs-fair-queue-lambda-cdk/package.json @@ -0,0 +1,20 @@ +{ + "name": "eventbridge-sqs-fair-queue-lambda-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/eventbridge-sqs-fair-queue-lambda-cdk/src/index.js b/eventbridge-sqs-fair-queue-lambda-cdk/src/index.js new file mode 100644 index 000000000..4d2bf8564 --- /dev/null +++ b/eventbridge-sqs-fair-queue-lambda-cdk/src/index.js @@ -0,0 +1,19 @@ +exports.handler = async (event) => { + for (const record of event.Records) { + const body = JSON.parse(record.body); + const detail = body.detail || {}; + + console.log( + JSON.stringify({ + tenantId: detail.tenantId, + orderId: detail.orderId, + amount: detail.amount, + messageGroupId: record.attributes?.MessageGroupId, + messageId: record.messageId, + timestamp: new Date().toISOString(), + }) + ); + } + + return { batchItemFailures: [] }; +}; diff --git a/eventbridge-sqs-fair-queue-lambda-cdk/tsconfig.json b/eventbridge-sqs-fair-queue-lambda-cdk/tsconfig.json new file mode 100644 index 000000000..15e54de36 --- /dev/null +++ b/eventbridge-sqs-fair-queue-lambda-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"] +}