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:

  1. MyDatabaseCluster: This is the resource we need to be ready.
  2. DatabaseReadySignalHandle: This is a special CloudFormation resource that acts as a temporary endpoint. CloudFormation creates a pre-signed URL associated with this handle.
  3. WaitForDatabaseHealthy: This WaitCondition resource tells CloudFormation to pause stack creation. It’s configured to listen for signals sent to the DatabaseReadySignalHandle. It expects exactly one signal (Count: 1) within 900 seconds (Timeout: 900).
  4. SignalDatabaseReady: This Lambda function is the "signaler." It’s triggered by CloudFormation during stack creation. Its code checks the status of MyDatabaseCluster. If the cluster is available, it uses the cfnresponse helper to send a SUCCESS signal back to the DatabaseReadySignalHandle. If not, it sends a FAILED signal, or simply exits, relying on CloudFormation to retry or the Timeout to eventually expire.
  5. LambdaExecutionRole: Standard IAM role for the Lambda function to grant it permissions to rds:DescribeDBClusters and CloudWatch Logs.
  6. MyWebServerInstance: This EC2 instance is configured with DependsOn: WaitForDatabaseHealthy. This is the crucial part. CloudFormation will not attempt to create this EC2 instance until the WaitForDatabaseHealthy condition 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.

Want structured learning?

Take the full Cloudformation course →