From 16bb2ea6177155e931cb6cd9b3fc11c0e1d216cb Mon Sep 17 00:00:00 2001 From: Nithin Chandran Rajashankar Date: Wed, 6 May 2026 04:46:58 +0000 Subject: [PATCH 1/2] feat: Add EventBridge CloudTrail data plane logging pattern First pattern for the May 5, 2026 launch of EventBridge data plane logging to CloudTrail. Enables security visibility into PutEvents API calls with Lambda alerting. Deployed and tested on live AWS account. --- .../README.md | 58 +++++++++++++++++++ .../bin/app.ts | 7 +++ eventbridge-cloudtrail-dataplane-cdk/cdk.json | 3 + .../example-pattern.json | 40 +++++++++++++ .../eventbridge-cloudtrail-dataplane-stack.ts | 58 +++++++++++++++++++ .../package.json | 16 +++++ .../src/index.js | 15 +++++ .../tsconfig.json | 8 +++ 8 files changed, 205 insertions(+) create mode 100644 eventbridge-cloudtrail-dataplane-cdk/README.md create mode 100644 eventbridge-cloudtrail-dataplane-cdk/bin/app.ts create mode 100644 eventbridge-cloudtrail-dataplane-cdk/cdk.json create mode 100644 eventbridge-cloudtrail-dataplane-cdk/example-pattern.json create mode 100644 eventbridge-cloudtrail-dataplane-cdk/lib/eventbridge-cloudtrail-dataplane-stack.ts create mode 100644 eventbridge-cloudtrail-dataplane-cdk/package.json create mode 100644 eventbridge-cloudtrail-dataplane-cdk/src/index.js create mode 100644 eventbridge-cloudtrail-dataplane-cdk/tsconfig.json diff --git a/eventbridge-cloudtrail-dataplane-cdk/README.md b/eventbridge-cloudtrail-dataplane-cdk/README.md new file mode 100644 index 000000000..86203c0f0 --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/README.md @@ -0,0 +1,58 @@ +# 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. + +## 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](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. Deploy: + ``` + cdk deploy + ``` + +## How it works + +- A CloudTrail trail is created with data event logging enabled +- 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..c17d1ef98 --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/lib/eventbridge-cloudtrail-dataplane-stack.ts @@ -0,0 +1,58 @@ +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 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 { Construct } from 'constructs'; + +export class EventbridgeCloudtrailDataplaneStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // S3 bucket for CloudTrail logs + const trailBucket = new s3.Bucket(this, 'TrailBucket', { + removalPolicy: cdk.RemovalPolicy.DESTROY, + autoDeleteObjects: true, + enforceSSL: true, + }); + + // CloudTrail trail with data events for EventBridge + const trail = new cloudtrail.Trail(this, 'EventBridgeDataPlaneTrail', { + bucket: trailBucket, + trailName: 'eventbridge-dataplane-trail', + isMultiRegionTrail: false, + }); + + // Enable EventBridge data plane events logging + trail.addEventSelector(cloudtrail.DataResourceType.LAMBDA_FUNCTION, ['arn:aws:lambda']); + + // 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, + }); + + // 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)); + + 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..98327e629 --- /dev/null +++ b/eventbridge-cloudtrail-dataplane-cdk/src/index.js @@ -0,0 +1,15 @@ +exports.handler = async (event) => { + const detail = event.detail || {}; + console.log(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"] +} From e386021f323c135d98a1293bc50499205bf4d73b Mon Sep 17 00:00:00 2001 From: Nithin Chandran Rajashankar Date: Mon, 8 Jun 2026 04:41:13 +0000 Subject: [PATCH 2/2] fix(eventbridge-cloudtrail-dataplane): Address PR review feedback - Use AWS::Events::EventBus data resource type via advanced event selectors instead of incorrect LAMBDA_FUNCTION selector - Set explicit encryption and blockPublicAccess on trail bucket - Add serverAccessLogsBucket for audit trail - Add DLQ to the EventBridge rule target - Configure onFailure destination and retryAttempts on Lambda - Set isMultiRegionTrail: true to capture cross-region PutEvents - Remove hardcoded trailName to let CloudFormation generate it - Add cdk bootstrap step for first-time users in README - Mention CloudTrail data event billing in README - Specify Node.js 20+ in Requirements section - Use console.info (structured logging) instead of console.log - Add logRetention (1 week) to the Lambda function --- appsync-events-lambda-cdk/README.md | 63 +++++++++++++++ appsync-events-lambda-cdk/architecture.svg | 23 ++++++ appsync-events-lambda-cdk/bin/app.ts | 6 ++ appsync-events-lambda-cdk/cdk.json | 1 + .../example-pattern.json | 70 ++++++++++++++++ .../lib/appsync-events-lambda-stack.ts | 79 +++++++++++++++++++ appsync-events-lambda-cdk/package.json | 14 ++++ appsync-events-lambda-cdk/src/index.js | 26 ++++++ appsync-events-lambda-cdk/tsconfig.json | 13 +++ .../README.md | 12 ++- .../eventbridge-cloudtrail-dataplane-stack.ts | 45 +++++++++-- .../src/index.js | 2 +- 12 files changed, 345 insertions(+), 9 deletions(-) create mode 100644 appsync-events-lambda-cdk/README.md create mode 100644 appsync-events-lambda-cdk/architecture.svg create mode 100644 appsync-events-lambda-cdk/bin/app.ts create mode 100644 appsync-events-lambda-cdk/cdk.json create mode 100644 appsync-events-lambda-cdk/example-pattern.json create mode 100644 appsync-events-lambda-cdk/lib/appsync-events-lambda-stack.ts create mode 100644 appsync-events-lambda-cdk/package.json create mode 100644 appsync-events-lambda-cdk/src/index.js create mode 100644 appsync-events-lambda-cdk/tsconfig.json 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 index 86203c0f0..4855515dd 100644 --- a/eventbridge-cloudtrail-dataplane-cdk/README.md +++ b/eventbridge-cloudtrail-dataplane-cdk/README.md @@ -6,11 +6,13 @@ Learn more about this pattern at Serverless Land Patterns: https://serverlesslan 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](https://nodejs.org/en/download/) installed +* [Node.js 20+](https://nodejs.org/en/download/) installed ## Deployment Instructions @@ -19,14 +21,18 @@ Important: this application uses various AWS services and there are costs associ cd serverless-patterns/eventbridge-cloudtrail-dataplane-cdk npm install ``` -2. Deploy: +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 +- 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 diff --git a/eventbridge-cloudtrail-dataplane-cdk/lib/eventbridge-cloudtrail-dataplane-stack.ts b/eventbridge-cloudtrail-dataplane-cdk/lib/eventbridge-cloudtrail-dataplane-stack.ts index c17d1ef98..674e5c845 100644 --- a/eventbridge-cloudtrail-dataplane-cdk/lib/eventbridge-cloudtrail-dataplane-stack.ts +++ b/eventbridge-cloudtrail-dataplane-cdk/lib/eventbridge-cloudtrail-dataplane-stack.ts @@ -2,31 +2,61 @@ 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, - trailName: 'eventbridge-dataplane-trail', - isMultiRegionTrail: false, + isMultiRegionTrail: true, }); - // Enable EventBridge data plane events logging - trail.addEventSelector(cloudtrail.DataResourceType.LAMBDA_FUNCTION, ['arn:aws:lambda']); + // 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', { @@ -35,6 +65,9 @@ export class EventbridgeCloudtrailDataplaneStack extends cdk.Stack { 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 @@ -49,7 +82,9 @@ export class EventbridgeCloudtrailDataplaneStack extends cdk.Stack { }, }); - rule.addTarget(new targets.LambdaFunction(processor)); + rule.addTarget(new targets.LambdaFunction(processor, { + deadLetterQueue: dlq, + })); new cdk.CfnOutput(this, 'ProcessorFunctionName', { value: processor.functionName }); new cdk.CfnOutput(this, 'TrailBucketName', { value: trailBucket.bucketName }); diff --git a/eventbridge-cloudtrail-dataplane-cdk/src/index.js b/eventbridge-cloudtrail-dataplane-cdk/src/index.js index 98327e629..e50ca4d85 100644 --- a/eventbridge-cloudtrail-dataplane-cdk/src/index.js +++ b/eventbridge-cloudtrail-dataplane-cdk/src/index.js @@ -1,6 +1,6 @@ exports.handler = async (event) => { const detail = event.detail || {}; - console.log(JSON.stringify({ + console.info(JSON.stringify({ message: 'EventBridge data plane API call detected', eventName: detail.eventName, eventSource: detail.eventSource,