Introduction:
CloudFormation templates are powerful tools for programmatically provisioning AWS resources. While they provide a wide range of predefined resources and templates, there are situations that call for additional custom actions. In this blog post, we will explore the process of creating and utilizing custom resources in CloudFormation to enhance your AWS resource management.
Why Do We Need Custom Resources?
CloudFormation templates excel at creating AWS resources through code. However, there are instances where you may require specific actions to be performed on the created resources. These actions might involve tasks such as creating directories within newly generated buckets, configuring OpenSearch indexes, inserting essential data into databases, or cleaning up resources when shutting down infrastructure. Custom resources offer the means to extend CloudFormation's capabilities, empowering you to address these specialized requirements seamlessly.
Creating a Custom Resource: Step-by-Step Guide
To create a custom resource using CloudFormation, follow these three steps:
Step 1: Create a Role for the Lambda Function
Start by creating a role that the Lambda function we will use. This role should have the necessary permissions to perform the required actions. In the example below, we grant the Lambda function permissions for executing code and accessing S3:
CustomLambdaRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
Path: "/"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/AWSLambdaExecute"
- "arn:aws:iam::aws:policy/AmazonS3FullAccess"
Description: "Custom Lambda Role"
Step 2: Create the Lambda Function
Next, create the Lambda function itself using CloudFormation. The example below demonstrates how to define the function and its properties:
CustomFunction:
Type: AWS::Lambda::Function
Properties:
Architectures:
- "x86_64"
Code:
ZipFile: |
import boto3
import json
import os
import urllib3
# Retrieve Environment Variables
S3Bucket=os.environ["S3Bucket"]
http = urllib3.PoolManager()
SUCCESS = "SUCCESS"
FAILED = "FAILED"
def lambda_handler(event, context):
# Retrieve parameters
dirs = event['ResourceProperties']['dirs']
response_data = {}
try:
if event['RequestType'] in ('Create', 'Update'):
createUpdateEvent(dirs)
elif event['RequestType'] == 'Delete':
deleteEvent()
# Everything OK... send the signal back
send(event,
context,
SUCCESS,
response_data)
except Exception as e:
response_data['Data'] = str(e)
send(event,
context,
FAILED,
response_data)
def createUpdateEvent(dirs):
s_3 = boto3.client('s3')
for dir_name in dirs:
print("Creating: ", str(dir_name))
s_3.put_object(Bucket=S3Bucket, Key=(dir_name + '/'))
def deleteEvent():
print("Deleting S3 content...")
b_operator = boto3.resource('s3')
b_operator.Bucket(str(S3Bucket)).objects.all().delete()
def send(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False):
responseUrl = event['ResponseURL']
print(responseUrl)
responseBody = {
'Status': responseStatus,
'Reason': 'See the details in CloudWatch Log Stream: ' + context.log_stream_name,
'PhysicalResourceId': physicalResourceId or context.log_stream_name,
'StackId': event['StackId'],
'RequestId': event['RequestId'],
'LogicalResourceId': event['LogicalResourceId'],
'NoEcho': noEcho,
'Data': responseData
}
json_responseBody = json.dumps(responseBody)
print("Response body:\n" + json_responseBody)
headers = {
'content-type': '',
'content-length': str(len(json_responseBody))
}
try:
response = http.request('PUT', responseUrl, body=json_responseBody.encode('utf-8'), headers=headers)
print("Status code: " + response.reason)
except Exception as e:
print("send(..) failed executing requests.put(..): " + str(e))
Step 3: Define the Custom Resource
Finally, define the custom resource itself in the CloudFormation template. Use the following code snippet as a reference:
CustomResource:
Type: Custom::CustomResource
Properties:
ServiceToken: !GetAtt CustomFunction.Arn
dirs: !Ref Dirs
Creating Parameters for the Custom Resource
To make the custom resource more flexible, you can add parameters to the CloudFormation template. In this example, we create a parameter named "Dirs" to specify a comma-delimited list of directories to be created:
Parameters:
Dirs:
Description: "Comma-delimited list of directories to create."
Type: CommaDelimitedList
Default: test-dir
Executing this updated CloudFormation template will create an S3 bucket and automatically create a folder within that bucket as part of your infrastructure setup. You can customize the directory names by providing a comma-delimited list of values for the "Dirs" parameter.
Conclusion:
Custom resources in CloudFormation offer a flexible way to extend the provisioning capabilities of AWS resources. By following the steps outlined in this blog post and using the updated Lambda code, you can create custom resources and seamlessly integrate them into your CodePipeline workflows. Whether it's setting up additional resources, performing specific actions, or cleaning up resources, custom resources provide a powerful solution to meet your specific requirements.
Top comments (0)