Using CloudFormation templates to implement network infrastructure

Please be aware that for the longer examples on this page, you will likely want to copy them into your favorite IDE, and inspect them there.

Using CloudFormation templates to implement network infrastructure

Typically I like to break a full stack into several smaller components. For example, we can usually break the network layer into it’s own self contained stack, which can then be maintained by the relevant specialists.

A basic VPC template contains:

  • The VPC object or container
  • An Internet Gateway (for Internet access)
  • Routing Tables
  • ACL Rules (network firewall)
  • At least one subnet

Building the actual template is actually fairly simple, but you’ll want to plan your CIDR ranges appropriately.

For the network unaware folks, a CIDR looks like 10.0.0.0/24 or 192.168.0.0/24. Typically folks will give a subnet a /24 (or 255.255.255.0) which allows for 254 usable hosts. A VPC typically will contain multiple subnets. So your VPC CIDR should contain all of your planned subnets.

So if I want three subnets (say 10.0.0.0/24, 10.0.1.0/24, and 10.0.2.0/24) my VPC CIDR should contain them (say 10.0.0.0/22).

For those (like myself) who don’t have CIDRs memorized, here’s a quick cheat sheet:

Netmask              Netmask (binary)                 CIDR     Notes    
--------------------------------------------------------------------------------
255.255.255.255  11111111.11111111.11111111.11111111  /32  Host (single addr)
255.255.255.254  11111111.11111111.11111111.11111110  /31  Unuseable
255.255.255.252  11111111.11111111.11111111.11111100  /30    2  useable
255.255.255.248  11111111.11111111.11111111.11111000  /29    6  useable
255.255.255.240  11111111.11111111.11111111.11110000  /28   14  useable
255.255.255.224  11111111.11111111.11111111.11100000  /27   30  useable
255.255.255.192  11111111.11111111.11111111.11000000  /26   62  useable
255.255.255.128  11111111.11111111.11111111.10000000  /25  126  useable
255.255.255.0    11111111.11111111.11111111.00000000  /24 "Class C" 254 useable

255.255.254.0    11111111.11111111.11111110.00000000  /23    2  Class C's
255.255.252.0    11111111.11111111.11111100.00000000  /22    4  Class C's
255.255.248.0    11111111.11111111.11111000.00000000  /21    8  Class C's
255.255.240.0    11111111.11111111.11110000.00000000  /20   16  Class C's
255.255.224.0    11111111.11111111.11100000.00000000  /19   32  Class C's
255.255.192.0    11111111.11111111.11000000.00000000  /18   64  Class C's
255.255.128.0    11111111.11111111.10000000.00000000  /17  128  Class C's
255.255.0.0      11111111.11111111.00000000.00000000  /16  "Class B"
     
255.254.0.0      11111111.11111110.00000000.00000000  /15    2  Class B's
255.252.0.0      11111111.11111100.00000000.00000000  /14    4  Class B's
255.248.0.0      11111111.11111000.00000000.00000000  /13    8  Class B's
255.240.0.0      11111111.11110000.00000000.00000000  /12   16  Class B's
255.224.0.0      11111111.11100000.00000000.00000000  /11   32  Class B's
255.192.0.0      11111111.11000000.00000000.00000000  /10   64  Class B's
255.128.0.0      11111111.10000000.00000000.00000000  /9   128  Class B's
255.0.0.0        11111111.00000000.00000000.00000000  /8   "Class A"
  
254.0.0.0        11111110.00000000.00000000.00000000  /7
252.0.0.0        11111100.00000000.00000000.00000000  /6
248.0.0.0        11111000.00000000.00000000.00000000  /5
240.0.0.0        11110000.00000000.00000000.00000000  /4
224.0.0.0        11100000.00000000.00000000.00000000  /3
192.0.0.0        11000000.00000000.00000000.00000000  /2
128.0.0.0        10000000.00000000.00000000.00000000  /1
0.0.0.0          00000000.00000000.00000000.00000000  /0   IP space

