Now we continue improving the VPC template from my previous blog entry “Starting with CloudFormation templates”
What we ended up with there was a VPC with one sub-net connected to the Internet. Or what is know in AWS lingo as a “Public Subnet”.
The goal now is a VPC with presence in tree Availability Zones with a “Public Subnet” in each, and a “Private Subnet” in each as well.
Humble beginnings
Before we go all out on tree Availability Zones, let us set it up with only one. It will not be that hard to expand the template to tree Availability Zones when we have got it up and running on one.
Public vs Private sub-net
The main difference between a Private and a Public sub-net is its routing to Internet.
Public sub-net
The Public sub-net is routed directly through an Internet Gateway so you need an Elastic IP on a resource accessing Internet. Resources can then also be available on the Internet through their Elastic IP.
Private sub-net
The Private sub-net does not have direct route to Internet, and can be configured in a manner it does not have access to Internet at all. We will allow access to the Internet through a NAT Gateway. Resources in a Private sub-net have no use of Elastic IP.
NAT Gateway
The NAT Gateway we talk about here is a managed service from AWS, and it comes with a price. Each instance of NAT Gateway costs $0.045 per hour and $0.045 per GB data transferred in or out. In our setup with tree Availability Zones and a NAT Gateway in each of them, we get up to $100 a month just for the Gateways alone.
You can read more about NAT Gateway pricing here
Private Sub-net in CloudFormation
As stated, the main difference is in the routing. And we know we need a NAT Gateway, let us check the documentation.
It gives us this skeleton:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: String
SubnetId: String
Tags:
- Tag
Both SubnetId
and AllocationId
are required. SubnetId
needs a reference
to a AWS::EC2::Subnet
resource. But AllocationId
is The allocation ID of an Elastic IP
address to associate with the NAT gateway.
So we check the documentation for Elastic IP and it gives us
Type: AWS::EC2::EIP
Properties:
Domain: String
InstanceId: String
PublicIpv4Pool: String
Tags:
- Tag
Of those, only Domain
is of interest, and as it is to be used with a NAT
Gateway we not care for EC2-Classic, so we set this to vpc
.
The documentation also mentions that if we are creating this resource in the same template as our VPC we need to create dependency on the VPC-gateway attachment.
A very important thing we can read from the documentation page is the return
values of the resource. If you ask for the !Ref
value, you will get the IP
address associated with the resource, but we want the allocation ID of it.
To access that value we need to use the
Fn::GetAtt
function. And we need to use it with the AllocationId
key word. Look for
that below. There we use the shorter !GetAtt
form of the function.
Dependency
You can give CloudFormation directions on order it must create resources. You
can do that by using the keyword DependsOn
. Like for example:
GatewayToInternet:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGW
NatGWIP:
DependsOn: GatewayToInternet
Type: AWS::EC2::EIP
Properties:
Domain: vpc
You do not need to specify the DependsOn
relationship if the dependency is
show through reference as seen here:
InternetGW:
Type: AWS::EC2::InternetGateway
GatewayToInternet:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGW
Here it would be superfluous to declare that GatewayToInternet
was dependent
on InternetGW
. You can read more about DependsOn
here
How a sub-net is defined
Now we need to look back to a normal sub-net and see what is needed to define it and its routing. The parts from my previous blog are:
SubNett:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.42.0/24
VpcId: !Ref VPC
RouteTablePublic:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
RoutePublic:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
RouteTableId: !Ref RouteTablePublic
GatewayId: !Ref InternetGW
Route2Subnet:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref RouteTablePublic
SubnetId: !Ref SubNett
So there is a route, in a route table. Then there is a sub-net, and then there is the association between sub-net and route table.
The difference from this to the Private sub-net is where to route the traffic not local to the VPC.
Route for a Private sub-net
As we stated in last blog, these are all the options for the Route resource:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: String
DestinationIpv6CidrBlock: String
EgressOnlyInternetGatewayId: String
GatewayId: String
InstanceId: String
NatGatewayId: String
NetworkInterfaceId: String
RouteTableId: String
TransitGatewayId: String
VpcPeeringConnectionId: String
Now, we will not use GatewayId
but the NatGatewayId
for the Private sub-net.
Private Sub-net
Putting the parts together, our Private sub-net definition becomes:
PrivateSubNett:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.43.0/24
VpcId: !Ref VPC
NatGWIP:
DependsOn: GatewayToInternet
Type: AWS::EC2::EIP
Properties:
Domain: vpc
NatGW:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NatGWIP.AllocationId
SubnetId: !Ref PrivateSubNett
RouteTablePrivate:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
RoutePrivate:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
RouteTableId: !Ref RouteTablePrivate
NatGatewayId: !Ref NatGW
Route2PrivateSubnet:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref RouteTablePrivate
SubnetId: !Ref PrivateSubNett
Putting the parts together
Now we combine the end product from previous blog and the last part, moving sections around so they are logically grouped together.
---
Description: This is an attempt to create a VPC in a Cloudformation stack
Resources:
VPC:
Type: 'AWS::EC2::VPC'
Properties:
CidrBlock: 10.0.0.0/16
InternetGW:
Type: AWS::EC2::InternetGateway
GatewayToInternet:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGW
NatGWIP:
DependsOn: GatewayToInternet
Type: AWS::EC2::EIP
Properties:
Domain: vpc
NatGW:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NatGWIP.AllocationId
SubnetId: !Ref PrivateSubNett
SubNett:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.42.0/24
VpcId: !Ref VPC
PrivateSubNett:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.43.0/24
VpcId: !Ref VPC
RouteTablePublic:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
RouteTablePrivate:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
RoutePublic:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
RouteTableId: !Ref RouteTablePublic
GatewayId: !Ref InternetGW
RoutePrivate:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
RouteTableId: !Ref RouteTablePrivate
NatGatewayId: !Ref NatGW
Route2Subnet:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref RouteTablePublic
SubnetId: !Ref SubNett
Route2PrivateSubnet:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref RouteTablePrivate
SubnetId: !Ref PrivateSubNett
This makes up a template that will create a Private and a Public sub-net.
More Availability Zones
Above, we did not say anything about the Availability Zone thing were instantiated in. But now we want to create 3 Public sub-nets and 3 Private sub-nets, with each Public and Private pair in distinct Availability Zones.
But let us recap quickly what we need to define.
- A VPC, only one.
- An Internet Gateway, only one.
- A VPC Gateway Attachment, only one.
- Elastic IPs, three of them, one for each NAT Gateway
- NAT Gateways, three of them, one for each Availability Zone.
- Sub-nets, six of them, one Private and one Public for each Availability Zone.
- Route Tables, four total, one for the Public sub-nets, and one for each of the Private sub-nets
- Routes, four total, one for the Public sub-nets, and one for each of the Private sub-nets
- Sub-net Associations, six total, one for each sub-net.
So that is a lot of resources that need to be spelled out. And the only thing that we are missing before we start copy’n’paste bonanza is a way to decide which Availability Zone to instantiate our resources.
Specifying the Availability Zone
For specifying the Availability Zone for our resources we use the fact that
the AWS::EC2::Subnet
resource has a AvailabilityZone
parameter that can be
set. So if we are to create a sub-net in the Stockholm region, we could use the
values:
- eu-north-1a
- eu-north-1b
- eu-north-1c
and we would be set.
But what if we want to use the template in Singapore? Then we would have to change those values. So that would be no good.
What we should do is to use the intrinsic function Fn::GetAZs
which returns
an array of the Availability Zones in a region. If the function only gets
empty ""
it defaults to the region the template is instantiated in. Then we
need to use the intrinsic function Fn::Select
to give use the first, the
second and the third Availability Zone in the region. (You can read more about
these functions
here
and
here)
The !Select
function takes an two element array, where the first element is
the index that you want, and the second element is the array you want to
select from, using the index given.
Combining everything
Let us combine this, but yet break it up in sections for clarity.
VPC and Internet Gateway
---
Description: This is an attempt to create a VPC in a Cloudformation stack
Resources:
VPC:
Type: 'AWS::EC2::VPC'
Properties:
CidrBlock: 10.0.0.0/16
InternetGW:
Type: AWS::EC2::InternetGateway
GatewayToInternet:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGW
Elastic IP addresses for the NAT Gateways
Here we start on the tedious repetition of similar resources, I prepend the name of all that kind of resources with the number associated with the Availability Zone index
NatGWIP0:
DependsOn: GatewayToInternet
Type: AWS::EC2::EIP
Properties:
Domain: vpc
NatGWIP1:
DependsOn: GatewayToInternet
Type: AWS::EC2::EIP
Properties:
Domain: vpc
NatGWIP2:
DependsOn: GatewayToInternet
Type: AWS::EC2::EIP
Properties:
Domain: vpc
All the sub-nets…
The IP addresses chosen for each sub-net is done this way to behave nicely in the next blog entry.
PublicSubnet0:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.0.0/24
VpcId: !Ref VPC
AvailabilityZone:
!Select
- 0
- !GetAZs ""
PrivateSubnet0:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.100.0/24
VpcId: !Ref VPC
AvailabilityZone:
!Select
- 0
- !GetAZs ""
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.1.0/24
VpcId: !Ref VPC
AvailabilityZone:
!Select
- 1
- !GetAZs ""
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.101.0/24
VpcId: !Ref VPC
AvailabilityZone:
!Select
- 1
- !GetAZs ""
PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.2.0/24
VpcId: !Ref VPC
AvailabilityZone:
!Select
- 2
- !GetAZs ""
PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.102.0/24
VpcId: !Ref VPC
AvailabilityZone:
!Select
- 2
- !GetAZs ""
The NAT Gateways
NatGW0:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NatGWIP0.AllocationId
SubnetId: !Ref PrivateSubnet0
NatGW1:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NatGWIP1.AllocationId
SubnetId: !Ref PrivateSubnet1
NatGW2:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NatGWIP2.AllocationId
SubnetId: !Ref PrivateSubnet2
The Route Tables
RouteTablePublic:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
RouteTablePrivate0:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
RouteTablePrivate1:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
RouteTablePrivate2:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
The Routes
RoutePublic:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
RouteTableId: !Ref RouteTablePublic
GatewayId: !Ref InternetGW
RoutePrivate0:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
RouteTableId: !Ref RouteTablePrivate0
NatGatewayId: !Ref NatGW0
RoutePrivate1:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
RouteTableId: !Ref RouteTablePrivate1
NatGatewayId: !Ref NatGW1
RoutePrivate2:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
RouteTableId: !Ref RouteTablePrivate2
NatGatewayId: !Ref NatGW2
Association between route tables and sub-nets
Route2PublicSubnet0:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref RouteTablePublic
SubnetId: !Ref PublicSubnet0
Route2PrivateSubnet0:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref RouteTablePrivate0
SubnetId: !Ref PrivateSubnet0
Route2PublicSubnet1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref RouteTablePublic
SubnetId: !Ref PublicSubnet1
Route2PrivateSubnet1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref RouteTablePrivate1
SubnetId: !Ref PrivateSubnet1
Route2PublicSubnet2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref RouteTablePublic
SubnetId: !Ref PublicSubnet2
Route2PrivateSubnet2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref RouteTablePrivate2
SubnetId: !Ref PrivateSubnet2
And there is still thing missing here. We need to export references to the resources created here, so that we can use them in other stacks. That and other improvements await in the next chapter.