Skip to content

Commit afd4e78

Browse files
committed
[New pattern] Using CDK to create custom resource with wait condition
1 parent 316e2d5 commit afd4e78

File tree

13 files changed

+572
-0
lines changed

13 files changed

+572
-0
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
*.js
2+
!jest.config.js
3+
*.d.ts
4+
node_modules
5+
6+
# CDK asset staging directory
7+
.cdk.staging
8+
cdk.out
9+
10+
dist/
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
*.ts
2+
!*.d.ts
3+
4+
# CDK asset staging directory
5+
.cdk.staging
6+
cdk.out
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Use AWS CloudFormation Wait Conditions for long-running custom resources
2+
3+
This project demonstrates how to implement [AWS CloudFormation](https://aws.amazon.com/cloudformation/) custom resources that can run for up to 12 hours using [Wait Conditions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-waitcondition.html).
4+
5+
AWS Lambda functions have a 15-minute execution timeout, limiting CloudFormation custom resources to short-running operations. This pattern extends custom resource execution time to 12 hours by decoupling lifecycle management from process execution.
6+
7+
![Architecture Diagram](./image/architecture.png)
8+
9+
## How it works
10+
11+
The architecture uses four components:
12+
13+
1. **Custom Resource Handler Lambda** - Receives CloudFormation lifecycle events and starts a Step Function execution, then returns success immediately to prevent timeouts
14+
2. **Step Function** - Orchestrates the long-running process with built-in retry mechanisms and error handling
15+
3. **Completion Signal Handler Lambda** - Sends success/failure signals to the Wait Condition Handle when the process completes
16+
4. **Wait Condition** - Blocks CloudFormation stack completion until receiving the completion signal
17+
18+
This approach enables asynchronous processing with proper CloudFormation integration, supporting use cases like database migrations, complex infrastructure provisioning, and third-party system integrations that exceed Lambda's 15-minute limit.
19+
20+
Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example.
21+
22+
## Requirements
23+
24+
* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources.
25+
* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured
26+
* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
27+
* [Node and NPM](https://nodejs.org/en/download/) installed
28+
* [AWS Cloud Development Kit](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) (AWS CDK) installed
29+
30+
## Deploy
31+
32+
1. Clone the project to your local working directory
33+
34+
```sh
35+
git clone https://github.com/aws-samples/serverless-patterns
36+
```
37+
38+
1. Change the working directory to this pattern's directory
39+
40+
```sh
41+
cd cdk-custom-resource-with-wait-condition
42+
```
43+
44+
1. Install the project dependencies
45+
46+
```sh
47+
npm install
48+
```
49+
50+
1. Deploy the stack to your default AWS account and region
51+
52+
```sh
53+
cdk deploy --require-approval never
54+
```
55+
56+
## Test
57+
58+
You can review Amazon CloudWatch logs for the Lambda functions and Step Function execution to confirm that the long-running process completed successfully and the wait condition was signaled.
59+
60+
## Cleanup
61+
62+
Run the given command to delete the resources that were created. It might take some time for the CloudFormation stack to get deleted.
63+
64+
```sh
65+
cdk destroy -f
66+
```
67+
68+
## Useful commands
69+
70+
* `npm run build` compile typescript to js
71+
* `npm run watch` watch for changes and compile
72+
* `npm run test` perform the jest unit tests
73+
* `npx cdk deploy` deploy this stack to your default AWS account/region
74+
* `npx cdk diff` compare deployed stack with current state
75+
* `npx cdk synth` emits the synthesized CloudFormation template
76+
77+
## References
78+
79+
1. [Custom resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html)
80+
2. [Using wait conditions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-waitcondition.html)
81+
3. [Implementing long running deployments with AWS CloudFormation Custom Resources using AWS Step Functions](https://aws.amazon.com/blogs/devops/implementing-long-running-deployments-with-aws-cloudformation-custom-resources-using-aws-step-functions/)
82+
83+
----
84+
Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
85+
86+
SPDX-License-Identifier: MIT-0
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/usr/bin/env node
2+
import { App } from 'aws-cdk-lib';
3+
import { DemoStack } from '../lib/demo-stack';
4+
5+
const app = new App();
6+
7+
// Deploy demo stack showing custom resource with wait condition pattern
8+
new DemoStack(app, 'CdkCustomResourceWithWaitConditionStack', {
9+
stackName: 'Custom-Resource-With-Wait-Condition-Demo',
10+
description: 'Demo of a custom resource with a wait condition',
11+
env: {
12+
region: process.env.CDK_DEFAULT_REGION,
13+
account: process.env.CDK_DEFAULT_ACCOUNT,
14+
},
15+
});
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
{
2+
"app": "npx ts-node --prefer-ts-exts bin/app.ts",
3+
"watch": {
4+
"include": [
5+
"**"
6+
],
7+
"exclude": [
8+
"README.md",
9+
"cdk*.json",
10+
"**/*.d.ts",
11+
"**/*.js",
12+
"tsconfig.json",
13+
"package*.json",
14+
"yarn.lock",
15+
"node_modules",
16+
"test"
17+
]
18+
},
19+
"context": {
20+
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
21+
"@aws-cdk/core:checkSecretUsage": true,
22+
"@aws-cdk/core:target-partitions": [
23+
"aws",
24+
"aws-cn"
25+
],
26+
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
27+
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
28+
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
29+
"@aws-cdk/aws-iam:minimizePolicies": true,
30+
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
31+
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
32+
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
33+
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
34+
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
35+
"@aws-cdk/core:enablePartitionLiterals": true,
36+
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
37+
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
38+
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
39+
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
40+
"@aws-cdk/aws-route53-patters:useCertificate": true,
41+
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
42+
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
43+
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
44+
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
45+
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
46+
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
47+
"@aws-cdk/aws-redshift:columnId": true,
48+
"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
49+
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
50+
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
51+
"@aws-cdk/aws-kms:aliasNameRef": true,
52+
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
53+
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
54+
"@aws-cdk/aws-efs:denyAnonymousAccess": true,
55+
"@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
56+
"@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
57+
"@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
58+
"@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
59+
"@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
60+
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
61+
"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true,
62+
"@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true,
63+
"@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true,
64+
"@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true,
65+
"@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true,
66+
"@aws-cdk/aws-eks:nodegroupNameAttribute": true,
67+
"@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
68+
"@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true,
69+
"@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false,
70+
"@aws-cdk/aws-s3:keepNotificationInImportedBucket": false,
71+
"@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false,
72+
"@aws-cdk/aws-ecs:disableEcsImdsBlocking": true,
73+
"@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true,
74+
"@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true,
75+
"@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true,
76+
"@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true,
77+
"@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true,
78+
"@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true,
79+
"@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true,
80+
"@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true,
81+
"@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true,
82+
"@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true,
83+
"@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true,
84+
"@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true
85+
}
86+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
"title": "Custom resource with wait condition",
3+
"description": "Use AWS CloudFormation Wait Conditions for long-running custom resources up to 12 hours",
4+
"language": "TypeScript",
5+
"level": "200",
6+
"framework": "AWS CDK",
7+
"introBox": {
8+
"headline": "How it works",
9+
"text": [
10+
"AWS Lambda functions have a 15-minute execution timeout, limiting CloudFormation custom resources to short-running",
11+
"operations. This pattern extends custom resource execution time to 12 hours using CloudFormation Wait Conditions.",
12+
"The architecture uses four components working together: A Custom Resource Handler Lambda receives CloudFormation",
13+
"lifecycle events and immediately starts a Step Function execution, then returns success to prevent timeouts.",
14+
"The Step Function orchestrates the long-running process with built-in retry mechanisms and error handling.",
15+
"When the process completes, a Completion Signal Handler Lambda sends success or failure signals to a Wait",
16+
"Condition Handle URL. The Wait Condition blocks CloudFormation stack completion until receiving the signal.",
17+
"This decouples custom resource lifecycle management from process execution, enabling asynchronous processing",
18+
"with proper CloudFormation integration. The Step Function provides visual workflow monitoring and state management",
19+
"while the Wait Condition ensures stack operations complete only after the long-running process finishes.",
20+
"The pattern deploys two Lambda functions, one Step Function, a Wait Condition Handle, and a Wait Condition.",
21+
"Use cases include database migrations, complex infrastructure provisioning, and third-party system integrations",
22+
"that exceed Lambda's 15-minute limit."
23+
]
24+
},
25+
"gitHub": {
26+
"template": {
27+
"repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/cdk-custom-resource-with-wait-condition",
28+
"templateURL": "serverless-patterns/cdk-custom-resource-with-wait-condition",
29+
"projectFolder": "cdk-custom-resource-with-wait-condition",
30+
"templateFile": "lib/demo-stack.ts"
31+
}
32+
},
33+
"resources": {
34+
"bullets": [
35+
{
36+
"text": "Custom resources",
37+
"link": "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html"
38+
},
39+
{
40+
"text": "Using wait conditions",
41+
"link": "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-waitcondition.html"
42+
},
43+
{
44+
"text": "Implementing long running deployments with AWS CloudFormation Custom Resources using AWS Step Functions",
45+
"link": "https://aws.amazon.com/blogs/devops/implementing-long-running-deployments-with-aws-cloudformation-custom-resources-using-aws-step-functions/"
46+
}
47+
]
48+
},
49+
"deploy": {
50+
"text": [
51+
"cdk deploy --require-approval never"
52+
]
53+
},
54+
"testing": {
55+
"text": [
56+
"Review Amazon CloudWatch logs for the Lambda functions and Step Function execution to confirm that the",
57+
"long-running process completed successfully and the wait condition was signaled."
58+
]
59+
},
60+
"cleanup": {
61+
"text": [
62+
"Delete the stack: <code>cdk destroy -f</code>."
63+
]
64+
},
65+
"authors": [
66+
{
67+
"name": "Dmitry Gulin",
68+
"bio": "Senior Delivery Consultant, AWS.",
69+
"linkedin": "dmitry-gulin"
70+
}
71+
]
72+
}
184 KB
Loading
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { CfnWaitConditionHandle, RemovalPolicy, Stack, StackProps, CustomResource, CfnWaitCondition } from 'aws-cdk-lib';
2+
import { Architecture, LoggingFormat, Runtime } from 'aws-cdk-lib/aws-lambda';
3+
import { NodejsFunction, NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs';
4+
import { LogGroup, LogGroupProps, RetentionDays } from 'aws-cdk-lib/aws-logs';
5+
import { StateMachine, DefinitionBody } from 'aws-cdk-lib/aws-stepfunctions';
6+
import { Construct } from 'constructs';
7+
8+
/**
9+
* Demo stack showing custom resource with wait condition pattern.
10+
* Uses Step Functions for long-running processes and wait conditions for synchronization.
11+
*/
12+
export class DemoStack extends Stack {
13+
constructor(scope: Construct, id: string, props?: StackProps) {
14+
super(scope, id, props);
15+
16+
// Create unique wait condition handle for each deployment
17+
// Note: WaitCondition resources don't support updates, requiring new handles per deployment
18+
const resourceName: string = `WaitConditionHandle-${Date.now()}`;
19+
const cfnWaitConditionHandle = new CfnWaitConditionHandle(this, resourceName);
20+
21+
// Common configuration for Lambda functions
22+
const commonLambdaProps: Partial<NodejsFunctionProps> = {
23+
architecture: Architecture.ARM_64,
24+
loggingFormat: LoggingFormat.JSON,
25+
runtime: Runtime.NODEJS_22_X,
26+
memorySize: 256,
27+
};
28+
29+
// Common configuration for CloudWatch log groups
30+
const commonLogGroupProps: Partial<LogGroupProps> = {
31+
removalPolicy: RemovalPolicy.DESTROY,
32+
retention: RetentionDays.ONE_WEEK,
33+
};
34+
35+
// Lambda function that handles custom resource lifecycle events
36+
// Starts Step Function execution and returns immediately
37+
const customResourceHandler = new NodejsFunction(this, 'CustomResourceHandler', {
38+
...commonLambdaProps,
39+
functionName: 'CustomResourceHandler',
40+
entry: 'lib/lambda/custom-resource-handler.mts',
41+
logGroup: new LogGroup(this, 'CustomResourceHandlerLogGroup', {
42+
...commonLogGroupProps,
43+
logGroupName: `/demo/CustomResourceHandler`,
44+
}),
45+
});
46+
47+
// Lambda function that sends completion signals to wait condition handles
48+
const sendCompletionSignalHandler = new NodejsFunction(this, 'SendCompletionSignalHandler', {
49+
...commonLambdaProps,
50+
functionName: 'SendCompletionSignalHandler',
51+
entry: 'lib/lambda/send-completion-signal.mts',
52+
logGroup: new LogGroup(this, 'SendCompletionSignalHandlerLogGroup', {
53+
...commonLogGroupProps,
54+
logGroupName: `/demo/SendCompletionSignalHandler`,
55+
}),
56+
});
57+
58+
// Step Function that simulates a long-running process
59+
// Invokes completion signal Lambda when process finishes
60+
const longRunningProcessStateMachine = new StateMachine(this, 'LongRunningProcessStateMachine', {
61+
definitionBody: DefinitionBody.fromFile('lib/sfn/long-running-process.asl.json'),
62+
stateMachineName: 'LongRunningProcessStateMachine',
63+
definitionSubstitutions: {
64+
SendCompletionSignalLambdaArn: sendCompletionSignalHandler.functionArn,
65+
},
66+
logs: {
67+
destination: new LogGroup(this, 'LongRunningProcessStateMachineLogGroup', {
68+
...commonLogGroupProps,
69+
logGroupName: `/demo/LongRunningProcessStateMachine`,
70+
}),
71+
},
72+
});
73+
74+
// Grant permissions for Lambda functions to interact with Step Function
75+
longRunningProcessStateMachine.grantStartExecution(customResourceHandler);
76+
sendCompletionSignalHandler.grantInvoke(longRunningProcessStateMachine);
77+
78+
// Custom resource that triggers the long-running process
79+
const customResource = new CustomResource(this, 'CustomResource', {
80+
serviceToken: customResourceHandler.functionArn,
81+
properties: {
82+
WaitConditionHandle: cfnWaitConditionHandle.ref,
83+
StateMachineArn: longRunningProcessStateMachine.stateMachineArn,
84+
}
85+
});
86+
87+
// Wait condition that blocks stack completion until process finishes
88+
const waitCondition = new CfnWaitCondition(this, 'WaitCondition', {
89+
count: 1,
90+
handle: cfnWaitConditionHandle.ref,
91+
timeout: '60', // 60 seconds timeout
92+
});
93+
94+
// Ensure wait condition depends on custom resource and state machine
95+
waitCondition.node.addDependency(customResource);
96+
waitCondition.node.addDependency(longRunningProcessStateMachine);
97+
}
98+
}

0 commit comments

Comments
 (0)