Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(trigger): Allow trigger to work with Lambda functions with long timeouts #23062

Merged
merged 11 commits into from Dec 16, 2022
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),
DerkSchooltink marked this conversation as resolved.
Show resolved Hide resolved
},
});

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,7 +69,16 @@
]
},
"HandlerArn": {
"Ref": "MyFunctionCurrentVersion197490AF2cb2bc11080c1ef11d3b49c1f1603957"
"Ref": "MyTriggerFunctionCurrentVersion61957CE160cd5b4c06c4d00191dc10a647ea0777"
},
"InvocationType": "RequestResponse",
"Timeout": {
"amount": 2,
"unit": {
"label": "minutes",
"isoLabel": "M",
"inMillis": 60000
}
}
},
"DependsOn": [
Expand All @@ -78,11 +87,11 @@
"UpdateReplacePolicy": "Delete",
"DeletionPolicy": "Delete"
},
"MyFunctionCurrentVersion197490AF2cb2bc11080c1ef11d3b49c1f1603957": {
"MyTriggerFunctionCurrentVersion61957CE160cd5b4c06c4d00191dc10a647ea0777": {
"Type": "AWS::Lambda::Version",
"Properties": {
"FunctionName": {
"Ref": "MyFunction3BAA72D1"
"Ref": "MyTriggerFunction056842F6"
}
}
},
Expand Down Expand Up @@ -124,7 +133,29 @@
[
{
"Fn::GetAtt": [
"MyFunction3BAA72D1",
"MyTriggerFunction056842F6",
"Arn"
]
},
":*"
]
]
}
]
},
{
"Effect": "Allow",
"Action": [
"lambda:InvokeFunction"
],
"Resource": [
{
"Fn::Join": [
"",
[
{
"Fn::GetAtt": [
"MyLambdaFunction67CCA873",
"Arn"
]
},
Expand Down Expand Up @@ -186,6 +217,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,6 +359,15 @@
},
"HandlerArn": {
"Ref": "MySecondFunctionCurrentVersion7D497B5D173a4bb1f758991022ea97d651403362"
},
"InvocationType": "RequestResponse",
"Timeout": {
"amount": 2,
"unit": {
"label": "minutes",
"isoLabel": "M",
"inMillis": 60000
}
}
},
"UpdateReplacePolicy": "Delete",
Expand Down