CloudFormation Wait Conditions are your secret weapon for orchestrating complex deployments where one stack’s completion needs to trigger another’s start, or where a specific external event must occur before proceeding.
Let’s see this in action. Imagine you have a web application that needs to be deployed, but before you can even think about spinning up the web servers, you need a fully provisioned and healthy database cluster. You could just put the database stack creation before the web app stack creation in your overall deployment pipeline, but what if the database stack takes a while to become healthy? You’d be waiting blindly. Or worse, what if the database fails to become healthy? Your web app stack would start and then immediately fail.
This is where Wait Conditions come in. They allow you to pause a CloudFormation stack’s creation (or update) until a specific signal is received. This signal can come from an external resource you’ve configured.
Here’s a simplified WaitCondition resource in CloudFormation:
Resources:
MyDatabaseCluster:
Type: AWS::RDS::DBCluster
Properties:
Engine: aurora-postgresql
MasterUsername: admin
MasterUserPassword: YourSecurePassword
AllocatedStorage: 20
DBClusterIdentifier: my-app-db-cluster
# ... other RDS properties
WaitForDatabaseHealthy:
Type: AWS::CloudFormation::WaitCondition
Properties:
Handle: !Ref MyDatabaseCluster # This is a placeholder! We'll fix this.
Timeout: 900 # Wait for 15 minutes
MyWebServerInstance:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-0abcdef1234567890
InstanceType: t3.micro
# ... other EC2 properties
# This instance depends on the database being ready
DependsOn: WaitForDatabaseHealthy
The problem with the Handle property above is that MyDatabaseCluster is an RDS resource, not a direct signal provider. The Handle property for a WaitCondition needs to be a pre-signed URL. This URL is generated by a AWS::CloudFormation::WaitConditionHandle resource. This handle is what actually receives the "signal" that the condition has been met.
Let’s correct that. We need a WaitConditionHandle and a mechanism to signal it when the database is ready. The most common way to do this is with a Lambda function.
Here’s a more complete example:
Resources:
# 1. The resource we're waiting for (e.g., a database)
MyDatabaseCluster:
Type: AWS::RDS::DBCluster
Properties:
Engine: aurora-postgresql
MasterUsername: admin
MasterUserPassword: YourSecurePassword
AllocatedStorage: 20
DBClusterIdentifier: my-app-db-cluster
Tags:
- Key: Environment
Value: Production
# ... other RDS properties
# 2. The handle that CloudFormation will use to receive signals
DatabaseReadySignalHandle:
Type: AWS::CloudFormation::WaitConditionHandle
# 3. The WaitCondition resource that pauses stack creation
WaitForDatabaseHealthy:
Type: AWS::CloudFormation::WaitCondition
Properties:
Handle: !Ref DatabaseReadySignalHandle
Timeout: 900 # Wait for 15 minutes (900 seconds)
# We expect exactly one success signal.
# If this count isn't met, the WaitCondition fails.
Count: 1
# 4. A Lambda function that checks the database status and sends the signal
SignalDatabaseReady:
Type: AWS::Lambda::Function
Properties:
FunctionName: SignalDatabaseReadyFunction
Handler: index.handler
Role: !GetAtt LambdaExecutionRole.Arn
Runtime: python3.9
Timeout: 30 # Seconds
Code:
ZipFile: |
import json
import boto3
import cfnresponse # This is a helper module provided by AWS Lambda for CloudFormation
import os
rds_client = boto3.client('rds')
def handler(event, context):
physical_resource_id = event['PhysicalResourceId'] # The WaitCondition resource's ID
request_type = event['RequestType'] # Create, Update, Delete
if request_type == 'Create':
try:
# Get the DB Cluster Identifier from the event's ResourceProperties
db_cluster_identifier = event['ResourceProperties']['DBClusterIdentifier']
# Check the DB cluster status
response = rds_client.describe_db_clusters(DBClusterIdentifier=db_cluster_identifier)
cluster_status = response['DBClusters'][0]['Status']
if cluster_status == 'available':
print(f"Database cluster {db_cluster_identifier} is available. Sending success signal.")
# Send a SUCCESS signal to the WaitConditionHandle
# The 'Data' field is optional, but can be useful for debugging.
cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, physical_resource_id, Data={"Status": "Available"})
else:
print(f"Database cluster {db_cluster_identifier} is not yet available. Status: {cluster_status}. Will retry later.")
# If not available, don't send a signal yet. Lambda will be re-invoked by CloudFormation
# based on the WaitCondition's polling mechanism or a subsequent Create/Update event.
# For a robust solution, you'd implement a retry mechanism here or rely on CloudFormation's
# inherent retries for Lambda functions.
cfnresponse.send(event, context, cfnresponse.FAILED, {"Error": f"DB Cluster not available: {cluster_status}"}, physical_resource_id)
except Exception as e:
print(f"Error checking DB cluster status or sending signal: {e}")
cfnresponse.send(event, context, cfnresponse.FAILED, {"Error": str(e)}, physical_resource_id)
elif request_type == 'Delete':
# For deletion, we don't need to signal anything specific for the WaitCondition itself.
# The WaitCondition will be deleted as part of the stack deletion.
cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, physical_resource_id)
else: # Update
cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, physical_resource_id) # No-op for updates in this example
# IAM Role for the Lambda function
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: LambdaLoggingPolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: "*"
- PolicyName: RdsDescribePolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- rds:DescribeDBClusters
Resource: "*" # In a real scenario, scope this down to your specific DB cluster ARN
# 5. An SNS Topic to notify the Lambda function when the DB cluster is ready
# This is a more robust way than polling, but requires the DB to publish its status.
# For simplicity in this example, we'll stick to a Lambda that *checks* the status.
# If you were using CloudFormation Custom Resources, you could signal directly.
# For this pattern, the Lambda *is* the custom resource that signals.
# 6. The actual resource that depends on the database being ready
MyWebServerInstance:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-0abcdef1234567890 # Replace with a valid AMI ID
InstanceType: t3.micro
SubnetId: subnet-xxxxxxxxxxxxxxxxx # Replace with your subnet ID
SecurityGroupIds:
- sg-xxxxxxxxxxxxxxxxx # Replace with your security group ID
Tags:
- Key: Name
Value: MyAppWebServer
# This instance will only be created AFTER the WaitForDatabaseHealthy condition is met.
DependsOn: WaitForDatabaseHealthy
In this setup:
MyDatabaseCluster: This is the resource we need to be ready.DatabaseReadySignalHandle: This is a special CloudFormation resource that acts as a temporary endpoint. CloudFormation creates a pre-signed URL associated with this handle.WaitForDatabaseHealthy: ThisWaitConditionresource tells CloudFormation to pause stack creation. It’s configured to listen for signals sent to theDatabaseReadySignalHandle. It expects exactly one signal (Count: 1) within 900 seconds (Timeout: 900).SignalDatabaseReady: This Lambda function is the "signaler." It’s triggered by CloudFormation during stack creation. Its code checks the status ofMyDatabaseCluster. If the cluster isavailable, it uses thecfnresponsehelper to send aSUCCESSsignal back to theDatabaseReadySignalHandle. If not, it sends aFAILEDsignal, or simply exits, relying on CloudFormation to retry or theTimeoutto eventually expire.LambdaExecutionRole: Standard IAM role for the Lambda function to grant it permissions tords:DescribeDBClustersand CloudWatch Logs.MyWebServerInstance: This EC2 instance is configured withDependsOn: WaitForDatabaseHealthy. This is the crucial part. CloudFormation will not attempt to create this EC2 instance until theWaitForDatabaseHealthycondition successfully receives its signal.
The magic happens because the Lambda function is invoked by CloudFormation when the WaitForDatabaseHealthy resource is encountered. The Lambda function then performs a check that is external to CloudFormation’s direct resource lifecycle management. Once the check passes, the Lambda function sends a signal back to CloudFormation via the pre-signed URL provided by the WaitConditionHandle. CloudFormation receives this signal, the WaitCondition is satisfied, and then it proceeds to create the resources that depend on it.
This pattern is incredibly powerful for decoupling complex application stacks where dependencies aren’t directly representable within CloudFormation’s intrinsic dependencies. You can use it to wait for external services, health checks, or even manual approvals before proceeding with a deployment.