Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion packages/@aws-cdk/aws-events-targets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,37 @@ Currently supported are:
See the README of the `@aws-cdk/aws-events` library for more information on
EventBridge.

## LogGroup
## Invoke a Lambda function

Use the `LambdaFunction` target to invoke a lambda function.

The code snippet below creates an event rule with a Lambda function as a target triggered for every events from `aws.ec2` source. You can optionally attach a [dead letter queue](https://docs.aws.amazon.com/eventbridge/latest/userguide/rule-dlq.html).

```ts
import * as lambda from "@aws-cdk/aws-lambda";
import * as events from "@aws-cdk/aws-events";
import * as targets from "@aws-cdk/aws-events-targets";

const fn = new lambda.Function(stack, 'MyFunc', {
runtime: lambda.Runtime.NODEJS_10_X,
handler: 'index.handler',
code: lambda.Code.fromInline(`exports.handler = ${handler.toString()}`),
});

const rule = new events.Rule(this, 'rule', {
eventPattern: {
source: ["aws.ec2"],
},
});

const queue = new sqs.Queue(stack, 'Queue');

rule.addTarget(new targets.LambdaFunction(fn, {
deadLetterQueue: queue, // Optional: add a dead letter queue
}));
```

## Log an event into a LogGroup

Use the `LogGroup` target to log your events in a CloudWatch LogGroup.

Expand Down
15 changes: 14 additions & 1 deletion packages/@aws-cdk/aws-events-targets/lib/lambda.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as events from '@aws-cdk/aws-events';
import * as lambda from '@aws-cdk/aws-lambda';
import { addLambdaPermission } from './util';
import * as sqs from '@aws-cdk/aws-sqs';
import { addLambdaPermission, addToDeadLetterQueueResourcePolicy } from './util';

/**
* Customize the Lambda Event Target
Expand All @@ -14,6 +15,13 @@ export interface LambdaFunctionProps {
* @default the entire EventBridge event
*/
readonly event?: events.RuleTargetInput;

/**
* The SQS Queue to be used as deadLetterQueue.
*
* @default none
*/
readonly deadLetterQueue?: sqs.IQueue;
}

/**
Expand All @@ -32,9 +40,14 @@ export class LambdaFunction implements events.IRuleTarget {
// Allow handler to be called from rule
addLambdaPermission(rule, this.handler);

if (this.props.deadLetterQueue) {
addToDeadLetterQueueResourcePolicy(rule, this.props.deadLetterQueue);
}

return {
id: '',
arn: this.handler.functionArn,
deadLetterConfig: this.props.deadLetterQueue ? { arn: this.props.deadLetterQueue?.queueArn } : undefined,
input: this.props.event,
targetResource: this.handler,
};
Expand Down
36 changes: 35 additions & 1 deletion packages/@aws-cdk/aws-events-targets/lib/util.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as events from '@aws-cdk/aws-events';
import * as iam from '@aws-cdk/aws-iam';
import * as lambda from '@aws-cdk/aws-lambda';
import { Construct, ConstructNode, IConstruct, Names } from '@aws-cdk/core';
import * as sqs from '@aws-cdk/aws-sqs';
import { Construct, ConstructNode, IConstruct, Names, Stack } from '@aws-cdk/core';

/**
* Obtain the Role for the EventBridge event
Expand Down Expand Up @@ -45,3 +46,36 @@ export function addLambdaPermission(rule: events.IRule, handler: lambda.IFunctio
});
}
}


export function addToDeadLetterQueueResourcePolicy(rule: events.IRule, queue: sqs.IQueue) {
const ruleParsedStack = Stack.of(rule);
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of the Stack.region, you should be using rule.env.region (and same for account).

This will properly account for the account and region of imported resources.

const queueParsedStack = Stack.of(queue);

if (ruleParsedStack.region !== queueParsedStack.region) {
Copy link
Contributor

Choose a reason for hiding this comment

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

To compare region and account, you need to use this function:

/**
 * Whether two string probably contain the same environment dimension (region or account)
 *
 * Used to compare either accounts or regions, and also returns true if both
 * are unresolved (in which case both are expted to be "current region" or "current account").
 */
function sameEnvDimension(dim1: string, dim2: string) {
  return [TokenComparison.SAME, TokenComparison.BOTH_UNRESOLVED].includes(Token.compareStrings(dim1, dim2));
}

The values might be tokens and it's not guaranteed they will be the same string in that case.

throw new Error(`Cannot assign Dead Letter Queue to the rule ${rule}. Both the queue and the rule must be in the same region`);
}

// Skip Resource Policy creation if the Queue is not in the same account.
// There is no way to add a target onto an imported rule, so we can assume we will run the following code only
// in the account where the rule is created.
Comment on lines +63 to +65
Copy link
Contributor

Choose a reason for hiding this comment

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

There is also a common use case where a stack creates a rule on a bus in another account. Will this work when the rule is in a different account than the target and the queue?

https://aws.amazon.com/blogs/compute/simplifying-cross-account-access-with-amazon-eventbridge-resource-policies/

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, looks like it will work based on the blog (last role).

But it does mean that the rule and the queue can be in separate accounts. The comment is just not quite accurate.


