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: add service timeout to custom resources #30557

Closed
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ new AwsCustomResource(stack, 'DescribeVpcAttribute', {
},
policy: AwsCustomResourcePolicy.fromSdkCalls({ resources: AwsCustomResourcePolicy.ANY_RESOURCE }),
timeout: cdk.Duration.minutes(3),
serviceTimeout: cdk.Duration.minutes(15),
vpc: vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
});
Expand Down
31 changes: 31 additions & 0 deletions packages/aws-cdk-lib/core/lib/custom-resource.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Construct } from 'constructs';
import { CfnResource } from './cfn-resource';
import { Duration } from './duration';
import { RemovalPolicy } from './removal-policy';
import { Resource } from './resource';
import { Token } from './token';
Expand Down Expand Up @@ -91,6 +92,16 @@ export interface CustomResourceProps {
*/
readonly removalPolicy?: RemovalPolicy;

/**
* The ServiceTimeout property from Cloudformation
*
* The value must be a duration between 1 and 3600 seconds.
* The default value is 1800 seconds (30 minutes).
*
* @default Duration.minutes(30)
*/
readonly serviceTimeout?: Duration;

/**
* Convert all property keys to pascal case.
*
Expand Down Expand Up @@ -131,6 +142,7 @@ export class CustomResource extends Resource {
const type = renderResourceType(props.resourceType);
const pascalCaseProperties = props.pascalCaseProperties ?? false;
const properties = pascalCaseProperties ? uppercaseProperties(props.properties || {}) : (props.properties || {});
const serviceTimeout = renderServiceTimeout(props.serviceTimeout) || '1800';

this.resource = new CfnResource(this, 'Default', {
type,
Expand All @@ -143,6 +155,10 @@ export class CustomResource extends Resource {
this.resource.applyRemovalPolicy(props.removalPolicy, {
default: RemovalPolicy.DESTROY,
});

if (serviceTimeout) {
this.resource.addPropertyOverride('ServiceTimeout', serviceTimeout);
}
}

/**
Expand Down Expand Up @@ -214,3 +230,18 @@ function renderResourceType(resourceType?: string) {

return resourceType;
}

function renderServiceTimeout(serviceTimeout?: Duration): string|undefined {

if (!serviceTimeout) {
return undefined;
}

let timeoutSeconds = serviceTimeout.toSeconds();

if (timeoutSeconds < 1 || timeoutSeconds > 3600) {
throw new Error('ServiceTimeout must be an integer from 1 to 3600');
}

return timeoutSeconds.toString();
}
26 changes: 25 additions & 1 deletion packages/aws-cdk-lib/core/test/custom-resource.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { toCloudFormation } from './util';
import { CustomResource, RemovalPolicy, Stack } from '../lib';
import { CustomResource, Duration, RemovalPolicy, Stack } from '../lib';

describe('custom resource', () => {
test('simple case provider identified by service token', () => {
Expand Down Expand Up @@ -82,6 +82,30 @@ describe('custom resource', () => {
});
});

test('custom cfn timeout', () => {
// GIVEN
const stack = new Stack();

// WHEN
new CustomResource(stack, 'MyCustomResource', {
serviceToken: 'MyServiceToken',
serviceTimeout: Duration.minutes(5),
});

// THEN
expect(toCloudFormation(stack)).toEqual({
Resources: {
MyCustomResource: {
Type: 'AWS::CloudFormation::CustomResource',
Properties: {
ServiceToken: 'MyServiceToken',
},
ServiceTimeout: '300',
},
},
});
});

test('resource type must begin with "Custom::"', () => {
// GIVEN
const stack = new Stack();
Expand Down
31 changes: 31 additions & 0 deletions packages/aws-cdk-lib/custom-resources/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ new lambda.Function(this, 'OnEventHandler', {

### Timeouts

#### User-Defined Lambda Function Timeouts
Users are responsible to define the timeouts for the AWS Lambda functions for
user-defined handlers. It is recommended not to exceed a **14 minutes** timeout,
since all framework functions are configured to time out after 15 minutes, which
Expand All @@ -309,6 +310,36 @@ implement an [asynchronous provider](#asynchronous-providers-iscomplete), and
then configure the timeouts for the asynchronous retries through the
`queryInterval` and the `totalTimeout` options.

```ts
// This example shows how to set the timeout for the User-Defined Lambda function
new AwsCustomResource(stack, 'DescribeVpcAttribute', {
// The rest of your code
timeout: cdk.Duration.minutes(3),
});
```

#### CloudFormation Timeout
You can specify `ServiceTimeout` to set the maximum time that CloudFormation will
wait for the custom resource provider to respond. The default is 60 minutes.
You can either set this value on the `AwsCustomResource` construct or directly on the
`CustomResource` construct (both are L2).
The default is 30 minutes.
```ts
// This example shows how to set the timeout on CloudFormation
new AwsCustomResource(stack, 'CustomResource', {
// ... the rest of your code
serviceTimeout: cdk.Duration.minutes(15),
});
```
Or:
```ts
// This example shows how to set the timeout on CloudFormation
new CustomResource(stack, 'CustomResource', {
// ... the rest of your code
serviceTimeout: cdk.Duration.minutes(15),
});
```

### Provider Framework Examples

This module includes a few examples for custom resource implementations:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as ec2 from '../../../aws-ec2';
import * as iam from '../../../aws-iam';
import * as logs from '../../../aws-logs';
import * as cdk from '../../../core';
import { Annotations } from '../../../core';
import { Annotations, Duration } from '../../../core';
import { AwsCustomResourceSingletonFunction } from '../../../custom-resource-handlers/dist/custom-resources/aws-custom-resource-provider.generated';
import * as cxapi from '../../../cx-api';
import { awsSdkToIamAction } from '../helpers-internal/sdk-info';
Expand Down Expand Up @@ -341,6 +341,16 @@ export interface AwsCustomResourceProps {
*/
readonly timeout?: cdk.Duration;

/**
* The ServiceTimeout property from Cloudformation
*
* The value must be a duration between 1 and 3600 seconds.
* The default value is 1800 seconds (30 minutes).
*
* @default Duration.minutes(30)
*/
readonly serviceTimeout?: Duration;

/**
* The memory size for the singleton Lambda function implementing this custom resource.
*
Expand Down Expand Up @@ -519,6 +529,7 @@ export class AwsCustomResource extends Construct implements iam.IGrantable {
const create = props.onCreate || props.onUpdate;
this.customResource = new cdk.CustomResource(this, 'Resource', {
resourceType: props.resourceType || 'Custom::AWS',
serviceTimeout: props.serviceTimeout || cdk.Duration.minutes(30),
serviceToken: provider.functionArn,
pascalCaseProperties: true,
removalPolicy: props.removalPolicy,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,47 @@ test('timeout defaults to 2 minutes', () => {
});
});

test('cfn timeout defaults to 30 minutes', () => {
// GIVEN
const stack = new cdk.Stack();

// WHEN
new AwsCustomResource(stack, 'AwsSdk', {
onCreate: {
service: 'service',
action: 'action',
physicalResourceId: PhysicalResourceId.of('id'),
},
policy: AwsCustomResourcePolicy.fromSdkCalls({ resources: AwsCustomResourcePolicy.ANY_RESOURCE }),
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::CustomResource', {
Timeout: '1800',
});
});

test('set cfn timeout to 5 minutes', () => {
// GIVEN
const stack = new cdk.Stack();

// WHEN
new AwsCustomResource(stack, 'AwsSdk', {
serviceTimeout: cdk.Duration.minutes(5),
onCreate: {
service: 'service',
action: 'action',
physicalResourceId: PhysicalResourceId.of('id'),
},
policy: AwsCustomResourcePolicy.fromSdkCalls({ resources: AwsCustomResourcePolicy.ANY_RESOURCE }),
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::CustomResource', {
Timeout: '300',
});
});

test('memorySize defaults to 512 M if installLatestAwsSdk is true', () => {
// GIVEN
const stack = new cdk.Stack();
Expand Down
Loading