CloudFormation Hooks let you enforce custom policies on your infrastructure before it gets deployed, acting as a gatekeeper for your CloudFormation stacks.

Let’s see it in action. Imagine you want to ensure all S3 buckets deployed via CloudFormation are not publicly accessible.

Here’s a simplified template.yaml for an S3 bucket:

AWSTemplateFormatVersion: '2010-09-09'
Description: A simple S3 bucket

Resources:
  MyS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: my-non-compliant-bucket-example
      # Oops! Missing PublicAccessBlockConfiguration

Without a hook, this would deploy just fine. But with a hook, it will fail before the AWS::S3::Bucket resource is even created.

To set this up, you first define a Lambda function that will act as your hook. This function receives notifications from CloudFormation about pending resource deployments and returns a PROVISION_FAILED or PROVISION_SUCCESS status.

Here’s a Python Lambda function s3_compliance_hook.py:

import json
import boto3

s3_client = boto3.client('s3')

def lambda_handler(event, context):
    print(f"Received event: {json.dumps(event)}")

    request_type = event['RequestType']
    resource_properties = event['ResourceProperties']
    logical_resource_id = event['LogicalResourceId']
    stack_id = event['StackId']
    physical_resource_id = event.get('PhysicalResourceId', '') # May be empty on create

    # We only care about CREATE and UPDATE operations for the S3::Bucket resource
    if request_type in ['Create', 'Update'] and resource_properties.get('ServiceToken') is None: # ServiceToken check avoids triggering on the hook's own Lambda resource
        bucket_name = resource_properties.get('BucketName')
        public_access_block = resource_properties.get('PublicAccessBlockConfiguration', {})

        if not bucket_name:
            print("BucketName not found in properties, skipping check.")
            return {
                'Status': 'SUCCESS',
                'PhysicalResourceId': physical_resource_id or context.log_stream_name
            }

        is_publicly_accessible = True # Assume it's public until proven otherwise

        if public_access_block.get('BlockPublicAcls', False) or \
           public_access_block.get('IgnorePublicAcls', False) or \
           public_access_block.get('BlockPublicPolicy', False) or \
           public_access_block.get('RestrictPublicBuckets', False):
            is_publicly_accessible = False

        if is_publicly_accessible:
            print(f"Non-compliant S3 bucket detected: {bucket_name}. Public access is not blocked.")
            return {
                'Status': 'FAILED',
                'Reason': f"S3 bucket '{bucket_name}' must have PublicAccessBlockConfiguration enabled to prevent public access."
            }
        else:
            print(f"S3 bucket '{bucket_name}' is compliant.")
            return {
                'Status': 'SUCCESS',
                'PhysicalResourceId': physical_resource_id or context.log_stream_name
            }
    else:
        print(f"Skipping check for RequestType: {request_type}, LogicalResourceId: {logical_resource_id}")
        return {
            'Status': 'SUCCESS',
            'PhysicalResourceId': physical_resource_id or context.log_stream_name
        }

Next, you register this Lambda function as a CloudFormation Hook. You’ll need to create a TypeConfiguration resource in your CloudFormation template.

AWSTemplateFormatVersion: '2010-09-09'
Description: Registering an S3 compliance hook

