×

How to create a complete VPC with automatic subnet calculation using Rubycfn

This blog post describes how you can create a complete VPC, including the calculation of subnets with the use of the Fn::Cidr intrinsic function, an internet gateway, route tables, routes and subnet associations using Rubycfn. The product of this Rubycfn script is a CloudFormation template that you can deploy.

Check out Rubycfn at https://github.com/dennisvink/rubycfn or try out the online Rubycfn compiler at https://rubycfn.com/

Introduction

Rubycfn is a CloudFormation abstraction layer and deployment orchestration tool. In this blog post I will show you how you can use Rubycfn to create a VPC for which all subnet CIDRs are automatically calculated and all dependant resources are created for you. The result is a CloudFormation template that you can use to roll out a complete VPC in minutes.

Prerequisites

You must have a ruby installed. In addition, you must have the rubycfn gem installed: gem install rubycfn

The script

Save the script below as vpc.rb or another convenient name. Then type: cat vpc.rb | rubycfn to generate the CloudFormation template. By default the script generates a VPC with a CIDR block of 10.0.0.0/16. If you want to change the CIDR block, simply type:

export VPC_CIDR_BLOCK="10.100.0.0/16"
cat vpc.rb | rubycfn

Here is the complete script:

# Definition of subnets to create. The offset must be unique.
def subnets
  [
    {
      "es_private": {
        "owner": "binx",
        "public": false,
        "offset": 1
      }
    },
    {
      "ec2_public": {
        "owner": "binx",
        "public": true,
        "offset": 2
      }
    },
    {
      "ec2_private": {
        "owner": "binx",
        "public": false,
        "offset": 3
      }
    },
    {
      "bastion_public": {
        "owner": "binx",
        "public": true,
        "offset": 4
      }
    }
  ]
end

# export VPC_CIDR_BLOCK to desired CIDR range.
# Defaults to 10.0.0.0/16.
variable :cidr_block,
         default: "10.0.0.0/16",
         value: ENV["VPC_CIDR_BLOCK"]

# Set the Stack description
description "Rubycfn Generated VPC Stack (#{cidr_block})"

# Create the VPC
resource :vpc,
         type: "AWS::EC2::VPC" do |r|
  r.property(:cidr_block) { cidr_block }
  r.property(:enable_dns_support) { true }
  r.property(:enable_dns_hostnames) { true }
end

# Create the Internet Gateway
resource :internet_gateway,
         type: "AWS::EC2::InternetGateway"

# Create route
resource :route,
         type: "AWS::EC2::Route" do |r|
  r.property(:destination_cidr_block) { "0.0.0.0/0" }
  r.property(:gateway_id) { :internet_gateway.ref }
  r.property(:route_table_id) { :route_table.ref }
end

# Create and tag route table
resource :route_table,
         type: "AWS::EC2::RouteTable" do |r|
  r.property(:vpc_id) { :vpc.ref }
  r.property(:tags) do
    [
      {
        "Key": "Environment",
        "Value": "VPC Route Table"
      }
    ]
  end
end

# Attach the VPC to the Gateway
resource :vpc_gateway_attachment,
         type: "AWS::EC2::VPCGatewayAttachment" do |r|
  r.depends_on %w(Vpc)
  r.property(:internet_gateway_id) { :internet_gateway.ref }
  r.property(:vpc_id) { :vpc.ref }
end

