How to Create Custom AWSCloudFormation Hooks

How to Create Custom AWSCloudFormation Hooks
February 28, 2022

Developers today provision resources to build applications using security, operational, and cost optimization best practices. While provisioning these resources, developers must maintain compliance for them, and the compliance status is usually known after the resources are provisioned. The non-compliant resources then must be deleted after the provisioning because of no checks for them; this problem is handled by a new CloudFormation feature called AWS CloudFormation Hooks.

CloudFormation Hooks is an extension type in the AWS CloudFormation registry and makes it easier to distribute and consume Hooks publicly or privately, with versioning support.

This blog will show you how to author and deploy a hook from your private registry. In this blog, we will try to stop provisioning a CloudFormation stack that creates a Security Group with ’0.0.0.0/0’ at ingress.

Before we start performing the compliance for this particular use case, you can go through the basic terminologies here and what is a Type configuration (which will be used while we create the CloudFormation stack via CLI)

Creating Custom CloudFormation Hooks

Ensure you have the prerequisites mentioned below and are familiar with AWS CloudFormation templates, Python, and Docker (although we won’t be using Docker here, it is best to know about platform-independent packaging of Python dependencies).

1. AWS Account and AWS CLI V2.

2. Download and install Python 3.6 or later.

3. Use the commands given below to install the cloudformation-cli(cfn) and Python language plugin. Also, upgrade these using the another link given.

Installation link:

$ pip3 install cloudformation-cli cloudformation-cli-python-plugin

Upgrade link: (Make sure to Upgrade!)

$ pip3 install --upgrade cloudformation-cli cloudformation-cli-python-plugin

4. Make sure you have configured AWS CLI with your credentials.

$ aws configure
Default region name [region]: <preferred region>

5. Permissions needed:

Initiate the Project for The CF Hook

Since we use a private repository, we will work with AWS cli on our local machine in this example.

1. We first create a directory and initiate the project inside it. We use the cfn init command to initiate the project. This creates your hook project and generates the required files. Run the following commands: (Our directory name used here is “first hook”)

$ mkdir firsthook

$ cd firsthook

$ cfn init

2. The cloud formationcli prompts you to choose between options to create a new resource, module, or hook. Since we want to create a hook, we choose ‘h.’

3. Next prompt will be to name the hook type, which is used to map the Typeattribute for resources in CF template. Make sure this name is consistent throughout this compliance practice.

4. Next will be to choose a language plugin, the supported languages include Python 3.6, Python 3.7, and Java. We choose Python 3.7 as you can see below:

5. To make the development easier, it is recommended to choose Docker for packaging, but since we are doing a very simple activity and we won’t be needing packaging Python dependencies, we choose not to opt for docker as shown: After you complete the initialization of the project in the directory, you should be able to see the below files:

Create The Hook

A hook contains two major components, one is the specification part which is defined by JSON schema, and another is a set of handlers at each invocation point. After creating these components, the hook must be registered and enabled in the used AWS account.

Step 1: Crafting the schema

The JSON schema defines the hook, its properties, and the attributes. An example JSON schema file is created when the project is initiated (<hook name>.json). You can use this JSON schema file to make the changes. Since we are going to use our hook to govern the Security Ingress rule, our JSON schema will look something like this:

{

“typeName”: “Rapyder::COETest::firsthook”,

“description”: “Validates that Security Groups do not allow inbound traffic from any address (0.0.0.0/0/ or ::/0).”,

“sourceUrl”: “https://github.com/aws-cloudformation/aws-cloudformation-samples/tree/main/hooks/python-hooks/security-group-open-ingress”,

“documentationUrl”: “https://github.com/aws-cloudformation/aws-cloudformation-samples/blob/main/hooks/python-hooks/security-group-open-ingress/README.md”,

“typeConfiguration”: {

“properties”: {},

“additionalProperties”: false

},

“required”: [],

“handlers”: {

“preCreate”: {

“targetNames”: ,

“permissions”: []

},

“preUpdate”: {

“targetNames”: ,

“permissions”: []

}

},

“additionalProperties”: false

}

Here under the “preCreate” handler, the target are “AWS::EC2::SecurityGroup”, “AWS::EC2::SecurityGroupIngress”. This means everytime a Security Group resource and Ingress rules are created; this hook will be invoked. Then “preUpdate” will be invoked before an update (when a rule is changed or another SG is added) is completed, that is the update must pass this hook for the specified targets.

Step 2: Generate The Hook Project Package

Next step is to generate a hook project package. Use command below:

$ cfn generate

Generated files for Rapyder::COETest::firsthook

The cloudformation-cli will create empty handler functions. Each handler created corresponds to a hook invocation point.

Step 3: Hook Handler Code

Copy the below code in handler.py which is located in the src/ Rapyder_COETest_firsthook directory. Replace the code inside it with the below one:

import logging

 

from cloudformation_cli_python_lib import (

HandlerErrorCode,

Hook,

HookInvocationPoint,

OperationStatus,

ProgressEvent

)

 

from .models import HookHandlerRequest, TypeConfigurationModel

 

# Use this logger to forward log messages to CloudWatch Logs.

LOG = logging.getLogger(__name__)

TYPE_NAME = “Rapyder::SecurityGroup::firsthook”

 

hook = Hook(TYPE_NAME, TypeConfigurationModel)

test_entrypoint = hook.test_entrypoint

 

supported_types = [“AWS::EC2::SecurityGroup”, “AWS::EC2::SecurityGroupIngress”]

 

def non_compliant(msg):

LOG.debug(f”returning FAILED: {HandlerErrorCode.NonCompliant} {msg}”)

