Extending CloudFormation's Power: Creating Custom Resources for Enhanced AWS Resource Management
Tags: cloudformation, customresource, serverless, codepipeline
Published: 2023-07-04 17:38:24

**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.