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:
TypeandTypeName: Which AWS resource type this hook applies to (e.g.,AWS::S3::Bucket).Configuration.HookConfiguration.Types: When the hook should run.RESOURCE_PREREQUISITEmeans 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.