├── .github └── PULL_REQUEST_TEMPLATE.md ├── Boto3 ├── README.md ├── example.template ├── lambda │ ├── macro.py │ └── resource.py └── macro.template ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Count ├── README.md ├── deploy.sh ├── event.json ├── event_bad.json ├── requirements.txt ├── src │ ├── __init__.py │ └── index.py ├── template.yaml ├── test.yaml └── test_2.yaml ├── ExecutionRoleBuilder ├── ExecutionRoleBuilderCFnMacro.packaged.template ├── README.md ├── example.template ├── lambda │ ├── index.py │ └── policytemplates.py └── macro.template ├── Explode ├── .gitignore ├── README.md ├── lambda │ └── explode.py ├── macro.yml └── test.yml ├── LICENSE ├── NOTICE ├── Public-and-Private-Subnet-per-AZ ├── Create-Macro.yaml ├── Create-Stack.yaml └── README.md ├── PyPlate ├── README.md ├── python.yaml └── python_example.yaml ├── README.md ├── S3Objects ├── README.md ├── example.template ├── lambda │ ├── macro.py │ └── resource.py └── macro.template ├── ShortHand ├── README.md ├── example.template ├── lambda │ ├── convert.py │ ├── index.py │ ├── resolve.py │ └── spec.json └── macro.template ├── StackMetrics ├── README.md ├── example.template ├── lambda │ ├── cfnresponse.py │ ├── index.py │ └── resource.py └── macro.template └── StringFunctions ├── README.md ├── string.yaml └── string_example.yaml /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 7 | -------------------------------------------------------------------------------- /Boto3/README.md: -------------------------------------------------------------------------------- 1 | # How to install and use the Boto3 macro in your AWS account 2 | 3 | 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. 4 | 5 | A typical use case for this macro might be, for example, to provide some basic configuration of resources. 6 | 7 | ## Deploying 8 | 9 | 1. You will need an S3 bucket to store the CloudFormation artifacts: 10 | * If you don't have one already, create one with `aws s3 mb s3://` 11 | 12 | 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. 13 | 14 | ```shell 15 | aws cloudformation package \ 16 | --template-file macro.template \ 17 | --s3-bucket \ 18 | --output-template-file packaged.template 19 | ``` 20 | 21 | 3. Deploy the packaged CloudFormation template to a CloudFormation stack: 22 | 23 | ```shell 24 | aws cloudformation deploy \ 25 | --stack-name boto3-macro \ 26 | --template-file packaged.template \ 27 | --capabilities CAPABILITY_IAM 28 | ``` 29 | 30 | 4. To test out the macro's capabilities, try launching the provided example template: 31 | 32 | ```shell 33 | aws cloudformation deploy \ 34 | --stack-name boto3-macro-example \ 35 | --template-file example.template 36 | ``` 37 | 38 | ## Usage 39 | 40 | To make use of the macro, add `Transform: Boto3` to the top level of your CloudFormation template. 41 | 42 | Here is a trivial example template that adds a readme file to a new CodeCommit repository: 43 | 44 | ```yaml 45 | Transform: Boto3 46 | Resources: 47 | Repo: 48 | Type: AWS::CodeCommit::Repository 49 | Properties: 50 | RepositoryName: my-repo 51 | 52 | AddReadme: 53 | Type: Boto3::CodeCommit.put_file 54 | Mode: Create 55 | Properties: 56 | RepositoryName: !GetAtt Repo.Name 57 | BranchName: master 58 | FileContent: "Hello, world!" 59 | FilePath: README.md 60 | CommitMessage: Add a readme file 61 | Name: CloudFormation 62 | ``` 63 | 64 | ## Features 65 | 66 | ### Resource type 67 | 68 | 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. 69 | 70 | The `Type` must start with `Boto3::` and be followed by the name of a client, a `.` and finally the name of a method. 71 | 72 | The client name will be converted to lower case so that you can use resource names that look similar to other CloudFormation resource types. 73 | 74 | Examples: 75 | * `Boto3::CodeCommit.put_file` 76 | * `Boto3::IAM.put_user_permissions_boundary` 77 | * `Boto3::EC2.create_snapshot` 78 | 79 | ### Resource mode 80 | 81 | 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. 82 | 83 | The `Mode` may either be a string or a list of strings. For example: 84 | 85 | * `Mode: Create` 86 | * `Mode: Delete` 87 | * `Mode: [Create, Update]` 88 | 89 | ### Resource properties 90 | 91 | 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. 92 | 93 | ### Controlling the order of execution 94 | 95 | You can use the standard CloudFormation property `DependsOn` when you need to ensure that your `Boto3` resources are executed in the correct order. 96 | 97 | ## Examples 98 | 99 | 100 | The following resource: 101 | 102 | ```yaml 103 | ChangeBinaryTypes: 104 | Type: Boto3::CloudFormation.execute_change_set 105 | Mode: [Create, Update] 106 | Properties: 107 | ChangeSetName: !Ref ChangeSet 108 | StackName: !Ref Stack 109 | ``` 110 | 111 | will result in running the equivalent of the following: 112 | 113 | ```python 114 | boto3.client("cloudformation").execute_change_set(changeSetName=, stackName=) 115 | ``` 116 | 117 | when the stack is created or updated. 118 | 119 | ## Author 120 | 121 | [Steve Engledow](https://linkedin.com/in/stilvoid) 122 | Senior Solutions Builder 123 | Amazon Web Services 124 | -------------------------------------------------------------------------------- /Boto3/example.template: -------------------------------------------------------------------------------- 1 | Transform: Boto3 2 | 3 | Resources: 4 | Repo: 5 | Type: AWS::CodeCommit::Repository 6 | Properties: 7 | RepositoryName: my-repo 8 | 9 | AddReadme: 10 | Type: Boto3::CodeCommit.put_file 11 | Mode: Create 12 | Properties: 13 | RepositoryName: !GetAtt Repo.Name 14 | BranchName: master 15 | FileContent: "Hello, world" 16 | FilePath: README.md 17 | CommitMessage: Add another README.md 18 | Name: CloudFormation 19 | -------------------------------------------------------------------------------- /Boto3/lambda/macro.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You 4 | # may not use this file except in compliance with the License. A copy of 5 | # the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is 10 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | # ANY KIND, either express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | import os 15 | 16 | PREFIX = "Boto3::" 17 | 18 | LAMBDA_ARN = os.environ["LAMBDA_ARN"] 19 | 20 | def handle_template(request_id, template): 21 | for name, resource in template.get("Resources", {}).items(): 22 | if resource["Type"].startswith(PREFIX): 23 | resource.update({ 24 | "Type": "Custom::Boto3", 25 | "Version": "1.0", 26 | "Properties": { 27 | "ServiceToken": LAMBDA_ARN, 28 | "Mode": resource.get("Mode", ["Create", "Update"]), 29 | "Action": resource["Type"][len(PREFIX):], 30 | "Properties": resource.get("Properties", {}), 31 | }, 32 | }) 33 | 34 | if "Mode" in resource: 35 | del resource["Mode"] 36 | 37 | return template 38 | 39 | def handler(event, context): 40 | fragment = event["fragment"] 41 | status = "success" 42 | 43 | try: 44 | fragment = handle_template(event["requestId"], event["fragment"]) 45 | except Exception as e: 46 | status = "failure" 47 | 48 | return { 49 | "requestId": event["requestId"], 50 | "status": status, 51 | "fragment": fragment, 52 | } 53 | -------------------------------------------------------------------------------- /Boto3/lambda/resource.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You 4 | # may not use this file except in compliance with the License. A copy of 5 | # the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is 10 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | # ANY KIND, either express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | from urllib2 import build_opener, HTTPHandler, Request 15 | import base64 16 | import boto3 17 | import httplib 18 | import json 19 | 20 | def sendResponse(event, context, status, message): 21 | body = json.dumps({ 22 | "Status": status, 23 | "Reason": message, 24 | "StackId": event['StackId'], 25 | "RequestId": event['RequestId'], 26 | "LogicalResourceId": event['LogicalResourceId'], 27 | "PhysicalResourceId": event["ResourceProperties"]["Action"], 28 | "Data": {}, 29 | }) 30 | 31 | request = Request(event['ResponseURL'], data=body) 32 | request.add_header('Content-Type', '') 33 | request.add_header('Content-Length', len(body)) 34 | request.get_method = lambda: 'PUT' 35 | 36 | opener = build_opener(HTTPHandler) 37 | response = opener.open(request) 38 | 39 | def execute(action, properties): 40 | action = action.split(".") 41 | 42 | if len(action) != 2: 43 | return "FAILED", "Invalid boto3 call: {}".format(".".join(action)) 44 | 45 | client, function = action[0], action[1] 46 | 47 | try: 48 | client = boto3.client(client.lower()) 49 | except Exception as e: 50 | return "FAILED", "boto3 error: {}".format(e) 51 | 52 | try: 53 | function = getattr(client, function) 54 | except Exception as e: 55 | return "FAILED", "boto3 error: {}".format(e) 56 | 57 | properties = { 58 | key[0].lower() + key[1:]: value 59 | for key, value in properties.items() 60 | } 61 | 62 | try: 63 | function(**properties) 64 | except Exception as e: 65 | return "FAILED", "boto3 error: {}".format(e) 66 | 67 | return "SUCCESS", "Completed successfully" 68 | 69 | def handler(event, context): 70 | print("Received request:", json.dumps(event, indent=4)) 71 | 72 | request = event["RequestType"] 73 | properties = event["ResourceProperties"] 74 | 75 | if any(prop not in properties for prop in ("Action", "Properties")): 76 | print("Bad properties", properties) 77 | return sendResponse(event, context, "FAILED", "Missing required parameters") 78 | 79 | mode = properties["Mode"] 80 | 81 | if request == mode or request in mode: 82 | status, message = execute(properties["Action"], properties["Properties"]) 83 | return sendResponse(event, context, status, message) 84 | 85 | return sendResponse(event, context, "SUCCESS", "No action taken") 86 | -------------------------------------------------------------------------------- /Boto3/macro.template: -------------------------------------------------------------------------------- 1 | Transform: AWS::Serverless-2016-10-31 2 | 3 | Resources: 4 | ResourceFunction: 5 | Type: AWS::Serverless::Function 6 | Properties: 7 | Runtime: python2.7 8 | CodeUri: lambda 9 | Handler: resource.handler 10 | Policies: PowerUserAccess 11 | 12 | MacroFunction: 13 | Type: AWS::Serverless::Function 14 | Properties: 15 | Runtime: python3.6 16 | CodeUri: lambda 17 | Handler: macro.handler 18 | Environment: 19 | Variables: 20 | LAMBDA_ARN: !GetAtt ResourceFunction.Arn 21 | 22 | Macro: 23 | Type: AWS::CloudFormation::Macro 24 | Properties: 25 | Name: Boto3 26 | FunctionName: !GetAtt MacroFunction.Arn 27 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/aws-cloudformation/aws-cloudformation-macros/issues), or [recently closed](https://github.com/aws-cloudformation/aws-cloudformation-macros/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws-cloudformation/aws-cloudformation-macros/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/aws-cloudformation/aws-cloudformation-macros/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /Count/README.md: -------------------------------------------------------------------------------- 1 | # Count CloudFormation Macro 2 | 3 | The `Count` macro provides a template-wide `Count` property for CloudFormation resources. It allows you to specify multiple resources of the same type without having to cut and paste. 4 | 5 | ## How to install and use the Count macro in your AWS account 6 | 7 | ### Deploying 8 | 9 | 1. You will need an S3 bucket to store the CloudFormation artifacts: 10 | * If you don't have one already, create one with `aws s3 mb s3://` 11 | 12 | 2. Package the Macro 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. 13 | 14 | ```shell 15 | aws cloudformation package \ 16 | --template-file template.yaml \ 17 | --s3-bucket \ 18 | --output-template-file packaged.yaml 19 | ``` 20 | 21 | 3. Deploy the packaged CloudFormation template to a CloudFormation stack: 22 | 23 | ```shell 24 | aws cloudformation deploy \ 25 | --stack-name Count-macro \ 26 | --template-file packaged.yaml \ 27 | --capabilities CAPABILITY_IAM 28 | ``` 29 | 30 | 4. To test out the macro's capabilities, try launching the provided example template: 31 | 32 | ```shell 33 | aws cloudformation deploy \ 34 | --stack-name Count-test \ 35 | --template-file test.yaml \ 36 | --capabilities CAPABILITY_IAM 37 | ``` 38 | 39 | ### Usage 40 | 41 | To make use of the macro, add `Transform: Count` to the top level of your CloudFormation template. 42 | 43 | To create multiple copies of a resource, add a Count propert with an integer value. 44 | 45 | ```yaml 46 | AWSTemplateFormatVersion: "2010-09-09" 47 | Transform: Count 48 | Resources: 49 | Bucket: 50 | Type: AWS::S3::Bucket 51 | Count: 3 52 | SQS: 53 | Type: AWS:::SQS::Queue 54 | Count: 2 55 | ``` 56 | #### Note 57 | This will cause the resource "Bucket" to be multiplied 3 times. The new template will contain Bucket1, Bucket2 and Bucket3 but will not contain Bucket as this will be removed. 58 | 59 | ### Using decimal placeholders 60 | When resources are multiplied, you can put a decimal placeholder %d into any string value that you wish to be replaced with the iterator index number. 61 | 62 | e.g. 63 | ```yaml 64 | AWSTemplateFormatVersion: "2010-09-09" 65 | Transform: Count 66 | Resources: 67 | Bucket: 68 | Type: AWS::S3::Bucket 69 | Properties: 70 | Tags: 71 | - Key: TestKey 72 | Value: my bucket %d 73 | Count: 3 74 | ``` 75 | 76 | Using this example, the processed template will result become: 77 | ```yaml 78 | AWSTemplateFormatVersion: "2010-09-09" 79 | Resources: 80 | Bucket1: 81 | Type: AWS::S3::Bucket 82 | Properties: 83 | Tags: 84 | - Key: TestKey 85 | Value: my bucket 1 86 | Bucket2: 87 | Type: AWS::S3::Bucket 88 | Properties: 89 | Tags: 90 | - Key: TestKey 91 | Value: my bucket 2 92 | Bucket3: 93 | Type: AWS::S3::Bucket 94 | Properties: 95 | Tags: 96 | - Key: TestKey 97 | Value: my bucket 3 98 | ``` 99 | 100 | ### Important - Naming resources 101 | 102 | You cannot use Count on resources that use a hardcoded name (`Name:` property). Duplicate names will cause a CloudFormation runtime failure. 103 | If you wish to specify a name then you can use the decimal place holder %d in the name which will cause the name to incorporate the iterator value. 104 | 105 | e.g. 106 | ```yaml 107 | AWSTemplateFormatVersion: "2010-09-09" 108 | Resources: 109 | Bucket1: 110 | Type: AWS::S3::Bucket 111 | Properties: 112 | BucketName: MyBucket%d 113 | ``` 114 | 115 | ## Author 116 | 117 | [Jose Ferraris](https://github.com/j0lly) 118 | AWS ProServ DevOps Consultant 119 | Amazon Web Services 120 | -------------------------------------------------------------------------------- /Count/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Thx to @sengledo ;) 4 | 5 | if [ -z "${ARTIFACT_BUCKET}" ]; then 6 | echo "This deployment script needs an S3 bucket to store CloudFormation artifacts." 7 | echo "You can also set this by doing: export ARTIFACT_BUCKET=my-bucket-name" 8 | echo 9 | read -p "S3 bucket to store artifacts: " ARTIFACT_BUCKET 10 | fi 11 | 12 | MACRO_NAME=$(basename $(pwd)) 13 | 14 | aws cloudformation package \ 15 | --template-file template.yaml \ 16 | --s3-bucket ${ARTIFACT_BUCKET} \ 17 | --output-template-file packaged.yaml 18 | 19 | aws cloudformation deploy \ 20 | --stack-name ${MACRO_NAME}-macro \ 21 | --template-file packaged.yaml \ 22 | --capabilities CAPABILITY_IAM 23 | 24 | aws cloudformation deploy \ 25 | --stack-name ${MACRO_NAME}-test \ 26 | --template-file test.yaml \ 27 | --capabilities CAPABILITY_IAM 28 | -------------------------------------------------------------------------------- /Count/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "requestId": 43242, 3 | "fragment": { 4 | "Resources": { 5 | "HelloBucket": { 6 | "Type": "AWS::S3::Bucket", 7 | "Count": 2, 8 | "Properties": { 9 | "AccessControl": "PublicRead" 10 | } 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Count/event_bad.json: -------------------------------------------------------------------------------- 1 | { 2 | "requestId": 43242, 3 | "fragment": { 4 | "Resources": { 5 | "HelloBucket": { 6 | "Type": "AWS::S3::Bucket", 7 | "Count": 3, 8 | "Properties": { 9 | "AccessControl": "PublicRead" 10 | } 11 | }, 12 | "HelloBucket1": { 13 | "Type": "AWS::S3::Bucket", 14 | "Count": 3, 15 | "Properties": { 16 | "AccessControl": "PublicRead" 17 | } 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Count/requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.20.0 2 | -------------------------------------------------------------------------------- /Count/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-cloudformation/aws-cloudformation-macros/e5cb01b3d33b948993688fc739b6ab813f238a63/Count/src/__init__.py -------------------------------------------------------------------------------- /Count/src/index.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | 4 | def process_template(template): 5 | new_template = copy.deepcopy(template) 6 | status = 'success' 7 | 8 | for name, resource in template['Resources'].items(): 9 | if 'Count' in resource: 10 | #Get the number of times to multiply the resource 11 | count = new_template['Resources'][name].pop('Count') 12 | print("Found 'Count' property with value {} in '{}' resource....multiplying!".format(count,name)) 13 | #Remove the original resource from the template but take a local copy of it 14 | resourceToMultiply = new_template['Resources'].pop(name) 15 | #Create a new block of the resource multiplied with names ending in the iterator and the placeholders substituted 16 | resourcesAfterMultiplication = multiply(name, resourceToMultiply, count) 17 | if not set(resourcesAfterMultiplication.keys()) & set(new_template['Resources'].keys()): 18 | new_template['Resources'].update(resourcesAfterMultiplication) 19 | else: 20 | status = 'failed' 21 | return status, template 22 | else: 23 | print("Did not find 'Count' property in '{}' resource....Nothing to do!".format(name)) 24 | return status, new_template 25 | 26 | def update_placeholder(resource_structure, iteration): 27 | #Convert the json into a string 28 | resourceString = json.dumps(resource_structure) 29 | #Count the number of times the placeholder is found in the string 30 | placeHolderCount = resourceString.count('%d') 31 | 32 | #If the placeholder is found then replace it 33 | if placeHolderCount > 0: 34 | print("Found {} occurrences of decimal placeholder in JSON, replacing with iterator value {}".format(placeHolderCount, iteration)) 35 | #Make a list of the values that we will use to replace the decimal placeholders - the values will all be the same 36 | placeHolderReplacementValues = [iteration] * placeHolderCount 37 | #Replace the decimal placeholders using the list - the syntax below expands the list 38 | resourceString = resourceString % (*placeHolderReplacementValues,) 39 | #Convert the string back to json and return it 40 | return json.loads(resourceString) 41 | else: 42 | print("No occurences of decimal placeholder found in JSON, therefore nothing will be replaced") 43 | return resource_structure 44 | 45 | def multiply(resource_name, resource_structure, count): 46 | resources = {} 47 | #Loop according to the number of times we want to multiply, creating a new resource each time 48 | for iteration in range(1, (count + 1)): 49 | print("Multiplying '{}', iteration count {}".format(resource_name,iteration)) 50 | multipliedResourceStructure = update_placeholder(resource_structure,iteration) 51 | resources[resource_name+str(iteration)] = multipliedResourceStructure 52 | return resources 53 | 54 | 55 | def handler(event, context): 56 | result = process_template(event['fragment']) 57 | return { 58 | 'requestId': event['requestId'], 59 | 'status': result[0], 60 | 'fragment': result[1], 61 | } 62 | -------------------------------------------------------------------------------- /Count/template.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | AWSTemplateFormatVersion: '2010-09-09' 3 | Transform: AWS::Serverless-2016-10-31 4 | Description: > 5 | Count macro 6 | A simple iterator for creating multipledentical resources 7 | 8 | Resources: 9 | Macro: 10 | Type: AWS::CloudFormation::Macro 11 | Properties: 12 | Name: Count 13 | FunctionName: !GetAtt CountMacroFunction.Arn 14 | CountMacroFunction: 15 | Type: AWS::Serverless::Function 16 | Properties: 17 | CodeUri: src 18 | Handler: index.handler 19 | Runtime: python3.6 20 | Timeout: 5 21 | -------------------------------------------------------------------------------- /Count/test.yaml: -------------------------------------------------------------------------------- 1 | Transform: 2 | - Count 3 | Resources: 4 | Bucket: 5 | Type: AWS::S3::Bucket 6 | Count: 3 7 | 8 | -------------------------------------------------------------------------------- /Count/test_2.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: 3 | - Count 4 | Resources: 5 | BucketToCopy: 6 | Type: AWS::S3::Bucket 7 | Properties: 8 | Tags: 9 | - Key: TestKey 10 | Value: my bucket %d 11 | - Key: Another key 12 | Value: "%d value" 13 | Count: 3 14 | -------------------------------------------------------------------------------- /ExecutionRoleBuilder/ExecutionRoleBuilderCFnMacro.packaged.template: -------------------------------------------------------------------------------- 1 | Resources: 2 | Function: 3 | Properties: 4 | CodeUri: s3://cf-templates-1nytk39ncp83s-us-west-2/8145574c6b892d83062fa8b4d4b2bd2d 5 | Handler: index.handler 6 | Runtime: python2.7 7 | Type: AWS::Serverless::Function 8 | Macro: 9 | Properties: 10 | FunctionName: 11 | Fn::GetAtt: 12 | - Function 13 | - Arn 14 | Name: ExecutionRoleBuilder 15 | Type: AWS::CloudFormation::Macro 16 | Transform: AWS::Serverless-2016-10-31 17 | -------------------------------------------------------------------------------- /ExecutionRoleBuilder/README.md: -------------------------------------------------------------------------------- 1 | # Execution Role Builder CloudFormation Macro 2 | 3 | The `Execution Role Builder` macro provides a more natural syntax for developers to express the permissions they want to attach to IAM execution roles for their applications, while simultaneously providing IAM administrators with a way to templatize those permissions. When used in conjunction with permission boundaries, this provides an effective solution for delegated role creation. 4 | 5 | See below for instructions to install and use the macro and for a full description of the macro's features. 6 | 7 | ## How to install and use the ShortHand macro in your AWS account 8 | 9 | ### Deploying 10 | 11 | 1. You will need an S3 bucket to store the CloudFormation artifacts: 12 | * If you don't have one already, create one with `aws s3 mb s3://` 13 | 14 | 2. While optional, this macro is normally expected to work in conjunction with permission boundaries: 15 | * If you don't have one already, follow [this example](https://aws.amazon.com/blogs/security/delegate-permission-management-to-developers-using-iam-permissions-boundaries/) 16 | * Note the ARN of the permission boundary for use below. 17 | 18 | 3. Package the Macro 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. 19 | 20 | ```shell 21 | aws cloudformation package \ 22 | --template-file macro.template \ 23 | --s3-bucket \ 24 | --output-template-file ExecutionRoleBuilderCFnMacro.packaged.template 25 | ``` 26 | 27 | 4. Deploy the packaged CloudFormation template to a CloudFormation stack: 28 | 29 | ```shell 30 | aws cloudformation deploy \ 31 | --stack-name ExecutionRoleBuilderCFnMacro.packaged.template \ 32 | --template-file ExecutionRoleBuilderCFnMacro \ 33 | --capabilities CAPABILITY_IAM 34 | ``` 35 | 36 | 5. To test out the macro's capabilities, try launching the provided example template: 37 | 38 | ```shell 39 | aws cloudformation deploy \ 40 | --stack-name executionrolebuilder-macro-example \ 41 | --template-file example.template \ 42 | --parameter-overrides PermissionBoundaryArn= \ 43 | --capabilities CAPABILITY_IAM 44 | ``` 45 | 46 | ### Usage 47 | 48 | To make use of the macro, add `Transform: ExecutionRoleBuilder` to the top level of your CloudFormation template. 49 | 50 | Then specify permissions using a more natural syntax: 51 | 52 | ```yaml 53 | AWSTemplateFormatVersion: "2010-09-09" 54 | Transform: ExecutionRoleBuilder 55 | Parameters: 56 | PermissionBoundaryArn: 57 | Type: String 58 | Description: ARN for the Permission Boundary Policy 59 | Resources: 60 | ExecutionRoleBuilderMacroTestRole: 61 | Type: "AWS::IAM::Role" 62 | Properties: 63 | Type: "Lambda" 64 | Name: "ExecutionRoleForAppA" 65 | Path: "/boundedexecroles/" 66 | PermissionsBoundary: 67 | Ref: PermissionBoundaryArn 68 | Permissions: 69 | - ReadOnly: "arn:aws:s3:::mygreatbucket1" 70 | - ReadWrite: "arn:aws:dynamodb:us-west-2:123456789012:table/table1" 71 | - ReadOnly: "arn:aws:ssm:us-west-2:123456789012:parameter/dev/myapp1/*" 72 | - ReadOnly: "arn:aws:kms:us-west-2:123456789012:key/a8f4be2b-5fcd-zzzz-yyyy-xxxxxxxxxxxx" 73 | ``` 74 | 75 | After you have the basics working, customize the policy templates within the lambda function to tailor the resulting policies as desired. 76 | 77 | ### Important 78 | 79 | The lambda function associated with this CFn macro is transforming short hand permission syntax into proper CloudFormation used to construct IAM policies. The proper use of boundary policies provides that outer boundary of what is permissible within those policies, but regardless, proper care should be taken to understand who has the ability to alter the lambda function, and that the function contents are what you expect them to be. 80 | 81 | ## Author 82 | 83 | [Quint Van Deman](www.linkedin.com/in/quint-van-deman) 84 | Prinicipal Business Development Manager 85 | Amazon Web Services 86 | -------------------------------------------------------------------------------- /ExecutionRoleBuilder/example.template: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: ExecutionRoleBuilder 3 | Parameters: 4 | PermissionBoundaryArn: 5 | Type: String 6 | Description: ARN for the Permission Boundary Policy 7 | Resources: 8 | ExecutionRoleBuilderMacroTestRole: 9 | Type: "AWS::IAM::Role" 10 | Properties: 11 | Type: "Lambda" 12 | Name: "ExecutionRoleForAppA" 13 | Path: "/boundedexecroles/" 14 | PermissionsBoundary: 15 | Ref: PermissionBoundaryArn 16 | Permissions: 17 | - ReadOnly: "arn:aws:s3:::mygreatbucket1" 18 | - ReadWrite: "arn:aws:dynamodb:us-west-2:123456789012:table/table1" 19 | - ReadOnly: "arn:aws:ssm:us-west-2:123456789012:parameter/dev/myapp1/*" 20 | - ReadOnly: "arn:aws:kms:us-west-2:123456789012:key/a8f4be2b-5fcd-zzzz-yyyy-xxxxxxxxxxxx" 21 | -------------------------------------------------------------------------------- /ExecutionRoleBuilder/lambda/index.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). You 5 | # may not use this file except in compliance with the License. A copy of 6 | # the License is located at 7 | # 8 | # http://aws.amazon.com/apache2.0/ 9 | # 10 | # or in the "license" file accompanying this file. This file is 11 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 12 | # ANY KIND, either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | import json 16 | import uuid 17 | from policytemplates import * 18 | 19 | # Variable for the default role path, if a role path is not provided 20 | defaultrolepath = '/boundedexecutionroles/' 21 | 22 | # Core function handler 23 | def handler(event, context): 24 | return { 25 | "requestId": event["requestId"], 26 | "status": "success", 27 | "fragment": convert_template(event["fragment"]), 28 | } 29 | 30 | # Function to convert/expand the template 31 | def convert_template(fragment): 32 | # Debug output 33 | print ('This was the fragment: {}'.format(fragment)) 34 | 35 | # Loop through each resource in the template 36 | resources = fragment['Resources'] 37 | for resource in resources: 38 | print ('Determining if {} is an IAM role'.format(resource)) 39 | resourcejson = resources[resource] 40 | # If the resource is an IAM Role, expand the shorthand notation to the proper 41 | # CloudFormation using the function below, otherwise leave the resource as is 42 | if resourcejson['Type'] == 'AWS::IAM::Role': 43 | print ('Found a role: {}'.format(resource)) 44 | # Expanding role 45 | resources[resource] = expand_role(resourcejson) 46 | 47 | # Debug output 48 | print ('This is the transformed fragment: {}'.format(fragment)) 49 | # Return the converted/expanded template fragment 50 | return fragment 51 | 52 | # Function to expand shorthand role definitions into proper CloudFormation 53 | def expand_role(rolefragment): 54 | # Debug output 55 | print ('This is the role fragment: {}'.format(rolefragment)) 56 | 57 | # Extract shorthand properties for role type, name, and desired permissions 58 | roletype = rolefragment['Properties']['Type'] 59 | rolename = rolefragment['Properties']['Name'] 60 | permissions = rolefragment['Properties']['Permissions'] 61 | 62 | # Get the basic role template (from policytemplates.py) and do a simple string 63 | # replace to set the name and the AWS service principal for the trust policy (e.g. lambda) 64 | returnval = roletemplate.replace('',roletype.lower()) 65 | returnval = returnval.replace('',rolename) 66 | # Load this as json to form the initial basis of the function return value 67 | returnvaljson = json.loads(returnval) 68 | 69 | # If the shorthand notation included a list of managed policy ARNs pass those though as-is 70 | if 'ManagedPolicyArns' in rolefragment['Properties']: 71 | returnvaljson['Properties']['ManagedPolicyArns'] = rolefragment['Properties']['ManagedPolicyArns'] 72 | 73 | # If the shorthand notation included a permission boundary pass that through as-is 74 | if 'PermissionsBoundary' in rolefragment['Properties']: 75 | returnvaljson['Properties']['PermissionsBoundary'] = rolefragment['Properties']['PermissionsBoundary'] 76 | 77 | # If the shorthand notation included a role path pass that through as-is 78 | # If it did not, provide an opinionated configuration using the variable above 79 | if 'Path' in rolefragment['Properties']: 80 | returnvaljson['Properties']['Path'] = rolefragment['Properties']['Path'] 81 | else: 82 | returnvaljson['Properties']['Path'] = defaultrolepath 83 | 84 | # Loop through each of the short hand permissions 85 | for permission in permissions: 86 | # Debug output 87 | print ('permission: {}'.format(permission)) 88 | # Split each shorthand permission into an action group (e.g. ReadOnly) and the associated Resource 89 | for actiongroup,resource in permission.items(): 90 | print ('actiongroup: {}, resource: {}'.format(actiongroup,resource)) 91 | # Use the function below to extract the service (e.g. S3) from the resource ARN 92 | service = servicefromresource(resource) 93 | print ('service: {}'.format(service)) 94 | # Lookup the given policy snippet from policytemplates.py based on the service & action group 95 | # If the necessary snippet isn't included in policytemplates.py err out 96 | if service in policytemplates and actiongroup in policytemplates[service]: 97 | policytemplate = policytemplates[service][actiongroup] 98 | else: 99 | # TODO: Better error handling 100 | raise Exception('No policy template found for service: {} and actiongroup: {}'.format(service,actiongroup)) 101 | # Substitute the placeholder in the template for the actual resource 102 | policytemplate = policytemplate.replace('',resource) 103 | # Policy names must be unique, appending a UUID is a simple way to guarantee that 104 | uuidval = str(uuid.uuid4()) 105 | policytemplate = policytemplate.replace('', uuidval) 106 | # Convert the policy snippet to json and add it as an inline policy to the overall return values 107 | policytemplatejson = json.loads(policytemplate) 108 | print ('adding policy: {}'.format(policytemplate)) 109 | returnvaljson['Properties']['Policies'].append(policytemplatejson) 110 | 111 | # In addition to the permissions in the shorthand notation add the 'allroles' policy template 112 | # This template is used to provide permissions like CloudWatchLogs instead of forcing each 113 | # developer to repeatedly specify common permissions 114 | uuidval = str(uuid.uuid4()) 115 | allrolespolicytemplate = policytemplates['allroles']['default'] 116 | allrolespolicytemplate = allrolespolicytemplate.replace('', uuidval) 117 | allrolespolicytemplatejson = json.loads(allrolespolicytemplate) 118 | print ('adding policy: {}'.format(allrolespolicytemplate)) 119 | returnvaljson['Properties']['Policies'].append(allrolespolicytemplatejson) 120 | 121 | # Return the expanded proper CloudFormation 122 | return returnvaljson 123 | 124 | # Simple function to return the AWS service (e.g. S3) from a given resource ARN 125 | def servicefromresource(resource): 126 | return resource.split(':')[2] 127 | -------------------------------------------------------------------------------- /ExecutionRoleBuilder/lambda/policytemplates.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). You 5 | # may not use this file except in compliance with the License. A copy of 6 | # the License is located at 7 | # 8 | # http://aws.amazon.com/apache2.0/ 9 | # 10 | # or in the "license" file accompanying this file. This file is 11 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 12 | # ANY KIND, either express or implied. See the License for the specific 13 | # language governing permissions and limitations under the License. 14 | 15 | # This file is used to hold policy snippets. 16 | # TODO: Move these snippets to managed policies under a hierarchy 17 | # (e.g. /policytemplates/s3/readonly) and adjust the code accordingly 18 | 19 | # Overall role template with substitution tokens for the ROLENAME & ROLETYPE 20 | roletemplate = ''' 21 | { 22 | "Type": "AWS::IAM::Role", 23 | "Properties": { 24 | "RoleName" : "", 25 | "AssumeRolePolicyDocument": { 26 | "Version" : "2012-10-17", 27 | "Statement": [ { 28 | "Effect": "Allow", 29 | "Principal": { 30 | "Service": [ ".amazonaws.com" ] 31 | }, 32 | "Action": [ "sts:AssumeRole" ] 33 | } ] 34 | }, 35 | "Policies": [ ] 36 | } 37 | } 38 | ''' 39 | 40 | # Policy template for Read Only access to a single S3 bucket 41 | # TODO: Customize this policy as desired using allowable AWS IAM syntax. 42 | # The substituion token will be populated with the actual resource at runtime 43 | s3_readonly_template = ''' 44 | { 45 | "PolicyName": "S3-ReadOnly-", 46 | "PolicyDocument" : { 47 | "Version": "2012-10-17", 48 | "Statement": [ 49 | { 50 | "Effect": "Allow", 51 | "Action": [ "s3:GetBucketLocation", "s3:ListAllMyBuckets" ], 52 | "Resource": "*" 53 | }, 54 | { 55 | "Effect": "Allow", 56 | "Action": ["s3:ListBucket"], 57 | "Resource": "" 58 | }, 59 | { 60 | "Effect": "Allow", 61 | "Action": "s3:GetObject", 62 | "Resource": "/*" 63 | } 64 | ] } 65 | } 66 | ''' 67 | 68 | # Policy template for Read Write access to a single S3 bucket 69 | # TODO: Customize this policy as desired using allowable AWS IAM syntax. 70 | # The substituion token will be populated with the actual resource at runtime 71 | s3_readwrite_template = ''' 72 | { 73 | "PolicyName": "S3-ReadWrite-", 74 | "PolicyDocument" : { 75 | "Version": "2012-10-17", 76 | "Statement": [ 77 | { 78 | "Effect": "Allow", 79 | "Action": [ "s3:GetBucketLocation", "s3:ListAllMyBuckets" ], 80 | "Resource": "*" 81 | }, 82 | { 83 | "Effect": "Allow", 84 | "Action": ["s3:ListBucket"], 85 | "Resource": "" 86 | }, 87 | { 88 | "Effect": "Allow", 89 | "Action": [ "s3:GetObject", "s3:PutObject" ], 90 | "Resource": "/*" 91 | } 92 | ] } 93 | } 94 | ''' 95 | 96 | # Policy template for Read Only access to a single DynamoDB table 97 | # TODO: Customize this policy as desired using allowable AWS IAM syntax. 98 | # The substituion token will be populated with the actual resource at runtime 99 | ddb_readonly_template = ''' 100 | { 101 | "PolicyName": "DDB-ReadOnly-", 102 | "PolicyDocument" : { 103 | "Version": "2012-10-17", 104 | "Statement": [ 105 | { 106 | "Effect": "Allow", 107 | "Action": "dynamodb:GetItem", 108 | "Resource": "" 109 | } 110 | ] } 111 | } 112 | ''' 113 | 114 | # Policy template for Read Write access to a single DynamoDB table 115 | # TODO: Customize this policy as desired using allowable AWS IAM syntax. 116 | # The substituion token will be populated with the actual resource at runtime 117 | ddb_readwrite_template = ''' 118 | { 119 | "PolicyName": "DDB-ReadWrite-", 120 | "PolicyDocument" : { 121 | "Version": "2012-10-17", 122 | "Statement": [ 123 | { 124 | "Effect": "Allow", 125 | "Action": [ "dynamodb:GetItem", "dynamodb:PutItem" ], 126 | "Resource": "" 127 | } 128 | ] } 129 | } 130 | ''' 131 | 132 | # Policy template for Read Only access to SSM parameter store values under a given path 133 | # TODO: Customize this policy as desired using allowable AWS IAM syntax. 134 | # The substituion token will be populated with the actual resource at runtime 135 | ssm_readonly_template = ''' 136 | { 137 | "PolicyName": "SSM-ReadOnly-", 138 | "PolicyDocument" : { 139 | "Version": "2012-10-17", 140 | "Statement": [ 141 | { 142 | "Action": [ 143 | "ssm:GetParameter", 144 | "ssm:GetParameters" 145 | ], 146 | "Effect": "Allow", 147 | "Resource": [ 148 | "" 149 | ] 150 | } 151 | ] } 152 | } 153 | ''' 154 | 155 | # Policy template for Read Only access to a single KMS Key 156 | # TODO: Customize this policy as desired using allowable AWS IAM syntax. 157 | # The substituion token will be populated with the actual resource at runtime 158 | kms_readonly_template = ''' 159 | { 160 | "PolicyName": "KMS-ReadOnly-", 161 | "PolicyDocument" : { 162 | "Version": "2012-10-17", 163 | "Statement": [ 164 | { 165 | "Action": "kms:Decrypt", 166 | "Effect": "Allow", 167 | "Resource": "" 168 | } 169 | ] } 170 | } 171 | ''' 172 | 173 | # Policy template that will be added to all roles built by the macro. This eliminates 174 | # the need to specify permissions that are always desired on each and every role. 175 | # The policy below provides CloudWatchLogs, CodeDeploy, and SSM Session Manager access. 176 | # TODO: Customize this policy as desired using allowable AWS IAM syntax. 177 | all_roles_template = ''' 178 | { 179 | "PolicyName": "All-Roles-", 180 | "PolicyDocument" : { 181 | "Version": "2012-10-17", 182 | "Statement": [ 183 | { 184 | "Effect" : "Allow", 185 | "Action" : "logs:*", 186 | "Resource" : "arn:aws:logs:*:*:*" 187 | }, 188 | { 189 | "Effect": "Allow", 190 | "Action": [ 191 | "s3:Get*", 192 | "s3:List*" 193 | ], 194 | "Resource": [ 195 | "arn:aws:s3:::aws-codedeploy-us-east-2/*", 196 | "arn:aws:s3:::aws-codedeploy-us-east-1/*", 197 | "arn:aws:s3:::aws-codedeploy-us-west-1/*", 198 | "arn:aws:s3:::aws-codedeploy-us-west-2/*" 199 | ] 200 | }, 201 | { 202 | "Effect": "Allow", 203 | "Action": [ 204 | "ssm:UpdateInstanceInformation", 205 | "ssmmessages:CreateControlChannel", 206 | "ssmmessages:CreateDataChannel", 207 | "ssmmessages:OpenControlChannel", 208 | "ssmmessages:OpenDataChannel" 209 | ], 210 | "Resource": "*" 211 | }, 212 | { 213 | "Effect": "Allow", 214 | "Action": [ 215 | "s3:GetEncryptionConfiguration" 216 | ], 217 | "Resource": "*" 218 | } 219 | ] } 220 | } 221 | ''' 222 | 223 | # TODO: Add policy snippets for other services (e.g. SNS, SQS, etc) and other 224 | # action groups (e.g. ReadOnly, ReadWrite, FullAccess) using the samples above 225 | # as a reference 226 | 227 | # Simple data structure to hold all of the policy templates for easier lookup/reference 228 | policytemplates = {} 229 | # Add the S3 policy templates 230 | s3policytemplates = {} 231 | s3policytemplates['ReadOnly'] = s3_readonly_template 232 | s3policytemplates['ReadWrite'] = s3_readwrite_template 233 | policytemplates['s3'] = s3policytemplates 234 | # Add the DynamoDB policy templates 235 | ddbpolicytemplates = {} 236 | ddbpolicytemplates['ReadOnly'] = ddb_readonly_template 237 | ddbpolicytemplates['ReadWrite'] = ddb_readwrite_template 238 | policytemplates['dynamodb'] = ddbpolicytemplates 239 | # Add the SSM policy templates 240 | ssmpolicytemplates = {} 241 | ssmpolicytemplates['ReadOnly'] = ssm_readonly_template 242 | policytemplates['ssm'] = ssmpolicytemplates 243 | # Add the KMS policy templates 244 | kmspolicytemplates = {} 245 | kmspolicytemplates['ReadOnly'] = kms_readonly_template 246 | policytemplates['kms'] = kmspolicytemplates 247 | # Add the all roles policy templates 248 | allrolestemplates = {} 249 | allrolestemplates['default'] = all_roles_template 250 | policytemplates['allroles'] = allrolestemplates 251 | -------------------------------------------------------------------------------- /ExecutionRoleBuilder/macro.template: -------------------------------------------------------------------------------- 1 | Transform: AWS::Serverless-2016-10-31 2 | 3 | Resources: 4 | Function: 5 | Type: AWS::Serverless::Function 6 | Properties: 7 | Runtime: python2.7 8 | CodeUri: lambda 9 | Handler: index.handler 10 | 11 | Macro: 12 | Type: AWS::CloudFormation::Macro 13 | Properties: 14 | Name: ExecutionRoleBuilder 15 | FunctionName: !GetAtt Function.Arn 16 | -------------------------------------------------------------------------------- /Explode/.gitignore: -------------------------------------------------------------------------------- 1 | packaged.yml 2 | -------------------------------------------------------------------------------- /Explode/README.md: -------------------------------------------------------------------------------- 1 | # Explode CloudFormation Macro 2 | 3 | The `Explode` macro provides a template-wide `Explode` property for CloudFormation resources. Similar to the Count macro, it will create multiple copies of a template Resource, but looks up values to inject into each copy in a Mapping. 4 | 5 | ## How to install and use the Explode macro in your AWS account 6 | 7 | ### Deploying 8 | 9 | 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://` 10 | 11 | 2. Package the Macro 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. 12 | 13 | ```shell 14 | aws cloudformation package \ 15 | --template-file macro.yaml \ 16 | --s3-bucket \ 17 | --output-template-file packaged.yaml 18 | ``` 19 | 20 | 3. Deploy the packaged CloudFormation template to a CloudFormation stack: 21 | 22 | ```shell 23 | aws cloudformation deploy \ 24 | --stack-name Explode-macro \ 25 | --template-file packaged.yaml \ 26 | --capabilities CAPABILITY_IAM 27 | ``` 28 | 29 | 4. To test out the macro's capabilities, try launching the provided example template: 30 | 31 | ```shell 32 | aws cloudformation deploy \ 33 | --stack-name Explode-test \ 34 | --template-file test.yaml \ 35 | --capabilities CAPABILITY_IAM 36 | ``` 37 | 38 | ### Usage 39 | 40 | To make use of the macro, add `Transform: Explode` to the top level of your CloudFormation template. 41 | 42 | Add a mapping (to the `Mappings` section of your template) which contains the instances of the resource values you want to use. Each entry in the mapping will be used for another copy of the resource, and the values inside it will be copied into that instance. The entry name will be appended to the template resource name, unless a value `ResourceName` is given, which if present will be used as the complete resource name. 43 | 44 | For the resource you want to explode, add an `ExplodeMap` value at the top level pointing at the entry from your Mappings which should be used. You can use the same mapping against multiple resource entries. 45 | 46 | Inside the resource properties, you can use `!Explode KEY` to pull the value of `KEY` out of your mapping. 47 | 48 | An example is probably in order: 49 | 50 | ```yaml 51 | AWSTemplateFormatVersion: "2010-09-09" 52 | Transform: Explode 53 | Mappings: 54 | BucketMap: 55 | Monthly: 56 | ResourceName: MyThirtyDayBucket 57 | Retention: 30 58 | Yearly: 59 | Retention: 365 60 | 61 | Resources: 62 | Bucket: 63 | ExplodeMap: BucketMap 64 | Type: AWS::S3::Bucket 65 | Properties: 66 | LifecycleConfiguration: 67 | Rules: 68 | - 69 | ExpirationInDays: !Explode Retention 70 | Status: Enabled 71 | ``` 72 | 73 | This will result in two Bucket resources; one named `MyThirtyDayBucket` with a 74 | lifecycle rule for 30 day retention, and another named `BucketYearly` with 365 75 | day retention. 76 | 77 | ### Important - Naming resources 78 | 79 | You cannot use Explode on resources that use a hardcoded name (`Name:` 80 | property). Duplicate names will cause a CloudFormation runtime failure. 81 | If you wish to specify a name then you must use `!Explode` with a mapped value 82 | to make each resource's name unique. 83 | 84 | For example: 85 | 86 | ```yaml 87 | AWSTemplateFormatVersion: "2010-09-09" 88 | Mappings: 89 | BucketMap: 90 | Example: 91 | Name: MyExampleBucket 92 | Resources: 93 | Bucket: 94 | Type: AWS::S3::Bucket 95 | ExplodeMap: BucketMap 96 | Properties: 97 | BucketName: !Explode Name 98 | ``` 99 | 100 | ## Author 101 | 102 | [James Seward](https://github.com/jamesoff); AWS Solutions Architect, Amazon Web Services 103 | -------------------------------------------------------------------------------- /Explode/lambda/explode.py: -------------------------------------------------------------------------------- 1 | """ 2 | CloudFormation template transform macro: Explode 3 | """ 4 | 5 | import re 6 | import sys 7 | 8 | 9 | EXPLODE_RE = re.compile(r'(?i)!Explode (?P\w+)') 10 | 11 | 12 | def walk_resource(resource, map_data): 13 | """Recursively process a resource.""" 14 | new_resource = {} 15 | for key, value in resource.items(): 16 | if isinstance(value, dict): 17 | new_resource[key] = walk_resource(value, map_data) 18 | elif isinstance(value, list): 19 | new_resource[key] = [walk_resource(x, map_data) for x in value] 20 | elif isinstance(value, str): 21 | match = EXPLODE_RE.search(value) 22 | while match: 23 | explode_key = match.group('explode_key') 24 | try: 25 | replace_value = map_data[explode_key] 26 | except KeyError: 27 | print("Missing item {} in mapping while processing {}: {}".format( 28 | explode_key, 29 | key, 30 | value)) 31 | if isinstance(replace_value, int): 32 | value = replace_value 33 | # No further explosion is possible on an int 34 | match = None 35 | else: 36 | value = value.replace(match.group(0), replace_value) 37 | match = EXPLODE_RE.search(value) 38 | new_resource[key] = value 39 | else: 40 | new_resource[key] = value 41 | return new_resource 42 | 43 | 44 | def handle_transform(template): 45 | """Go through template and explode resources.""" 46 | mappings = template['Mappings'] 47 | resources = template['Resources'] 48 | new_resources = {} 49 | for resource_name, resource in resources.items(): 50 | try: 51 | explode_map = resource['ExplodeMap'] 52 | del resource['ExplodeMap'] 53 | except KeyError: 54 | # This resource does not have an ExplodeMap, so copy it verbatim 55 | # and move on 56 | new_resources[resource_name] = resource 57 | continue 58 | try: 59 | explode_map_data = mappings[explode_map] 60 | except KeyError: 61 | # This resource refers to a mapping entry which doesn't exist, so 62 | # fail 63 | print('Unable to find mapping for exploding resource {}'.format(resource_name)) 64 | raise 65 | resource_instances = explode_map_data.keys() 66 | for resource_instance in resource_instances: 67 | new_resource = walk_resource(resource, explode_map_data[resource_instance]) 68 | if 'ResourceName' in explode_map_data[resource_instance]: 69 | new_resource_name = explode_map_data[resource_instance]['ResourceName'] 70 | else: 71 | new_resource_name = resource_name + resource_instance 72 | new_resources[new_resource_name] = new_resource 73 | template['Resources'] = new_resources 74 | return template 75 | 76 | 77 | def handler(event, _context): 78 | """Handle invocation in Lambda (when CloudFormation processes the Macro)""" 79 | fragment = event["fragment"] 80 | status = "success" 81 | 82 | try: 83 | fragment = handle_transform(event["fragment"]) 84 | except: 85 | status = "failure" 86 | 87 | return { 88 | "requestId": event["requestId"], 89 | "status": status, 90 | "fragment": fragment, 91 | } 92 | 93 | 94 | if __name__ == "__main__": 95 | """ 96 | If run from the command line, parse the file specified and output it 97 | This is quite naive; CF YAML tags like !GetAtt will break it (as will 98 | !Explode, but you can hide that in a string). Probably best to use JSON. 99 | Releatedly, always outputs JSON. 100 | """ 101 | if len(sys.argv) == 2: 102 | import json 103 | filename = sys.argv[1] 104 | if filename.endswith(".yml") or filename.endswith(".yaml"): 105 | try: 106 | import yaml 107 | except ImportError: 108 | print("Please install PyYAML to test yaml templates") 109 | sys.exit(1) 110 | with open(filename, 'r') as file_handle: 111 | loaded_fragment = yaml.safe_load(file_handle) 112 | elif filename.endswith(".json"): 113 | with open(sys.argv[1], 'r') as file_handle: 114 | loaded_fragment = json.load(file_handle) 115 | else: 116 | print("Test file needs to end .yaml, .yml or .json") 117 | sys.exit(1) 118 | new_fragment = handle_transform(loaded_fragment) 119 | print(json.dumps(new_fragment)) 120 | -------------------------------------------------------------------------------- /Explode/macro.yml: -------------------------------------------------------------------------------- 1 | Transform: AWS::Serverless-2016-10-31 2 | 3 | Resources: 4 | MacroFunction: 5 | Type: AWS::Serverless::Function 6 | Properties: 7 | Runtime: python3.7 8 | CodeUri: lambda 9 | Handler: explode.handler 10 | 11 | Macro: 12 | Type: AWS::CloudFormation::Macro 13 | Properties: 14 | Name: Explode 15 | FunctionName: !GetAtt MacroFunction.Arn 16 | -------------------------------------------------------------------------------- /Explode/test.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Transform: Explode 3 | Mappings: 4 | BucketMap: 5 | Monthly: 6 | ResourceName: MyThirtyDayBucket 7 | Retention: 30 8 | Yearly: 9 | Retention: 365 10 | 11 | Resources: 12 | Bucket: 13 | ExplodeMap: BucketMap 14 | Type: AWS::S3::Bucket 15 | Properties: 16 | LifecycleConfiguration: 17 | Rules: 18 | - 19 | ExpirationInDays: "!Explode Retention" 20 | Status: Enabled 21 | NonExplodingBucket: 22 | Type: AWS::S3::Bucket 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | AWS CloudFormation Macros 2 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /Public-and-Private-Subnet-per-AZ/Create-Macro.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: Create Macro Template 3 | Resources: 4 | TransformExecutionRole: 5 | Type: AWS::IAM::Role 6 | Properties: 7 | AssumeRolePolicyDocument: 8 | Version: 2012-10-17 9 | Statement: 10 | - Effect: Allow 11 | Principal: 12 | Service: [lambda.amazonaws.com] 13 | Action: ['sts:AssumeRole'] 14 | Path: / 15 | Policies: 16 | - PolicyName: root 17 | PolicyDocument: 18 | Version: 2012-10-17 19 | Statement: 20 | - Effect: Allow 21 | Action: ['logs:*'] 22 | Resource: 'arn:aws:logs:*:*:*' 23 | - Effect: Allow 24 | Action: ['ec2:*'] 25 | Resource: '*' 26 | TransformFunctionPermissions: 27 | Type: AWS::Lambda::Permission 28 | Properties: 29 | Action: 'lambda:InvokeFunction' 30 | FunctionName: !GetAtt TransformFunction.Arn 31 | Principal: 'cloudformation.amazonaws.com' 32 | TransformFunction: 33 | Type: AWS::Lambda::Function 34 | Properties: 35 | Code: 36 | ZipFile: | 37 | import boto3 38 | import copy 39 | 40 | def handler(event, context): 41 | 42 | # Globals 43 | region = event['region'] 44 | accountId = event['accountId'] 45 | fragment = event['fragment'] 46 | transformId = event['transformId'] 47 | params = event['params'] 48 | requestId = event['requestId'] 49 | templateParameterValues = event['templateParameterValues'] 50 | 51 | ec2 = boto3.client('ec2', region_name=region) 52 | 53 | # Retrieves availability zones for this region 54 | response = ec2.describe_availability_zones() 55 | AZs = response['AvailabilityZones'] 56 | 57 | #Grab resources that need to be duplicated 58 | VPCPubSn1 = fragment['Resources']['VPCPubSn1'] 59 | VPCPrivSn1 = fragment['Resources']['VPCPrivSn1'] 60 | PubSnRtAssoc1 = fragment['Resources']['VPCPubSn1RtAssoc'] 61 | PubSnRtAssoc1 = fragment['Resources']['VPCPrivSn1RtAssoc'] 62 | 63 | #iterate and add new resources 64 | for i in range(1,len(AZs)+1): # Create a range from 1 - total number of AZs 65 | if not i == 1: # Only add resources if there is more than 1 AZ 66 | 67 | # Create new Public Subnet based of VPCPubSN1 68 | fragment['Resources']['VPCPubSn' + str(i)] = copy.deepcopy(VPCPubSn1) 69 | fragment['Resources']['VPCPubSn' + str(i)]['Properties']['CidrBlock']['Fn::Select'][0] = i - 1 70 | fragment['Resources']['VPCPubSn' + str(i)]['Properties']['AvailabilityZone']['Fn::Select'][0] = str(i - 1) 71 | 72 | # Create new Private Subnet based of VPCPrivSN1 73 | fragment['Resources']['VPCPrivSn' + str(i)] = copy.deepcopy(VPCPrivSn1) 74 | fragment['Resources']['VPCPrivSn' + str(i)]['Properties']['CidrBlock']['Fn::Select'][0] = len(AZs) + i - 1 75 | fragment['Resources']['VPCPrivSn' + str(i)]['Properties']['AvailabilityZone']['Fn::Select'][0] = str(i - 1) 76 | 77 | # Create Public RT Association 78 | fragment['Resources']['VPCPubSn' + str(i) + 'RtAssoc'] = {} 79 | fragment['Resources']['VPCPubSn' + str(i) + 'RtAssoc']['Type'] = 'AWS::EC2::SubnetRouteTableAssociation' 80 | fragment['Resources']['VPCPubSn' + str(i) + 'RtAssoc']['Properties'] = {} 81 | fragment['Resources']['VPCPubSn' + str(i) + 'RtAssoc']['Properties']['RouteTableId'] = {} 82 | fragment['Resources']['VPCPubSn' + str(i) + 'RtAssoc']['Properties']['RouteTableId']['Ref'] = 'VPCPubRt1' 83 | fragment['Resources']['VPCPubSn' + str(i) + 'RtAssoc']['Properties']['SubnetId'] = {} 84 | fragment['Resources']['VPCPubSn' + str(i) + 'RtAssoc']['Properties']['SubnetId']['Ref'] = 'VPCPubSn' + str(i) 85 | 86 | # Create Private RT Association 87 | fragment['Resources']['VPCPrivSn' + str(i) + 'RtAssoc'] = {} 88 | fragment['Resources']['VPCPrivSn' + str(i) + 'RtAssoc']['Type'] = 'AWS::EC2::SubnetRouteTableAssociation' 89 | fragment['Resources']['VPCPrivSn' + str(i) + 'RtAssoc']['Properties'] = {} 90 | fragment['Resources']['VPCPrivSn' + str(i) + 'RtAssoc']['Properties']['RouteTableId'] = {} 91 | fragment['Resources']['VPCPrivSn' + str(i) + 'RtAssoc']['Properties']['RouteTableId']['Ref'] = 'VPCPrivRt1' 92 | fragment['Resources']['VPCPrivSn' + str(i) + 'RtAssoc']['Properties']['SubnetId'] = {} 93 | fragment['Resources']['VPCPrivSn' + str(i) + 'RtAssoc']['Properties']['SubnetId']['Ref'] = 'VPCPrivSn' + str(i) 94 | 95 | r = {} 96 | r['requestId'] = requestId 97 | r['status'] = 'SUCCESS' 98 | r['fragment'] = fragment 99 | 100 | return r 101 | 102 | Handler: index.handler 103 | Runtime: python3.6 104 | Timeout: '60' 105 | Role: !GetAtt TransformExecutionRole.Arn 106 | Transform: 107 | Type: AWS::CloudFormation::Macro 108 | Properties: 109 | Name: CreateSubnetsPerAZ 110 | Description: Macro to create Subnets for every available AZ 111 | FunctionName: !GetAtt TransformFunction.Arn 112 | -------------------------------------------------------------------------------- /Public-and-Private-Subnet-per-AZ/Create-Stack.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Parameters: 3 | VPCCidrBlock: 4 | Type: String 5 | Description: 'The /16 CIDR block for the VPC (Format Example: 10.0.0.0/16)' 6 | MinLength: '9' 7 | MaxLength: '18' 8 | Default: 10.0.0.0/16 9 | AllowedPattern: '(\d{1,3})\.(\d{1,3})\.0\.0/16' 10 | ConstraintDescription: Must be a valid IP CIDR block with a /16 11 | KeyPair: 12 | Type: 'AWS::EC2::KeyPair::KeyName' 13 | Description: The name of an Amazon EC2 key pair in the region in which you are creating a CloudFormation stack. 14 | Mappings: 15 | AmazonLinuxNatAMI: 16 | ap-south-1: 17 | hvm: ami-7fd5f610 18 | eu-west-3: 19 | hvm: ami-519c2d2c 20 | eu-west-2: 21 | hvm: ami-756d8f12 22 | eu-west-1: 23 | hvm: ami-650c381c 24 | ap-northeast-3: 25 | hvm: ami-77fef00a 26 | ap-northeast-2: 27 | hvm: ami-8e0fa6e0 28 | ap-northeast-1: 29 | hvm: ami-cdd63eb2 30 | sa-east-1: 31 | hvm: ami-0b81dc67 32 | ca-central-1: 33 | hvm: ami-e3890987 34 | ap-southeast-1: 35 | hvm: ami-3093a64c 36 | ap-southeast-2: 37 | hvm: ami-943cebf6 38 | eu-central-1: 39 | hvm: ami-0097b5eb 40 | us-east-1: 41 | hvm: ami-54ca472b 42 | us-east-2: 43 | hvm: ami-882914ed 44 | us-west-1: 45 | hvm: ami-1a0e107a 46 | us-west-2: 47 | hvm: ami-78502100 48 | 49 | Resources: 50 | VPC: 51 | Type: 'AWS::EC2::VPC' 52 | Properties: 53 | CidrBlock: !Ref VPCCidrBlock 54 | InstanceTenancy: default 55 | EnableDnsSupport: true 56 | EnableDnsHostnames: true 57 | 58 | VPCPubSn1: 59 | Type: 'AWS::EC2::Subnet' 60 | Properties: 61 | CidrBlock: !Select [0, !Cidr [!GetAtt 'VPC.CidrBlock', 256, 8]] 62 | AvailabilityZone: !Select 63 | - '0' 64 | - !GetAZs 65 | Ref: 'AWS::Region' 66 | VpcId: !Ref VPC 67 | MapPublicIpOnLaunch: true 68 | 69 | VPCPrivSn1: 70 | Type: 'AWS::EC2::Subnet' 71 | Properties: 72 | CidrBlock: !Select [6, !Cidr [!GetAtt 'VPC.CidrBlock', 256, 8]] 73 | AvailabilityZone: !Select 74 | - '0' 75 | - !GetAZs 76 | Ref: 'AWS::Region' 77 | VpcId: !Ref VPC 78 | MapPublicIpOnLaunch: true 79 | 80 | VPCPubRt1: 81 | Type: 'AWS::EC2::RouteTable' 82 | Properties: 83 | VpcId: !Ref VPC 84 | VPCPubSn1RtAssoc: 85 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 86 | Properties: 87 | RouteTableId: !Ref VPCPubRt1 88 | SubnetId: !Ref VPCPubSn1 89 | 90 | VPCPrivRt1: 91 | Type: 'AWS::EC2::RouteTable' 92 | Properties: 93 | VpcId: !Ref VPC 94 | VPCPrivSn1RtAssoc: 95 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 96 | Properties: 97 | RouteTableId: !Ref VPCPrivRt1 98 | SubnetId: !Ref VPCPrivSn1 99 | 100 | VPCPubDefaultRoute: 101 | Type: 'AWS::EC2::Route' 102 | Properties: 103 | DestinationCidrBlock: 0.0.0.0/0 104 | RouteTableId: !Ref VPCPubRt1 105 | GatewayId: !Ref VPCIGW 106 | VPCPrivDefaultRoute: 107 | Type: 'AWS::EC2::Route' 108 | Properties: 109 | DestinationCidrBlock: 0.0.0.0/0 110 | RouteTableId: !Ref VPCPrivRt1 111 | InstanceId: !Ref VPCNatInstance 112 | 113 | VPCIGW: 114 | Type: 'AWS::EC2::InternetGateway' 115 | VPCIGWAttachment: 116 | Type: 'AWS::EC2::VPCGatewayAttachment' 117 | Properties: 118 | VpcId: !Ref VPC 119 | InternetGatewayId: !Ref VPCIGW 120 | 121 | SGAllTrafficFromVPC: 122 | Type: 'AWS::EC2::SecurityGroup' 123 | Properties: 124 | GroupName: SGAllTrafficFromVPC 125 | GroupDescription: VPN Traffic from VPC CIDR 126 | VpcId: !Ref VPC 127 | SecurityGroupIngress: 128 | - IpProtocol: '-1' 129 | CidrIp: !Ref VPCCidrBlock 130 | Description: All Traffic from VPC CIDR 131 | 132 | VPCNatInstance: 133 | Type: 'AWS::EC2::Instance' 134 | Properties: 135 | DisableApiTermination: false 136 | InstanceInitiatedShutdownBehavior: stop 137 | ImageId: !FindInMap 138 | - AmazonLinuxNatAMI 139 | - !Ref 'AWS::Region' 140 | - hvm 141 | InstanceType: t2.micro 142 | KeyName: !Ref KeyPair 143 | Monitoring: false 144 | NetworkInterfaces: 145 | - DeleteOnTermination: true 146 | Description: Primary network interface 147 | DeviceIndex: '0' 148 | SubnetId: !Ref VPCPubSn1 149 | GroupSet: 150 | - !Ref SGAllTrafficFromVPC 151 | AssociatePublicIpAddress: true 152 | 153 | Transform: 154 | - CreateSubnetsPerAZ 155 | -------------------------------------------------------------------------------- /Public-and-Private-Subnet-per-AZ/README.md: -------------------------------------------------------------------------------- 1 | ## Summary 2 | 3 | This is a Cloudformation Macro used to dynamically add a public and private subnet per Availability Zone when launching a template. When the CreateStack template is launched and a change set is created, the Macro (named 'CreateSubnetsPerAZ') will dynamically add resources to the template for a public and private subnet per available AZ 4 | 5 | ## How to Use 6 | 7 | - Create a stack using the `CreateMacro` template, which will create the Lambda function and register it as a CFN Macro (with the name: 'CreateSubnetsPerAZ') 8 | - Now the Macro is registered in your AWS account, and you can reference this macro in any CFN template using a top-level resource, similar to the following: 9 | 10 | ```yaml 11 | Transform: 12 | - CreateSubnetsPerAZ 13 | ``` 14 | 15 | - Create a stack using the `CreateStack` template. The template contains a VPC with a public and private subnet. When this template is launched and the change set is created, the macro will add a private and public subnet to every AZ available in that region. If this template is launched in us-west-2, there will be 2 public and 2 private subnets. If launched in us-east-1, there will be 6 public and 6 private subnets 16 | 17 | ## References 18 | 19 | See the recently announced CloudFormation Macros feature: [CloudFormation Macros](https://aws.amazon.com/blogs/aws/cloudformation-macros) 20 | 21 | ## /HT 22 | 23 | [@mike-mosher](https://github.com/mike-mosher) 24 | 25 | 26 | -------------------------------------------------------------------------------- /PyPlate/README.md: -------------------------------------------------------------------------------- 1 | # PyPlate 2 | 3 | Run arbitrary Python code in your CloudFormation templates. 4 | 5 | ## Basic Usage 6 | 7 | Place Python code as a literal block anywhere in your template. The literal block will be replaced with the contents of 8 | the `output` variable defined in your code. There are several variables available to your code: 9 | 10 | - `params`: dict containing the contents of the templateParameterValues 11 | - `template`: dict containing the entire template 12 | - `account_id`: AWS account ID 13 | - `region`: AWS region 14 | 15 | ```yaml 16 | AWSTemplateFormatVersion: "2010-09-09" 17 | Description: tests String macro functions 18 | Parameters: 19 | Tags: 20 | Default: "Env=Prod,Application=MyApp,BU=ModernisationTeam" 21 | Type: "CommaDelimitedList" 22 | Resources: 23 | S3Bucket: 24 | Type: "AWS::S3::Bucket" 25 | Properties: 26 | Tags: | 27 | #!PyPlate 28 | output = [] 29 | for tag in params['Tags']: 30 | key, value = tag.split('=') 31 | output.append({"Key": key, "Value": value}) 32 | Transform: [PyPlate] 33 | ``` 34 | 35 | ## Author 36 | 37 | [Jay McConnell](https://github.com/jaymccon) 38 | Partner Solutions Architect 39 | Amazon Web Services 40 | -------------------------------------------------------------------------------- /PyPlate/python.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Resources: 3 | TransformExecutionRole: 4 | Type: AWS::IAM::Role 5 | Properties: 6 | AssumeRolePolicyDocument: 7 | Version: 2012-10-17 8 | Statement: 9 | - Effect: Allow 10 | Principal: 11 | Service: [lambda.amazonaws.com] 12 | Action: ['sts:AssumeRole'] 13 | Path: / 14 | Policies: 15 | - PolicyName: root 16 | PolicyDocument: 17 | Version: 2012-10-17 18 | Statement: 19 | - Effect: Allow 20 | Action: ['logs:*'] 21 | Resource: 'arn:aws:logs:*:*:*' 22 | TransformFunction: 23 | Type: AWS::Lambda::Function 24 | Properties: 25 | Code: 26 | ZipFile: | 27 | import traceback 28 | import json 29 | 30 | 31 | def obj_iterate(obj, params): 32 | if isinstance(obj, dict): 33 | for k in obj: 34 | obj[k] = obj_iterate(obj[k], params) 35 | elif isinstance(obj, list): 36 | for i, v in enumerate(obj): 37 | obj[i] = obj_iterate(v, params) 38 | elif isinstance(obj, str): 39 | if obj.startswith("#!PyPlate"): 40 | params['output'] = None 41 | exec(obj, params) 42 | obj = params['output'] 43 | return obj 44 | 45 | 46 | def handler(event, context): 47 | 48 | print(json.dumps(event)) 49 | 50 | macro_response = { 51 | "requestId": event["requestId"], 52 | "status": "success" 53 | } 54 | try: 55 | params = { 56 | "params": event["templateParameterValues"], 57 | "template": event["fragment"], 58 | "account_id": event["accountId"], 59 | "region": event["region"] 60 | } 61 | response = event["fragment"] 62 | macro_response["fragment"] = obj_iterate(response, params) 63 | except Exception as e: 64 | traceback.print_exc() 65 | macro_response["status"] = "failure" 66 | macro_response["errorMessage"] = str(e) 67 | return macro_response 68 | 69 | Handler: index.handler 70 | Runtime: python3.11 71 | Role: !GetAtt TransformExecutionRole.Arn 72 | TransformFunctionPermissions: 73 | Type: AWS::Lambda::Permission 74 | Properties: 75 | Action: 'lambda:InvokeFunction' 76 | FunctionName: !GetAtt TransformFunction.Arn 77 | Principal: 'cloudformation.amazonaws.com' 78 | Transform: 79 | Type: AWS::CloudFormation::Macro 80 | Properties: 81 | Name: !Sub 'PyPlate' 82 | Description: Processes inline python in templates 83 | FunctionName: !GetAtt TransformFunction.Arn 84 | 85 | -------------------------------------------------------------------------------- /PyPlate/python_example.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: tests String macro functions 3 | Parameters: 4 | Tags: 5 | Default: "Env=Prod,Application=MyApp,BU=ModernisationTeam" 6 | Type: "CommaDelimitedList" 7 | Resources: 8 | S3Bucket: 9 | Type: "AWS::S3::Bucket" 10 | Properties: 11 | Tags: | 12 | #!PyPlate 13 | output = [] 14 | for tag in params['Tags']: 15 | key, value = tag.split('=') 16 | output.append({"Key": key, "Value": value}) 17 | Transform: [PyPlate] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS CloudFormation Macros 2 | 3 | ## NOTE that this repository is being archived in favor of similar content located here: 4 | 5 | https://github.com/aws-cloudformation/aws-cloudformation-templates/tree/main/aws/services/CloudFormation/MacrosExamples 6 | 7 | This repository hosts examples of AWS CloudFormation macros. 8 | 9 | ## Contents 10 | 11 | * [Boto3](Boto3) 12 | 13 | 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. 14 | 15 | * [Count](Count) 16 | 17 | The `Count` macro provides a template-wide `Count` property for CloudFormation resources. It allows you to specify multiple resources of the same type without having to cut and paste. 18 | 19 | * [ExecutionRoleBuilder](ExecutionRoleBuilder) 20 | 21 | The `Execution Role Builder` macro provides a more natural syntax for developers to express the permissions they want to attach to IAM execution roles for their applications, while simultaneously providing IAM administrators with a way to templatize those permissions. When used in conjunction with permission boundaries, this provides an effective solution for delegated role creation. 22 | 23 | * [Explode](Explode) 24 | 25 | The `Explode` macro provides a template-wide `Explode` property for CloudFormation resources. Similar to the Count macro, it will create multiple copies of a template Resource, but looks up values to inject into each copy in a Mapping. 26 | 27 | * [Public-and-Private-Subnet-per-AZ](Public-and-Private-Subnet-per-AZ) 28 | 29 | This is a Cloudformation Macro used to dynamically add a public and private subnet per Availability Zone when launching a template. When the CreateStack template is launched and a change set is created, the Macro (named 'CreateSubnetsPerAZ') will dynamically add resources to the template for a public and private subnet per available AZ 30 | 31 | * [PyPlate](PyPlate) 32 | 33 | Run arbitrary python code in your CloudFormation templates 34 | 35 | * [S3Objects](S3Objects) 36 | 37 | The `S3Objects` macro adds a new resource type: `AWS::S3::Object` which you can use to populate an S3 bucket. 38 | 39 | * [ShortHand](ShortHand) 40 | 41 | The `ShortHand` macro provides convenience syntax to allow you to create short CloudFormation templates that expand into larger documents upon deployment to a stack. 42 | 43 | * [StackMetrics](StackMetrics) 44 | 45 | When the `StackMetrics` macro is used in a CloudFormation template, any CloudFormation stack deployed from that template will output custom CloudWatch metrics for the stack. 46 | 47 | * [StringFunctions](StringFunctions) 48 | 49 | Provides string transformation utility functions. 50 | 51 | ## License 52 | 53 | This library is licensed under the Apache 2.0 License. 54 | -------------------------------------------------------------------------------- /S3Objects/README.md: -------------------------------------------------------------------------------- 1 | # How to install and use the S3Objects macro in your AWS account 2 | 3 | The `S3Objects` macro adds a new resource type: `AWS::S3::Object` which you can use to populate an S3 bucket. 4 | 5 | You can either create new S3 objects or copy S3 buckets from other buckets that you have permissions to access. 6 | 7 | As with any other CloudFormation resource, if you delete a stack containing S3 objects defined with this macro, those objects will be deleted. 8 | 9 | A typical use case for this macro might be, for example, to populate an S3 website with static assets. 10 | 11 | ## Deploying 12 | 13 | 1. You will need an S3 bucket to store the CloudFormation artifacts: 14 | * If you don't have one already, create one with `aws s3 mb s3://` 15 | 16 | 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. 17 | 18 | ```shell 19 | aws cloudformation package \ 20 | --template-file macro.template \ 21 | --s3-bucket \ 22 | --output-template-file packaged.template 23 | ``` 24 | 25 | 3. Deploy the packaged CloudFormation template to a CloudFormation stack: 26 | 27 | ```shell 28 | aws cloudformation deploy \ 29 | --stack-name s3objects-macro \ 30 | --template-file packaged.template \ 31 | --capabilities CAPABILITY_IAM 32 | ``` 33 | 34 | 4. To test out the macro's capabilities, try launching the provided example template: 35 | 36 | ```shell 37 | aws cloudformation deploy \ 38 | --stack-name s3objects-macro-example \ 39 | --template-file example.template \ 40 | --capabilities CAPABILITY_IAM 41 | ``` 42 | 43 | ## Usage 44 | 45 | To make use of the macro, add `Transform: S3Objects` to the top level of your CloudFormation template. 46 | 47 | Here is a trivial example template: 48 | 49 | ```yaml 50 | Transform: S3Objects 51 | Resources: 52 | Bucket: 53 | Type: AWS::S3::Bucket 54 | 55 | Object: 56 | Type: AWS::S3::Object 57 | Properties: 58 | Target: 59 | Bucket: !Ref Bucket 60 | Key: README.md 61 | ContentType: text/plain 62 | Body: Hello, world! 63 | ``` 64 | 65 | ## Features 66 | 67 | ### Creating a new S3 object 68 | 69 | To create a new S3 object, add an `AWS::S3::Object` resource to your template and specify the `Target` and `Body` properties. For example: 70 | 71 | ```yaml 72 | NewObject: 73 | Type: AWS::S3::Object 74 | Properties: 75 | Target: 76 | Bucket: !Ref TargetBucket 77 | Key: README.md 78 | Body: | 79 | # My text file 80 | 81 | This is my text file; 82 | there are many like it, 83 | but this one is mine. 84 | ``` 85 | 86 | The `Target` property has the following sub-properties: 87 | 88 | * `Bucket` (REQUIRED): The name of the bucket that will store the new object 89 | 90 | * `Key` (REQUIRED): The location within the bucket 91 | 92 | * `ACL` (OPTIONAL - Default `private`): Sets a [canned ACL](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl) for the new object 93 | 94 | The following sub-properties also apply if you are creating a new object (but not if you are copying an object from another S3 bucket): 95 | 96 | * `ContentType` (OPTIONAL): Sets a custom content type for the new object 97 | 98 | The `Body` property simply takes a string which will be used to populate the new object. 99 | 100 | ### Creating a new S3 object from binary data 101 | 102 | You can create a binary file by using the `Base64Body` property and supplying your content base64-encoded. For example: 103 | 104 | ```yaml 105 | SinglePixel: 106 | Type: AWS::S3::Object 107 | Properties: 108 | Target: 109 | Bucket: !Ref TargetBucket 110 | Key: 1pixel.gif 111 | Base64Body: R0lGODdhAQABAIABAP///0qIbCwAAAAAAQABAAACAkQBADs= 112 | ``` 113 | 114 | ### Copying an S3 object from another bucket 115 | 116 | To copy an S3 object, you need to specify the `Source` property as well as the `Target`. For example: 117 | 118 | ```yaml 119 | CopiedObject: 120 | Type: AWS::S3::Object 121 | Properties: 122 | Source: 123 | Bucket: !Ref SourceBucket 124 | Key: index.html 125 | Target: 126 | Bucket: !Ref TargetBucket 127 | Key: index.html 128 | ACL: public-read 129 | ``` 130 | 131 | The `Source` property has the following sub-properties: 132 | 133 | * `Bucket` (REQUIRED): The bucket to copy from 134 | 135 | * `Key` (REQUIRED): The key of the S3 object that will be copied 136 | 137 | ## Author 138 | 139 | [Steve Engledow](https://linkedin.com/in/stilvoid) 140 | Senior Solutions Builder 141 | Amazon Web Services 142 | -------------------------------------------------------------------------------- /S3Objects/example.template: -------------------------------------------------------------------------------- 1 | Transform: S3Objects 2 | 3 | Resources: 4 | Bucket: 5 | Type: AWS::S3::Bucket 6 | 7 | Object1: 8 | Type: AWS::S3::Object 9 | Properties: 10 | Target: 11 | Bucket: !Ref Bucket 12 | Key: README.md 13 | ContentType: text/markdown 14 | Body: | 15 | # My text file 16 | 17 | This is my text file; 18 | there are many like it, 19 | but this one is mine. 20 | 21 | Object2: 22 | Type: AWS::S3::Object 23 | Properties: 24 | Target: 25 | Bucket: !Ref Bucket 26 | Key: 1-pixel.gif 27 | ACL: public-read 28 | ContentType: image/png 29 | Base64Body: R0lGODdhAQABAIABAP///0qIbCwAAAAAAQABAAACAkQBADs= 30 | 31 | Object3: 32 | Type: AWS::S3::Object 33 | Properties: 34 | Source: 35 | Bucket: !GetAtt Object1.Bucket 36 | Key: !GetAtt Object1.Key 37 | Target: 38 | Bucket: !Ref Bucket 39 | Key: README-copy.md 40 | ACL: public-read 41 | -------------------------------------------------------------------------------- /S3Objects/lambda/macro.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You 4 | # may not use this file except in compliance with the License. A copy of 5 | # the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is 10 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | # ANY KIND, either express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | import boto3 15 | import os 16 | 17 | LAMBDA_ARN = os.environ["LAMBDA_ARN"] 18 | 19 | s3_client = boto3.client("s3") 20 | 21 | 22 | def handle_template(request_id, template): 23 | new_resources = {} 24 | 25 | for name, resource in list(template.get("Resources", {}).items()): 26 | if resource["Type"] == "AWS::S3::Object": 27 | props = resource["Properties"] 28 | 29 | if ( 30 | len( 31 | [ 32 | prop 33 | for prop in resource["Properties"] 34 | if prop in ["Body", "Base64Body", "Source"] 35 | ] 36 | ) 37 | != 1 38 | ): 39 | raise Exception( 40 | "You must specify exactly one of: Body, Base64Body, Source" 41 | ) 42 | 43 | target = props["Target"] 44 | 45 | if "ACL" not in target: 46 | target["ACL"] = "private" 47 | 48 | resource_props = { 49 | "ServiceToken": LAMBDA_ARN, 50 | "Target": target, 51 | } 52 | 53 | if "Body" in props: 54 | resource_props["Body"] = props["Body"] 55 | 56 | elif "Base64Body" in props: 57 | resource_props["Base64Body"] = props["Base64Body"] 58 | 59 | elif "Source" in props: 60 | resource_props["Source"] = props["Source"] 61 | 62 | new_resources[name] = { 63 | "Type": "Custom::S3Object", 64 | "Version": "1.0", 65 | "Properties": resource_props, 66 | } 67 | 68 | for name, resource in list(new_resources.items()): 69 | template["Resources"][name] = resource 70 | 71 | return template 72 | 73 | 74 | def handler(event, context): 75 | try: 76 | template = handle_template(event["requestId"], event["fragment"]) 77 | except Exception as e: 78 | return { 79 | "requestId": event["requestId"], 80 | "status": "failure", 81 | "fragment": event["fragment"], 82 | } 83 | 84 | return { 85 | "requestId": event["requestId"], 86 | "status": "success", 87 | "fragment": template, 88 | } 89 | -------------------------------------------------------------------------------- /S3Objects/lambda/resource.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You 4 | # may not use this file except in compliance with the License. A copy of 5 | # the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is 10 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | # ANY KIND, either express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | from urllib.request import build_opener, HTTPHandler, Request 15 | import base64 16 | import boto3 17 | import http.client 18 | import json 19 | 20 | s3_client = boto3.client("s3") 21 | 22 | 23 | def sendResponse(event, context, status, message): 24 | bucket = event["ResourceProperties"].get("Target", {}).get("Bucket") 25 | key = event["ResourceProperties"].get("Target", {}).get("Key") 26 | 27 | body = json.dumps( 28 | { 29 | "Status": status, 30 | "Reason": message, 31 | "StackId": event["StackId"], 32 | "RequestId": event["RequestId"], 33 | "LogicalResourceId": event["LogicalResourceId"], 34 | "PhysicalResourceId": f"s3://{bucket}/{key}", 35 | "Data": { 36 | "Bucket": bucket, 37 | "Key": key, 38 | }, 39 | } 40 | ) 41 | 42 | request = Request(event["ResponseURL"], data=body.encode("utf-8")) 43 | request.add_header("Content-Type", "") 44 | request.add_header("Content-Length", len(body)) 45 | request.get_method = lambda: "PUT" 46 | 47 | opener = build_opener(HTTPHandler) 48 | response = opener.open(request) 49 | 50 | 51 | def handler(event, context): 52 | print("Received request:", json.dumps(event, indent=4)) 53 | 54 | request = event["RequestType"] 55 | properties = event["ResourceProperties"] 56 | 57 | if "Target" not in properties or all( 58 | prop not in properties for prop in ["Body", "Base64Body", "Source"] 59 | ): 60 | return sendResponse(event, context, "FAILED", "Missing required parameters") 61 | 62 | target = properties["Target"] 63 | 64 | if request in ("Create", "Update"): 65 | if "Body" in properties: 66 | target.update( 67 | { 68 | "Body": properties["Body"], 69 | } 70 | ) 71 | 72 | s3_client.put_object(**target) 73 | 74 | elif "Base64Body" in properties: 75 | try: 76 | body = base64.b64decode(properties["Base64Body"]) 77 | except: 78 | return sendResponse(event, context, "FAILED", "Malformed Base64Body") 79 | 80 | target.update({"Body": body}) 81 | 82 | s3_client.put_object(**target) 83 | 84 | elif "Source" in properties: 85 | source = properties["Source"] 86 | 87 | s3_client.copy_object( 88 | CopySource=source, 89 | Bucket=target["Bucket"], 90 | Key=target["Key"], 91 | MetadataDirective="COPY", 92 | TaggingDirective="COPY", 93 | ACL=target["ACL"], 94 | ) 95 | 96 | else: 97 | return sendResponse(event, context, "FAILED", "Malformed body") 98 | 99 | return sendResponse(event, context, "SUCCESS", "Created") 100 | 101 | if request == "Delete": 102 | s3_client.delete_object( 103 | Bucket=target["Bucket"], 104 | Key=target["Key"], 105 | ) 106 | 107 | return sendResponse(event, context, "SUCCESS", "Deleted") 108 | 109 | return sendResponse(event, context, "FAILED", f"Unexpected: {request}") 110 | -------------------------------------------------------------------------------- /S3Objects/macro.template: -------------------------------------------------------------------------------- 1 | Transform: AWS::Serverless-2016-10-31 2 | 3 | Resources: 4 | ResourceFunction: 5 | Type: AWS::Serverless::Function 6 | Properties: 7 | Runtime: python3.8 8 | CodeUri: lambda 9 | Handler: resource.handler 10 | Policies: AmazonS3FullAccess 11 | 12 | MacroFunction: 13 | Type: AWS::Serverless::Function 14 | Properties: 15 | Runtime: python3.8 16 | CodeUri: lambda 17 | Handler: macro.handler 18 | Policies: AmazonS3FullAccess 19 | Environment: 20 | Variables: 21 | LAMBDA_ARN: !GetAtt ResourceFunction.Arn 22 | 23 | Macro: 24 | Type: AWS::CloudFormation::Macro 25 | Properties: 26 | Name: S3Objects 27 | FunctionName: !GetAtt MacroFunction.Arn 28 | -------------------------------------------------------------------------------- /ShortHand/README.md: -------------------------------------------------------------------------------- 1 | # ShortHand CloudFormation Macro 2 | 3 | The `ShortHand` macro provides convenience syntax to allow you to create short CloudFormation templates that expand into larger documents upon deployment to a stack. 4 | 5 | See below for instructions to install and use the macro and for a full description of the macro's features. 6 | 7 | ## How to install and use the ShortHand macro in your AWS account 8 | 9 | ### Deploying 10 | 11 | 1. You will need an S3 bucket to store the CloudFormation artifacts: 12 | * If you don't have one already, create one with `aws s3 mb s3://` 13 | 14 | 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. 15 | 16 | ```shell 17 | aws cloudformation package \ 18 | --template-file macro.template \ 19 | --s3-bucket \ 20 | --output-template-file packaged.template 21 | ``` 22 | 23 | 3. Deploy the packaged CloudFormation template to a CloudFormation stack: 24 | 25 | ```shell 26 | aws cloudformation deploy \ 27 | --stack-name shorthand-macro \ 28 | --template-file packaged.template \ 29 | --capabilities CAPABILITY_IAM 30 | ``` 31 | 32 | 4. To test out the macro's capabilities, try launching the provided example template: 33 | 34 | ```shell 35 | aws cloudformation deploy \ 36 | --stack-name shorthand-macro-example \ 37 | --template-file example.template \ 38 | --capabilities CAPABILITY_IAM 39 | ``` 40 | 41 | ### Usage 42 | 43 | To make use of the macro, add `Transform: ShortHand` to the top level of your CloudFormation template. 44 | 45 | Here is a trivial example template: 46 | 47 | ```yaml 48 | Transform: ShortHand 49 | Resources: 50 | - S3::Bucket 51 | ``` 52 | 53 | ## Features 54 | 55 | The ShortHand macro provides the following features to your CloudFormation templates: 56 | 57 | * A resource can be defined by a single string that contains its name, type, and proprties. 58 | 59 | For example: 60 | 61 | ```yaml 62 | "MyBucket AWS::S3::Bucket AccessControl=PublicRead" 63 | ``` 64 | 65 | This would translate into: 66 | 67 | ```yaml 68 | MyBucket: 69 | Type: AWS::S3::Bucket 70 | Properties: 71 | AccessControl: PublicRead 72 | ``` 73 | 74 | * You can omit the resource name and one will be generated for you. 75 | 76 | For example: 77 | 78 | ```yaml 79 | "AWS::S3::Bucket AccessControl=PublicRead" 80 | ``` 81 | 82 | * You can shorten the resource type name by omitting parts of it from the left. As long as the result unambiguously refers to a valid CloudFormation resource type, the `ShortHand` macro will deal with it. 83 | 84 | For example: 85 | 86 | ```yaml 87 | "Bucket AccessControl=PublicRead" 88 | ``` 89 | 90 | And: 91 | 92 | ```yaml 93 | "EC2::Instance" # We need the `EC2::` prefix as there are other resource types that end with `Instance` (e.g. `AWS::OpsWorks::Instance`) 94 | ``` 95 | 96 | * All string values automatically use `Fn::Sub` if the value contains a sequence like `${something}` 97 | 98 | For example: 99 | 100 | ```yaml 101 | "MyBucketPolicy BucketPolicy Bucket=${MyBucket}" 102 | ``` 103 | 104 | Will result in: 105 | 106 | ```yaml 107 | MyBucketPolicy: 108 | Type: AWS::S3::BucketPolicy 109 | Properties: 110 | Bucket: 111 | Fn::Sub: "${MyBucket}" 112 | ``` 113 | 114 | * You can address sub-properties using dot-notation. 115 | 116 | For example: 117 | 118 | ```yaml 119 | "MyBucket Bucket VersioningConfiguration.Status=Enabled" 120 | ``` 121 | 122 | * If you need to specify lots of properties (as is often the case) you can use object synax instead of a string. 123 | 124 | For example: 125 | 126 | ```yaml 127 | MyBucket S3::Bucket: 128 | AccessControl: PublicRead 129 | VersioningConfiguration.Status: Enabled 130 | ``` 131 | 132 | * To make all of these features possible, the `Resources` section of your template must now be an array rather than an object. 133 | 134 | A full example template would look like this: 135 | 136 | ```yaml 137 | Transform: ShortHand 138 | 139 | Parameters: 140 | Name: 141 | Type: String 142 | 143 | Resources: 144 | - S3::Bucket BucketName=${Name} 145 | ``` 146 | 147 | ## Author 148 | 149 | [Steve Engledow](https://linkedin.com/in/stilvoid) 150 | Senior Solutions Builder 151 | Amazon Web Services 152 | -------------------------------------------------------------------------------- /ShortHand/example.template: -------------------------------------------------------------------------------- 1 | Transform: ShortHand 2 | 3 | Resources: 4 | - S3::Bucket 5 | - S3::Bucket: 6 | AccessControl: PublicRead 7 | VersioningConfiguration.Status: Enabled 8 | - WebBucket S3::Bucket WebsiteConfiguration.IndexDocument=index.html 9 | -------------------------------------------------------------------------------- /ShortHand/lambda/convert.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You 4 | # may not use this file except in compliance with the License. A copy of 5 | # the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is 10 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | # ANY KIND, either express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | """ 15 | Convert an object from our custom format into something more explicit 16 | """ 17 | 18 | import resolve 19 | import re 20 | 21 | SUB_RE = re.compile(r"\$\{(?!!)") 22 | 23 | def handle_value(value): 24 | if SUB_RE.match(value): 25 | return { 26 | "Fn::Sub": value, 27 | } 28 | 29 | return value 30 | 31 | def unroll_props(rolled): 32 | props = {} 33 | 34 | for key, value in rolled.items(): 35 | key_parts = key.split(".") 36 | 37 | current = props 38 | 39 | for part in key_parts[:-1]: 40 | if part not in current: 41 | current[part] = {} 42 | current = current[part] 43 | 44 | current[key_parts[-1]] = handle_value(value) 45 | 46 | return props 47 | 48 | def parse_name(name): 49 | parts = name.split() 50 | 51 | ident = [ 52 | part 53 | for part in parts 54 | if "=" not in part 55 | ] 56 | 57 | props = unroll_props({ 58 | key: value 59 | for key, value in [ 60 | part.split("=") 61 | for part 62 | in parts if "=" in part 63 | ] 64 | }) 65 | 66 | return ident, props 67 | 68 | def convert(value): 69 | if isinstance(value, str): 70 | yield parse_name(value) 71 | elif isinstance(value, dict): 72 | for k, v in value.items(): 73 | ident, props = parse_name(k) 74 | props.update(unroll_props(v)) 75 | yield ident, props 76 | elif isinstance(value, list): 77 | for v in value: 78 | for ident, props in convert(v): 79 | yield ident, props 80 | else: 81 | raise Exception("Bad format at: {}".format(value)) 82 | 83 | def convert_template(template): 84 | resources = {} 85 | counts = {} 86 | 87 | for ident, props in convert(template.get("Resources", [])): 88 | resource = {} 89 | 90 | if len(ident) == 1: 91 | name = None 92 | resource["Type"] = ident[0] 93 | else: 94 | name = ident[0] 95 | resource["Type"] = ident[1] 96 | 97 | resource["Properties"] = props 98 | 99 | types = resolve.resource(resource["Type"]) 100 | 101 | if len(types) != 1: 102 | raise Exception("Ambiguous or unknown resource type: {}".format(resource["Type"])) 103 | 104 | resource["Type"] = types[0] 105 | 106 | # Handle un-named resources 107 | if not name: 108 | name = resource["Type"].split("::")[-1] 109 | if resource["Type"] not in counts: 110 | counts[resource["Type"]] = 1 111 | else: 112 | counts[resource["Type"]] += 1 113 | 114 | while "{}{}".format(name, counts[resource["Type"]]) in resources: 115 | counts[resource["Type"]] += 1 116 | 117 | name = "{}{}".format(name, counts[resource["Type"]]) 118 | 119 | resources[name] = resource 120 | 121 | template["Resources"] = resources 122 | 123 | return template 124 | -------------------------------------------------------------------------------- /ShortHand/lambda/index.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You 4 | # may not use this file except in compliance with the License. A copy of 5 | # the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is 10 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | # ANY KIND, either express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | from convert import convert_template 15 | 16 | def handler(event, context): 17 | return { 18 | "requestId": event["requestId"], 19 | "status": "success", 20 | "fragment": convert_template(event["fragment"]), 21 | } 22 | -------------------------------------------------------------------------------- /ShortHand/lambda/resolve.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You 4 | # may not use this file except in compliance with the License. A copy of 5 | # the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is 10 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | # ANY KIND, either express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | import json 15 | 16 | SPEC = json.load(open("spec.json")) 17 | 18 | def resource(name): 19 | """ 20 | Returns resource types that match `name`, working right-to-left 21 | E.g. S3::Bucket will match AWS::S3::Bucket 22 | """ 23 | 24 | return [ 25 | key for key in SPEC["ResourceTypes"].keys() 26 | if key.endswith(name) 27 | ] 28 | -------------------------------------------------------------------------------- /ShortHand/macro.template: -------------------------------------------------------------------------------- 1 | Transform: AWS::Serverless-2016-10-31 2 | 3 | Resources: 4 | Function: 5 | Type: AWS::Serverless::Function 6 | Properties: 7 | Runtime: python3.6 8 | CodeUri: lambda 9 | Handler: index.handler 10 | 11 | Macro: 12 | Type: AWS::CloudFormation::Macro 13 | Properties: 14 | Name: ShortHand 15 | FunctionName: !GetAtt Function.Arn 16 | -------------------------------------------------------------------------------- /StackMetrics/README.md: -------------------------------------------------------------------------------- 1 | # StackMetrics macro 2 | 3 | When the `StackMetrics` macro is used in a CloudFormation template, any CloudFormation stack deployed from that template will output custom CloudWatch metrics for the stack. 4 | 5 | * CloudFormation stack operations 6 | * Creates 7 | * Updates 8 | * Deletes 9 | * CloudFormation resources created 10 | 11 | Metrics are provided both per stack and overall across all stacks that are using the macro. 12 | 13 | The `macro.template` template also creates a simple dashboard for viewing the aggregated data from these metrics. 14 | 15 | See `example.template` for example usage. 16 | 17 | ## How to install and use the CloudFormation macro in your AWS account 18 | 19 | ### Deploying 20 | 21 | 1. You will need an S3 bucket to store the CloudFormation artifacts: 22 | * If you don't have one already, create one with `aws s3 mb s3://` 23 | 24 | 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. 25 | 26 | ```shell 27 | aws cloudformation package \ 28 | --template-file macro.template \ 29 | --s3-bucket \ 30 | --output-template-file packaged.template 31 | ``` 32 | 33 | 3. Deploy the packaged CloudFormation template to a CloudFormation stack: 34 | 35 | ```shell 36 | aws cloudformation deploy \ 37 | --stack-name stackmetrics-macro \ 38 | --template-file packaged.template \ 39 | --capabilities CAPABILITY_IAM 40 | ``` 41 | 42 | 4. To test out the macro's capabilities, try launching two stacks from the provided example template: 43 | 44 | ```shell 45 | aws cloudformation deploy \ 46 | --stack-name stackmetrics-macro-example-1 \ 47 | --template-file example.template \ 48 | --capabilities CAPABILITY_IAM 49 | 50 | aws cloudformation deploy \ 51 | --stack-name stackmetrics-macro-example-2 \ 52 | --template-file example.template \ 53 | --capabilities CAPABILITY_IAM 54 | ``` 55 | 56 | ### Usage 57 | 58 | To make use of the macro, add `Transform: StackMetrics` to the top level of your CloudFormation template. 59 | 60 | Here is a trivial example template: 61 | 62 | ```yaml 63 | Transform: StackMetrics 64 | Resources: 65 | Bucket: 66 | Type: S3::Bucket 67 | ``` 68 | 69 | To see the stack metrics, you can check the `CloudFormation-Stacks` dashboard in the CloudWatch console. 70 | 71 | ## Authors 72 | 73 | [Steve Engledow](https://linkedin.com/in/stilvoid) 74 | Senior Solutions Builder 75 | Amazon Web Services 76 | 77 | [Jason Gregson](https://linkedin.com/in/jgregson) 78 | Global Solutions Architect 79 | Amazon Web Services 80 | -------------------------------------------------------------------------------- /StackMetrics/example.template: -------------------------------------------------------------------------------- 1 | Transform: StackMetrics 2 | 3 | Resources: 4 | Bucket1: 5 | Type: AWS::S3::Bucket 6 | 7 | Bucket2: 8 | Type: AWS::S3::Bucket 9 | 10 | Bucket3: 11 | Type: AWS::S3::Bucket 12 | -------------------------------------------------------------------------------- /StackMetrics/lambda/cfnresponse.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Amazon Web Services, Inc. or its affiliates. All Rights Reserved. 2 | # This file is licensed to you under the AWS Customer Agreement (the "License"). 3 | # You may not use this file except in compliance with the License. 4 | # A copy of the License is located at http://aws.amazon.com/agreement/ . 5 | # This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied. 6 | # See the License for the specific language governing permissions and limitations under the License. 7 | 8 | from botocore.vendored import requests 9 | import json 10 | 11 | SUCCESS = "SUCCESS" 12 | FAILED = "FAILED" 13 | 14 | def send(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False): 15 | responseUrl = event['ResponseURL'] 16 | 17 | print(responseUrl) 18 | 19 | responseBody = {} 20 | responseBody['Status'] = responseStatus 21 | responseBody['Reason'] = 'See the details in CloudWatch Log Stream: ' + context.log_stream_name 22 | responseBody['PhysicalResourceId'] = physicalResourceId or context.log_stream_name 23 | responseBody['StackId'] = event['StackId'] 24 | responseBody['RequestId'] = event['RequestId'] 25 | responseBody['LogicalResourceId'] = event['LogicalResourceId'] 26 | responseBody['NoEcho'] = noEcho 27 | responseBody['Data'] = responseData 28 | 29 | json_responseBody = json.dumps(responseBody) 30 | 31 | print("Response body:\n" + json_responseBody) 32 | 33 | headers = { 34 | 'content-type' : '', 35 | 'content-length' : str(len(json_responseBody)) 36 | } 37 | 38 | try: 39 | response = requests.put(responseUrl, 40 | data=json_responseBody, 41 | headers=headers) 42 | print("Status code: " + response.reason) 43 | except Exception as e: 44 | print("send(..) failed executing requests.put(..): " + str(e)) 45 | -------------------------------------------------------------------------------- /StackMetrics/lambda/index.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You 4 | # may not use this file except in compliance with the License. A copy of 5 | # the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is 10 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | # ANY KIND, either express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | def handler(event, context): 15 | template = event["fragment"] 16 | 17 | template["Resources"]["StackMetrics"] = { 18 | "Type": "Custom::StackMetrics", 19 | "Properties": { 20 | "ServiceToken": { 21 | "Fn::ImportValue": "StackMetricsMacroFunction", 22 | }, 23 | "StackName": { 24 | "Ref": "AWS::StackName", 25 | }, 26 | "ResourceCount": len(template["Resources"].keys()), 27 | }, 28 | } 29 | 30 | return { 31 | "requestId": event["requestId"], 32 | "status": "success", 33 | "fragment": template, 34 | } 35 | -------------------------------------------------------------------------------- /StackMetrics/lambda/resource.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You 4 | # may not use this file except in compliance with the License. A copy of 5 | # the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "license" file accompanying this file. This file is 10 | # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | # ANY KIND, either express or implied. See the License for the specific 12 | # language governing permissions and limitations under the License. 13 | 14 | from datetime import datetime 15 | import boto3 16 | import cfnresponse 17 | import json 18 | 19 | client = boto3.client("cloudwatch") 20 | 21 | def log(stack, metric, value): 22 | # Do it for the stack 23 | client.put_metric_data( 24 | Namespace="CloudFormation", 25 | MetricData=[ 26 | { 27 | "MetricName": metric, 28 | "Unit": "Count", 29 | "Value": value, 30 | "Timestamp": datetime.now(), 31 | "Dimensions": [ 32 | { 33 | "Name": "By Stack Name", 34 | "Value": stack, 35 | }, 36 | ], 37 | }, 38 | ], 39 | ) 40 | 41 | client.put_metric_data( 42 | Namespace="CloudFormation", 43 | MetricData=[ 44 | { 45 | "MetricName": metric, 46 | "Unit": "Count", 47 | "Value": value, 48 | "Timestamp": datetime.now(), 49 | }, 50 | ], 51 | ) 52 | 53 | def handler(event, context): 54 | print("Received request:", json.dumps(event, indent=4)) 55 | 56 | action = event["RequestType"] 57 | 58 | stack = event["ResourceProperties"]["StackName"] 59 | resources = int(event["ResourceProperties"]["ResourceCount"]) 60 | 61 | try: 62 | log(stack, action, 1) 63 | 64 | if action == "Create": 65 | log(stack, "ResourceCount", resources) 66 | 67 | cfnresponse.send(event, context, cfnresponse.SUCCESS, {}, "{} metrics".format(stack)) 68 | except Exception as e: 69 | cfnresponse.send(event, context, cfnresponse.FAILED, { 70 | "Data": str(e), 71 | }, "{} metrics".format(stack)) 72 | -------------------------------------------------------------------------------- /StackMetrics/macro.template: -------------------------------------------------------------------------------- 1 | Transform: AWS::Serverless-2016-10-31 2 | 3 | Resources: 4 | ResourceFunction: 5 | Type: AWS::Serverless::Function 6 | Properties: 7 | Runtime: python3.6 8 | CodeUri: lambda 9 | Handler: resource.handler 10 | Policies: CloudWatchFullAccess 11 | 12 | MacroFunction: 13 | Type: AWS::Serverless::Function 14 | Properties: 15 | Runtime: python3.6 16 | CodeUri: lambda 17 | Handler: index.handler 18 | 19 | Macro: 20 | Type: AWS::CloudFormation::Macro 21 | Properties: 22 | Name: StackMetrics 23 | FunctionName: !GetAtt MacroFunction.Arn 24 | 25 | Dashboard: 26 | Type: AWS::CloudWatch::Dashboard 27 | Properties: 28 | DashboardName: CloudFormation-Stacks 29 | DashboardBody: | 30 | { 31 | "widgets": [ 32 | { 33 | "type": "metric", 34 | "x": 0, 35 | "y": 0, 36 | "width": 12, 37 | "height": 12, 38 | "properties": { 39 | "view": "timeSeries", 40 | "stacked": false, 41 | "metrics": [ 42 | [ "CloudFormation", "ResourceCount" ] 43 | ], 44 | "region": "eu-west-1", 45 | "title": "Resources created", 46 | "period": 300, 47 | "stat": "Sum" 48 | } 49 | }, 50 | { 51 | "type": "metric", 52 | "x": 12, 53 | "y": 0, 54 | "width": 12, 55 | "height": 12, 56 | "properties": { 57 | "view": "timeSeries", 58 | "stacked": true, 59 | "title": "Stack operations", 60 | "metrics": [ 61 | [ "CloudFormation", "Create" ], 62 | [ ".", "Delete" ], 63 | [ ".", "Update" ] 64 | ], 65 | "region": "eu-west-1", 66 | "period": 300, 67 | "stat": "Sum" 68 | } 69 | } 70 | ] 71 | } 72 | 73 | Outputs: 74 | ResourceFunction: 75 | Value: !GetAtt ResourceFunction.Arn 76 | Export: 77 | Name: StackMetricsMacroFunction 78 | 79 | -------------------------------------------------------------------------------- /StringFunctions/README.md: -------------------------------------------------------------------------------- 1 | # String functions 2 | 3 | Provides string transformation utility functions. 4 | 5 | ## Basic Usage 6 | 7 | Place the transform where you would like the output to be placed and provide the input string as the value for the 8 | InputString Parameter. The example below shows converting an input parameter to upper case and setting it as the value 9 | for a tag on an s3 bucket. 10 | 11 | ```yaml 12 | Parameters: 13 | InputString: 14 | Default: "This is a test input string" 15 | Type: String 16 | Resources: 17 | S3Bucket: 18 | Type: "AWS::S3::Bucket" 19 | Properties: 20 | Tags: 21 | - Key: Upper 22 | Value: 23 | 'Fn::Transform': 24 | - Name: 'String' 25 | Parameters: 26 | InputString: !Ref InputString 27 | Operation: Upper 28 | ``` 29 | 30 | ## Available Operations 31 | 32 | ### Upper 33 | 34 | Return a copy of the string with all the cased characters converted to uppercase. 35 | 36 | ### Lower 37 | 38 | Return a copy of the string with all the cased characters [4] converted to lowercase. 39 | 40 | ### Capitalize 41 | 42 | Return a copy of the string with its first character capitalized and the rest lowercased. 43 | 44 | ### Title 45 | 46 | Return a titlecased version of the string where words start with an uppercase character and the remaining characters 47 | are lowercase. 48 | 49 | ### SwapCase 50 | 51 | Return a copy of the string with uppercase characters converted to lowercase and vice versa. 52 | 53 | ### Strip 54 | 55 | Return a copy of the string with the leading and trailing characters removed. The `Chars` parameter is a string 56 | specifying the set of characters to be removed. If omitted default is to remove whitespace. The Chars argument is not a 57 | prefix or suffix; rather, all combinations of its values are stripped. 58 | 59 | #### Additional Parameters 60 | 61 | *Chars*: [optional] characters to strip from beginning and end of string 62 | 63 | ### Replace 64 | 65 | Return a copy of the string with all occurrences of substring `Old` replaced by `New`. 66 | 67 | #### Additional Parameters 68 | 69 | *Old*: [required] sub-string to search for 70 | 71 | *New*: [required] string to replace Old with 72 | 73 | ### MaxLength 74 | 75 | Return a copy of the string with a maximum length as specified by the `Length` parameter. Default is to strip 76 | characters from the end of the string. 77 | 78 | #### Additional Parameters 79 | 80 | *Length*: [required] maximum length of string 81 | 82 | *StripFrom*: [optional] specifying `Left` will strip characters from the beginning of the string, `Right` from the end 83 | (default) 84 | 85 | ## Author 86 | 87 | [Jay McConnell](https://github.com/jaymccon) 88 | Partner Solutions Architect 89 | Amazon Web Services 90 | -------------------------------------------------------------------------------- /StringFunctions/string.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Resources: 3 | TransformExecutionRole: 4 | Type: AWS::IAM::Role 5 | Properties: 6 | AssumeRolePolicyDocument: 7 | Version: 2012-10-17 8 | Statement: 9 | - Effect: Allow 10 | Principal: 11 | Service: [lambda.amazonaws.com] 12 | Action: ['sts:AssumeRole'] 13 | Path: / 14 | Policies: 15 | - PolicyName: root 16 | PolicyDocument: 17 | Version: 2012-10-17 18 | Statement: 19 | - Effect: Allow 20 | Action: ['logs:*'] 21 | Resource: 'arn:aws:logs:*:*:*' 22 | TransformFunction: 23 | Type: AWS::Lambda::Function 24 | Properties: 25 | Code: 26 | ZipFile: | 27 | import traceback 28 | 29 | 30 | def handler(event, context): 31 | response = { 32 | "requestId": event["requestId"], 33 | "status": "success" 34 | } 35 | try: 36 | operation = event["params"]["Operation"] 37 | input = event["params"]["InputString"] 38 | no_param_string_funcs = ["Upper", "Lower", "Capitalize", "Title", "SwapCase"] 39 | if operation in no_param_string_funcs: 40 | response["fragment"] = getattr(input, operation.lower())() 41 | elif operation == "Strip": 42 | chars = None 43 | if "Chars" in event["params"]: 44 | chars = event["params"]["Chars"] 45 | response["fragment"] = input.strip(chars) 46 | elif operation == "Replace": 47 | old = event["params"]["Old"] 48 | new = event["params"]["New"] 49 | response["fragment"] = input.replace(old, new) 50 | elif operation == "MaxLength": 51 | length = int(event["params"]["Length"]) 52 | if len(input) <= length: 53 | response["fragment"] = input 54 | elif "StripFrom" in event["params"]: 55 | if event["params"]["StripFrom"] == "Left": 56 | response["fragment"] = input[len(input)-length:] 57 | elif event["params"]["StripFrom"] != "Right": 58 | response["status"] = "failure" 59 | else: 60 | response["fragment"] = input[:length] 61 | else: 62 | response["status"] = "failure" 63 | except Exception: 64 | traceback.print_exc() 65 | response["status"] = "failure" 66 | macro_response["errorMessage"] = str(e) 67 | return response 68 | 69 | Handler: index.handler 70 | Runtime: python3.6 71 | Role: !GetAtt TransformExecutionRole.Arn 72 | TransformFunctionPermissions: 73 | Type: AWS::Lambda::Permission 74 | Properties: 75 | Action: 'lambda:InvokeFunction' 76 | FunctionName: !GetAtt TransformFunction.Arn 77 | Principal: 'cloudformation.amazonaws.com' 78 | Transform: 79 | Type: AWS::CloudFormation::Macro 80 | Properties: 81 | Name: 'String' 82 | Description: Provides various string processing functions 83 | FunctionName: !GetAtt TransformFunction.Arn 84 | 85 | -------------------------------------------------------------------------------- /StringFunctions/string_example.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: "2010-09-09" 2 | Description: tests String macro functions 3 | Parameters: 4 | InputString: 5 | Default: "This is a test input string" 6 | Type: String 7 | Resources: 8 | S3Bucket: 9 | Type: "AWS::S3::Bucket" 10 | Properties: 11 | Tags: 12 | - Key: Upper 13 | Value: 14 | 'Fn::Transform': 15 | Name: 'String' 16 | Parameters: 17 | InputString: !Ref InputString 18 | Operation: Upper 19 | - Key: Lower 20 | Value: 21 | 'Fn::Transform': 22 | Name: 'String' 23 | Parameters: 24 | InputString: !Ref InputString 25 | Operation: Lower 26 | - Key: Capitalize 27 | Value: 28 | 'Fn::Transform': 29 | Name: 'String' 30 | Parameters: 31 | InputString: !Ref InputString 32 | Operation: Capitalize 33 | - Key: Title 34 | Value: 35 | 'Fn::Transform': 36 | Name: 'String' 37 | Parameters: 38 | InputString: !Ref InputString 39 | Operation: Title 40 | - Key: Replace 41 | Value: 42 | 'Fn::Transform': 43 | Name: 'String' 44 | Parameters: 45 | InputString: !Ref InputString 46 | Operation: Replace 47 | Old: " " 48 | New: "_" 49 | - Key: Strip 50 | Value: 51 | 'Fn::Transform': 52 | Name: 'String' 53 | Parameters: 54 | InputString: !Ref InputString 55 | Operation: Strip 56 | Chars: Tgif 57 | - Key: ShortenLeft 58 | Value: 59 | 'Fn::Transform': 60 | Name: 'String' 61 | Parameters: 62 | InputString: !Ref InputString 63 | Operation: MaxLength 64 | Length: 4 65 | StripFrom: Left 66 | - Key: ShortenRight 67 | Value: 68 | 'Fn::Transform': 69 | Name: 'String' 70 | Parameters: 71 | InputString: !Ref InputString 72 | Operation: MaxLength 73 | Length: 4 74 | --------------------------------------------------------------------------------