CDK lets you define infrastructure as code, but sometimes CloudFormation doesn’t support the exact AWS resource you need, or it doesn’t support the specific configuration you want. That’s where custom resource providers come in.
Here’s a CDK stack that defines a custom resource provider and then uses it to create an S3 bucket with a specific SSE-KMS encryption configuration that wasn’t directly available in CloudFormation at the time.
from aws_cdk import (
aws_lambda as lambda_,
aws_iam as iam,
custom_resources as cr,
core
)
class CustomResourceProviderStack(core.Stack):
def __init__(self, scope: core.Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
# Define the Lambda function that will handle the custom resource requests
custom_resource_handler = lambda_.Function(
self, "CustomResourceHandler",
runtime=lambda_.Runtime.PYTHON_3_9,
handler="index.handler",
code=lambda_.Code.from_asset("lambda"), # Assuming your Lambda code is in a 'lambda' directory
initial_policy=[
iam.PolicyStatement(
actions=["s3:CreateBucket", "s3:PutBucketEncryption", "s3:DeleteBucket", "s3:GetBucketEncryption"],
resources=["*"] # In a real-world scenario, scope this down
)
]
)
# Define the custom resource provider
provider = cr.Provider(
self, "CustomResourceProvider",
on_event_handler=custom_resource_handler,
is_complete_handler=custom_resource_handler # For simplicity, using the same handler for isComplete
)
# Use the custom resource provider to create an S3 bucket with specific encryption
bucket_name = "my-special-encrypted-bucket-12345"
kms_key_arn = "arn:aws:kms:us-east-1:123456789012:key/your-kms-key-id" # Replace with your actual KMS key ARN
core.CustomResource(
self, "SpecialEncryptedBucket",
service_token=provider.service_token,
properties={
"BucketName": bucket_name,
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": kms_key_arn
}
)
The lambda/index.py would look something like this:
import boto3
import json
import logging
import cfnresponse # Provided by AWS Lambda Custom Resource support
logger = logging.getLogger()
logger.setLevel(logging.INFO)
s3 = boto3.client('s3')
def handler(event, context):
logger.info(f"Received event: {json.dumps(event)}")
request_type = event['RequestType']
response_data = {}
physical_resource_id = event.get('PhysicalResourceId')
try:
props = event['ResourceProperties']
bucket_name = props['BucketName']
sse_algorithm = props['SSEAlgorithm']
kms_master_key_id = props.get('KMSMasterKeyID') # Optional if SSEAlgorithm is not aws:kms
if request_type == 'Create':
logger.info(f"Creating bucket: {bucket_name}")
s3.create_bucket(Bucket=bucket_name)
if sse_algorithm == 'aws:kms':
logger.info(f"Applying SSE-KMS encryption to bucket: {bucket_name}")
s3.put_bucket_encryption(
Bucket=bucket_name,
ServerSideEncryptionConfiguration={
'Rules': [
{
'ApplyServerSideEncryptionByDefault': {
'SSEAlgorithm': sse_algorithm,
'KMSMasterKeyID': kms_master_key_id
}
}
]
}
)
# Add other SSE types as needed
response_data['BucketName'] = bucket_name
physical_resource_id = bucket_name # For Create, PhysicalResourceId is often the resource name
elif request_type == 'Update':
logger.info(f"Updating bucket: {bucket_name} (PhysicalResourceId: {physical_resource_id})")
# In a real scenario, you'd compare old and new properties and perform specific updates.
# For this example, we'll assume updates might involve re-applying encryption if changed.
if sse_algorithm == 'aws:kms':
logger.info(f"Re-applying SSE-KMS encryption to bucket: {bucket_name}")
s3.put_bucket_encryption(
Bucket=bucket_name,
ServerSideEncryptionConfiguration={
'Rules': [
{
'ApplyServerSideEncryptionByDefault': {
'SSEAlgorithm': sse_algorithm,
'KMSMasterKeyID': kms_master_key_id
}
}
]
}
)
response_data['BucketName'] = bucket_name
# PhysicalResourceId typically remains the same during updates
elif request_type == 'Delete':
logger.info(f"Deleting bucket: {bucket_name} (PhysicalResourceId: {physical_resource_id})")
try:
# Attempt to remove encryption before deleting the bucket
s3.delete_bucket_encryption(Bucket=bucket_name)
logger.info(f"Removed encryption from bucket: {bucket_name}")
except s3.exceptions.ClientError as e:
if e.response['Error']['Code'] == 'ClientError': # Or a more specific error for no encryption configured
logger.info(f"No encryption found on bucket {bucket_name} to delete.")
else:
raise # Re-raise other errors
s3.delete_bucket(Bucket=bucket_name)
cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data, physical_resource_id)
except Exception as e:
logger.error(f"Error processing event: {e}")
cfnresponse.send(event, context, cfnresponse.FAILED, {"Error": str(e)}, physical_resource_id)
The core idea is that you’re abstracting away the CloudFormation gap. The cr.Provider resource in CDK sets up the necessary infrastructure (usually a Lambda function and an SNS topic) to receive events from CloudFormation when a CustomResource is created, updated, or deleted. Your Lambda function then performs the actual AWS API calls.
What makes this powerful is that any AWS API call that can be made via the AWS SDK can be exposed as a CloudFormation resource. This unlocks capabilities for resources that are either new, have specific configuration options not yet exposed by CloudFormation, or require a sequence of operations.
The service_token is the key. It’s the ARN of the SNS topic that CloudFormation will publish custom resource events to. Your cr.Provider resource creates this and wires it up to your on_event_handler Lambda. When core.CustomResource is instantiated, CloudFormation sends a Create event to that SNS topic, which triggers your Lambda. Your Lambda performs the action and then signals back to CloudFormation via the cfnresponse module, telling it whether the operation succeeded or failed, and providing a PhysicalResourceId.
This allows you to manage any AWS resource or perform any action as if it were a native CloudFormation resource, all within your CDK application.
The next thing you’ll want to tackle is robust error handling within your Lambda, especially for the Delete operation where resources might already be gone or in an unexpected state.