Skip to content

Commit

Permalink
Add Boto3 macro
Browse files Browse the repository at this point in the history
  • Loading branch information
stilvoid committed Nov 2, 2018
1 parent 9d8362a commit c79e248
Show file tree
Hide file tree
Showing 5 changed files with 304 additions and 0 deletions.
123 changes: 123 additions & 0 deletions aws/services/CloudFormation/MacrosExamples/Boto3/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# How to install and use the Boto3 macro in your AWS account

The `Boto3` macro adds the ability to create CloudFormation resources that represent operations performed by [boto3](http://boto3.readthedocs.io/). Each `Boto3` resource represents one function call.

A typical use case for this macro might be, for example, to provide some basic configuration of resources.

## Deploying

1. You will need an S3 bucket to store the CloudFormation artifacts:
* If you don't have one already, create one with `aws s3 mb s3://<bucket name>`

2. Package the CloudFormation template. The provided template uses [the AWS Serverless Application Model](https://aws.amazon.com/about-aws/whats-new/2016/11/introducing-the-aws-serverless-application-model/) so must be transformed before you can deploy it.

```shell
aws cloudformation package \
--template-file macro.template \
--s3-bucket <your bucket name here> \
--output-template-file packaged.template
```

3. Deploy the packaged CloudFormation template to a CloudFormation stack:

```shell
aws cloudformation deploy \
--stack-name boto3-macro \
--template-file packaged.template \
--capabilities CAPABILITY_IAM
```

4. To test out the macro's capabilities, try launching the provided example template:
```shell
aws cloudformation deploy \
--stack-name boto3-macro-example \
--template-file example.template
```
## Usage
To make use of the macro, add `Transform: Boto3` to the top level of your CloudFormation template.
Here is a trivial example template that adds a readme file to a new CodeCommit repository:
```yaml
Transform: Boto3
Resources:
Repo:
Type: AWS::CodeCommit::Repository
Properties:
RepositoryName: my-repo
AddReadme:
Type: Boto3::CodeCommit.put_file
Mode: Create
Properties:
RepositoryName: !GetAtt Repo.Name
BranchName: master
FileContent: "Hello, world!"
FilePath: README.md
CommitMessage: Add a readme file
Name: CloudFormation
```
## Features
### Resource type
The resource `Type` is used to identify a [boto3 client](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/clients.html) and the method of that client to execute.
The `Type` must start with `Boto3::` and be followed by the name of a client, a `.` and finally the name of a method.
The client name will be converted to lower case so that you can use resource names that look similar to other CloudFormation resource types.
Examples:
* `Boto3::CodeCommit.put_file`
* `Boto3::IAM.put_user_permissions_boundary`
* `Boto3::EC2.create_snapshot`
### Resource mode
The resource may contain a `Mode` property which specifies whether the boto3 call should be made on `Create`, `Update`, `Delete` or any combination of those.
The `Mode` may either be a string or a list of strings. For example:
* `Mode: Create`
* `Mode: Delete`
* `Mode: [Create, Update]`
### Resource properties
The `Properties` of the resource will be passed to the specified boto3 method as arguments. The name of each property will be modified so that it started with a lower-case character so that you can use property names that look similar to other CloudFormation resource properties.
### Controlling the order of execution
You can use the standard CloudFormation property `DependsOn` when you need to ensure that your `Boto3` resources are executed in the correct order.
## Examples
The following resource:
```yaml
ChangeBinaryTypes:
Type: Boto3::CloudFormation.execute_change_set
Mode: [Create, Update]
Properties:
ChangeSetName: !Ref ChangeSet
StackName: !Ref Stack
```
will result in running the equivalent of the following:
```python
boto3.client("cloudformation").execute_change_set(changeSetName=<value of ChangeSet>, stackName=<value of StackName>)
```
when the stack is created or updated.
## Author
[Steve Engledow](https://linkedin.com/in/stilvoid)
Senior Solutions Builder
Amazon Web Services
18 changes: 18 additions & 0 deletions aws/services/CloudFormation/MacrosExamples/Boto3/example.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
Transform: Boto3

Resources:
Repo:
Type: AWS::CodeCommit::Repository
Properties:
RepositoryName: my-repo

AddReadme:
Type: Boto3::CodeCommit.put_file
Mode: Create
Properties:
RepositoryName: !GetAtt Repo.Name
BranchName: master
FileContent: "Hello, world"
FilePath: README.md
CommitMessage: Add another README.md
Name: CloudFormation
52 changes: 52 additions & 0 deletions aws/services/CloudFormation/MacrosExamples/Boto3/lambda/macro.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

import os

PREFIX = "Boto3::"

LAMBDA_ARN = os.environ["LAMBDA_ARN"]

def handle_template(request_id, template):
for name, resource in template.get("Resources", {}).items():
if resource["Type"].startswith(PREFIX):
resource.update({
"Type": "Custom::Boto3",
"Version": "1.0",
"Properties": {
"ServiceToken": LAMBDA_ARN,
"Mode": resource.get("Mode", ["Create", "Update"]),
"Action": resource["Type"][len(PREFIX):],
"Properties": resource.get("Properties", {}),
},
})

if "Mode" in resource:
del resource["Mode"]

return template

def handler(event, context):
fragment = event["fragment"]
status = "success"

try:
fragment = handle_template(event["requestId"], event["fragment"])
except Exception as e:
status = "failure"

return {
"requestId": event["requestId"],
"status": status,
"fragment": fragment,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

from urllib2 import build_opener, HTTPHandler, Request
import base64
import boto3
import httplib
import json

def sendResponse(event, context, status, message):
body = json.dumps({
"Status": status,
"Reason": message,
"StackId": event['StackId'],
"RequestId": event['RequestId'],
"LogicalResourceId": event['LogicalResourceId'],
"PhysicalResourceId": event["ResourceProperties"]["Action"],
"Data": {},
})

request = Request(event['ResponseURL'], data=body)
request.add_header('Content-Type', '')
request.add_header('Content-Length', len(body))
request.get_method = lambda: 'PUT'

opener = build_opener(HTTPHandler)
response = opener.open(request)

def execute(action, properties):
action = action.split(".")

if len(action) != 2:
return "FAILED", "Invalid boto3 call: {}".format(".".join(action))

client, function = action[0], action[1]

try:
client = boto3.client(client.lower())
except Exception as e:
return "FAILED", "boto3 error: {}".format(e)

try:
function = getattr(client, function)
except Exception as e:
return "FAILED", "boto3 error: {}".format(e)

properties = {
key[0].lower() + key[1:]: value
for key, value in properties.items()
}

try:
function(**properties)
except Exception as e:
return "FAILED", "boto3 error: {}".format(e)

return "SUCCESS", "Completed successfully"

def handler(event, context):
print("Received request:", json.dumps(event, indent=4))

request = event["RequestType"]
properties = event["ResourceProperties"]

if any(prop not in properties for prop in ("Action", "Properties")):
print("Bad properties", properties)
return sendResponse(event, context, "FAILED", "Missing required parameters")

mode = properties["Mode"]

if request == mode or request in mode:
status, message = execute(properties["Action"], properties["Properties"])
return sendResponse(event, context, status, message)

return sendResponse(event, context, "SUCCESS", "No action taken")
26 changes: 26 additions & 0 deletions aws/services/CloudFormation/MacrosExamples/Boto3/macro.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
Transform: AWS::Serverless-2016-10-31

Resources:
ResourceFunction:
Type: AWS::Serverless::Function
Properties:
Runtime: python2.7
CodeUri: lambda
Handler: resource.handler
Policies: PowerUserAccess

MacroFunction:
Type: AWS::Serverless::Function
Properties:
Runtime: python3.6
CodeUri: lambda
Handler: macro.handler
Environment:
Variables:
LAMBDA_ARN: !GetAtt ResourceFunction.Arn

Macro:
Type: AWS::CloudFormation::Macro
Properties:
Name: Boto3
FunctionName: !GetAtt MacroFunction.Arn

0 comments on commit c79e248

Please sign in to comment.