Skip to content

Commit

Permalink
feat: Add service timeout to custom resources
Browse files Browse the repository at this point in the history
  • Loading branch information
prazian committed Jun 22, 2024
1 parent 3b95777 commit 10c5e09
Show file tree
Hide file tree
Showing 6 changed files with 141 additions and 2 deletions.
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

0 comments on commit 10c5e09

Please sign in to comment.