Skip to content
Open
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
10 changes: 10 additions & 0 deletions cdk-custom-resource-with-wait-condition/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
*.js
!jest.config.js
*.d.ts
node_modules

# CDK asset staging directory
.cdk.staging
cdk.out

dist/
6 changes: 6 additions & 0 deletions cdk-custom-resource-with-wait-condition/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.ts
!*.d.ts

# CDK asset staging directory
.cdk.staging
cdk.out
86 changes: 86 additions & 0 deletions cdk-custom-resource-with-wait-condition/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Use AWS CloudFormation Wait Conditions for long-running custom resources

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).

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.

![Architecture Diagram](./image/architecture.png)

## How it works

The architecture uses four components:

1. **Custom Resource Handler Lambda** - Receives CloudFormation lifecycle events and starts a Step Function execution, then returns success immediately to prevent timeouts
2. **Step Function** - Orchestrates the long-running process with built-in retry mechanisms and error handling
3. **Completion Signal Handler Lambda** - Sends success/failure signals to the Wait Condition Handle when the process completes
4. **Wait Condition** - Blocks CloudFormation stack completion until receiving the completion signal

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.

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.

## Requirements

* [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.
* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured
* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [Node and NPM](https://nodejs.org/en/download/) installed
* [AWS Cloud Development Kit](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) (AWS CDK) installed

## Deploy

1. Clone the project to your local working directory

```sh
git clone https://github.com/aws-samples/serverless-patterns
```

1. Change the working directory to this pattern's directory

```sh
cd cdk-custom-resource-with-wait-condition
```

1. Install the project dependencies

```sh
npm install
```

1. Deploy the stack to your default AWS account and region

```sh
cdk deploy --require-approval never
```

## Test

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.

## Cleanup

Run the given command to delete the resources that were created. It might take some time for the CloudFormation stack to get deleted.

```sh
cdk destroy -f
```

## Useful commands

* `npm run build` compile typescript to js
* `npm run watch` watch for changes and compile
* `npm run test` perform the jest unit tests
* `npx cdk deploy` deploy this stack to your default AWS account/region
* `npx cdk diff` compare deployed stack with current state
* `npx cdk synth` emits the synthesized CloudFormation template

## References

1. [Custom resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html)
2. [Using wait conditions](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-waitcondition.html)
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/)

----
Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.

SPDX-License-Identifier: MIT-0
15 changes: 15 additions & 0 deletions cdk-custom-resource-with-wait-condition/bin/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env node
import { App } from 'aws-cdk-lib';
import { DemoStack } from '../lib/demo-stack';

const app = new App();

// Deploy demo stack showing custom resource with wait condition pattern
new DemoStack(app, 'CdkCustomResourceWithWaitConditionStack', {
stackName: 'Custom-Resource-With-Wait-Condition-Demo',
description: 'Demo of a custom resource with a wait condition',
env: {
region: process.env.CDK_DEFAULT_REGION,
account: process.env.CDK_DEFAULT_ACCOUNT,
},
});
86 changes: 86 additions & 0 deletions cdk-custom-resource-with-wait-condition/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
{
"app": "npx ts-node --prefer-ts-exts bin/app.ts",
"watch": {
"include": [
"**"
],
"exclude": [
"README.md",
"cdk*.json",
"**/*.d.ts",
"**/*.js",
"tsconfig.json",
"package*.json",
"yarn.lock",
"node_modules",
"test"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": [
"aws",
"aws-cn"
],
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
"@aws-cdk/aws-iam:minimizePolicies": true,
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
"@aws-cdk/core:enablePartitionLiterals": true,
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
"@aws-cdk/aws-route53-patters:useCertificate": true,
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
"@aws-cdk/aws-redshift:columnId": true,
"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
"@aws-cdk/aws-kms:aliasNameRef": true,
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
"@aws-cdk/aws-efs:denyAnonymousAccess": true,
"@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
"@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
"@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true,
"@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
"@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true,
"@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true,
"@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true,
"@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true,
"@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true,
"@aws-cdk/aws-eks:nodegroupNameAttribute": true,
"@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true,
"@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true,
"@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false,
"@aws-cdk/aws-s3:keepNotificationInImportedBucket": false,
"@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false,
"@aws-cdk/aws-ecs:disableEcsImdsBlocking": true,
"@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true,
"@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true,
"@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true,
"@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true,
"@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true,
"@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true,
"@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true,
"@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true,
"@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true,
"@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true,
"@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true,
"@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true
}
}
72 changes: 72 additions & 0 deletions cdk-custom-resource-with-wait-condition/example-pattern.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"title": "Custom resource with wait condition",
"description": "Use AWS CloudFormation Wait Conditions for long-running custom resources up to 12 hours",
"language": "TypeScript",
"level": "200",
"framework": "AWS CDK",
"introBox": {
"headline": "How it works",
"text": [
"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 using CloudFormation Wait Conditions.",
"The architecture uses four components working together: A Custom Resource Handler Lambda receives CloudFormation",
"lifecycle events and immediately starts a Step Function execution, then returns success to prevent timeouts.",
"The Step Function orchestrates the long-running process with built-in retry mechanisms and error handling.",
"When the process completes, a Completion Signal Handler Lambda sends success or failure signals to a Wait",
"Condition Handle URL. The Wait Condition blocks CloudFormation stack completion until receiving the signal.",
"This decouples custom resource lifecycle management from process execution, enabling asynchronous processing",
"with proper CloudFormation integration. The Step Function provides visual workflow monitoring and state management",
"while the Wait Condition ensures stack operations complete only after the long-running process finishes.",
"The pattern deploys two Lambda functions, one Step Function, a Wait Condition Handle, and a Wait Condition.",
"Use cases include database migrations, complex infrastructure provisioning, and third-party system integrations",
"that exceed Lambda's 15-minute limit."
]
},
"gitHub": {
"template": {
"repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/cdk-custom-resource-with-wait-condition",
"templateURL": "serverless-patterns/cdk-custom-resource-with-wait-condition",
"projectFolder": "cdk-custom-resource-with-wait-condition",
"templateFile": "lib/demo-stack.ts"
}
},
"resources": {
"bullets": [
{
"text": "Custom resources",
"link": "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html"
},
{
"text": "Using wait conditions",
"link": "https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-waitcondition.html"
},
{
"text": "Implementing long running deployments with AWS CloudFormation Custom Resources using AWS Step Functions",
"link": "https://aws.amazon.com/blogs/devops/implementing-long-running-deployments-with-aws-cloudformation-custom-resources-using-aws-step-functions/"
}
]
},
"deploy": {
"text": [
"cdk deploy --require-approval never"
]
},
"testing": {
"text": [
"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."
]
},
"cleanup": {
"text": [
"Delete the stack: <code>cdk destroy -f</code>."
]
},
"authors": [
{
"name": "Dmitry Gulin",
"bio": "Senior Delivery Consultant, AWS.",
"linkedin": "dmitry-gulin"
}
]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
98 changes: 98 additions & 0 deletions cdk-custom-resource-with-wait-condition/lib/demo-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { CfnWaitConditionHandle, RemovalPolicy, Stack, StackProps, CustomResource, CfnWaitCondition } from 'aws-cdk-lib';
import { Architecture, LoggingFormat, Runtime } from 'aws-cdk-lib/aws-lambda';
import { NodejsFunction, NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs';
import { LogGroup, LogGroupProps, RetentionDays } from 'aws-cdk-lib/aws-logs';
import { StateMachine, DefinitionBody } from 'aws-cdk-lib/aws-stepfunctions';
import { Construct } from 'constructs';

/**
* Demo stack showing custom resource with wait condition pattern.
* Uses Step Functions for long-running processes and wait conditions for synchronization.
*/
export class DemoStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);

// Create unique wait condition handle for each deployment
// Note: WaitCondition resources don't support updates, requiring new handles per deployment
const resourceName: string = `WaitConditionHandle-${Date.now()}`;
const cfnWaitConditionHandle = new CfnWaitConditionHandle(this, resourceName);

// Common configuration for Lambda functions
const commonLambdaProps: Partial<NodejsFunctionProps> = {
architecture: Architecture.ARM_64,
loggingFormat: LoggingFormat.JSON,
runtime: Runtime.NODEJS_22_X,
memorySize: 256,
};

// Common configuration for CloudWatch log groups
const commonLogGroupProps: Partial<LogGroupProps> = {
removalPolicy: RemovalPolicy.DESTROY,
retention: RetentionDays.ONE_WEEK,
};

// Lambda function that handles custom resource lifecycle events
// Starts Step Function execution and returns immediately
const customResourceHandler = new NodejsFunction(this, 'CustomResourceHandler', {
...commonLambdaProps,
functionName: 'CustomResourceHandler',
entry: 'lib/lambda/custom-resource-handler.mts',
logGroup: new LogGroup(this, 'CustomResourceHandlerLogGroup', {
...commonLogGroupProps,
logGroupName: `/demo/CustomResourceHandler`,
}),
});

// Lambda function that sends completion signals to wait condition handles
const sendCompletionSignalHandler = new NodejsFunction(this, 'SendCompletionSignalHandler', {
...commonLambdaProps,
functionName: 'SendCompletionSignalHandler',
entry: 'lib/lambda/send-completion-signal.mts',
logGroup: new LogGroup(this, 'SendCompletionSignalHandlerLogGroup', {
...commonLogGroupProps,
logGroupName: `/demo/SendCompletionSignalHandler`,
}),
});

// Step Function that simulates a long-running process
// Invokes completion signal Lambda when process finishes
const longRunningProcessStateMachine = new StateMachine(this, 'LongRunningProcessStateMachine', {
definitionBody: DefinitionBody.fromFile('lib/sfn/long-running-process.asl.json'),
stateMachineName: 'LongRunningProcessStateMachine',
definitionSubstitutions: {
SendCompletionSignalLambdaArn: sendCompletionSignalHandler.functionArn,
},
logs: {
destination: new LogGroup(this, 'LongRunningProcessStateMachineLogGroup', {
...commonLogGroupProps,
logGroupName: `/demo/LongRunningProcessStateMachine`,
}),
},
});

// Grant permissions for Lambda functions to interact with Step Function
longRunningProcessStateMachine.grantStartExecution(customResourceHandler);
sendCompletionSignalHandler.grantInvoke(longRunningProcessStateMachine);

// Custom resource that triggers the long-running process
const customResource = new CustomResource(this, 'CustomResource', {
serviceToken: customResourceHandler.functionArn,
properties: {
WaitConditionHandle: cfnWaitConditionHandle.ref,
StateMachineArn: longRunningProcessStateMachine.stateMachineArn,
}
});

// Wait condition that blocks stack completion until process finishes
const waitCondition = new CfnWaitCondition(this, 'WaitCondition', {
count: 1,
handle: cfnWaitConditionHandle.ref,
timeout: '60', // 60 seconds timeout
});

// Ensure wait condition depends on custom resource and state machine
waitCondition.node.addDependency(customResource);
waitCondition.node.addDependency(longRunningProcessStateMachine);
}
}
Loading