# Create 3 subnets for each defined subnet (1 per AZ)
subnets.each_with_index do |subnet, _subnet_count|
  subnet.each do |subnet_name, arguments|
    resource "#{subnet_name}_subnet".cfnize,
             type: "AWS::EC2::Subnet",
             amount: 3 do |r, index|
      r.property(:availability_zone) do
        {
          "Fn::GetAZs": ""
        }.fnselect(index)
      end
      r.property(:cidr_block) do
        [
          :vpc.ref("CidrBlock"),
          (3 * arguments[:offset]).to_s,
          (Math.log(256) / Math.log(2)).floor.to_s
        ].fncidr.fnselect(index + (3 * arguments[:offset]) - 3)
      end
      r.property(:map_public_ip_on_launch) { arguments[:public] }
      r.property(:tags) do
        [
          {
            "Key": "owner",
            "Value": arguments[:owner].to_s.cfnize
          },
          {
            "Key": "resource_type",
            "Value": subnet_name.to_s.cfnize
          }
        ]
      end
      r.property(:vpc_id) { :vpc.ref }
    end

    # Create subnet route table associations
    resource "#{subnet_name}_subnet_route_table_association".cfnize,
             amount: 3,
             type: "AWS::EC2::SubnetRouteTableAssociation" do |r, index|
      r.property(:route_table_id) { :route_table.ref }
      r.property(:subnet_id) { "#{subnet_name}_subnet#{index.zero? && "" || index + 1}".cfnize.ref }
    end

    # Generate outputs for these subnets
    3.times do |i|
      output "#{subnet_name}_subnet#{i.positive? ? (i + 1) : ""}_name".cfnize,
             value: "#{subnet_name}_subnet#{i.positive? ? (i + 1) : ""}".cfnize.ref
    end
  end
end

