Follow these steps in order to create a lambda function, and a macro that uses that function to transform your template, before launching the stack:
- Log in to your AWS account, and go to the Lambda console.
- Click Create function and Author from scratch.
- Give the function a descriptive name.
- Choose Python 3.6 as the Runtime.
- Choose to Create a Custom Role. A new screen opens, where you can create the role that will be associated with the lambda function. Give it a descriptive name:
Edit the policy document for the new Lambda execution role
- Click Edit to customize the policy. You need to give the lambda function permissions to write to CloudWatch logs, and to create a CloudTrail, and also give it permission to administer the S3 bucket. Paste in the following code:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
},
- Continue with the S3 permissions:
{
"Effect": "Allow",
"Resource": [
"arn:aws:s3:::*",
"arn:aws:s3:::*/*"
],
"Action": [
"s3:CreateBucket",
"s3:DeleteBucket",
"s3:PutObject",
"s3:DeleteObject"
]
},
- And finally, add CloudTrail permissions to the policy:
{
"Effect": "Allow",
"Resource": "*",
"Action": [
"cloudtrail:*"
]
}
]
}
- Click Allow in order to associate the new role with the lambda function.
- Back on the Lambda console, click Create function:
Author from scratch
- Paste the following code into the function code editor for index.js. Note that this code sample continues over several blocks:
"""
Lambda Handler for the CloudTrailBucket macro.
This macro transforms a resource with type "CloudTrailBucket" into
a bucket, a bucket policy, and a CloudTrail configuration that logs
activity to that bucket.
"""
def lambda_handler(event, _):
"Lambda handler function for the macro"
print(event)
- Continue by inspecting the fragment for the bucket name:
# Get the template fragment, which is the entire
# starting template
fragment = event['fragment']
bucket_name = None
# Look through resources to find one with type CloudTrailBucket
for k, r in fragment['Resources'].items():
if r['Type'] == 'CloudTrailBucket':
r['Type'] = 'AWS::S3::Bucket'
r['DeletionPolicy'] = 'Retain'
bucket_name = k
- Create the policy for the bucket (note that this code fragment is broken up over several entries):
bucket_policy = {
"Type" : "AWS::S3::BucketPolicy",
"Properties" : {
"Bucket" : {"Ref" : bucket_name},
"PolicyDocument" : {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AWSCloudTrailAclCheck",
"Effect": "Allow",
"Principal": {
"Service":"cloudtrail.amazonaws.com"
},
- Continue with the following code:
"Action": "s3:GetBucketAcl",
"Resource": {
"Fn::Join" : [
"", [
"arn:aws:s3:::", {
"Ref": bucket_name
}
]
]
}
},
- Continue with the following code:
{
"Sid": "AWSCloudTrailWrite",
"Effect": "Allow",
"Principal": {
"Service":"cloudtrail.amazonaws.com"
},
"Action": "s3:PutObject",
"Resource": {
"Fn::Join" : [
"", [
"arn:aws:s3:::", {
"Ref": bucket_name
},
"/AWSLogs/",
{
"Ref":"AWS::AccountId"
}, "/*"
]
]
},
- Finish the policy with this code:
"Condition": {
"StringEquals": {
"s3:x-amz-acl": "bucket-owner-full-control"
}
}
}
]
}
}
}
- Complete the transformation of the fragment and return it:
if bucket_name:
# Add the policy to the fragment
fragment['Resources'][bucket_name + 'BucketPolicy'] = bucket_policy
# Create the trail and add it to the fragment
trail = {
'DependsOn' : [bucket_name + 'BucketPolicy'],
'Type' : 'AWS::CloudTrail::Trail',
'Properties' : {
'S3BucketName' : {'Ref': bucket_name},
'IsLogging' : True
}
}
fragment['Resources'][bucket_name + 'Trail'] = trail
# Return the transformed fragment
return {
"requestId": event["requestId"],
"status": "success",
"fragment": fragment,
}
- Go to the CloudFormation console, where you will create two new stacks. The first stack creates the macro as a named resource in your account, and this can be used in any future template.
- Paste the following code into a file on your filesystem:
AWSTemplateFormatVersion: "2010-09-09"
Description: "This template creates the macro"
Parameters:
FunctionArn:
Type: String
Resources:
CloudTrailBucketMacro:
Type: AWS::CloudFormation::Macro
Properties:
Name: CloudTrailBucket
FunctionName: !Ref FunctionArn
- Select Upload a template to Amazon S3, and choose the file that you just created. Click Next, and give the stack a name.
- Paste in the function ARN of the Lambda function that you created earlier in this recipe.
- Click Next, and then Next on the following screen.
- Click Create. It may take a few minutes for the stack to be created. Wait until the status is CREATE_COMPLETE, before continuing.
- Now you will create a new (second) stack that makes use of the macro. Go back to the CloudFormation console, and click Create stack.
- Paste the following code into a file on your filesystem:
AWSTemplateFormatVersion: "2010-09-09"
Transform: CloudTrailBucket
Description: "This template will be transformed by the macro"
Resources:
MyCloudTrailBucket:
Type: CloudTrailBucket
- Select Upload a template to Amazon S3, and choose the file that you just created. Click Next, and give the stack a name.
- Click Next, and Next on the following screen.
- You will notice that the final confirmation screen is different than what you normally see when you create a stack. Since a macro is transforming your template, you will, instead, be creating and executing a change set:
Stack confirmation screen
- Check the boxes in order to acknowledge that IAM resources will be created in this stack.
- Click Create Change Set. You will get a summary of the resources that will be created:
Create a change set
- Click Execute in order to launch the stack.
- Once the stack is complete, go to the CloudTrail dashboard, and click View Trails. You should see your new trail, along with the newly created bucket name:
CloudTrail dashboard
- Click through to the S3 bucket, and inspect the contents. It may take a few minutes for the new audit logs to show up.
- Delete the stack to clean up the resources from this recipe.