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 dennisvink – rubycfn or try out the online Rubycfn compiler at 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 content
content "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",
"content": "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"
}
}
}
}