# Output the VPC CIDR range and VPC Id
output :vpc_cidr,
       value: :vpc.ref("CidrBlock)
output :vpc_id,
       value: :vpc.ref

Subnet definitions

This script generates subnets for 4 services: 3 private subnets for ElasticSearch, 3 public subnets for EC2, 3 private subnets for EC2 and 3 public subnets for a Bastion host.

The owner property tags the created subnets with owner as the key and binx as value.

public can be set to either true or false to indicate whether or not services launched into subnet should be reachable directly from the internet.

offset must be unique for each defined subnet. It is used to calculate the CIDR range for the subnets.

def subnets
  [
    {
      "es_private": {
        "owner": "binx",
        "public": false,
        "offset": 1
      }
    },
    {
      "ec2_public": {
        "owner": "binx",
        "public": true,
        "offset": 2
      }
    },
    {
      "ec2_private": {
        "owner": "binx",
        "public": false,
        "offset": 3
      }
    },
    {
      "bastion_public": {
        "owner": "binx",
        "public": true,
        "offset": 4
      }
    }
  ]
end

Resulting CloudFormation Template

The artifact of this Rubycfn script is a CloudFormation template that is significantly bigger and much less friendly to the human eye. Given the default CIDR of 10.0.0.0/16 the resulting template looks like this:

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Description": "Rubycfn Generated VPC Stack (10.0.0.0/16)",
  "Resources": {
    "BastionPublicSubnet": {
      "Properties": {
        "AvailabilityZone": {
          "Fn::Select": [
            0,
            {
              "Fn::GetAZs": ""
            }
          ]
        },
        "CidrBlock": {
          "Fn::Select": [
            9,
            {
              "Fn::Cidr": [
                {
                  "Fn::GetAtt": [
                    "Vpc",
                    "CidrBlock"
                  ]
                },
                "12",
                "8"
              ]
            }
          ]
        },
        "MapPublicIpOnLaunch": true,
        "Tags": [
          {
            "Key": "owner",
            "Value": "Binx"
          },
          {
            "Key": "resource_type",
            "Value": "BastionPublic"
          }
        ],
        "VpcId": {
          "Ref": "Vpc"
        }
      },
      "Type": "AWS::EC2::Subnet"
    },
    "BastionPublicSubnet2": {
      "Properties": {
        "AvailabilityZone": {
          "Fn::Select": [
            1,
            {
              "Fn::GetAZs": ""
            }
          ]
        },
        "CidrBlock": {
          "Fn::Select": [
            10,
            {
              "Fn::Cidr": [
                {
                  "Fn::GetAtt": [
                    "Vpc",
                    "CidrBlock"
                  ]
                },
                "12",
                "8"
              ]
            }
          ]
        },
        "MapPublicIpOnLaunch": true,
        "Tags": [
          {
            "Key": "owner",
            "Value": "Binx"
          },
          {
            "Key": "resource_type",
            "Value": "BastionPublic"
          }
        ],
        "VpcId": {
          "Ref": "Vpc"
        }
      },
      "Type": "AWS::EC2::Subnet"
    },
    "BastionPublicSubnet3": {
      "Properties": {
        "AvailabilityZone": {
          "Fn::Select": [
            2,
            {
              "Fn::GetAZs": ""
            }
          ]
        },
        "CidrBlock": {
          "Fn::Select": [
            11,
            {
              "Fn::Cidr": [
                {
                  "Fn::GetAtt": [
                    "Vpc",
                    "CidrBlock"
                  ]
                },
                "12",
                "8"
              ]
            }
          ]
        },
        "MapPublicIpOnLaunch": true,
        "Tags": [
          {
            "Key": "owner",
            "Value": "Binx"
          },
          {
            "Key": "resource_type",
            "Value": "BastionPublic"
          }
        ],
        "VpcId": {
          "Ref": "Vpc"
        }
      },
      "Type": "AWS::EC2::Subnet"
    },
    "BastionPublicSubnetRouteTableAssociation": {
      "Properties": {
        "RouteTableId": {
          "Ref": "RouteTable"
        },
        "SubnetId": {
          "Ref": "BastionPublicSubnet"
        }
      },
      "Type": "AWS::EC2::SubnetRouteTableAssociation"
    },
    "BastionPublicSubnetRouteTableAssociation2": {
      "Properties": {
        "RouteTableId": {
          "Ref": "RouteTable"
        },
        "SubnetId": {
          "Ref": "BastionPublicSubnet2"
        }
      },
      "Type": "AWS::EC2::SubnetRouteTableAssociation"
    },
    "BastionPublicSubnetRouteTableAssociation3": {
      "Properties": {
        "RouteTableId": {
          "Ref": "RouteTable"
        },
        "SubnetId": {
          "Ref": "BastionPublicSubnet3"
        }
      },
      "Type": "AWS::EC2::SubnetRouteTableAssociation"
    },
    "Ec2PrivateSubnet": {
      "Properties": {
        "AvailabilityZone": {
          "Fn::Select": [
            0,
            {
              "Fn::GetAZs": ""
            }
          ]
        },
        "CidrBlock": {
          "Fn::Select": [
            6,
            {
              "Fn::Cidr": [
                {
                  "Fn::GetAtt": [
                    "Vpc",
                    "CidrBlock"
                  ]
                },
                "9",
                "8"
              ]
            }
          ]
        },
        "MapPublicIpOnLaunch": false,
        "Tags": [
          {
            "Key": "owner",
            "Value": "Binx"
          },
          {
            "Key": "resource_type",
            "Value": "Ec2Private"
          }
        ],
        "VpcId": {
          "Ref": "Vpc"
        }
      },
      "Type": "AWS::EC2::Subnet"
    },
    "Ec2PrivateSubnet2": {
      "Properties": {
        "AvailabilityZone": {
          "Fn::Select": [
            1,
            {
              "Fn::GetAZs": ""
            }
          ]
        },
        "CidrBlock": {
          "Fn::Select": [
            7,
            {
              "Fn::Cidr": [
                {
                  "Fn::GetAtt": [
                    "Vpc",
                    "CidrBlock"
                  ]
                },
                "9",
                "8"
              ]
            }
          ]
        },
        "MapPublicIpOnLaunch": false,
        "Tags": [
          {
            "Key": "owner",
            "Value": "Binx"
          },
          {
            "Key": "resource_type",
            "Value": "Ec2Private"
          }
        ],
        "VpcId": {
          "Ref": "Vpc"
        }
      },
      "Type": "AWS::EC2::Subnet"
    },
    "Ec2PrivateSubnet3": {
      "Properties": {
        "AvailabilityZone": {
          "Fn::Select": [
            2,
            {
              "Fn::GetAZs": ""
            }
          ]
        },
        "CidrBlock": {
          "Fn::Select": [
            8,
            {
              "Fn::Cidr": [
                {
                  "Fn::GetAtt": [
                    "Vpc",
                    "CidrBlock"
                  ]
                },
                "9",
                "8"
              ]
            }
          ]
        },
        "MapPublicIpOnLaunch": false,
        "Tags": [
          {
            "Key": "owner",
            "Value": "Binx"
          },
          {
            "Key": "resource_type",
            "Value": "Ec2Private"
          }
        ],
        "VpcId": {
          "Ref": "Vpc"
        }
      },
      "Type": "AWS::EC2::Subnet"
    },
    "Ec2PrivateSubnetRouteTableAssociation": {
      "Properties": {
        "RouteTableId": {
          "Ref": "RouteTable"
        },
        "SubnetId": {
          "Ref": "Ec2PrivateSubnet"
        }
      },
      "Type": "AWS::EC2::SubnetRouteTableAssociation"
    },
    "Ec2PrivateSubnetRouteTableAssociation2": {
      "Properties": {
        "RouteTableId": {
          "Ref": "RouteTable"
        },
        "SubnetId": {
          "Ref": "Ec2PrivateSubnet2"
        }
      },
      "Type": "AWS::EC2::SubnetRouteTableAssociation"
    },
    "Ec2PrivateSubnetRouteTableAssociation3": {
      "Properties": {
        "RouteTableId": {
          "Ref": "RouteTable"
        },
        "SubnetId": {
          "Ref": "Ec2PrivateSubnet3"
        }
      },
      "Type": "AWS::EC2::SubnetRouteTableAssociation"
    },
    "Ec2PublicSubnet": {
      "Properties": {
        "AvailabilityZone": {
          "Fn::Select": [
            0,
            {
              "Fn::GetAZs": ""
            }
          ]
        },
        "CidrBlock": {
          "Fn::Select": [
            3,
            {
              "Fn::Cidr": [
                {
                  "Fn::GetAtt": [
                    "Vpc",
                    "CidrBlock"
                  ]
                },
                "6",
                "8"
              ]
            }
          ]
        },
        "MapPublicIpOnLaunch": true,
        "Tags": [
          {
            "Key": "owner",
            "Value": "Binx"
          },
          {
            "Key": "resource_type",
            "Value": "Ec2Public"
          }
        ],
        "VpcId": {
          "Ref": "Vpc"
        }
      },
      "Type": "AWS::EC2::Subnet"
    },
    "Ec2PublicSubnet2": {
      "Properties": {
        "AvailabilityZone": {
          "Fn::Select": [
            1,
            {
              "Fn::GetAZs": ""
            }
          ]
        },
        "CidrBlock": {
          "Fn::Select": [
            4,
            {
              "Fn::Cidr": [
                {
                  "Fn::GetAtt": [
                    "Vpc",
                    "CidrBlock"
                  ]
                },
                "6",
                "8"
              ]
            }
          ]
        },
        "MapPublicIpOnLaunch": true,
        "Tags": [
          {
            "Key": "owner",
            "Value": "Binx"
          },
          {
            "Key": "resource_type",
            "Value": "Ec2Public"
          }
        ],
        "VpcId": {
          "Ref": "Vpc"
        }
      },
      "Type": "AWS::EC2::Subnet"
    },
    "Ec2PublicSubnet3": {
      "Properties": {
        "AvailabilityZone": {
          "Fn::Select": [
            2,
            {
              "Fn::GetAZs": ""
            }
          ]
        },
        "CidrBlock": {
          "Fn::Select": [
            5,
            {
              "Fn::Cidr": [
                {
                  "Fn::GetAtt": [
                    "Vpc",
                    "CidrBlock"
                  ]
                },
                "6",
                "8"
              ]
            }
          ]
        },
        "MapPublicIpOnLaunch": true,
        "Tags": [
          {
            "Key": "owner",
            "Value": "Binx"
          },
          {
            "Key": "resource_type",
            "Value": "Ec2Public"
          }
        ],
        "VpcId": {
          "Ref": "Vpc"
        }
      },
      "Type": "AWS::EC2::Subnet"
    },
    "Ec2PublicSubnetRouteTableAssociation": {
      "Properties": {
        "RouteTableId": {
          "Ref": "RouteTable"
        },
        "SubnetId": {
          "Ref": "Ec2PublicSubnet"
        }
      },
      "Type": "AWS::EC2::SubnetRouteTableAssociation"
    },
    "Ec2PublicSubnetRouteTableAssociation2": {
      "Properties": {
        "RouteTableId": {
          "Ref": "RouteTable"
        },
        "SubnetId": {
          "Ref": "Ec2PublicSubnet2"
        }
      },
      "Type": "AWS::EC2::SubnetRouteTableAssociation"
    },
    "Ec2PublicSubnetRouteTableAssociation3": {
      "Properties": {
        "RouteTableId": {
          "Ref": "RouteTable"
        },
        "SubnetId": {
          "Ref": "Ec2PublicSubnet3"
        }
      },
      "Type": "AWS::EC2::SubnetRouteTableAssociation"
    },
    "EsPrivateSubnet": {
      "Properties": {
        "AvailabilityZone": {
          "Fn::Select": [
            0,
            {
              "Fn::GetAZs": ""
            }
          ]
        },
        "CidrBlock": {
          "Fn::Select": [
            0,
            {
              "Fn::Cidr": [
                {
                  "Fn::GetAtt": [
                    "Vpc",
                    "CidrBlock"
                  ]
                },
                "3",
                "8"
              ]
            }
          ]
        },
        "MapPublicIpOnLaunch": false,
        "Tags": [
          {
            "Key": "owner",
            "Value": "Binx"
          },
          {
            "Key": "resource_type",
            "Value": "EsPrivate"
          }
        ],
        "VpcId": {
          "Ref": "Vpc"
        }
      },
      "Type": "AWS::EC2::Subnet"
    },
    "EsPrivateSubnet2": {
      "Properties": {
        "AvailabilityZone": {
          "Fn::Select": [
            1,
            {
              "Fn::GetAZs": ""
            }
          ]
        },
        "CidrBlock": {
          "Fn::Select": [
            1,
            {
              "Fn::Cidr": [
                {
                  "Fn::GetAtt": [
                    "Vpc",
                    "CidrBlock"
                  ]
                },
                "3",
                "8"
              ]
            }
          ]
        },
        "MapPublicIpOnLaunch": false,
        "Tags": [
          {
            "Key": "owner",
            "Value": "Binx"
          },
          {
            "Key": "resource_type",
            "Value": "EsPrivate"
          }
        ],
        "VpcId": {
          "Ref": "Vpc"
        }
      },
      "Type": "AWS::EC2::Subnet"
    },
    "EsPrivateSubnet3": {
      "Properties": {
        "AvailabilityZone": {
          "Fn::Select": [
            2,
            {
              "Fn::GetAZs": ""
            }
          ]
        },
        "CidrBlock": {
          "Fn::Select": [
            2,
            {
              "Fn::Cidr": [
                {
                  "Fn::GetAtt": [
                    "Vpc",
                    "CidrBlock"
                  ]
                },
                "3",
                "8"
              ]
            }
          ]
        },
        "MapPublicIpOnLaunch": false,
        "Tags": [
          {
            "Key": "owner",
            "Value": "Binx"
          },
          {
            "Key": "resource_type",
            "Value": "EsPrivate"
          }
        ],
        "VpcId": {
          "Ref": "Vpc"
        }
      },
      "Type": "AWS::EC2::Subnet"
    },
    "EsPrivateSubnetRouteTableAssociation": {
      "Properties": {
        "RouteTableId": {
          "Ref": "RouteTable"
        },
        "SubnetId": {
          "Ref": "EsPrivateSubnet"
        }
      },
      "Type": "AWS::EC2::SubnetRouteTableAssociation"
    },
    "EsPrivateSubnetRouteTableAssociation2": {
      "Properties": {
        "RouteTableId": {
          "Ref": "RouteTable"
        },
        "SubnetId": {
          "Ref": "EsPrivateSubnet2"
        }
      },
      "Type": "AWS::EC2::SubnetRouteTableAssociation"
    },
    "EsPrivateSubnetRouteTableAssociation3": {
      "Properties": {
        "RouteTableId": {
          "Ref": "RouteTable"
        },
        "SubnetId": {
          "Ref": "EsPrivateSubnet3"
        }
      },
      "Type": "AWS::EC2::SubnetRouteTableAssociation"
    },
    "InternetGateway": {
      "Type": "AWS::EC2::InternetGateway"
    },
    "Route": {
      "Properties": {
        "DestinationCidrBlock": "0.0.0.0/0",
        "GatewayId": {
          "Ref": "InternetGateway"
        },
        "RouteTableId": {
          "Ref": "RouteTable"
        }
      },
      "Type": "AWS::EC2::Route"
    },
    "RouteTable": {
      "Properties": {
        "Tags": [
          {
            "Key": "Environment",
            "Value": "VPC Route Table"
          }
        ],
        "VpcId": {
          "Ref": "Vpc"
        }
      },
      "Type": "AWS::EC2::RouteTable"
    },
    "Vpc": {
      "Properties": {
        "CidrBlock": "10.0.0.0/16",
        "EnableDnsHostnames": true,
        "EnableDnsSupport": true
      },
      "Type": "AWS::EC2::VPC"
    },
    "VpcGatewayAttachment": {
      "DependsOn": [
        "Vpc"
      ],
      "Properties": {
        "InternetGatewayId": {
          "Ref": "InternetGateway"
        },
        "VpcId": {
          "Ref": "Vpc"
        }
      },
      "Type": "AWS::EC2::VPCGatewayAttachment"
    }
  },
  "Outputs": {
    "BastionPublicSubnet2Name": {
      "Value": {
        "Ref": "BastionPublicSubnet2"
      }
    },
    "BastionPublicSubnet3Name": {
      "Value": {
        "Ref": "BastionPublicSubnet3"
      }
    },
    "BastionPublicSubnetName": {
      "Value": {
        "Ref": "BastionPublicSubnet"
      }
    },
    "Ec2PrivateSubnet2Name": {
      "Value": {
        "Ref": "Ec2PrivateSubnet2"
      }
    },
    "Ec2PrivateSubnet3Name": {
      "Value": {
        "Ref": "Ec2PrivateSubnet3"
      }
    },
    "Ec2PrivateSubnetName": {
      "Value": {
        "Ref": "Ec2PrivateSubnet"
      }
    },
    "Ec2PublicSubnet2Name": {
      "Value": {
        "Ref": "Ec2PublicSubnet2"
      }
    },
    "Ec2PublicSubnet3Name": {
      "Value": {
        "Ref": "Ec2PublicSubnet3"
      }
    },
    "Ec2PublicSubnetName": {
      "Value": {
        "Ref": "Ec2PublicSubnet"
      }
    },
    "EsPrivateSubnet2Name": {
      "Value": {
        "Ref": "EsPrivateSubnet2"
      }
    },
    "EsPrivateSubnet3Name": {
      "Value": {
        "Ref": "EsPrivateSubnet3"
      }
    },
    "EsPrivateSubnetName": {
      "Value": {
        "Ref": "EsPrivateSubnet"
      }
    },
    "VpcCidr": {
      "Value": {
        "Fn::GetAtt": [
          "Vpc",
          "CidrBlock"
        ]
      }
    },
    "VpcId": {
      "Value": {
        "Ref": "Vpc"
      }
    }
  }
}
Picture of Dennis Vink
Dennis Vink
AWS Consultant