Skip to content

New pattern - eventbridge-cloudtrail-dataplane-cdk#3100

Open
NithinChandranR-AWS wants to merge 2 commits into
aws-samples:mainfrom
NithinChandranR-AWS:NithinChandranR-AWS-feature-eventbridge-cloudtrail-dataplane-cdk
Open

New pattern - eventbridge-cloudtrail-dataplane-cdk#3100
NithinChandranR-AWS wants to merge 2 commits into
aws-samples:mainfrom
NithinChandranR-AWS:NithinChandranR-AWS-feature-eventbridge-cloudtrail-dataplane-cdk

Conversation

@NithinChandranR-AWS

Copy link
Copy Markdown
Contributor

Description

First pattern for Amazon EventBridge data plane logging to AWS CloudTrail (launched May 5, 2026).

What it does

  • Enables CloudTrail data plane event logging for EventBridge
  • EventBridge rule captures PutEvents API calls from CloudTrail
  • Lambda function alerts on caller identity, source IP, event bus, and entry count

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.

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.
@NithinChandranR-AWS

Copy link
Copy Markdown
Contributor Author

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.

Comment on lines +22 to +31
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

@parikhudit parikhudit Jun 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@parikhudit parikhudit Jun 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also suggest a quick smoke-test command in the README that asserts the Lambda actually logged a record.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment on lines +15 to +19
const trailBucket = new s3.Bucket(this, 'TrailBucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
enforceSSL: true,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — added encryption: S3_MANAGED and blockPublicAccess: BLOCK_ALL to both buckets.

Comment on lines +41 to +52
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));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a shared SQS DLQ for the EventBridge target. Failed deliveries go there with 14-day retention.

Comment on lines +32 to +38
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,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Configure onFailure and retryAttempts on the async Lambda. Even with the EventBridge-side DLQ, function-level destinations give clearer signal for code-level failures.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point, flipped to isMultiRegionTrail: true.

```
cd serverless-patterns/eventbridge-cloudtrail-dataplane-cdk
npm install
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add cdk bootstrap for first time users

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed — letting CloudFormation generate the name now.

Comment on lines +15 to +18
const trailBucket = new s3.Bucket(this, 'TrailBucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
enforceSSL: true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommendation: Mention that CloudTrail data events are billed separately and link pricing

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@parikhudit parikhudit Jun 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specify Node.js 18+ or appropriate in Requirements (function uses Node.js 20.x)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to Node.js 20+.

@@ -0,0 +1,15 @@
exports.handler = async (event) => {
const detail = event.detail || {};
console.log(JSON.stringify({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switched to console.info — with loggingFormat: JSON on the Lambda, CloudWatch will pick up the structured fields automatically.

Comment on lines +34 to +37
handler: 'index.handler',
code: lambda.Code.fromAsset('src'),
timeout: cdk.Duration.seconds(10),
loggingFormat: lambda.LoggingFormat.JSON,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added logRetention: ONE_WEEK. Keeps costs down for a demo pattern.

@parikhudit

parikhudit commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

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:

  • Correctness (blocking): the trail uses a basic LAMBDA_FUNCTION selector, so it logs Lambda invokes, not EventBridge PutEvents. EventBridge data events need an advanced event selector with resources.type = AWS::Events::EventBus. As written, the rule may never fire. (Already added as comment)

  • Differentiation: even once fixed, the pattern is very close to s3-eventbridge. Please add an EventBridge-specific angle that the other patterns do not cover e.g., auditing which producers publish to which buses, scoping the selector to specific named/custom buses, or a Lambda that validates/flags unexpected event sources, etc and update the README to explain why so. However, you would need to do research and find a unique pattern.

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants