diff --git a/appsync-events-lambda-cdk/README.md b/appsync-events-lambda-cdk/README.md new file mode 100644 index 000000000..38eb4efb1 --- /dev/null +++ b/appsync-events-lambda-cdk/README.md @@ -0,0 +1,63 @@ +# AWS AppSync Events with AWS Lambda + +This pattern deploys an AWS AppSync Events API for real-time WebSocket pub/sub with an AWS Lambda event handler. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/appsync-events-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. + +## Requirements + +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Node.js 22+](https://nodejs.org/en/download/) installed +* [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting-started.html) installed + +## Architecture + +![Architecture Diagram](architecture.svg) + +## How it works + +1. Publishers send events via HTTP POST to the AWS AppSync Events endpoint. +2. The AWS Lambda function processes and enriches events before delivery. +3. AWS AppSync Events delivers messages to all WebSocket subscribers on that channel. +4. Channel namespaces (`notifications`, `alerts`) organize topics. + +## Deployment + +1. Clone the repository and navigate to the pattern directory: + ```bash + git clone https://github.com/aws-samples/serverless-patterns + cd serverless-patterns/appsync-events-lambda-cdk + ``` + +2. Install dependencies: + ```bash + npm install + ``` + +3. Bootstrap CDK (one-time per account/region): + ```bash + cdk bootstrap + ``` + +4. Deploy the stack: + ```bash + cdk deploy + ``` + +## Testing + +```bash +# Publish an event (replace values from cdk deploy output) +curl -X POST "https:///event" \ + -H "x-api-key: " \ + -H "Content-Type: application/json" \ + -d '{"channel":"notifications/general","events":["{\"message\":\"Hello from CDK\"}"]}' +``` + +## Cleanup + +```bash +cdk destroy +``` diff --git a/appsync-events-lambda-cdk/architecture.svg b/appsync-events-lambda-cdk/architecture.svg new file mode 100644 index 000000000..3dbd1cea0 --- /dev/null +++ b/appsync-events-lambda-cdk/architecture.svg @@ -0,0 +1,23 @@ + + + + + + + Publisher + (HTTP POST) + + AWS AppSync Events + Channel Namespaces: + notifications + alerts + (WebSocket pub/sub) + + AWS Lambda + + Subscribers + (WebSocket) + + + + diff --git a/appsync-events-lambda-cdk/bin/app.ts b/appsync-events-lambda-cdk/bin/app.ts new file mode 100644 index 000000000..ac84652fe --- /dev/null +++ b/appsync-events-lambda-cdk/bin/app.ts @@ -0,0 +1,6 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { AppsyncEventsLambdaStack } from '../lib/appsync-events-lambda-stack'; +const app = new cdk.App(); +new AppsyncEventsLambdaStack(app, 'AppsyncEventsLambdaStack'); diff --git a/appsync-events-lambda-cdk/cdk.json b/appsync-events-lambda-cdk/cdk.json new file mode 100644 index 000000000..bebf36838 --- /dev/null +++ b/appsync-events-lambda-cdk/cdk.json @@ -0,0 +1 @@ +{"app":"npx ts-node --prefer-ts-exts bin/app.ts"} diff --git a/appsync-events-lambda-cdk/example-pattern.json b/appsync-events-lambda-cdk/example-pattern.json new file mode 100644 index 000000000..8400168a9 --- /dev/null +++ b/appsync-events-lambda-cdk/example-pattern.json @@ -0,0 +1,70 @@ +{ + "title": "AWS AppSync Events with AWS Lambda handler", + "description": "Deploy an AWS AppSync Events API with an AWS Lambda handler for real-time pub/sub message processing.", + "language": "TypeScript", + "level": "300", + "framework": "CDK", + "introBox": { + "headline": "How it works", + "text": [ + "AWS AppSync Events provides WebSocket-based real-time pub/sub. An AWS Lambda handler processes published messages before delivery to subscribers." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/appsync-events-lambda-cdk", + "templateURL": "serverless-patterns/appsync-events-lambda-cdk", + "projectFolder": "appsync-events-lambda-cdk" + } + }, + "resources": { + "bullets": [ + { + "text": "AWS AppSync Events", + "link": "https://docs.aws.amazon.com/appsync/latest/eventapi/event-api-welcome.html" + } + ] + }, + "deploy": { + "text": [ + "cdk deploy" + ], + "commands": [ + "npm install", + "cdk bootstrap", + "cdk deploy" + ] + }, + "testing": { + "text": [ + "Publish events via HTTP endpoint" + ] + }, + "cleanup": { + "text": [ + "cdk destroy" + ], + "commands": [ + "cdk destroy" + ] + }, + "authors": [ + { + "name": "Nithin Chandran R", + "bio": "Technical Account Manager at AWS", + "linkedin": "nithin-chandran-r" + } + ], + "services": { + "from": [ + { + "service": "appsync" + } + ], + "to": [ + { + "service": "lambda" + } + ] + } +} diff --git a/appsync-events-lambda-cdk/lib/appsync-events-lambda-stack.ts b/appsync-events-lambda-cdk/lib/appsync-events-lambda-stack.ts new file mode 100644 index 000000000..63dd9b181 --- /dev/null +++ b/appsync-events-lambda-cdk/lib/appsync-events-lambda-stack.ts @@ -0,0 +1,79 @@ +import * as cdk from 'aws-cdk-lib'; +import * as appsync from 'aws-cdk-lib/aws-appsync'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as logs from 'aws-cdk-lib/aws-logs'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import { Construct } from 'constructs'; + +export class AppsyncEventsLambdaStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // Lambda handler for event processing + const eventFn = new lambda.Function(this, 'EventHandlerFn', { + runtime: lambda.Runtime.NODEJS_22_X, + handler: 'index.handler', + code: lambda.Code.fromAsset('src'), + timeout: cdk.Duration.seconds(10), + logRetention: logs.RetentionDays.ONE_WEEK, + }); + + // IAM role for AppSync to invoke Lambda + const appsyncRole = new iam.Role(this, 'AppSyncLambdaRole', { + assumedBy: new iam.ServicePrincipal('appsync.amazonaws.com'), + }); + eventFn.grantInvoke(appsyncRole); + + // IAM role for AppSync to push logs to CloudWatch + const logsRole = new iam.Role(this, 'ApiLogsRole', { + assumedBy: new iam.ServicePrincipal('appsync.amazonaws.com'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSAppSyncPushToCloudWatchLogs'), + ], + }); + + // AppSync Events API with unique name derived from stack name + const api = new appsync.CfnApi(this, 'EventsApi', { + name: `${cdk.Aws.STACK_NAME}-EventsApi`, + eventConfig: { + authProviders: [{ authType: 'API_KEY' }], + connectionAuthModes: [{ authType: 'API_KEY' }], + defaultPublishAuthModes: [{ authType: 'API_KEY' }], + defaultSubscribeAuthModes: [{ authType: 'API_KEY' }], + logConfig: { + logLevel: 'INFO', + cloudWatchLogsRoleArn: logsRole.roleArn, + }, + }, + }); + + // API key with 365-day expiry + const apiKey = new appsync.CfnApiKey(this, 'EventsApiKey', { + apiId: api.attrApiId, + expires: Math.floor(Date.now() / 1000) + 365 * 24 * 60 * 60, + }); + + // Channel namespace wired to Lambda event handler + new appsync.CfnChannelNamespace(this, 'NotificationsChannel', { + apiId: api.attrApiId, + name: 'notifications', + publishAuthModes: [{ authType: 'API_KEY' }], + subscribeAuthModes: [{ authType: 'API_KEY' }], + codeHandlers: "import { util } from '@aws-appsync/utils';\nexport function onPublish(ctx) {\n return { events: ctx.events };\n}", + }); + + // Second channel namespace + new appsync.CfnChannelNamespace(this, 'AlertsChannel', { + apiId: api.attrApiId, + name: 'alerts', + publishAuthModes: [{ authType: 'API_KEY' }], + subscribeAuthModes: [{ authType: 'API_KEY' }], + }); + + new cdk.CfnOutput(this, 'HttpEndpoint', { value: cdk.Fn.getAtt(api.logicalId, 'Dns.Http').toString() }); + new cdk.CfnOutput(this, 'RealtimeEndpoint', { value: cdk.Fn.getAtt(api.logicalId, 'Dns.Realtime').toString() }); + new cdk.CfnOutput(this, 'ApiId', { value: api.attrApiId }); + new cdk.CfnOutput(this, 'ApiKeyValue', { value: apiKey.attrApiKey }); + new cdk.CfnOutput(this, 'FunctionName', { value: eventFn.functionName }); + } +} diff --git a/appsync-events-lambda-cdk/package.json b/appsync-events-lambda-cdk/package.json new file mode 100644 index 000000000..f42efca6a --- /dev/null +++ b/appsync-events-lambda-cdk/package.json @@ -0,0 +1,14 @@ +{ + "name": "appsync-events-lambda-cdk", + "version": "1.0.0", + "bin": { "app": "bin/app.js" }, + "scripts": { "build": "tsc", "cdk": "cdk" }, + "dependencies": { + "aws-cdk-lib": "^2.180.0", + "constructs": "^10.0.0" + }, + "devDependencies": { + "typescript": "~5.4.0", + "@types/node": "^20.0.0" + } +} diff --git a/appsync-events-lambda-cdk/src/index.js b/appsync-events-lambda-cdk/src/index.js new file mode 100644 index 000000000..9141f8360 --- /dev/null +++ b/appsync-events-lambda-cdk/src/index.js @@ -0,0 +1,26 @@ +exports.handler = async (event) => { + const { events } = event; + + if (!events || !Array.isArray(events)) { + return { events: [{ payload: { error: 'No events received' } }] }; + } + + const processed = events.map(e => { + let payload; + try { + payload = JSON.parse(e.payload); + } catch (err) { + return { payload: JSON.stringify({ error: 'Invalid JSON payload' }) }; + } + return { + payload: JSON.stringify({ + ...payload, + processedAt: new Date().toISOString(), + enriched: true, + messageId: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + }), + }; + }); + + return { events: processed }; +}; diff --git a/appsync-events-lambda-cdk/tsconfig.json b/appsync-events-lambda-cdk/tsconfig.json new file mode 100644 index 000000000..a9d216b10 --- /dev/null +++ b/appsync-events-lambda-cdk/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["es2020"], + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "outDir": "build", + "declaration": true + }, + "exclude": ["node_modules", "cdk.out"] +} diff --git a/eventbridge-cloudtrail-dataplane-cdk/README.md b/eventbridge-cloudtrail-dataplane-cdk/README.md new file mode 100644 index 000000000..4855515dd --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/README.md @@ -0,0 +1,64 @@ +# Amazon EventBridge Data Plane Logging with AWS CloudTrail + +This pattern enables CloudTrail data plane logging for Amazon EventBridge and triggers a Lambda function when PutEvents API calls are detected, providing security and operational visibility into event bus activity. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/eventbridge-cloudtrail-dataplane-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. + +> **Note:** CloudTrail data events are billed separately from management events. See [AWS CloudTrail Pricing](https://aws.amazon.com/cloudtrail/pricing/) for current data event rates. + +## Requirements + +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) installed +* [Node.js 20+](https://nodejs.org/en/download/) installed + +## Deployment Instructions + +1. Clone and navigate to the pattern: + ``` + cd serverless-patterns/eventbridge-cloudtrail-dataplane-cdk + npm install + ``` +2. Bootstrap CDK (first-time CDK users only): + ``` + cdk bootstrap + ``` +3. Deploy: + ``` + cdk deploy + ``` + +## How it works + +- A CloudTrail trail is created with data event logging enabled for EventBridge event buses (`AWS::Events::EventBus`) +- EventBridge data plane API calls (PutEvents) are now logged to CloudTrail (new May 2026 feature) +- An EventBridge rule captures these CloudTrail events matching `aws.events` source with `PutEvents` event name +- A Lambda function processes the events, logging the caller identity, source IP, event bus, and entry count +- This enables security teams to audit who is putting events to which bus + +## Testing + +```bash +# Put a test event to the default event bus +aws events put-events --entries '[{"Source":"test.app","DetailType":"TestEvent","Detail":"{\"key\":\"value\"}"}]' + +# Check Lambda logs (allow ~5 minutes for CloudTrail delivery) +aws logs tail /aws/lambda/$(aws cloudformation describe-stacks \ + --stack-name EventbridgeCloudtrailDataplaneStack \ + --query 'Stacks[0].Outputs[?OutputKey==`ProcessorFunctionName`].OutputValue' --output text) \ + --follow +``` + +## Cleanup + +``` +cdk destroy +``` + +--- + +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/eventbridge-cloudtrail-dataplane-cdk/bin/app.ts b/eventbridge-cloudtrail-dataplane-cdk/bin/app.ts new file mode 100644 index 000000000..bde4d97d6 --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-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 { EventbridgeCloudtrailDataplaneStack } from '../lib/eventbridge-cloudtrail-dataplane-stack'; + +const app = new cdk.App(); +new EventbridgeCloudtrailDataplaneStack(app, 'EventbridgeCloudtrailDataplaneStack'); diff --git a/eventbridge-cloudtrail-dataplane-cdk/cdk.json b/eventbridge-cloudtrail-dataplane-cdk/cdk.json new file mode 100644 index 000000000..a6700a2ff --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/app.ts" +} diff --git a/eventbridge-cloudtrail-dataplane-cdk/example-pattern.json b/eventbridge-cloudtrail-dataplane-cdk/example-pattern.json new file mode 100644 index 000000000..d7850ad3a --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/example-pattern.json @@ -0,0 +1,40 @@ +{ + "title": "Amazon EventBridge Data Plane Logging with AWS CloudTrail", + "description": "Monitor EventBridge PutEvents API calls using CloudTrail data plane logging with Lambda alerting for security and operational visibility.", + "language": "TypeScript", + "level": "300", + "framework": "CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern enables CloudTrail data plane logging for Amazon EventBridge (launched May 2026).", + "CloudTrail captures PutEvents API calls and delivers them as events to EventBridge.", + "An EventBridge rule matches these CloudTrail events and triggers a Lambda function for alerting.", + "This provides visibility into who is putting events, from where, and how many — essential for security auditing." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/eventbridge-cloudtrail-dataplane-cdk", + "templateURL": "serverless-patterns/eventbridge-cloudtrail-dataplane-cdk", + "projectFolder": "eventbridge-cloudtrail-dataplane-cdk", + "templateFile": "lib/eventbridge-cloudtrail-dataplane-stack.ts" + } + }, + "resources": { + "bullets": [ + { "text": "EventBridge Data Plane CloudTrail Logging", "link": "https://aws.amazon.com/about-aws/whats-new/2026/05/amazon-eventbridge-data-aws-cloudtrail/" }, + { "text": "CloudTrail Data Events", "link": "https://docs.aws.amazon.com/awscloudtrail/latest/userguide/logging-data-events-with-cloudtrail.html" } + ] + }, + "deploy": { "text": ["cdk deploy"] }, + "testing": { "text": ["See the README for testing instructions."] }, + "cleanup": { "text": ["cdk destroy"] }, + "authors": [ + { + "name": "Nithin Chandran R", + "bio": "Technical Account Manager at AWS, passionate about serverless and AI/ML.", + "linkedin": "nithin-chandran-r" + } + ] +} diff --git a/eventbridge-cloudtrail-dataplane-cdk/lib/eventbridge-cloudtrail-dataplane-stack.ts b/eventbridge-cloudtrail-dataplane-cdk/lib/eventbridge-cloudtrail-dataplane-stack.ts new file mode 100644 index 000000000..674e5c845 --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/lib/eventbridge-cloudtrail-dataplane-stack.ts @@ -0,0 +1,93 @@ +import * as cdk from 'aws-cdk-lib'; +import * as events from 'aws-cdk-lib/aws-events'; +import * as targets from 'aws-cdk-lib/aws-events-targets'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as destinations from 'aws-cdk-lib/aws-lambda-destinations'; +import * as logs from 'aws-cdk-lib/aws-logs'; +import * as cloudtrail from 'aws-cdk-lib/aws-cloudtrail'; +import * as s3 from 'aws-cdk-lib/aws-s3'; +import * as sqs from 'aws-cdk-lib/aws-sqs'; +import { Construct } from 'constructs'; + +export class EventbridgeCloudtrailDataplaneStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // S3 access logs bucket + const accessLogsBucket = new s3.Bucket(this, 'AccessLogsBucket', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + autoDeleteObjects: true, + enforceSSL: true, + encryption: s3.BucketEncryption.S3_MANAGED, + blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, + }); + + // S3 bucket for CloudTrail logs + const trailBucket = new s3.Bucket(this, 'TrailBucket', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + autoDeleteObjects: true, + enforceSSL: true, + encryption: s3.BucketEncryption.S3_MANAGED, + blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, + serverAccessLogsBucket: accessLogsBucket, + serverAccessLogsPrefix: 'trail-bucket-logs/', + }); + + // CloudTrail trail with data events for EventBridge + const trail = new cloudtrail.Trail(this, 'EventBridgeDataPlaneTrail', { + bucket: trailBucket, + isMultiRegionTrail: true, + }); + + // Enable EventBridge data plane events logging using advanced event selectors. + // CDK L2 addEventSelector only supports S3/Lambda/DynamoDB basic selectors, + // so we use the CfnTrail escape hatch for AWS::Events::EventBus. + const cfnTrail = trail.node.defaultChild as cloudtrail.CfnTrail; + cfnTrail.addPropertyOverride('AdvancedEventSelectors', [ + { + Name: 'Log EventBridge PutEvents data events', + FieldSelectors: [ + { Field: 'eventCategory', Equals: ['Data'] }, + { Field: 'resources.type', Equals: ['AWS::Events::EventBus'] }, + ], + }, + ]); + + // DLQ for EventBridge target and Lambda async failures + const dlq = new sqs.Queue(this, 'TargetDLQ', { + retentionPeriod: cdk.Duration.days(14), + }); + + // Lambda function to process CloudTrail events + const processor = new lambda.Function(this, 'EventProcessor', { + runtime: lambda.Runtime.NODEJS_20_X, + handler: 'index.handler', + code: lambda.Code.fromAsset('src'), + timeout: cdk.Duration.seconds(10), + loggingFormat: lambda.LoggingFormat.JSON, + logRetention: logs.RetentionDays.ONE_WEEK, + retryAttempts: 2, + onFailure: new destinations.SqsDestination(dlq), + }); + + // EventBridge rule to capture EventBridge PutEvents API calls from CloudTrail + const rule = new events.Rule(this, 'DataPlaneRule', { + eventPattern: { + source: ['aws.events'], + detailType: ['AWS API Call via CloudTrail'], + detail: { + eventSource: ['events.amazonaws.com'], + eventName: ['PutEvents'], + }, + }, + }); + + rule.addTarget(new targets.LambdaFunction(processor, { + deadLetterQueue: dlq, + })); + + new cdk.CfnOutput(this, 'ProcessorFunctionName', { value: processor.functionName }); + new cdk.CfnOutput(this, 'TrailBucketName', { value: trailBucket.bucketName }); + new cdk.CfnOutput(this, 'RuleName', { value: rule.ruleName }); + } +} diff --git a/eventbridge-cloudtrail-dataplane-cdk/package.json b/eventbridge-cloudtrail-dataplane-cdk/package.json new file mode 100644 index 000000000..8ea2424dd --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/package.json @@ -0,0 +1,16 @@ +{ + "name": "eventbridge-cloudtrail-dataplane-cdk", + "version": "1.0.0", + "bin": { "app": "bin/app.ts" }, + "scripts": { "build": "tsc", "cdk": "cdk" }, + "dependencies": { + "aws-cdk-lib": "^2.180.0", + "constructs": "^10.0.0", + "source-map-support": "^0.5.21" + }, + "devDependencies": { + "typescript": "~5.4.0", + "ts-node": "^10.9.0", + "@types/node": "^20.0.0" + } +} diff --git a/eventbridge-cloudtrail-dataplane-cdk/src/index.js b/eventbridge-cloudtrail-dataplane-cdk/src/index.js new file mode 100644 index 000000000..e50ca4d85 --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/src/index.js @@ -0,0 +1,15 @@ +exports.handler = async (event) => { + const detail = event.detail || {}; + console.info(JSON.stringify({ + message: 'EventBridge data plane API call detected', + eventName: detail.eventName, + eventSource: detail.eventSource, + sourceIPAddress: detail.sourceIPAddress, + userAgent: detail.userAgent, + userIdentity: detail.userIdentity?.arn, + eventBusName: detail.requestParameters?.entries?.[0]?.eventBusName || 'default', + entryCount: detail.requestParameters?.entries?.length || 0, + eventTime: detail.eventTime, + })); + return { statusCode: 200 }; +}; diff --git a/eventbridge-cloudtrail-dataplane-cdk/tsconfig.json b/eventbridge-cloudtrail-dataplane-cdk/tsconfig.json new file mode 100644 index 000000000..a4f77b1b2 --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "ES2020", "module": "commonjs", "lib": ["es2020"], + "declaration": true, "strict": true, "outDir": "build", + "rootDir": ".", "skipLibCheck": true, "forceConsistentCasingInFileNames": true + }, + "exclude": ["node_modules", "build"] +}