diff --git a/packages/@aws-cdk-testing/framework-integ/test/custom-resources/test/aws-custom-resource/integ.aws-custom-resource-vpc.ts b/packages/@aws-cdk-testing/framework-integ/test/custom-resources/test/aws-custom-resource/integ.aws-custom-resource-vpc.ts index ff979dae61cfb..4b1eef64949d6 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/custom-resources/test/aws-custom-resource/integ.aws-custom-resource-vpc.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/custom-resources/test/aws-custom-resource/integ.aws-custom-resource-vpc.ts @@ -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 }, }); diff --git a/packages/aws-cdk-lib/core/lib/custom-resource.ts b/packages/aws-cdk-lib/core/lib/custom-resource.ts index 5e77f7f77e220..df469b8019c5c 100644 --- a/packages/aws-cdk-lib/core/lib/custom-resource.ts +++ b/packages/aws-cdk-lib/core/lib/custom-resource.ts @@ -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'; @@ -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. * @@ -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, @@ -143,6 +155,10 @@ export class CustomResource extends Resource { this.resource.applyRemovalPolicy(props.removalPolicy, { default: RemovalPolicy.DESTROY, }); + + if (serviceTimeout) { + this.resource.addPropertyOverride('ServiceTimeout', serviceTimeout); + } } /** @@ -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(); +} diff --git a/packages/aws-cdk-lib/core/test/custom-resource.test.ts b/packages/aws-cdk-lib/core/test/custom-resource.test.ts index c792e30834cc8..2476b4d2c41d8 100644 --- a/packages/aws-cdk-lib/core/test/custom-resource.test.ts +++ b/packages/aws-cdk-lib/core/test/custom-resource.test.ts @@ -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', () => { @@ -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(); diff --git a/packages/aws-cdk-lib/custom-resources/README.md b/packages/aws-cdk-lib/custom-resources/README.md index b0b6494af09aa..ef1e6872a5b06 100644 --- a/packages/aws-cdk-lib/custom-resources/README.md +++ b/packages/aws-cdk-lib/custom-resources/README.md @@ -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 @@ -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: diff --git a/packages/aws-cdk-lib/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts b/packages/aws-cdk-lib/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts index 9000a3fb1d70f..59aecad4dc4fc 100644 --- a/packages/aws-cdk-lib/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts +++ b/packages/aws-cdk-lib/custom-resources/lib/aws-custom-resource/aws-custom-resource.ts @@ -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'; @@ -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. * @@ -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, diff --git a/packages/aws-cdk-lib/custom-resources/test/aws-custom-resource/aws-custom-resource.test.ts b/packages/aws-cdk-lib/custom-resources/test/aws-custom-resource/aws-custom-resource.test.ts index a000ffed675f7..fc2e06905c2dd 100644 --- a/packages/aws-cdk-lib/custom-resources/test/aws-custom-resource/aws-custom-resource.test.ts +++ b/packages/aws-cdk-lib/custom-resources/test/aws-custom-resource/aws-custom-resource.test.ts @@ -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();