Resources:
  S3ComplianceHookLambdaRole:
    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: CloudFormationHookPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action: cloudformation:SignalResource
                Resource: '*' # In a real-world scenario, restrict this to the specific hook ARN

  S3ComplianceHookLambda:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: s3-compliance-hook-example
      Handler: index.lambda_handler
      Role: !GetAtt S3ComplianceHookLambdaRole.Arn
      Runtime: python3.9
      Code:
        ZipFile: |
          import json
          import boto3

          s3_client = boto3.client('s3')

          def lambda_handler(event, context):
              print(f"Received event: {json.dumps(event)}")

              request_type = event['RequestType']
              resource_properties = event['ResourceProperties']
              logical_resource_id = event['LogicalResourceId']
              stack_id = event['StackId']
              physical_resource_id = event.get('PhysicalResourceId', '')

              if request_type in ['Create', 'Update'] and resource_properties.get('ServiceToken') is None:
                  bucket_name = resource_properties.get('BucketName')
                  public_access_block = resource_properties.get('PublicAccessBlockConfiguration', {})

                  if not bucket_name:
                      print("BucketName not found in properties, skipping check.")
                      return {
                          'Status': 'SUCCESS',
                          'PhysicalResourceId': physical_resource_id or context.log_stream_name
                      }

                  is_publicly_accessible = True

                  if public_access_block.get('BlockPublicAcls', False) or \
                     public_access_block.get('IgnorePublicAcls', False) or \
                     public_access_block.get('BlockPublicPolicy', False) or \
                     public_access_block.get('RestrictPublicBuckets', False):
                      is_publicly_accessible = False

                  if is_publicly_accessible:
                      print(f"Non-compliant S3 bucket detected: {bucket_name}. Public access is not blocked.")
                      return {
                          'Status': 'FAILED',
                          'Reason': f"S3 bucket '{bucket_name}' must have PublicAccessBlockConfiguration enabled to prevent public access."
                      }
                  else:
                      print(f"S3 bucket '{bucket_name}' is compliant.")
                      return {
                          'Status': 'SUCCESS',
                          'PhysicalResourceId': physical_resource_id or context.log_stream_name
                      }
              else:
                  print(f"Skipping check for RequestType: {request_type}, LogicalResourceId: {logical_resource_id}")
                  return {
                      'Status': 'SUCCESS',
                      'PhysicalResourceId': physical_resource_id or context.log_stream_name
                  }

  S3ComplianceHookTypeConfig:
    Type: AWS::CloudFormation::HookTypeConfig
    Properties:
      Type: AWS::S3::Bucket
      TypeName: Bucket
      Configuration:
        HookConfiguration:
          Types:
            - RESOURCE_PREREQUISITE
          TargetStacks:
            - '*' # Apply to all stacks
          AutoPublish: False
          InvocationConditions:
            # This condition ensures the hook runs only when PublicAccessBlockConfiguration is NOT present
            # It's a bit of a workaround to avoid unnecessary Lambda invocations.
            # A more robust approach might involve parsing resource properties directly.
            Expression: "not .ResourceProperties.PublicAccessBlockConfiguration"
      HandlerArn: !GetAtt S3ComplianceHookLambda.Arn

The AWS::CloudFormation::HookTypeConfig resource is key. It tells CloudFormation:

  • Type and TypeName: Which AWS resource type this hook applies to (e.g., AWS::S3::Bucket).
  • Configuration.HookConfiguration.Types: When the hook should run. RESOURCE_PREREQUISITE means it runs before the resource is provisioned.
  • Configuration.HookConfiguration.TargetStacks: Which stacks this hook applies to. '*' means all stacks.
  • Configuration.HandlerArn: The ARN of the Lambda function to execute.

Now, let’s try to deploy the original S3 bucket template after registering the hook:

AWSTemplateFormatVersion: '2010-09-09'
Description: A simple S3 bucket

Resources:
  MyS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: my-non-compliant-bucket-example
      # Missing PublicAccessBlockConfiguration

When you run aws cloudformation deploy --template-file template.yaml --stack-name my-test-stack, CloudFormation will first look at the AWS::S3::Bucket resource. It sees the RESOURCE_PREREQUISITE hook registered for AWS::S3::Bucket. It invokes your s3-compliance-hook-example Lambda function with the details of the MyS3Bucket resource before attempting to create it.

Your Lambda function checks the ResourceProperties and finds that PublicAccessBlockConfiguration is missing. It returns {'Status': 'FAILED', 'Reason': '...'}. CloudFormation then stops the stack deployment and reports the failure.

The most surprising thing about CloudFormation Hooks is that they can intercept any resource type, including custom resources and even CloudFormation’s own internal resources (though this is less common). You’re not limited to AWS-managed types.

The mental model is that CloudFormation becomes an orchestrator that consults these hooks. For RESOURCE_PREREQUISITE hooks, it’s a "yes/no" question before proceeding. For RESOURCE_মধ্যবর্তী (intermediate) hooks (which run after creation but before rollback), it’s more of a "check and report" mechanism.

You control hooks via the AWS::CloudFormation::HookTypeConfig resource and the logic within your Lambda function. The InvocationConditions field in HookTypeConfig is powerful; it allows you to define expressions that determine when the hook logic actually runs, preventing unnecessary Lambda invocations for resources or properties that don’t require a check.

If you were to deploy a compliant bucket:

AWSTemplateFormatVersion: '2010-09-09'
Description: A simple S3 bucket

Resources:
  MyCompliantS3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: my-compliant-bucket-example
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

The hook would execute, see PublicAccessBlockConfiguration is present and set to block public access, and return SUCCESS. CloudFormation would then proceed to create the S3 bucket.

The next challenge is handling hooks that might fail due to transient issues, or how to manage hook versions and rollbacks.

Want structured learning?

Take the full Cloudformation course →