return ProgressEvent(

status=OperationStatus.FAILED,

errorCode=HandlerErrorCode.NonCompliant,

message=msg

)

 

def is_open(sg_list):

for sg in sg_list:

if sg.get(‘CidrIp’) == ‘0.0.0.0/0’ or sg.get(‘CidrIpv6’) == ‘::/0’:

return True

return False

 

@hook.handler(HookInvocationPoint.CREATE_PRE_PROVISION)

@hook.handler(HookInvocationPoint.UPDATE_PRE_PROVISION)

def pre_handler(_s, request: HookHandlerRequest, _c, type_configuration: TypeConfigurationModel) -> ProgressEvent:

LOG.setLevel(logging.DEBUG)

LOG.debug(f”request: {request.__dict__}”)

LOG.debug(f”type_configuration: {type_configuration.__dict__ if type_configuration else dict()}”)

 

cfn_model = request.hookContext.targetModel.get(“resourceProperties”, {})

cfn_type = request.hookContext.targetName

 

# If we get a type that we don’t care about, we should return InvalidRequest

if cfn_type not in supported_types:

LOG.error(“returning invalidRequest”)

return ProgressEvent(

status=OperationStatus.FAILED,

errorCode=HandlerErrorCode.InvalidRequest,

message=f”This hook only supports {supported_types}”

)

 

if cfn_type == “AWS::EC2::SecurityGroup”:

security_groups = cfn_model.get(“SecurityGroupIngress”, [])

else:

security_groups = [cfn_model] if cfn_model else []

 

# Fail if an open ingress rule is found

if is_open(security_groups):

return non_compliant(“Security Group cannot contain rules allow all destinations (0.0.0.0/0 or ::/0)”)

 

# Operation is compliant, return success

LOG.debug(“returning SUCCESS”)

return ProgressEvent(status=OperationStatus.SUCCESS)

Step 4: Registering The Created Hook

1.Before submitting the hook, dry run the command for submitting to check for errors. (If using Docker, it is crucial to ensure it is up and running before running the command, else you will run into a “Unhandled exception” error.)

2. After the dry run is successful, submit using:$ cfn submit –set-default

After submitting, this command calls the API to register your hook and keeps polling for registration until finished. If any errors occur, then you will know its details with detailed message.

After successful registration, you should see {‘ProgressStatus’: ‘COMPLETE’} which means your hook is registered.

Updating the hook: One can update the hook by updating the handler code and repeating the Registering hook steps.

Step 5: Enabling the Registered Hook

Follow the below steps to enable the hook:

1.Check if you can see your hook in the list after you run the command below:

$ aws cloudformation list-types

2. Copy the hook arn from the response.

3. Now, you need to modify your hook’s type configuration properties. There are three configuration properties, TargetStacks, FailureMode, andSsmKey more details here.

4. Since we are working via our local machine cmd, we can use the command:aws cloudformation –region us-east-1 set-type-configuration –configuration file://type_config.json –type HOOK –type-name Rapyder::COETest::firsthook

Here, the configuration file contains the Hook configuration schema. Create a file in the same folder and run the above command. Also, instead of the hook arn, we use the type name, which is from the JSON schema we defined earlier.

Alternatively, you can use:$ aws cloudformation set-type-configuration

–configuration
\”{\\\”CloudFormationConfiguration\\\”:{\\\”HookConfiguration\\\”:{\\\”TargetStacks\\\”:\\\”ALL\\\”,\\\”FailureMode\\\”:\\\”FAIL\”}}}\”

–type-arn $HOOK_TYPE_ARN

Caution: If you activate hooks from the public registry, you must set the type configuration to ensure the hooks apply to all stacks.

Note: The type_config.json content:

{

\”CloudFormationConfiguration\”: {

\”HookConfiguration\”: {

\”TargetStacks\”: \”ALL\”,

\”FailureMode\”: \”FAIL\”,

\”Properties\”: {}

}

}

}

Testing Your CloudFormation Hook

Create a CloudFormation template named OpenSecurityGroup.yml. Use the below code which will create a Security Group with ingress rule allowing 0.0.0.0/0:AWSTemplateFormatVersion: \”2010-09-09\”

Resources:

SecurityGroup:

Type: AWS::EC2::SecurityGroup

Properties:

GroupDescription: \”Open Ingress Rules! Beware!\”

SecurityGroupIngress:

Type: AWS::EC2::SecurityGroupIngress

Properties:

GroupId: !GetAtt SecurityGroup.GroupId

IpProtocol: -1

CidrIp: 10.0.0.0/16

Next, create using the AWS CLI using the command:aws cloudformation create-stack –stack-name my-first-hook-stack –template-body file://OpenSecurityGroup.yml

The expected behaviour is that the stack creating will fail with details in CloudFormation events:(From console)

Hence, compliance works!

Be sure to clean up the resources and deregister the hooks using:$ aws cloudformation deregister-type –type HOOK –type-name Rapyder::COETest::firsthook

In the future, we will explore more use cases related to this CloudFormation and AWS Resource creation compliance, stay tuned!

Cloud Computing Insights and Resources

data warehouse migration

Accelerate and Simplify Your Data Warehouse Migration with AWS & Rapyder 

Data warehouse migration is a critical process that many organizations undergo to modernize their data infrastructure, improve performance, and enable […]

Cloud Consulting

6 Reasons to Collaborate with a Cloud Consulting Firm in 2024

The technology landscape keeps evolving, without a break, and the shift towards cloud solutions is undeniable. Companies are increasingly embracing […]

cloud computing

10 Secrets of Optimum Utilization of Clouds 

Cloud computing has emerged as a significant trend in recent years, transforming how businesses operate and delivering a range of […]