Skip to content

Commit

Permalink
feat(trigger): Allow trigger to work with Lambda functions with long …
Browse files Browse the repository at this point in the history
…timeouts (#23062)

Implements #23058
----

### All Submissions:

* [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md)

### Adding new Unconventional Dependencies:

* [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-new-unconventional-dependencies)

### New Features

* [x] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)?
	* [x] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)?

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
DerkSchooltink committed Dec 16, 2022
1 parent 1a62dc4 commit 9fd3811
Show file tree
Hide file tree
Showing 7 changed files with 280 additions and 30 deletions.
27 changes: 27 additions & 0 deletions packages/@aws-cdk/triggers/README.md
Expand Up @@ -39,6 +39,33 @@ new triggers.TriggerFunction(stack, 'MyTrigger', {
In the above example, the AWS Lambda function defined in `myLambdaFunction` will
be invoked when the stack is deployed.

It is also possible to trigger a predefined Lambda function by using the `Trigger` construct:

```ts
import * as lambda from '@aws-cdk/aws-lambda';
import * as triggers from '@aws-cdk/triggers';
import { Stack } from '@aws-cdk/core';

declare const stack: Stack;

const func = new lambda.Function(stack, 'MyFunction', {
handler: 'index.handler',
runtime: lambda.Runtime.NODEJS_14_X,
code: lambda.Code.fromInline('foo'),
});

new triggers.Trigger(stack, 'MyTrigger', {
handler: func,
timeout: Duration.minutes(10),
invocationType: triggers.InvocationType.EVENT,
});
```

Addition properties can be used to fine-tune the behaviour of the trigger.
The `timeout` property can be used to determine how long the invocation of the function should take.
The `invocationType` property can be used to change the invocation type of the function.
This might be useful in scenarios where a fire-and-forget strategy for invoking the function is sufficient.

## Trigger Failures

If the trigger handler fails (e.g. an exception is raised), the CloudFormation
Expand Down
22 changes: 16 additions & 6 deletions packages/@aws-cdk/triggers/lib/lambda/index.ts
Expand Up @@ -3,11 +3,16 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import * as AWS from 'aws-sdk';

export type InvokeFunction = (functionName: string) => Promise<AWS.Lambda.InvocationResponse>;
export type InvokeFunction = (functionName: string, invocationType: string, timeout: number) => Promise<AWS.Lambda.InvocationResponse>;

export const invoke: InvokeFunction = async (functionName) => {
const lambda = new AWS.Lambda();
const invokeRequest = { FunctionName: functionName };
export const invoke: InvokeFunction = async (functionName, invocationType, timeout) => {
const lambda = new AWS.Lambda({
httpOptions: {
timeout,
},
});

const invokeRequest = { FunctionName: functionName, InvocationType: invocationType };
console.log({ invokeRequest });

// IAM policy changes can take some time to fully propagate
Expand Down Expand Up @@ -51,7 +56,10 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent
throw new Error('The "HandlerArn" property is required');
}

const invokeResponse = await invoke(handlerArn);
const invocationType = event.ResourceProperties.InvocationType;
const timeout = event.ResourceProperties.Timeout;

const invokeResponse = await invoke(handlerArn, invocationType, timeout);

if (invokeResponse.StatusCode !== 200) {
throw new Error(`Trigger handler failed with status code ${invokeResponse.StatusCode}`);
Expand All @@ -68,7 +76,9 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent
*/
function parseError(payload: string | undefined): string {
console.log(`Error payload: ${payload}`);
if (!payload) { return 'unknown handler error'; }
if (!payload) {
return 'unknown handler error';
}
try {
const error = JSON.parse(payload);
const concat = [error.errorMessage, error.trace].filter(x => x).join('\n');
Expand Down
39 changes: 39 additions & 0 deletions packages/@aws-cdk/triggers/lib/trigger.ts
Expand Up @@ -2,6 +2,7 @@ import { join } from 'path';
import * as lambda from '@aws-cdk/aws-lambda';
import { CustomResource, CustomResourceProvider, CustomResourceProviderRuntime } from '@aws-cdk/core';
import { Construct, IConstruct, Node } from 'constructs';
import { Duration } from '../../core';

/**
* Interface for triggers.
Expand Down Expand Up @@ -61,6 +62,28 @@ export interface TriggerOptions {
readonly executeOnHandlerChange?: boolean;
}

/**
* The invocation type to apply to a trigger. This determines whether the trigger function should await the result of the to be triggered function or not.
*/
export enum InvocationType {
/**
* Invoke the function synchronously. Keep the connection open until the function returns a response or times out.
* The API response includes the function response and additional data.
*/
EVENT = 'Event',

/**
* Invoke the function asynchronously. Send events that fail multiple times to the function's dead-letter queue (if one is configured).
* The API response only includes a status code.
*/
REQUEST_RESPONSE = 'RequestResponse',

/**
* Validate parameter values and verify that the user or role has permission to invoke the function.
*/
DRY_RUN = 'DryRun'
}

/**
* Props for `Trigger`.
*/
Expand All @@ -69,6 +92,20 @@ export interface TriggerProps extends TriggerOptions {
* The AWS Lambda function of the handler to execute.
*/
readonly handler: lambda.Function;

/**
* The invocation type to invoke the Lambda function with.
*
* @default RequestResponse
*/
readonly invocationType?: InvocationType;

/**
* The timeout of the invocation call of the Lambda function to be triggered.
*
* @default Duration.minutes(2)
*/
readonly timeout?: Duration;
}

/**
Expand All @@ -95,6 +132,8 @@ export class Trigger extends Construct implements ITrigger {
serviceToken: provider.serviceToken,
properties: {
HandlerArn: handlerArn,
InvocationType: props.invocationType ?? 'RequestResponse',
Timeout: props.timeout?.toMilliseconds() ?? Duration.minutes(2).toMilliseconds(),
},
});

Expand Down
Expand Up @@ -3,13 +3,13 @@
"Topic198E71B3E": {
"Type": "AWS::SNS::Topic",
"DependsOn": [
"MyFunctionTriggerDB129D7B"
"MyTriggerFunctionTrigger5424E7A7"
]
},
"Topic269377B75": {
"Type": "AWS::SNS::Topic"
},
"MyFunctionServiceRole3C357FF2": {
"MyTriggerFunctionServiceRole1BB78C29": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
Expand Down Expand Up @@ -40,26 +40,26 @@
]
}
},
"MyFunction3BAA72D1": {
"MyTriggerFunction056842F6": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"ZipFile": "exports.handler = function() { console.log(\"hi\"); };"
},
"Role": {
"Fn::GetAtt": [
"MyFunctionServiceRole3C357FF2",
"MyTriggerFunctionServiceRole1BB78C29",
"Arn"
]
},
"Handler": "index.handler",
"Runtime": "nodejs14.x"
"Runtime": "nodejs16.x"
},
"DependsOn": [
"MyFunctionServiceRole3C357FF2"
"MyTriggerFunctionServiceRole1BB78C29"
]
},
"MyFunctionTriggerDB129D7B": {
"MyTriggerFunctionTrigger5424E7A7": {
"Type": "Custom::Trigger",
"Properties": {
"ServiceToken": {
Expand All @@ -69,20 +69,22 @@
]
},
"HandlerArn": {
"Ref": "MyFunctionCurrentVersion197490AF2cb2bc11080c1ef11d3b49c1f1603957"
}
"Ref": "MyTriggerFunctionCurrentVersion61957CE160cd5b4c06c4d00191dc10a647ea0777"
},
"InvocationType": "RequestResponse",
"Timeout": 120000
},
"DependsOn": [
"Topic269377B75"
],
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete"
},
"MyFunctionCurrentVersion197490AF2cb2bc11080c1ef11d3b49c1f1603957": {
"MyTriggerFunctionCurrentVersion61957CE160cd5b4c06c4d00191dc10a647ea0777": {
"Type": "AWS::Lambda::Version",
"Properties": {
"FunctionName": {
"Ref": "MyFunction3BAA72D1"
"Ref": "MyTriggerFunction056842F6"
}
}
},
Expand Down Expand Up @@ -124,7 +126,29 @@
[
{
"Fn::GetAtt": [
"MyFunction3BAA72D1",
"MyTriggerFunction056842F6",
"Arn"
]
},
":*"
]
]
}
]
},
{
"Effect": "Allow",
"Action": [
"lambda:InvokeFunction"
],
"Resource": [
{
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"MyLambdaFunction67CCA873",
"Arn"
]
},
Expand Down Expand Up @@ -186,6 +210,87 @@
"AWSCDKTriggerCustomResourceProviderCustomResourceProviderRoleE18FAF0A"
]
},
"MyLambdaFunctionServiceRole313A4D46": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
}
}
],
"Version": "2012-10-17"
},
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
]
]
}
]
}
},
"MyLambdaFunction67CCA873": {
"Type": "AWS::Lambda::Function",
"Properties": {
"Code": {
"ZipFile": "exports.handler = function() { await setTimeout(3*60*1000, \"hi\"); };"
},
"Role": {
"Fn::GetAtt": [
"MyLambdaFunctionServiceRole313A4D46",
"Arn"
]
},
"Handler": "index.handler",
"Runtime": "nodejs16.x",
"Timeout": 900
},
"DependsOn": [
"MyLambdaFunctionServiceRole313A4D46"
]
},
"MyLambdaFunctionCurrentVersion4FAB80ECdc4d4e257bb2b44c9c4b9231f0d16f4c": {
"Type": "AWS::Lambda::Version",
"Properties": {
"FunctionName": {
"Ref": "MyLambdaFunction67CCA873"
}
}
},
"MyTrigger": {
"Type": "Custom::Trigger",
"Properties": {
"ServiceToken": {
"Fn::GetAtt": [
"AWSCDKTriggerCustomResourceProviderCustomResourceProviderHandler97BECD91",
"Arn"
]
},
"HandlerArn": {
"Ref": "MyLambdaFunctionCurrentVersion4FAB80ECdc4d4e257bb2b44c9c4b9231f0d16f4c"
},
"InvocationType": "Event",
"Timeout": 60000
},
"DependsOn": [
"Topic198E71B3E",
"Topic269377B75"
],
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete"
},
"MySecondFunctionServiceRole5B930841": {
"Type": "AWS::IAM::Role",
"Properties": {
Expand Down Expand Up @@ -247,7 +352,9 @@
},
"HandlerArn": {
"Ref": "MySecondFunctionCurrentVersion7D497B5D173a4bb1f758991022ea97d651403362"
}
},
"InvocationType": "RequestResponse",
"Timeout": 120000
},
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete"
Expand Down
22 changes: 19 additions & 3 deletions packages/@aws-cdk/triggers/test/integ.triggers.ts
@@ -1,21 +1,37 @@
import * as lambda from '@aws-cdk/aws-lambda';
import * as sns from '@aws-cdk/aws-sns';
import { App, Stack } from '@aws-cdk/core';
import { App, Duration, Stack } from '@aws-cdk/core';
import * as triggers from '../lib';
import { InvocationType } from '../lib';

const app = new App();
const stack = new Stack(app, 'MyStack');

const topic1 = new sns.Topic(stack, 'Topic1');
const topic2 = new sns.Topic(stack, 'Topic2');

const trigger = new triggers.TriggerFunction(stack, 'MyFunction', {
runtime: lambda.Runtime.NODEJS_14_X,
const triggerFunction = new triggers.TriggerFunction(stack, 'MyTriggerFunction', {
runtime: lambda.Runtime.NODEJS_16_X,
handler: 'index.handler',
code: lambda.Code.fromInline('exports.handler = function() { console.log("hi"); };'),
executeBefore: [topic1],
});

const func = new lambda.Function(stack, 'MyLambdaFunction', {
runtime: lambda.Runtime.NODEJS_16_X,
handler: 'index.handler',
timeout: Duration.minutes(15),
code: lambda.Code.fromInline('exports.handler = function() { await setTimeout(3*60*1000, "hi"); };'),
});

const trigger = new triggers.Trigger(stack, 'MyTrigger', {
handler: func,
invocationType: InvocationType.EVENT,
timeout: Duration.minutes(1),
executeAfter: [topic1],
});

triggerFunction.executeAfter(topic2);
trigger.executeAfter(topic2);

new triggers.TriggerFunction(stack, 'MySecondFunction', {
Expand Down

0 comments on commit 9fd3811

Please sign in to comment.