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.

Jónas Helgi Pálsson

Senior Systems Consultant at Redpill Linpro

Jónas joined Redpill Linpro over a decade ago and has in that period worked as both a consultant and a system administrator. Main focus currently for Jónas is AWS and infrastructure on that platform. Previously been working with KVM and OpenStack, dabbles with programming and has a soft spot for openSUSE.

Just-Make-toolbox

make is a utility for automating builds. You specify the source and the build file and make will determine which file(s) have to be re-built. Using this functionality in make as an all-round tool for command running as well, is considered common practice. Yes, you could write Shell scripts for this instead and they would be probably equally good. But using make has its own charm (and gets you karma points).

Even this ... [continue reading]

Containerized Development Environment

Published on February 28, 2024

Ansible-runner

Published on February 27, 2024