This is the second part of a 3-part article covering the basics of AWS through using CloudFormation. For the first part of this article, click here.
Last week I covered the basics of CloudFormation – giving an idea of how a template is generally structured and all of the specific elements. This time, I am going into detail on how to set up Amazon VPC. Again, Amazon VPC is the first part of almost any AWS deployment – think of it as the datacenter and network layer. Not much else can happen unless these two items are present, can they? There are a few exceptions to this rule, such as Route 53, Amazon’s DNS hosting service, but even Route 53 can be provisioned in a VPC to provide private hosted zones that are not externally available.
And again, if you would like to follow along at home, remember to check out the template on the GitHub project.
Regions and Availability Zones
Two key concepts that should be explained while discussing VPCs are the concepts of regions and availability zones.
As a modern computing infrastructure that serves a wide variety of clients, AWS datacenters are spread throughout the world. These are known as regions. Examples of the regions would be the AWS datacenters in Virginia (us-east-1
), North California (us-west-1
), and Oregon (us-west-2
).
Within each of these regions are availability zones, which according to the EC2 FAQ, are so separated from each other that even physical failures such as power outages or even fire at one availability zone would not affect others.
VPCs can span availability zones but not regions. In order to interconnect regions, a peer needs to be set up, or the VPCs need to be connected via other means, such as using a VPN.
Private and Public Networks
It takes a little getting used to how private and public networks work in a VPC if one is used to engineering their own networks.
First off, the difference between a private and a public subnet is very subtle – public subnets have an internet gateway attached to them, and instances require a public IP address attached to them to be able to get internet access. Instances that do not have public IPs cannot access the internet on their own, regardless if they are in a public subnet.
To solve the problem, one can use a NAT instance. The concept of this explained below. What I do not cover are some of the other considerations that need to be thought of because of this “manual” process, such as redundancy and security of the network. Hopefully, Amazon will consider automating this piece of the infrastructure soon, as it is the single manual setup element in the VPC platform and hence probably the one prone to failure the most.
Advanced VPC topics
I do not cover more in-depth VPC topics here, however due to the scale of some of Amazon’s customers, it is only natural that they would have a wide range of options for connecting enterprise networks to a VPC.
Consult the Network Administrator Guide for help on these integration topics (such as using VPNs, or routing protocols like BGP).
VPCs in CloudFormation
As there are a lot of VPC components in the CloudFormation template, I have broken it up some of the items into respective sub-sections.
The root VPC resource is defined as an AWS::EC2::VPC
resource:
"VCTSLabVPC1": { "Type": "AWS::EC2::VPC", "Properties": { "CidrBlock": "10.0.0.0/16", "EnableDnsHostnames": true, "Tags": [ { "Key": "resclass", "Value": "vcts-lab-vpc" } ] } }
A couple of other things to note here: CidrBlock
cannot be any bigger than a /16, so if you get errors mentioning something about the network address being invalid, try reducing the network size. EnableDnsHostnames
allows DNS hosts to be assigned to instances as they start up in this VPC – having this off may be useful if DNS will be managed outside of AWS, but otherwise it’s generally a good idea to enable this.
Subnets, gateways, and routes
There are a couple of examples on subnets in the CloudFormation template, since it makes use of a NAT instance as well. This section will discuss the default (public) subnet to start with, and I will expand into the private subnet during the NAT instance section.
"VCTSLabSubnet1": { "Type": "AWS::EC2::Subnet", "Properties": { "CidrBlock": "10.0.0.0/24", "MapPublicIpOnLaunch": true, "Tags": [ { "Key": "resclass", "Value": "vcts-lab-subnet" }, { "Key": "subnet-type", "Value": "public" } ], "VpcId": { "Ref": "VCTSLabVPC1" } } }, "VCTSLabGateway": { "Type": "AWS::EC2::InternetGateway", "Properties": { "Tags": [ { "Key": "resclass", "Value": "vcts-lab-gateway" } ] } }, "VCTSLabGatewayAttachment": { "Type": "AWS::EC2::VPCGatewayAttachment", "Properties": { "InternetGatewayId": { "Ref": "VCTSLabGateway" }, "VpcId": { "Ref": "VCTSLabVPC1" } } }, "VCTSLabPublicRouteTable": { "Type": "AWS::EC2::RouteTable", "Properties": { "VpcId": { "Ref": "VCTSLabVPC1" }, "Tags": [ { "Key": "resclass", "Value": "vcts-lab-routetable" }, { "Key": "routetable-type", "Value": "public" } ] } }, "VCTSLabPublicDefaultRoute": { "Type": "AWS::EC2::Route", "Properties": { "DestinationCidrBlock": "0.0.0.0/0", "GatewayId": { "Ref": "VCTSLabGateway" }, "RouteTableId": { "Ref": "VCTSLabPublicRouteTable" } } }, "VCTSLabPublicSubnet1Assoc": { "Type": "AWS::EC2::SubnetRouteTableAssociation", "Properties": { "SubnetId": { "Ref": "VCTSLabSubnet1" }, "RouteTableId": { "Ref": "VCTSLabPublicRouteTable" } } }
There are a lot of things going on here. First, the subnet is defined with the AWS::EC2::Subnet
resource type. The MapPublicIpOnLaunch
property makes this the de facto public subnet, as anything launched in this subnet will get a public IP address. However, this is only part of the story, as without a gateway, public IP address assignments will not be possible or functional.
To that end, various routing resoruces are created: a gateway (type AWS::EC2::InternetGateway
), a route table (AWS::EC2::RouteTable
), and a default route (resource type AWS::EC2::Route
). This effectively makes a route table with a default route, however it also needs to be attached to the gateway that is created and the subnet. This is done by using the AWS::EC2::VPCGatewayAttachment
and AWS::EC2::SubnetRouteTableAssociation
resources.
At this point, the subnet is now ready for use, however, without security policies may be not of much use, or extremely insecure.
Security groups
Shown below is the CloudFormation resource for the public subnet’s security group. This is defined as type AWS::EC2::SecurityGroup
, and is named for the fact that it will mainly run on the NAT instance only.
"VCTSNatSecurityGroup": { "Type": "AWS::EC2::SecurityGroup", "Properties": { "Tags": [ { "Key": "resclass", "Value": "vcts-lab-sg" } ], "GroupDescription": "NAT (External) VCTS security group", "VpcId": { "Ref": "VCTSLabVPC1" }, "SecurityGroupIngress": [ { "IpProtocol": "tcp", "CidrIp": "10.0.1.0/24", "FromPort": "80", "ToPort": "80" }, { "IpProtocol": "tcp", "CidrIp": "10.0.1.0/24", "FromPort": "443", "ToPort": "443" }, { "IpProtocol": "tcp", "CidrIp": { "Ref": "SSHAllowIPAddress" }, "FromPort": "22", "ToPort": "22" } ], "SecurityGroupEgress": [ { "IpProtocol": "tcp", "CidrIp": "0.0.0.0/0", "FromPort": "22", "ToPort": "22" }, { "IpProtocol": "tcp", "CidrIp": "0.0.0.0/0", "FromPort": "80", "ToPort": "80" }, { "IpProtocol": "tcp", "CidrIp": "0.0.0.0/0", "FromPort": "443", "ToPort": "443" } ] } }
Access rules are defined through the SecurityGroupIngress
(inbound) and SecurityGroupEgress
(outbound) properties. Here, only SSH traffic is being allowed inbound, from the IP address supplied via the SSHAllowIPAddress
parameter. SSH is also being allowed general outbound – this allows the ability to “bounce” off the NAT instance into the private network. HTTP and HTTPS are being allowed both inbound and outbound generally – this might be confusing at first, seeing as there is some redundancy with this and the (not shown) load balancer access list, but consider the fact that the NAT instance has to handle traffic going in both directions – so the HTTP will need to come in to the NAT instance and the back out to the internet, so the access list has to allow for both directions.
Two security groups are not shown here – the load balancer group (named VCTSElbSecurityGroup
) and the private group (named VCTSPrivateSecurityGroup
). The former simply allows HTTP traffic generally, and the latter is a general allow for any traffic flowing into private instances.
One last thing to note – keep in mind that there are two kinds of network security concepts in a VPC – security groups, as shown here, and network ACLs, which I do not discuss. The former are applied at the instance level, and the latter are applied at the subnet level. At the very least, there should be some security applied on the instance level, however, adding the network ACLs can allow for some fallback barring that. As an example, the private subnet is very loose, and could benefit from an ACL being applied to it, just in case a public address got assigned to it (even though, as the CloudFormation template is currently set up, that is impossible). The VPC Security Comparison document gives a great breakdown on the differences.
NAT instances
Below are the CloudFormation template snippets for the NAT instance. There is some information overlap here, as the subnets and route table items have mainly been explained already, and the network security group has already been shown above, so it is not being shown again. So given that, I will just show the relevant subnet and route entries, and then briefly explain the NAT instance itself, which is an EC2 instance, and of course I will be describing EC2 in detail later on.
Here are the private subnet and routes. Note how MapPublicIpOnLaunch
is off. Also, there is a hook into the availability zone that the public subnet is in, as load balancing can break if the private and public subnets are inconsistently created in different availability zones. Also, the private subnet uses the NAT instance as the default route.
"VCTSLabSubnet2": { "Type": "AWS::EC2::Subnet", "Properties": { "CidrBlock": "10.0.1.0/24", "MapPublicIpOnLaunch": false, "Tags": [ { "Key": "resclass", "Value": "vcts-lab-subnet" }, { "Key": "subnet-type", "Value": "private" } ], "VpcId": { "Ref": "VCTSLabVPC1" }, "AvailabilityZone": { "Fn::GetAtt" : [ "VCTSLabSubnet1", "AvailabilityZone" ] } } }, "VCTSLabPrivateRouteTable": { "Type": "AWS::EC2::RouteTable", "Properties": { "VpcId": { "Ref": "VCTSLabVPC1" }, "Tags": [ { "Key": "resclass", "Value": "vcts-lab-routetable" }, { "Key": "routetable-type", "Value": "private" } ] } }, "VCTSLabPrivateDefaultRoute": { "Type": "AWS::EC2::Route", "Properties": { "DestinationCidrBlock": "0.0.0.0/0", "InstanceId": { "Ref": "VCTSLabNatGw" }, "RouteTableId": { "Ref": "VCTSLabPrivateRouteTable" } } }, "VCTSLabPrivateSubnet2Assoc": { "Type": "AWS::EC2::SubnetRouteTableAssociation", "Properties": { "SubnetId": { "Ref": "VCTSLabSubnet2" }, "RouteTableId": { "Ref": "VCTSLabPrivateRouteTable" } } }
And here is the NAT instance itself. Again, I am not describing it here in detail, as it is an EC2 instance and will be explained in its own section. However, do note that the AMI does map to a list of Amazon Linux instances specifically configured for NAT use – there are scripts on these instances that set up the NAT table and IP forwarding. Also, the NAT instance does not have 2 interfaces in each of the subnets, like a traditional router – traffic flows in through the VPC’s own routers in a way that only an IP in the public subnet is required.
"VCTSLabNatGw": { "Type": "AWS::EC2::Instance", "Properties": { "ImageId": { "Fn::FindInMap": [ "NatRegionMap", { "Ref": "AWS::Region" }, "AMI" ] }, "InstanceType": "t2.micro", "KeyName": { "Ref": "KeyPair" }, "SubnetId": { "Ref": "VCTSLabSubnet1" }, "SourceDestCheck": false, "SecurityGroupIds": [ { "Ref": "VCTSNatSecurityGroup" } ], "Tags": [ { "Key": "resclass", "Value": "vcts-lab-natgw" } ], "UserData": { "Fn::Base64": { "Fn::Join": [ "", [ "#!/bin/bash -xen", "/usr/bin/yum -y updaten", "echo "/opt/aws/bin/cfn-signal -e $? ", " --stack ", { "Ref": "AWS::StackName" }, " ", " --resource VCTSLabNatGw ", " --region ", { "Ref": "AWS::Region" }, " ", " && sed -i 's#^/opt/aws/bin/cfn-signal .*\$##g' ", " /etc/rc.local" >> /etc/rc.localn", "/sbin/rebootn" ]]}} }, "CreationPolicy" : { "ResourceSignal" : { "Count" : 1, "Timeout" : "PT10M" } } }
Next Article – ELB and EC2
Stay tuned for the conclusion of this 3-part article, were I discuss setting up ELB and EC2 and their respective items in the CloudFormation template!