New pattern - eventbridge-cloudtrail-dataplane-cdk#3100
Conversation
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.
|
Hi @biswanathmukherjee 👋 This demonstrates CloudTrail data plane events → EventBridge — a unique integration using the new data event filtering (2026). First pattern showing S3 data-plane events triggering Lambda via EventBridge. Deployed and tested. |
| 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 |
There was a problem hiding this comment.
Wrong CloudTrail data event resource type; this trail does not capture EventBridge PutEvents. addEventSelector(DataResourceType.LAMBDA_FUNCTION, ['arn:aws:lambda']) adds a basic event selector that logs Lambda Invoke data events for every Lambda function in the account/region. EventBridge PutEvents data events require an advanced event selector with resources.type = AWS::Events::EventBus, per the EventBridge / CloudTrail integration docs. The CDK L2 DataResourceType enum only exposes LAMBDA_FUNCTION and S3_OBJECT, so we need CfnTrail with AdvancedEventSelectors. Kindly check and confirm.
There was a problem hiding this comment.
I'd also suggest a quick smoke-test command in the README that asserts the Lambda actually logged a record.
There was a problem hiding this comment.
After fixing above, you may want to drop or update the "allow ~5 minutes" note in the README test step, as CloudTrail data events would not typically take that long to reach EventBridge.
There was a problem hiding this comment.
Good catch — you're right, LAMBDA_FUNCTION doesn't capture EventBridge PutEvents at all. Switched to AWS::Events::EventBus via advanced event selectors (the CDK L2 addEventSelector doesn't support this resource type yet, so I used the CfnTrail escape hatch).
| const trailBucket = new s3.Bucket(this, 'TrailBucket', { | ||
| removalPolicy: cdk.RemovalPolicy.DESTROY, | ||
| autoDeleteObjects: true, | ||
| enforceSSL: true, | ||
| }); |
There was a problem hiding this comment.
Set explicit encryption and blockPublicAccess on the trail bucket. This bucket holds CloudTrail audit logs, so best to be explicit even though Amazon S3 now defaults to SSE-S3 at the API.
There was a problem hiding this comment.
Done — added encryption: S3_MANAGED and blockPublicAccess: BLOCK_ALL to both buckets.
| 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)); |
There was a problem hiding this comment.
Add a DLQ to the EventBridge target. Without deadLetterQueue on the LambdaFunction target, failed invocations would end up in the void after EventBridge's default retries.
There was a problem hiding this comment.
Added a shared SQS DLQ for the EventBridge target. Failed deliveries go there with 14-day retention.
| 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, | ||
| }); |
There was a problem hiding this comment.
Configure onFailure and retryAttempts on the async Lambda. Even with the EventBridge-side DLQ, function-level destinations give clearer signal for code-level failures.
There was a problem hiding this comment.
Done — set retryAttempts: 2 and onFailure: new SqsDestination(dlq) on the Lambda.
| const trail = new cloudtrail.Trail(this, 'EventBridgeDataPlaneTrail', { | ||
| bucket: trailBucket, | ||
| trailName: 'eventbridge-dataplane-trail', | ||
| isMultiRegionTrail: false, |
There was a problem hiding this comment.
isMultiRegionTrail: false misses cross-region PutEvents. For an audit/security pattern, the trail should capture all regions of the account. Console-created trails are multi-region by default. Suggest isMultiRegionTrail: true. Also you may want to call out in README that EventBridge rules are region-local, multi-region detection requires deploying the rule in each region or fanning in via cross-region buses.
There was a problem hiding this comment.
Fair point, flipped to isMultiRegionTrail: true.
| ``` | ||
| cd serverless-patterns/eventbridge-cloudtrail-dataplane-cdk | ||
| npm install | ||
| ``` |
There was a problem hiding this comment.
Add cdk bootstrap for first time users
There was a problem hiding this comment.
Added a cdk bootstrap step in the deployment instructions.
| // CloudTrail trail with data events for EventBridge | ||
| const trail = new cloudtrail.Trail(this, 'EventBridgeDataPlaneTrail', { | ||
| bucket: trailBucket, | ||
| trailName: 'eventbridge-dataplane-trail', |
There was a problem hiding this comment.
Please drop the hardcoded trailName.
Although trailName: 'eventbridge-dataplane-trail' is unique per account/region. A second deployment of the stack (e.g. to test a change in a different stack name) could fail with Trail already exists. For a sample, the simplest fix is to remove the property and let CDK auto-generate a unique name. If a stable name is desired, expose it as a CfnParameter.
There was a problem hiding this comment.
Removed — letting CloudFormation generate the name now.
| const trailBucket = new s3.Bucket(this, 'TrailBucket', { | ||
| removalPolicy: cdk.RemovalPolicy.DESTROY, | ||
| autoDeleteObjects: true, | ||
| enforceSSL: true, |
There was a problem hiding this comment.
The bucket holding CloudTrail logs has no serverAccessLogsBucket configured. For an audit-trail bucket, S3 server access logging adds a second-tier audit record of who accessed/modified the audit logs themselves. This is a defense-in-depth recommendation rather than a hard requirement.
There was a problem hiding this comment.
Added a dedicated accessLogsBucket with serverAccessLogsBucket pointing to it.
|
|
||
| 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. |
There was a problem hiding this comment.
Recommendation: Mention that CloudTrail data events are billed separately and link pricing
There was a problem hiding this comment.
Added a callout box in the README noting CloudTrail data events are billed separately, with a link to the pricing page.
|
|
||
| * [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 |
There was a problem hiding this comment.
Specify Node.js 18+ or appropriate in Requirements (function uses Node.js 20.x)
There was a problem hiding this comment.
Updated to Node.js 20+.
| @@ -0,0 +1,15 @@ | |||
| exports.handler = async (event) => { | |||
| const detail = event.detail || {}; | |||
| console.log(JSON.stringify({ | |||
There was a problem hiding this comment.
usingconsole.log(JSON.stringify(...)) here, on Node.js 20 with LoggingFormat.JSON you can use context.logger.info({...}) directly. The return { statusCode: 200 } would be unused for async EventBridge invocations
There was a problem hiding this comment.
Switched to console.info — with loggingFormat: JSON on the Lambda, CloudWatch will pick up the structured fields automatically.
| handler: 'index.handler', | ||
| code: lambda.Code.fromAsset('src'), | ||
| timeout: cdk.Duration.seconds(10), | ||
| loggingFormat: lambda.LoggingFormat.JSON, |
There was a problem hiding this comment.
The processor function has no logRetention (or pre-created logGroup with retention) configured. The auto-created log group /aws/lambda/ will retain logs forever, accruing CloudWatch Logs storage cost. So either set retention or give a callout in README and wherever applicable.
There was a problem hiding this comment.
Added logRetention: ONE_WEEK. Keeps costs down for a demo pattern.
|
Thanks for the submission. Heads-up that the repo already has s3-eventbridge ("S3 -> CloudTrail -> EventBridge") which is architecturally identical to this pattern, self-provisioned trail + data-event selector + EventBridge rule on AWS API Call via CloudTrail. There are few other similar patterns as well. The only thing that makes yours distinct is monitoring EventBridge's own PutEvents data plane. Two issues to resolve before we can take it:
For these reasons, my inclination is towards enriching one of the existing patterns (as we have many), rather than adding a new one. Would love to hear your thoughts. |
- 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
Description
First pattern for Amazon EventBridge data plane logging to AWS CloudTrail (launched May 5, 2026).
What it does
Testing
Deployed and tested. CloudTrail trail created, EventBridge rule configured, Lambda processor with JSON logging verified.
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.