January 16th, 2020
Leveraging CloudFormation Parameter Constraints to Enforce Resource Configuration
By Will Nave

CloudFormation templates are the AWS supported and preferred method of defining and implementing AWS resources and services as versionable and deployable code. CloudFormation parameters are the primary means of configuring resource properties in a CloudFormation template. Drilling down one level deeper, CloudFormation parameter constraints are a series of optional parameter attributes that provide a means of narrowing the scope of acceptable parameter values. Parameter constraints improve the reliability of resource configuration, reduce runtime errors, and help enforce AWS best practices.

During the initial development of a template, many parameters are defined and employed in a fashion similar to the example depicted below. A logical ID representing the parameter accompanied by a brief description and a <Type that is either “String” or “Number.”


Parameters:

    DatabasePort:
        Description: Database TCP Port Number
        Type: String

    AllowedIpOrigin:
        Description: IP Address allowed to connect to the database port
        Type: String

    DatabaseSecurityGroupId:
        Description: The database security group id
        Type: String

Resources:

  DatabasePortIngressRule:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref DatabaseSecurityGroupId
      IpProtocol: "tcp"
      FromPort: !Ref DatabasePort
      ToPort: !Ref DatabasePort
      CidrIp: !Ref AllowedIpOrigin


In the above example, we are defining a Security Group Ingress rule. The ingress rule is defined using five properties and three parameters; DatabasePort, AllowedIpOrigin, and DatabaseSecurityGroupId.

What happens in the above example if someone attempts to create a CloudFormation stack with an AllowedIpOrigin of “twenty”? The stack would be launched, pass initial validation, and fail to deploy because the provided values were not valid for the associated property in the DatabasePortIngressRule resource. And what would happen if the stack was deployed with an AllowedIpOrigin of 0.0.0.0/0?

Answer: Not much.

The stack would be launched and the Ingress rule would be created. However, should we be allowing an ingress rule on our database security group that allows wide-open access to our database port? (Spoiler: No, we should not) Here is the example from above with the application of appropriate CloudFormation Parameter constraints:


Parameters:

    DatabasePort:
        Description: Database TCP Port Number
        Type: Number
        ConstraintDescription: Database port must be 3306, 5432, or 1433
        AllowedValues:
        - 3306
        - 5432
        - 1433

    AllowedIpOrigin:
        Description: IP Address allowed to connect to the database port
        Type: String
        ConstraintDescription: Requires a /32 CIDR block
        AllowedPattern: "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/(32))$"

    DatabaseSecurityGroupId:
        Description: The database security group id
        Type: AWS::EC2::SecurityGroup::Id
        ConstraintDescription: Must be a valid AWS VPC Security Group Id

Resources:

  DatabasePortIngressRule:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref DatabaseSecurityGroupId
      IpProtocol: "tcp"
      FromPort: !Ref DatabasePort
      ToPort: !Ref DatabasePort
      CidrIp: !Ref AllowedIpOrigin


Type Constraints

The first and simplest way to constrain a parameter is by further restricting the scope of the defined parameter’s Type attribute. Instead of using a broadly scoped parameter type like String or Number, you should create parameters that are scoped to specific AWS-Specific parameter types whenever possible. In the revised example above I changed the DatabaseSecurityGroup parameter from a type of String to an AWS-specific type of AWS::EC2::SecurityGroup::Id. AWS-specific parameter types, like AWS::EC2::SecurityGroup::Id, enforce that parameters you pass are valid, existing resources in your AWS account.


Parameters:

    DatabaseSecurityGroupId:
        Description: The database security group id
        Type: AWS::EC2::SecurityGroup::Id
        ConstraintDescription: Must be a valid AWS VPC Security Group Id

String Constraints

In some instances, you may not be able to scope a parameter type down to a corresponding AWS-specific parameter type. In those situations, you will more than likely settle on working with parameters of a String or Number type.

Parameters of the String type have the following attributes available for use as constraints:

  • AllowedValues – An array containing the list of values allowed for the parameter.
  • AllowedPattern – A regular expression that represents the patterns to allow for String types.
  • MaxLength – An integer value that determines the largest number of characters you want to allow for String types.
  • MinLength – An integer value that determines the smallest number of characters you want to allow for String types.