With the brief detour into CIDRs over, we should take a look at the required CloudFormation resources:

  1. The VPC object or container.

    "VPC" : {
        "Type" : "AWS::EC2::VPC",
        "Properties" : {
            "CidrBlock" : "10.0.0.0/22",
            "EnableDnsSupport" : true,
            "EnableDnsHostnames" : true
        }
    }
    
  2. An Internet Gateway (for Internet access).

    "InternetGateway" : {
        "Type" : "AWS::EC2::InternetGateway",
        "Properties" : {
        }
    }
    

    And attach the gateway to your VPC.

    "AttachGateway" : {
        "Type" : "AWS::EC2::VPCGatewayAttachment",
        "Properties" : {
            "VpcId" : { "Ref" : "VPC" },
            "InternetGatewayId" : { "Ref" : "InternetGateway" }
        }
    }
    
  3. Add a routing table

    "PublicRouteTable" : {
        "Type" : "AWS::EC2::RouteTable",
        "Properties" : {
            "VpcId" : {"Ref" : "VPC"}
        }
    }
    

    And create a public default route.

    "PublicRoute" : {
        "Type" : "AWS::EC2::Route",
        "DependsOn" : "AttachGateway",
        "Properties" : {
            "RouteTableId" : { "Ref" : "PublicRouteTable" },
            "DestinationCidrBlock" : "0.0.0.0/0",
            "GatewayId" : { "Ref" : "InternetGateway" }
        }
    }
    
  4. Create the ACL container.

    "PublicSubnetAcl" : {
        "Type" : "AWS::EC2::NetworkAcl",
        "Properties" : {
            "VpcId" : {"Ref" : "VPC"}
        }
    }
    

    Create bidirectional rules (ACLs are stateless). These examples allow all traffic - AWS typically recommends that you use security groups as opposed to the Network ALCs.

    "PublicInSubnetAclEntry" : {
        "Type" : "AWS::EC2::NetworkAclEntry",
        "Properties" : {
            "NetworkAclId" : {"Ref" : "PublicSubnetAcl"},
            "RuleNumber" : "32000",
            "Protocol" : "-1",
            "RuleAction" : "allow",
            "Egress" : "false",
            "CidrBlock" : "0.0.0.0/0",
            "Icmp" : { "Code" : "-1", "Type" : "-1"},
            "PortRange" : {"From" : "1", "To" : "65535"}
        }
    },
    "PublicOutSubnetAclEntry" : {
        "Type" : "AWS::EC2::NetworkAclEntry",
        "Properties" : {
            "NetworkAclId" : {"Ref" : "PublicSubnetAcl"},
            "RuleNumber" : "32000",
            "Protocol" : "-1",
            "RuleAction" : "allow",
            "Egress" : "true",
            "CidrBlock" : "0.0.0.0/0",
            "Icmp" : { "Code" : "-1", "Type" : "-1"},
            "PortRange" : {"From" : "1", "To" : "65535"}
        }
    }
    
  5. At least one subnet.

    "PublicSubnet1" : {
        "Type" : "AWS::EC2::Subnet",
        "Properties" : {
            "VpcId" : { "Ref" : "VPC" },
            "CidrBlock" : "10.0.0.0/24",
            "AvailabilityZone" : "us-west-2a"
        }
    }
    

So with all those resources described, here’s them together as a static template:

{
    "AWSTemplateFormatVersion" : "2010-09-09",
    "Description" : "Provisions a very small VPC with 1 public subnets.",
    "Resources" : {
        "VPC" : {
            "Type" : "AWS::EC2::VPC",
            "Properties" : {
                "CidrBlock" : "10.0.0.0/22",
                "EnableDnsSupport" : true,
                "EnableDnsHostnames" : true
            }
        },
        "InternetGateway" : {
            "Type" : "AWS::EC2::InternetGateway",
            "Properties" : {
            }
        },
        "AttachGateway" : {
            "Type" : "AWS::EC2::VPCGatewayAttachment",
            "Properties" : {
                "VpcId" : { "Ref" : "VPC" },
                "InternetGatewayId" : { "Ref" : "InternetGateway" }
            }
        },
        "PublicSubnet1" : {
            "Type" : "AWS::EC2::Subnet",
            "Properties" : {
                "VpcId" : { "Ref" : "VPC" },
                "CidrBlock" : "10.0.0.0/24",
                "AvailabilityZone" : "us-west-2a"
            }
        },
        "PublicRouteTable" : {
            "Type" : "AWS::EC2::RouteTable",
            "Properties" : {
                "VpcId" : {"Ref" : "VPC"}
            }
        },
        "PublicRoute" : {
            "Type" : "AWS::EC2::Route",
            "DependsOn" : "AttachGateway",
            "Properties" : {
                "RouteTableId" : { "Ref" : "PublicRouteTable" },
                "DestinationCidrBlock" : "0.0.0.0/0",
                "GatewayId" : { "Ref" : "InternetGateway" }
            }
        },
        "PublicSubnetAcl" : {
            "Type" : "AWS::EC2::NetworkAcl",
            "Properties" : {
                "VpcId" : {"Ref" : "VPC"}
            }
        },
        "PublicInSubnetAclEntry" : {
            "Type" : "AWS::EC2::NetworkAclEntry",
            "Properties" : {
                "NetworkAclId" : {"Ref" : "PublicSubnetAcl"},
                "RuleNumber" : "32000",
                "Protocol" : "-1",
                "RuleAction" : "allow",
                "Egress" : "false",
                "CidrBlock" : "0.0.0.0/0",
                "Icmp" : { "Code" : "-1", "Type" : "-1"},
                "PortRange" : {"From" : "1", "To" : "65535"}
            }
        },
        "PublicOutSubnetAclEntry" : {
            "Type" : "AWS::EC2::NetworkAclEntry",
            "Properties" : {
                "NetworkAclId" : {"Ref" : "PublicSubnetAcl"},
                "RuleNumber" : "32000",
                "Protocol" : "-1",
                "RuleAction" : "allow",
                "Egress" : "true",
                "CidrBlock" : "0.0.0.0/0",
                "Icmp" : { "Code" : "-1", "Type" : "-1"},
                "PortRange" : {"From" : "1", "To" : "65535"}
            }
        },
        "PublicSubnetAclAssociation1" : {
            "Type" : "AWS::EC2::SubnetNetworkAclAssociation",
            "Properties" : {
                "SubnetId" : { "Ref" : "PublicSubnet1" },
                "NetworkAclId" : {"Ref" : "PublicSubnetAcl"}
            }
        },
        "PublicSubnetRTAssociation1" : {
            "Type" : "AWS::EC2::SubnetRouteTableAssociation",
            "Properties" : {
                "SubnetId" : { "Ref" : "PublicSubnet1" },
                "RouteTableId" : {"Ref" : "PublicRouteTable"}
            }
        }
    }
}