if (ruleParsedStack.account === queueParsedStack.account) {
const policyStatementId = `AllowEventRule${Names.nodeUniqueId(rule.node)}`;

queue.addToResourcePolicy(new iam.PolicyStatement({
sid: policyStatementId,
principals: [new iam.ServicePrincipal('events.amazonaws.com')],
effect: iam.Effect.ALLOW,
actions: ['sqs:SendMessage'],
resources: [queue.queueArn],
conditions: {
ArnEquals: {
'aws:SourceArn': rule.ruleArn,
},
},
}));
} else {
// Maybe we could post a warning telling the user to create the permission in the target account manually ?
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,93 @@
]
}
}
},
"Timer30894E3BB": {
"Type": "AWS::Events::Rule",
"Properties": {
"ScheduleExpression": "rate(2 minutes)",
"State": "ENABLED",
"Targets": [
{
"Arn": {
"Fn::GetAtt": [
"MyFunc8A243A2C",
"Arn"
]
},
"DeadLetterConfig": {
"Arn": {
"Fn::GetAtt": [
"Queue4A7E3555",
"Arn"
]
}
},
"Id": "Target0"
}
]
}
},
"Timer3AllowEventRulelambdaeventsTimer3107B9373B17EFAE0": {
"Type": "AWS::Lambda::Permission",
"Properties": {
"Action": "lambda:InvokeFunction",
"FunctionName": {
"Fn::GetAtt": [
"MyFunc8A243A2C",
"Arn"
]
},
"Principal": "events.amazonaws.com",
"SourceArn": {
"Fn::GetAtt": [
"Timer30894E3BB",
"Arn"
]
}
}
},
"Queue4A7E3555": {
"Type": "AWS::SQS::Queue"
},
"QueuePolicy25439813": {
"Type": "AWS::SQS::QueuePolicy",
"Properties": {
"PolicyDocument": {
"Statement": [
{
"Action": "sqs:SendMessage",
"Condition": {
"ArnEquals": {
"aws:SourceArn": {
"Fn::GetAtt": [
"Timer30894E3BB",
"Arn"
]
}
}
},
"Effect": "Allow",
"Principal": {
"Service": "events.amazonaws.com"
},
"Resource": {
"Fn::GetAtt": [
"Queue4A7E3555",
"Arn"
]
},
"Sid": "AllowEventRulelambdaeventsTimer3107B9373"
}
],
"Version": "2012-10-17"
},
"Queues": [
{
"Ref": "Queue4A7E3555"
}
]
}
}
}
}
12 changes: 12 additions & 0 deletions packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as events from '@aws-cdk/aws-events';
import * as lambda from '@aws-cdk/aws-lambda';
import * as sqs from '@aws-cdk/aws-sqs';
import * as cdk from '@aws-cdk/core';
import * as targets from '../../lib';

Expand All @@ -23,6 +24,17 @@ const timer2 = new events.Rule(stack, 'Timer2', {
});
timer2.addTarget(new targets.LambdaFunction(fn));


const timer3 = new events.Rule(stack, 'Timer3', {
schedule: events.Schedule.rate(cdk.Duration.minutes(2)),
});

const queue = new sqs.Queue(stack, 'Queue');

timer3.addTarget(new targets.LambdaFunction(fn, {
deadLetterQueue: queue,
}));

app.synth();

/* eslint-disable no-console */
Expand Down
47 changes: 47 additions & 0 deletions packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import '@aws-cdk/assert/jest';
import * as events from '@aws-cdk/aws-events';
import * as lambda from '@aws-cdk/aws-lambda';
import * as sqs from '@aws-cdk/aws-sqs';
import * as cdk from '@aws-cdk/core';
import * as constructs from 'constructs';
import * as targets from '../../lib';
Expand Down Expand Up @@ -126,6 +127,52 @@ test('lambda handler and cloudwatch event across stacks', () => {
expect(eventStack).toCountResources('AWS::Lambda::Permission', 1);
});

test('use a Dead Letter Queue for the rule target', () => {
// GIVEN
const app = new cdk.App();
const stack = new cdk.Stack(app, 'Stack');

const fn = new lambda.Function(stack, 'MyLambda', {
code: new lambda.InlineCode('foo'),
handler: 'bar',
runtime: lambda.Runtime.PYTHON_2_7,
});

const queue = new sqs.Queue(stack, 'Queue');

new events.Rule(stack, 'Rule', {
schedule: events.Schedule.rate(cdk.Duration.minutes(1)),
targets: [new targets.LambdaFunction(fn, {
deadLetterQueue: queue,
})],
});

expect(() => app.synth()).not.toThrow();

// the Permission resource should be in the event stack
expect(stack).toHaveResource('AWS::Events::Rule', {
Targets: [
{
Arn: {
'Fn::GetAtt': [
'MyLambdaCCE802FB',
'Arn',
],
},
DeadLetterConfig: {
Arn: {
'Fn::GetAtt': [
'Queue4A7E3555',
'Arn',
],
},
},
Id: 'Target0',
},
],
});
});

function newTestLambda(scope: constructs.Construct) {
return new lambda.Function(scope, 'MyLambda', {
code: new lambda.InlineCode('foo'),
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-events/lib/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ export class Rule extends Resource implements IRule {
kinesisParameters: targetProps.kinesisParameters,
runCommandParameters: targetProps.runCommandParameters,
batchParameters: targetProps.batchParameters,
deadLetterConfig: targetProps.deadLetterConfig,
sqsParameters: targetProps.sqsParameters,
input: inputProps && inputProps.input,
inputPath: inputProps && inputProps.inputPath,
Expand Down
6 changes: 6 additions & 0 deletions packages/@aws-cdk/aws-events/lib/target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ export interface RuleTargetConfig {
*/
readonly batchParameters?: CfnRule.BatchParametersProperty;

/**
* Contains information about a dead-letter queue configuration.
* @default no dead-letter queue set
*/
readonly deadLetterConfig?: CfnRule.DeadLetterConfigProperty;

/**
* The Amazon ECS task definition and task count to use, if the event target
* is an Amazon ECS task.
Expand Down