Custom Resources

You can extend the capabilities of CloudFormation with custom resources by delegating work to a Lambda function that is specially crafted to interact with the CloudFormation service. In your code, you implement the create, update, and delete actions, and then you send a response with the status of the operation.

In this lab, you will create a custom resource that generates an SSH key and stores it in SSM parameter store.

Create the Lambda role

First we will create the IAM role that will be used by your new Lambda function.

Pay attention to the access granted to create, delete and describe keypairs, plus access to Systems Manager Parameter Store. These privileges are more permissive than are typical in a production environment. We use them here as an example only.

  1. Log into your AWS account, be sure to select the N. Virginia region, and then open the IAM console. Our first step will be to create an execution role for a Lambda function we will be creating.
  2. Click on Roles, and then Create role.
  3. Select Lambda, and then Next: permissions.
  4. Click Next: Tags (we will skip the policy creation for the moment).
  5. Click Next: review.
  6. Enter a name for the role (such as lambda-ssh-key-gen) and then Create role.
  7. Now click the new role you just created, and then Add inline policy. Add inline policy
  8. Click the JSON tab of the Create policy page and paste this into the field, and then click Review policy:
    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Effect": "Allow",
                "Action": [
                    "logs:CreateLogGroup",
                    "logs:CreateLogStream",
                    "logs:PutLogEvents"
                ],
                "Resource": "arn:aws:logs:*:*:*"
            },
            {
                "Effect": "Allow",
                "Action": [
                    "ec2:CreateKeyPair",
                    "ec2:DescribeKeyPairs",
                    "ssm:PutParameter"
                ],
                "Resource": "*"
            },
            {
                "Effect": "Allow",
                "Action": [
                    "ec2:DeleteKeyPair",
                    "ssm:DeleteParameter"
                ],
                "Resource": "*"
            }
        ]
    }
    
  9. Give the policy a name (such as lambda-policy) and then Create policy

Create the Lambda function

Now we will create the actual Lambda function.

  1. Open the Lambda console (https://console.aws.amazon.com/lambda/home?region=us-east-1#/discover) and click on Create function.
  2. Select Author from scratch, give the function a name (such as ssh-key-gen), select Python 3.8 as the runtime, expand Permissions, select Use an existing role, and finally select the role that you created in the previous step. Then click Create function. Your next screen should be similar to this: Lambda created screen
  3. Now we will now download the source code for this function:
    File name Purpose Download
    custom_resource_lambda.py Creates the Lambda function used in this lab Download the code
  4. With the code downloaded, open it with a plain text editor, copy the entire file’s content, and overwrite the lambda_function.py code in the Lambda console.
  5. Finally, click on Deploy to push make your new code live.

Examine what the function does

Now let’s examine the function to see what we are doing to implement the custom resource. Every lambda function has a handler that is called by the lambda environment in response to a trigger. This is the entry point for any Lambda function.

def lambda_handler(event, context):
    """Lambda handler for the custom resource"""
    try:
        return custom_resource_handler(event, context)
    except Exception:
        log_exception()
        raise

The handler function must determine the type of request and send a response back to CloudFormation. In this code snippet, we handle a create request from CloudFormation.

if event['RequestType'] == 'Create':
    try:
        print("Creating key name %s" % str(pem_key_name))
        key = ec2.create_key_pair(KeyName=pem_key_name)
        key_material = key['KeyMaterial']
        ssm_client = boto3.client('ssm')
        param = ssm_client.put_parameter(
            Name=pem_key_name,
            Value=key_material,
            Type='SecureString')
        print(param)
        print(f'The parameter {pem_key_name} has been created.')
        response = 'SUCCESS'
    except Exception as e:
        print(f'There was an error {e} creating and committing ' +\
            f'key {pem_key_name} to the parameter store')
        log_exception()
        response = 'FAILED'
    send_response(event, context, response)
    return

Responses are sent to the CloudFormation endpoints as HTTPS PUTs. This code snippet uses urllib, which is a part of the Python 3 standard library.

def send_response(event, context, response):
    """Send a response to CloudFormation to handle the custom resource lifecycle"""
    response_body = {
        'Status': response,
        'Reason': 'See details in CloudWatch Log Stream: ' + \
            context.log_stream_name,
        'PhysicalResourceId': context.log_stream_name,
        'StackId': event['StackId'],
        'RequestId': event['RequestId'],
        'LogicalResourceId': event['LogicalResourceId'],
    }
    print('RESPONSE BODY: \n' + dumps(response_body))
    data = dumps(response_body).encode('utf-8')
    req = urllib.request.Request(
        event['ResponseURL'],
        data,
        headers={'Content-Length': len(data), 'Content-Type': ''})
    req.get_method = lambda: 'PUT'
    try:
        with urllib.request.urlopen(req) as resp:
            print(f'response.status: {resp.status}, ' +
                  f'response.reason: {resp.reason}')
            print('response from cfn: ' + resp.read().decode('utf-8'))
    except urllib.error.URLError:
        log_exception()
        raise Exception('Received non-200 response while sending response to AWS CloudFormation')
    return True

Use the Lambda in a CloudFormation Stack

With the new custom resource Lambda created, let’s create a CloudFormation stack that uses it.

  1. Click Save to save the function. Copy the ARN at the top right of the screen - you will need it in a few moments. Amazon Resource Name
  2. Download the CloudFormation template from this link:
    File name Purpose Download
    custom_resource_cfn.yml Creates the CloudFormation stack Download the template
  3. Go to the CloudFormation console and click Create Stack.
  4. Select Upload a template to Amazon S3 and upload the template that you downloaded a moment ago, and then proceed to the next screen.
  5. Give the stack a unique name such as ssh-key-gen-cr and paste in the ARN from the function you created earlier into the FunctionArn parameter text box.
  6. Fill out the remaining parameters, Click Next, then Next on the following screen.
  7. Check the box that reads I acknowledge that AWS CloudFormation might create IAM resources. and then click Create.
  8. Once the stack has completed creation (it might take a few minutes), go to the EC2 console and confirm the creation of your new instance.

    Any trouble creating the new EC2 instance can be found in the CloudWatch Logs log stream for the Lambda function you created earlier. The most common issue is creating an EC2 instance in a private subnet (rather than public).

  9. Go to the Systems Manager Console, view Parameter Store and confirm that the key has been stored. It was stored with the Secure String setting, which uses KMS to encrypt the parameter value.
  10. Download your SSH key from Parameter Store (not the EC2 console!) and store it in a .pem file with permissions set to 600 on Linux or Mac. You can download it from the console by selecting the key and clicking Show under the Value at the bottom of the screen. Copy-paste everything beginning with -----BEGIN RSA PRIVATE KEY----- and ending with -----END RSA PRIVATE KEY-----.
  11. As an alternative to using the console to copy and paste the key, use the following shell commands (assuming that you kept the default key name of MyKey01, and that you have a .ssh directory in your home directory)
    aws ssm get-parameter --name MyKey01 --with-decryption \
        --query Parameter.Value --output text > ~/.ssh/MyKey01.pem
       
    chmod 600 ~/.ssh/MyKey01.pem
       
    ssh -i ~/.ssh/MyKey01.pem ec2-user@<PUBLIC DNS>
    

    This is a private key, and in a production setting you must take steps to ensure that this key is not compromised!

  12. Log in to the newly created EC2 instance to confirm that the key was associated with the instance.
  13. Delete the stack, Lambda function, and then CloudWatch Logs log group created by it.