A static template on it’s own is not very useful - you need to manually edit it in order to customize if for each deploy. This is where parameters come in handy. For this template we’ll need two parameters for the VPC and two parameters per subnet that we have in the template.

So our parameters:

  1. VPC CIDR
    "VPCCIDR" : {
        "Description" : "The IP Address space used by this VPC.",
        "Type" : "String",
        "Default" : "10.0.0.0/16",
        "AllowedPattern" : "(\\\\d{1,3})\\\\.(\\\\d{1,3})\\\\.(\\\\d{1,3})\\\\.(\\\\d{1,3})/(\\\\d{1,2})"
    }
    
  2. A nickname for our VPC
        "VPCName" : {
            "Description" : "The name of this VPC. Tags the VPC for identification.",
            "Type" : "String",
            "Default" : "Basic VPC"
        }
    
  3. Subnet 1 CIDR
    "PublicSubnet1CIDR" : {
        "Description" : "The IP Address space used by this subnet.",
        "Type" : "String",
        "Default" : "10.0.1.0/24",
        "AllowedPattern" : "(\\\\d{1,3})\\\\.(\\\\d{1,3})\\\\.(\\\\d{1,3})\\\\.(\\\\d{1,3})/(\\\\d{1,2})"
    }
    
  4. Subnet 1 Region
    "PublicSubnet1AZ" : {
        "Description" : "The availaibility zone to place this subnet in.",
        "Type" : "String",
        "Default" : "us-west-2a",
        "AllowedValues" : \["us-west-2a","us-west-2b","us-west-2c"\]
    }
    

KNOW ABOUT THE DIFFERENT PARAMETER TYPES YOU CAN USE! They are super handy.

Here’s our basic completed VPC template with added outputs:

cf-snippet-vpc.json
{
  "AWSTemplateFormatVersion" : "2010-09-09",
  "Description" : "Provisions a very small VPC with 1 public subnets.",
  "Parameters" : {
        "VPCCIDR" : {
            "Description" : "The IP Address space used by this VPC.",
            "Type" : "String",
            "Default" : "10.0.0.0/16",
            "AllowedPattern" : "(\\\\d{1,3})\\\\.(\\\\d{1,3})\\\\.(\\\\d{1,3})\\\\.(\\\\d{1,3})/(\\\\d{1,2})"
        },
        "VPCName" : {
            "Description" : "The name of this VPC. Tags the VPC for identification.",
            "Type" : "String",
            "Default" : "Basic VPC"
        },
        "PublicSubnet1CIDR" : {
            "Description" : "The IP Address space used by this subnet.",
            "Type" : "String",
            "Default" : "10.0.1.0/24",
            "AllowedPattern" : "(\\\\d{1,3})\\\\.(\\\\d{1,3})\\\\.(\\\\d{1,3})\\\\.(\\\\d{1,3})/(\\\\d{1,2})"
        },
        "PublicSubnet1AZ" : {
            "Description" : "The availaibility zone to place this subnet in.",
            "Type" : "String",
            "Default" : "us-west-2a",
            "AllowedValues" : \["us-west-2a","us-west-2b","us-west-2c"\]
        }
    },
    "Resources" : {
        "VPC" : {
            "Type" : "AWS::EC2::VPC",
            "Properties" : {
                "CidrBlock" : {
                    "Ref" : "VPCCIDR"
                },
                "EnableDnsSupport" : true,
                "EnableDnsHostnames" : true,
                "Tags" : \[{"Key" : "Name", "Value" : {
                    "Ref" : "VPCName"
                }}\]
            }
        },
        "InternetGateway" : {
            "Type" : "AWS::EC2::InternetGateway",
            "Properties" : { 
                "Tags" : \[{"Key" : "Name", "Value" : {
                    "Ref" : "VPCName"
                }}\]
            }
        },
        "AttachGateway" : {
            "Type" : "AWS::EC2::VPCGatewayAttachment",
            "Properties" : {
                "VpcId" : { "Ref" : "VPC" },
                "InternetGatewayId" : { "Ref" : "InternetGateway" }
            }
        },
        "PublicSubnet1" : {
            "Type" : "AWS::EC2::Subnet",
            "Properties" : {
                "VpcId" : { "Ref" : "VPC" },
                "CidrBlock" : { "Ref" : "PublicSubnet1CIDR" },
                "AvailabilityZone" : { "Ref" : "PublicSubnet1AZ" },
                "Tags" : \[{"Key" : "Name", "Value" :
                    {"Fn::Join" : \["", \["", {"Ref" : "VPCName"}, "\_", { "Ref" : "PublicSubnet1AZ" }\]\]}
                }\]
            }
        },
        "PublicRouteTable" : {
            "Type" : "AWS::EC2::RouteTable",
            "Properties" : {
                "VpcId" : {"Ref" : "VPC"}
            }
        },
        "PublicRoute" : {
            "Type" : "AWS::EC2::Route",
            "DependsOn" : "AttachGateway",
            "Properties" : {
                "RouteTableId" : { "Ref" : "PublicRouteTable" },
                "DestinationCidrBlock" : "0.0.0.0/0",
                "GatewayId" : { "Ref" : "InternetGateway" }
            }
        },
        "PublicSubnetAcl" : {
            "Type" : "AWS::EC2::NetworkAcl",
            "Properties" : {
                "VpcId" : {"Ref" : "VPC"}
            }
        },
        "PublicInSubnetAclEntry" : {
            "Type" : "AWS::EC2::NetworkAclEntry",
            "Properties" : {
                "NetworkAclId" : {"Ref" : "PublicSubnetAcl"},
                "RuleNumber" : "32000",
                "Protocol" : "-1",
                "RuleAction" : "allow",
                "Egress" : "false",
                "CidrBlock" : "0.0.0.0/0",
                "Icmp" : { "Code" : "-1", "Type" : "-1"},
                "PortRange" : {"From" : "1", "To" : "65535"}
            }
        },
        "PublicOutSubnetAclEntry" : {
            "Type" : "AWS::EC2::NetworkAclEntry",
            "Properties" : {
                "NetworkAclId" : {"Ref" : "PublicSubnetAcl"},
                "RuleNumber" : "32000",
                "Protocol" : "-1",
                "RuleAction" : "allow",
                "Egress" : "true",
                "CidrBlock" : "0.0.0.0/0",
                "Icmp" : { "Code" : "-1", "Type" : "-1"},
                "PortRange" : {"From" : "1", "To" : "65535"}
            }
        },
        "PublicSubnetAclAssociation1" : {
            "Type" : "AWS::EC2::SubnetNetworkAclAssociation",
            "Properties" : {
                "SubnetId" : { "Ref" : "PublicSubnet1" },
                "NetworkAclId" : {"Ref" : "PublicSubnetAcl"}
            }
        },
        "PublicSubnetRTAssociation1" : {
            "Type" : "AWS::EC2::SubnetRouteTableAssociation",
            "Properties" : {
                "SubnetId" : { "Ref" : "PublicSubnet1" },
                "RouteTableId" : {"Ref" : "PublicRouteTable"}
            }
        }
    },
    "Outputs" : {
        "VPC" : {
            "Value" : {"Ref" : "VPC"},
            "Description" : "Information about the value"
        },
        "PublicRouteTable" : {
            "Value" : {"Ref" : "PublicRouteTable"},
            "Description" : "Information about the value"
        },
        "Subnet1ID" : {
            "Value" : {"Ref" : "PublicSubnet1"},
            "Description" : "Information about the value"
        }
        "Subnet1AZ" : {
            "Value" : { "Ref" : "PublicSubnet1AZ" },
            "Description" : "Information about the value"
        }
    }
}