The AllowedValues attribute allows you to provide a list of strings or numbers as the only allowed values to be chosen for the parameter at runtime. In the CloudFormation console, this is presented as a drop-down list. The DatabasePort parameter from the revised example above, although being a “Number” type parameter, leverages the AllowedValues attribute in the same manner as it would be leveraged for a String parameter type:


Parameters:

    DatabasePort:
        Description: Database TCP Port Number
        Type: Number
        ConstraintDescription: Database port must be 3306, 5432, or 1433
        AllowedValues:
        - 3306
        - 5432
        - 1433


If your only requirement is constraining a parameter within the bounds of a specific character limit, the MinLength and MaxLength attributes can be associated and defined. You can choose to leverage them individually or together.


Parameters:

    DatabaseReadOnlyUser:
        Description: Database Read-only User Username
        Type: String
        ConstraintDescription: Must be between 6 and 20 characters in length
        MinLength: 6
        MaxLength: 20


The AllowedPattern attribute evaluates the supplied value of an attribute against a regular expression. If the supplied parameter value does not match the regular expression at runtime, the deployment of the CloudFormation stack fails.

Common uses for the AllowedPattern attribute as a parameter constraint:

  • Email address validation
  • Password complexity policy validation
  • Username character limit and allowed character validation
  • IPv4 IP address validation

The AllowedIPOrigin parameter in the revised example above uses the AllowedPattern attribute to ensure that a valid IPv4 address with a /32 CIDR is the only allowed value for the parameter:


Parameters:

    AllowedIpOrigin:
        Description: IP Address allowed to connect to the database port
        Type: String
        ConstraintDescription: Requires a /32 CIDR Block
        AllowedPattern: "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/(32))$"


Here is a list of useful regular expressions that I frequently use with the AllowedPattern attribute:

  • Basic email address structure: ^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$
  • A valid IPv4 IP Address structure: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$
  • Valid CIDR Block for use with AWS VPC and associated resources: ^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\\/([1-2][6-9]|3[0-2]))?$
  • Passphrase Complexity Check (must start with a letter and minimum length of 12 alphanumeric characters including special characters): ^[A-Za-z][a-zA-Z0-9@%$_-]{12,}$

Tools like regex101.com are exceedingly helpful when trying to develop regular expressions for use with the AllowedPattern parameter attribute. You can quickly create and test pattern matches on your regular expressions before integrating them with your template. And if you would like to save and share a pattern, you can use the built in “SAVE & SHARE” functionality to generate a URL that links to your regular expression.

Numeric Constraints

Parameters with Number as their type have the following attributes available to implement constraints:

  • AllowedValues – An array containing the list of values allowed for the parameter.
  • MaxValue – A numeric value that determines the largest numeric value you want to allow for Number types.
  • MinValue – A numeric value that determines the smallest numeric value you want to allow for Number types.

The AllowedValues attribute is used the same as with String parameter types. Only numeric values found in the list of the AllowedValues are seen as valid Parameter values at runtime.

The MinValue and MaxValue attributes work similarly to the MinLength and MaxLength attributes associated with Strings. Both of these attributes can be used to define a range of numbers that are acceptable values for the parameter.


Parameters:

    ApplicationPort:
        Type: Number
        Description: The TCP port number for the Application
        MinValue: 1024
        MaxValue: 65535
        ConstraintDescription: Must be between 1024 and 65535
        Default: 3129

Other Considerations

While CloudFormation parameter constraints can go a long way in the reliable configuration of resources in conformance to best practices, it is not the only means to that end. The future state of the CloudFormation service or your specific solution may dictate the use of Mappings, SSM parameters, CloudFormation Exports/Imports, or some other new feature/service that does not yet exist. Writing templates that will be deployed into production environments shouldn’t be a “set it and forget it” type of task.

The power of CloudFormation lies in the ability to iterate upon and continually improve your templates. Just because the parameter and constraint you originally designed the template with were adequate at the time of initial deployment, does not mean it will be adequate the next time you need to deploy the same stack. Be mindful of current standards and best practices, as well as the ever-changing features of the CloudFormation service and API. Relative to working with CloudFormation, I reference and review the resources below on a regular basis:

  • CloudFormation Best Practices: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/best-practices.html
  • CloudFormation User Guide: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/Welcome.html

If you need help with AWS CloudFormation—or any of your AWS projects—let us know at info@1Strategy.com. We’d be happy to schedule a consultation with you to go over your needs.