This CloudFormation application provides a suite of lambda-backed custom resources and macros that extend the functionality of the CloudFormation language, simplifying and streamlining the deployment and management of AWS resources and services.
Custom resources, deployed as AWS Lambda functions, can be called during a stack's lifecycle to create, update, or delete resources outside CloudFormation-managed resources. Using these custom resources, you can deploy and manage a wide range of AWS and third-party resources and services not natively supported by CloudFormation, while utilizing the power of CloudFormation's declarative language.
AWS builders can benefit from this CloudFormation application by simplifying their deployment processes, reducing errors, and improving their ability to manage their AWS environments. The following are just a few use cases:
- 🚀 Advanced landing zone configuration with AWS Organizations.
- 🛣 Simplified Route53 subdomain delegation.
- 🪣 Creating and managing S3 objects.
- 🧩 Deploying and managing AWS resources not natively supported by CloudFormation.
- 📝 Reserving address pools in a Subnet.
- 🛠 Programmatically retrieving and setting configuration variables in your AWS environment via CloudFormation templates.
- 🔗 Integrating with third-party services to deploy and manage resources.
- 🔒 Implementing advanced security features such as automatically rotating passwords and secrets.
The only custom resource currently available is BotoHook. It is a custom resource that allows you to call any boto method for CREATE, UPDATE, or DELETE CloudFormation resource lifecycle hooks and returns the complete Boto3 method response as Data fields, which can be retrieved in your configuration files using Fn::GetAtt. The custom resource's physical resource ID can be generated using a JMESPath expression.
This repository will continue to add more CloudFormation extensions, and contributions are welcome!
- The missing piece(s) of your AWS CloudFormation puzzles 🧩
The CustomResources transform is a macro deployed alongside custom resources by this application that lets you reference said custom resources using a human-friendly Type instead of AWS::CloudFormation::CustomResource (requiring to import the ServiceToken ARN in your templates). When a template references CustomResources, and you're creating or updating stacks using change sets, the Lambda-backed macro updates all resources prefixed with CustomResources:: with their corresponding AWS::CloudFormation::CustomResource and ServiceToken.
Use the CustomResources transform at the top level of a template. You can't use the CustomResources transform as an embedded transform in any other template section.
Transform: CustomResourcesYou must attach all required permissions to the BotoHook function role for the custom resource to work as intended (Alternatively, you can manually attach any AWS-managed policies at your own risk). The ID of the execution role used by the Lambda function is exported as an Application output and can be imported using the Fn::ImportValue intrinsic function:
SubnetCidrReservationHookPolicy: Type: AWS::IAM::Policy Properties: PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - ec2:CreateSubnetCidrReservation - ec2:DeleteSubnetCidrReservation Resource: - "*" PolicyName: Fn::Join: - "-" - - ec2 - subnet_cidr_reservation_mgmt Roles: - Fn::ImportValue: Fn::Join: - ":" - - Ref: AWS::AccountId - Ref: AWS::Region - customresources - botohook-function-execution-role-idTo declare this entity in your AWS CloudFormation template, use the following syntax:
Type: CustomResources::Boto::Hook Properties: Create: BotoHandlerConfig Update: BotoHandlerConfig Delete: BotoHandlerConfigBotoHandlerConfig definition:
Client: String Method: String PhysicalResourceId: String Parameters: Json-
CreateSpecifies the configuration of the CloudFormation
CREATElifecycle hook Boto3 handler.Required: No
Type: BotoHandlerConfig
-
UpdateSpecifies the configuration of the CloudFormation
UPDATElifecycle hook Boto3 handler. If unset, the resource will call theCreatehook handler if any.Required: No
Type: BotoHandlerConfig
-
DeleteSpecifies the configuration of the CloudFormation
DELETElifecycle hook Boto3 handler.Required: No
Type: BotoHandlerConfig
-
BotoHandlerConfig.ClientSpecifies the Boto3 client to use.Required: Yes
Type: String
-
BotoHandlerConfig.MethodSpecifies the Boto3 method to call.
Required: Yes
Type: String
-
BotoHandlerConfig.PhysicalResourceIdA valid JMESPath expression to retrieve the custom resource's physical resource ID from the Boto3 response.
Required: No
Type: String
-
BotoHandlerConfig.ParametersThe Boto3 method parameters.
Required: No
Type: Json
The Fn::GetAtt intrinsic function returns the Boto response of the Create handler (situationally, the Update handler).
You can add as many lifecycle hook handle to a Boto::Hook as needed:
BotoHookwill invoke theCreatehook during both the resourceCREATEandUPDATElifecycle stages unless you specify a customUpdatehandler. As defined in the AWS Custom Resources specifications, after anUPDATEstage, theDELETEstage will be then invoked with the old parameters if thePhysicalResourceIdreturned by the custom resource handler changes.
PutObjectHook: Type: CustomResources::Boto::Hook Properties: Create: Client: s3 Method: put_object PhysicalResourceId: Fn::Sub: "'s3://${BucketName}/${Key}'" # Note the simple commas. PhysicalResourceId is a JMESPath expression, not a simple string. Parameters: Bucket: Ref: BucketName Key: Ref: Key Body: | Hello, world! Delete: Client: s3 Method: delete_object Parameters: Bucket: Ref: BucketName Key: Ref: KeyYou can use the Fn::GetAtt intrinsic function if you need to reference an Output returned by one handler in another:
CreateSubnetCidrReservationHook: Type: CustomResources::Boto::Hook Properties: Create: Client: ec2 Method: create_subnet_cidr_reservation PhysicalResourceId: SubnetCidrReservation.SubnetCidrReservationId Parameters: SubnetId: Ref: SubnetId Cidr: Ref: CidrBlock ReservationType: explicit Description: An IPv4 address range of subnet addresses reserved in the Subnet. TagSpecifications: - ResourceType: subnet-cidr-reservation Tags: - Key: Name Value: SubnetCidrReservation DeleteSubnetCidrReservationHook: Type: CustomResources::Boto::Hook Properties: Delete: Client: ec2 Method: delete_subnet_cidr_reservation Parameters: SubnetCidrReservationId: Fn::GetAtt: - CreateSubnetCidrReservationHook - SubnetCidrReservation.SubnetCidrReservationIdWhen associated with AWS StackSets and AWS Organizations, BotoHook can be a powerful tool, used to grant organizational accounts additional capabilities, such as:
- modifying a Route53 hosted zone ID in the StackSet management account;
- getting the account's friendly name from the Organization.
To do so, you must configure cross-account invocation for the Lambda functions deployed by this application:
OrganizationBotohookFunctionPermission: Type: AWS::Lambda::Permission Metadata: Description: Allow organizational accounts to invoke the BotoHook lambda-backed CloudFormation custom resource in this account. Properties: Action: lambda:InvokeFunction FunctionName: Fn::ImportValue: Fn::Join: - ':' - - Ref: AWS::AccountId - Ref: AWS::Region - customresources - botohook-service-token Principal: "*" PrincipalOrgID: Ref: OrganizationIdStackSets and BotoHook can allow you to retrieve information about your Organization management account from other organizational accounts:
OrganizationsDescribeAccountStackSet: Type: AWS::CloudFormation::StackSet Properties: AutoDeployment: Enabled: true RetainStacksOnAccountRemoval: true Capabilities: [] Description: An example StackSet enabling accounts in the Organization to describe accounts by ID. ManagedExecution: Active: true OperationPreferences: RegionConcurrencyType: PARALLEL Parameters: - ParameterKey: BotohookServiceToken ParameterValue: Fn::ImportValue: Fn::Join: - ":" - - Ref: AWS::AccountId - Ref: AWS::Region - customresources - botohook-service-token PermissionModel: SERVICE_MANAGED StackSetName: OrganizationsDescribeAccount StackInstancesGroup: - DeploymentTargets: OrganizationalUnitIds: - Ref: OrganizationalUnitId Regions: - Ref: AWS::Region TemplateBody: | AWSTemplateFormatVersion: 2010-09-09 Description: Get the friendly account name in the Organization it belongs to. Parameters: BotohookServiceToken: AllowedPattern: ^arn:[^\s:]+:lambda:[^\s:]+:[0-9]{12}:function:[^\s:]+$ ConstraintDescription: You must specify a valid AWS Lambda function Amazon Resource Name (ARN). Description: The Amazon Resource Name (ARN) of this account's 'BotoHook' function. Type: String Resources: # This will execute on the StackSet management account. OrganizationsDescribeAccountHook: Type: AWS::CloudFormation::CustomResource Properties: ServiceToken: Ref: BotohookServiceToken Create: Client: organizations Method: describe_account PhysicalResourceId: Account.Name Parameters: AccountId: Ref: AWS::AccountIdStackSets and BotoHook enable advanced configuration patterns for Organizations, such as automated Route53 subdomain delegation:
OrganizationsDelegateRoute53SubdomainStackSet: Type: AWS::CloudFormation::StackSet Properties: AutoDeployment: Enabled: true RetainStacksOnAccountRemoval: true Capabilities: [] Description: An example StackSet delegating subdomains to accounts of an organization ManagedExecution: Active: true OperationPreferences: RegionConcurrencyType: PARALLEL Parameters: - ParameterKey: BotohookServiceToken ParameterValue: Fn::ImportValue: Fn::Join: - ":" - - Ref: AWS::AccountId - Ref: AWS::Region - customresources - botohook-service-token PermissionModel: SERVICE_MANAGED StackSetName: OrganizationsDelegateRoute53Subdomain StackInstancesGroup: - DeploymentTargets: OrganizationalUnitIds: - Ref: OrganizationalUnitId Regions: - Ref: AWS::Region TemplateBody: | AWSTemplateFormatVersion: 2010-09-09 Description: Get the friendly account name in the Organization it belongs to. Parameters: BotohookServiceToken: AllowedPattern: ^arn:[^\s:]+:lambda:[^\s:]+:[0-9]{12}:function:[^\s:]+$ ConstraintDescription: You must specify a valid AWS Lambda function Amazon Resource Name (ARN). Description: The Amazon Resource Name (ARN) of this account's 'BotoHook' function. Type: String OrganizationDomainName: Description: The fully qualified top-level domain name to configure on Route53. Type: String AllowedPattern: ^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]\.{0,1}$ ConstraintDescription: You must specify a fully qualified domain name, for example, www.example.com. OrganizationHostedZoneId: AllowedPattern: ^Z[0-9A-Z]{1,31}$ ConstraintDescription: You must specify a valid AWS Route53 hosted zone identifier (ID). Description: The identifier (ID) of the AWS Route53 hosted zone for the given domain. Type: String Resources: OrganizationsDescribeAccountHook: Type: AWS::CloudFormation::CustomResource Properties: ServiceToken: Ref: BotohookServiceToken Create: Client: organizations Method: describe_account PhysicalResourceId: Account.Name Parameters: AccountId: Ref: AWS::AccountId HostedZone: Type: AWS::Route53::HostedZone Properties: HostedZoneConfig: Comment: Fn::Sub: - A public hosted zone routing traffic on the internet for domain '${OrganizationSubDomainName}'. - OrganizationSubDomainName: Fn::Join: - . - - Fn::GetAtt: - OrganizationsDescribeAccountHook - Account.Name - Ref: OrganizationDomainName Name: Fn::Join: - . - - Fn::GetAtt: - OrganizationsDescribeAccountHook - Account.Name - Ref: OrganizationDomainName OrganizationChangeResourceRecordSetsNameHook: Type: AWS::CloudFormation::CustomResource Properties: ServiceToken: Ref: BotohookServiceToken Create: Client: route53 Method: change_resource_record_sets PhysicalResourceId: ChangeInfo.Id Parameters: HostedZoneId: Ref: OrganizationHostedZoneId ChangeBatch: Changes: - Action: CREATE ResourceRecordSet: Name: Fn::Join: - . - - Fn::GetAtt: - OrganizationsDescribeAccountHook - Account.Name - Ref: OrganizationDomainName Type: NS TTL: Type::Int: 300 ResourceRecords: - Value: Fn::Select: - 0 - Fn::GetAtt: - HostedZone - NameServers - Value: Fn::Select: - 1 - Fn::GetAtt: - HostedZone - NameServers - Value: Fn::Select: - 2 - Fn::GetAtt: - HostedZone - NameServers - Value: Fn::Select: - 3 - Fn::GetAtt: - HostedZone - NameServers Update: Client: route53 Method: change_resource_record_sets PhysicalResourceId: ChangeInfo.Id Parameters: HostedZoneId: Ref: OrganizationHostedZoneId ChangeBatch: Changes: - Action: UPSERT ResourceRecordSet: Name: Fn::Join: - . - - Fn::GetAtt: - OrganizationsDescribeAccountHook - Account.Name - Ref: OrganizationDomainName Type: NS TTL: Type::Int: 300 ResourceRecords: - Value: Fn::Select: - 0 - Fn::GetAtt: - HostedZone - NameServers - Value: Fn::Select: - 1 - Fn::GetAtt: - HostedZone - NameServers - Value: Fn::Select: - 2 - Fn::GetAtt: - HostedZone - NameServers - Value: Fn::Select: - 3 - Fn::GetAtt: - HostedZone - NameServers Delete: Client: route53 Method: change_resource_record_sets PhysicalResourceId: ChangeInfo.Id Parameters: HostedZoneId: Ref: OrganizationHostedZoneId ChangeBatch: Changes: - Action: DELETE ResourceRecordSet: Name: Fn::Join: - . - - Fn::GetAtt: - OrganizationsDescribeAccountHook - Account.Name - Ref: OrganizationDomainName Type: NS TTL: Type::Int: 300 ResourceRecords: - Value: Fn::Select: - 0 - Fn::GetAtt: - HostedZone - NameServers - Value: Fn::Select: - 1 - Fn::GetAtt: - HostedZone - NameServers - Value: Fn::Select: - 2 - Fn::GetAtt: - HostedZone - NameServers - Value: Fn::Select: - 3 - Fn::GetAtt: - HostedZone - NameServers - Lambda-backed CustomResource's ResourceProperties converts int and boolean properties to strings. This can be a problem for strictly typed APIs.
BotoHookimplements intrinsic-function-like syntax to force the type of parameters. It supports the following types:- Integers:
TTL: Type::Int: "300" # is transformed to: TTL: 300
- Floats:
Ratio: Type::Float: "0.8" # is transformed to: Ratio: 0.8
- Booleans:
EncryptionEnabled: Type::Bool: "true" # is transformed to: EncryptionEnabled: true
- Integers:
You can easily deploy and test the latest version of application using the CloudFormation CreateStack wizard: Click here to deploy this application to your AWS Account.
- AWS CLI installed and configured with appropriate permissions
- AWS SAM CLI installed
Install the application with AWS SAM CLI as follows:
-
Clone the repository to your local machine. Open a terminal and navigate to the root directory of the cloned repository.
-
Build the application using AWS SAM:
sam build --template-file customresources.template.yml
-
Package the CloudFormation application by running the following command:
sam package --s3-bucket <your-s3-bucket-name> --template-file customresources.template.yml --output-template-file packaged.yaml
Replace
<your-s3-bucket-name>with the name of an S3 bucket in your AWS account. -
Deploy the CloudFormation application by running the following command:
sam deploy --template-file packaged.yaml --stack-name <your-stack-name> --capabilities CAPABILITY_NAMED_IAM
Replace
<your-stack-name>with a unique name for your CloudFormation stack
Alternatively, you can also let SAM guide you through this process:
sam build --template-file customresources.template.yml --guided && sam deployOnce the CloudFormation stack has been created, you can instanciate custom resources by defining them in your CloudFormation templates.
Thank you for considering contributing to this project! Bug reports, feature requests, and code contributions are welcome.
This project is licensed under the terms of the MIT license.
MIT License Copyright (c) 2023 Alexis Facques Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information on the MIT License, see opensource.org/licenses/MIT.