Introduction To AWSCloudFormation Hooks

February 28, 2022

Introduction To AWSCloudFormation Hooks

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

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

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

Before we start on to 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

Make sure you have the below mentioned prerequisites and are familiar with AWS CloudFormation templates, Python, and Docker (although we won’t be using Docker here, it is best to have the knowledge for 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 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 are using a private repository, we will be working with AWS cli on our local machine in this example.

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

$ mkdir firsthook

$ cd firsthook

$ cfn init

2. The cloudformation-cli prompts 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 these components are created, the hook must be registered and enabled in AWS account being used.

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, the 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 future, we will explore more use cases related to this CloudFormation and AWS Resource creation compliance, stay tuned!

Cloud Computing Insights and Resources

What is a Hybrid Cloud Strategy? What are its Advantages?

A hybrid cloud strategy is a method that companies use to decide which portions of hybrid cloud infrastructure are most …

What is a Hybrid Cloud Strategy? What are its Advantages? Read More »

AWS Launches the Second Infrastructure Region in India – Hyderabad

On November 22, 2022, AWS launched its new AWS region in Hyderabad. The AWS Asia-Pacific Hyderabad region is the second …

AWS Launches the Second Infrastructure Region in India – Hyderabad Read More »

Three Ways Cloud is Improving Customer Experience

Three Ways Cloud is Improving Customer Experience 

Ever since the cloud rose to popularity in the 2000s owing to its various advantages over traditional computing, businesses have …

Three Ways Cloud is Improving Customer Experience  Read More »