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.
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.
lambda-ssh-key-gen
) and then Create role.{
"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": "*"
}
]
}
lambda-policy
) and then Create policyNow we will create the actual Lambda function.
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:
File name | Purpose | Download |
---|---|---|
custom_resource_lambda.py |
Creates the Lambda function used in this lab | Download the code |
lambda_function.py
code in the Lambda console.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
With the new custom resource Lambda created, let’s create a CloudFormation stack that uses it.
File name | Purpose | Download |
---|---|---|
custom_resource_cfn.yml |
Creates the CloudFormation stack | Download the template |
ssh-key-gen-cr
and paste in the ARN from the function you created earlier into the FunctionArn parameter text box.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).
-----BEGIN RSA PRIVATE KEY-----
and ending with -----END RSA PRIVATE KEY-----
.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!