(to the tune of the Ewoks Victory Song. Now it can be stuck in your head, and not just mine)
Picking up from last time, I needed to start creating things inside my Virtual Private Cloud, or VPC. The first things to create are subnets – public subnets, in particular. Without a public subnet, nothing that I run in the VPC can be accessed from the internet – nor can they access the internet in turn.
(Caveat: I’m not a networking engineer. This network design represents a standard industry practice, but may not be necessary or suitable for your needs)
What’s a Subnet?
It’s a miniature virtual network inside of the VPC. It’s analogous to an ethernet network. The servers within a subnet can all talk to each other – but can’t talk outside the subnet without instructions
What else do we need?
Besides the subnet, we will need to define:
- an internet gateway, to allow servers in the subnet
- a network routing table, with routes, to tell servers to use the gateway.
Also, we won’t be making one subnet. We’ll make two. This is a minor tease for later, but we need to make two because when we create a load balancer, we will need two public subnets for it. Note that everything we’re doing in this post is free (this will not be true for later posts!)
The Stack
--- | |
AWSTemplateFormatVersion: '2010-09-09' | |
Description: | |
The Public Subnet, and associated routing information | |
# Metadata: # no metadata | |
Parameters: | |
Environment: | |
Type: String | |
Description: | |
Stack Environment Prefix. | |
PrimaryAvailabilityZone: | |
Type: AWS::EC2::AvailabilityZone::Name | |
Default: us-east-1a # Probably shouldn't set a default, as it makes this region dependent | |
SecondaryAvailabilityZone: | |
Type: AWS::EC2::AvailabilityZone::Name | |
Default: us-east-1b # Probably shouldn't set a default, as it makes this region dependent | |
#Mappings: | |
# Conditions: # No Conditions at this time. | |
# Transform: # No Transforms at this time | |
Resources: | |
# We need to create a VPC Gateway, and then attach it to the VPC. | |
VPCGateway: | |
# Using an Internet Gateway for now; may change to a VPN gateway if needed, but one step at a time. | |
Type: AWS::EC2::InternetGateway | |
Properties: | |
Tags: | |
- Key: Name | |
Value: !Sub "${Environment} VPC Internet Gateway" | |
VPCGatewayAttachment: | |
Type: AWS::EC2::VPCGatewayAttachment | |
Properties: | |
InternetGatewayId: !Ref VPCGateway | |
VpcId: | |
Fn::ImportValue: !Sub "${Environment}::VPC" | |
# We need a subnet for publicly available servers. We need two, so that we can register | |
# a load balancer. | |
PublicSubnetAZ1: | |
Type: AWS::EC2::Subnet | |
Properties: | |
CidrBlock: 10.0.1.0/24 # 10.0.10.0 -> 10.0.1.255 | |
MapPublicIpOnLaunch: false # We will use elastic IPs for public-facing servers. | |
AvailabilityZone: !Ref PrimaryAvailabilityZone | |
VpcId: | |
Fn::ImportValue: !Sub "${Environment}::VPC" | |
Tags: | |
- Key: Name | |
Value: !Sub "${Environment} Public Subnet AZ1" | |
PublicSubnetAZ2: | |
Type: AWS::EC2::Subnet | |
Properties: | |
CidrBlock: 10.0.2.0/24 # 10.0.20.0 -> 10.0.2.255 | |
MapPublicIpOnLaunch: false # We will use elastic IPs for public-facing servers. | |
AvailabilityZone: !Ref SecondaryAvailabilityZone | |
VpcId: | |
Fn::ImportValue: !Sub "${Environment}::VPC" | |
Tags: | |
- Key: Name | |
Value: !Sub "${Environment} Public Subnet AZ2" | |
# In order for subnets to receive traffic from the public, we need to create | |
# routing tables and rules. | |
PublicRouteTable: | |
Type: AWS::EC2::RouteTable | |
Properties: | |
VpcId: | |
Fn::ImportValue: !Sub "${Environment}::VPC" | |
Tags: | |
- Key: Name | |
Value: !Sub "${Environment} Public Route Table" | |
PublicSubnetRoute: | |
Type: AWS::EC2::Route | |
Properties: | |
RouteTableId: !Ref PublicRouteTable | |
DestinationCidrBlock: 0.0.0.0/0 # We have no idea what IPs may be assigned; got to go global | |
GatewayId: !Ref VPCGateway | |
# The route can not be configured until the gateway is attached to the subnet. | |
DependsOn: VPCGatewayAttachment | |
PublicRouteTableAssociationAZ1: | |
Type: AWS::EC2::SubnetRouteTableAssociation | |
Properties: | |
SubnetId: !Ref PublicSubnetAZ1 | |
RouteTableId: !Ref PublicRouteTable | |
PublicRouteTableAssociationAZ2: | |
Type: AWS::EC2::SubnetRouteTableAssociation | |
Properties: | |
SubnetId: !Ref PublicSubnetAZ2 | |
RouteTableId: !Ref PublicRouteTable | |
Outputs: | |
PublicSubnetAZ1: | |
Description: The publicly accessible subnet | |
Value: !Ref PublicSubnetAZ1 | |
Export: | |
Name: !Sub "${Environment}::PublicSubnetAZ1" | |
PublicSubnetAZ2: | |
Description: The publicly accessible subnet | |
Value: !Ref PublicSubnetAZ2 | |
Export: | |
Name: !Sub "${Environment}::PublicSubnetAZ2" |
Break It Down
Parameters
In this section, we once again ask for the stack environment. We’re going to be doing this every time, as it lets us distinguish between a production stack and testing stacks. We also get the user to specify two Availability Zones – the subnets will get created in those.
VPCGateway
The first two resources relate to the internet gateway. First, we define it, and then we attach it. This two-stage approach is fairly common with CloudFormation (though they sometimes have shortcuts)
This introduces the use of intrinsic functions. These are functions that get called as part of processing the CloudFormation stack. When using YAML, these can be called using the verbose mode (e.g. Fn::ImportValue
), or with a shortcut (e.g. !Sub
). However, you can’t do the shortcut twice.
The ImportValue
function is the most important here. It makes a connection between this stack and the Globals stack made last time – it lets us use a reference to the VPC defined there, without having to look up the ID and either hardcode it or pass it as a parameter. By using the Sub
function, I make sure I get the VPC for this test environment.
Public Subnet 1 and Public Subnet 2
The next two resources list the two public subnets – PublicSubnetAZ1
and PublicSubnetAZ2
. (AZ stands for ‘Availability Zone’).
One important option here is the CidrBlock
. This specifies the range of IP addresses that can be addressed within the subnet. I don’t want them overlapping (I don’t even know if you can!), so I use a simple technique to allocate the range.
Another thing to note is that I don’t actually want to assign public IPs to instances automatically. One reason is that the IPs get changed every time you create a server; that’s okay for temporary servers, but for long-term ones I want something more permanent. Amazon provides this in the form of Elastic IP addresses (yes, that’s another teaser for a later post)
Routing Tables
Right now, those subnets aren’t actually public. I need to put in a network route table, so that servers in these subnets (and yes, there aren’t any yet) can send traffic to the internet.
The PublicRouteTable
holds the routes. The PublicSubnetRoute
describes the route – sending all traffic out via the previously created VPCGateway
. Note that there is a dependency on the VPCGatewayAttachment
– the route can’t be associated with the gateway until it’s attached to a VPC, so we have to wait for that to get setup.
Finally, the route table gets associated with the newly created subnets.
Outputs
We don’t use the subnets here – that happens later. So we export the subnets out for wider consumption, just like the VPC was exported last time.
What’s next?
From here, we’ll go on to create a ‘bastion host’. This is a server that we can SSH into, so that we can then SSH into other servers. It will be the only server that we expose directly to the world (and, even then, only via SSH).
This bastion host will also do double-duty as a NAT Gateway, so that servers in private subnets can communicate with the outside internet.
All that will be coming up next time.