Skip to content

Software Factory

AWS Cloudformation – Part 2

05 Aug 2019

by

Olivier Robert

In Part1, we worked on a template to create a VPC. We will extend this template with subnets and route tables. We’ll have public and private subnets.

Private subnets will be used for everything we want to shield from a direct internet access.

Public subnets means we want to be able to connect to services directly over an internet connection.

Our VPC will need an internet gateway. The default routing for our public subnet will be the Intenet Gateway.

Private subnets can be accessed from public subnets or services if this is what we want, but they will have no routing to the Internet Gateway and thus not internet access inbound, or outbound. An EC2 instance deployed in a private subnet will not be able to get updates from the internet for example. To remedy this situation, we can use a NAT Gateway.

The Nat Gateway, deployed in the public subnet, will relay internet connections initiated from the private subnet to the internet via the Internet Gateway. But there is still no way to directly connect from the internet to the private subnet. This is what we want.

Routing. We will have to create route tables and default routings and then associate public and private subnets to specific routing tables.

If you have read until here, have a look at this AWS scenario. It will clear things up.

Mmmm, one more thing, we will use Availability Zones to our advantage and distribute our subnets so that they are not all created in the same AZ.

Adding an Internet Gateway is straight forward:

Resources:
[...]
  IGW:
    Type: AWS::EC2::InternetGateway
    Properties: 
      Tags:
        - Key: Name
          Value: "Project X"

The NAT Gateway will need an Elastic IP allocated to it.

Resources:
[...]
  EIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

Domain is set to vpc to allocate the address for use with instances in a VPC. In our case, the NAT Gateway.

Resources:
[...]
  NatGW:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId:
        Fn::GetAtt:
          - EIP
          - AllocationId
      SubnetId: !Ref VPCPublicSubnet1
      Tags:
        - Key: Name
          Value: "Project X"

We use the intrinsic function GetAtt to retrieve the allocation ID attribute from the Elastic IP resource we named EIP.

For the subnet in which the NAT Gateway should be deployed in, we use the Ref intrinsic function to reference a public subnet. That public subnet has not been defined yet, but we will name it "VPCPublicSubnet1".

For now, we'll continue with the NAT Gateway and attach it to our VPC and specify the Internet Gateway.

Resources:
[...]
  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties: 
      InternetGatewayId: !Ref IGW
      VpcId: !Ref myVPC

Notice that we use resource references.

If you wonder how resources are created, Cloudformation creates independent resources in parallel. Implicit dependencies are detected when the property of one resource is referring to another resource (for instance an Internet Gateway in a VPCGatewayAttachment). Explicit dependencies can be declared with the DependsOn attribute. When we are using Ref to reference resources, we are creating implicit dependencies. The referenced resources should be created first. Sometimes, when things get complicated, explicit dependencies can solve issues.

In public subnets, we'll want automatic public IP allocations and as we are going to create 3 public subnets, we will distribute them over Availability Zones.

Resources:
[...]
  VPCPublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      MapPublicIpOnLaunch: True
      CidrBlock: !Ref PublicSubnet1CIDR
      AvailabilityZone: 
        Fn::Select: 
          - 0
          - Fn::GetAZs: ""
      Tags:
        - Key: Name
          Value: ProjectXPub1
      VpcId: !Ref myVPC
  
  VPCPublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      MapPublicIpOnLaunch: True
      CidrBlock: !Ref PublicSubnet2CIDR
      AvailabilityZone: 
        Fn::Select: 
          - 1
          - Fn::GetAZs: ""
      Tags:
        - Key: Name
          Value: ProjectXPub2
      VpcId: !Ref myVPC

  VPCPublicSubnet3:
    Type: AWS::EC2::Subnet
    Properties:
      MapPublicIpOnLaunch: True
      CidrBlock: !Ref PublicSubnet3CIDR
      AvailabilityZone: 
        Fn::Select: 
          - 2
          - Fn::GetAZs: ""
      Tags:
        - Key: Name
          Value: ProjectXPub3
      VpcId: !Ref myVPC

Remember our NAT Gateway resource? Now the reference to VPCPublicSubnet1 is valid.

MapPublicIpOnLaunch is useful because instances launched in public subnets will get a public IP automatically.

The GetAZs intrisic function returns an array of Availability Zones. We just pick one in the array for each subnet with Select.

We introduced PublicSubnet<#>CIDR which does not exist yet! These are references to parameters. Let's sort this out.

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
    - Label:
        default: Network Configuration
      Parameters:
      - VPCCIDR
      - PublicSubnet1CIDR
      - PublicSubnet2CIDR
      - PublicSubnet3CIDR
    ParameterLabels:
      VPCCIDR:
        default: VPC CIDR
      PublicSubnet1CIDR:
        default: Public subnet 1 CIDR
      PublicSubnet2CIDR:
        default: Public subnet 2 CIDR
      PublicSubnet3CIDR:
        default: Public subnet 3 CIDR

Parameters:
[...]
  PublicSubnet1CIDR:
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$
    ConstraintDescription: Must be a valid IP range in x.x.x.x/x notation
    Default: 10.0.1.0/24
    Description: CIDR Block for the public DMZ subnet 1 located in Availability Zone 1
    Type: String
  PublicSubnet2CIDR:
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$
    ConstraintDescription: Must be a valid IP range in x.x.x.x/x notation
    Default: 10.0.2.0/24
    Description: CIDR Block for the public DMZ subnet 2 located in Availability Zone 2
    Type: String
  PublicSubnet3CIDR:
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$
    ConstraintDescription: Must be a valid IP range in x.x.x.x/x notation
    Default: 10.0.3.0/24
    Description: CIDR Block for the public DMZ subnet 3 located in Availability Zone 3
    Type: String

I added parameter labels as well in the metadata section.

Next, private subnets where we do not need public adresses.

So far, our entire template looks like this:

AWSTemplateFormatVersion: 2010-09-09
Description: VPC template for Project X

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
    - Label:
        default: Network Configuration
      Parameters:
      - VPCCIDR
      - PublicSubnet1CIDR
      - PublicSubnet2CIDR
      - PublicSubnet3CIDR
      - PrivateSubnet1CIDR
      - PrivateSubnet2CIDR
      - PrivateSubnet3CIDR
    ParameterLabels:
      VPCCIDR:
        default: VPC CIDR
      PublicSubnet1CIDR:
        default: Public subnet 1 CIDR
      PublicSubnet2CIDR:
        default: Public subnet 2 CIDR
      PublicSubnet3CIDR:
        default: Public subnet 3 CIDR
      PrivateSubnet1CIDR:
        default: Private subnet 1 CIDR
      PrivateSubnet2CIDR:
        default: Private subnet 2 CIDR
      PrivateSubnet3CIDR:
        default: Private subnet 3 CIDR

Parameters:
  VPCCIDR:
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$
    ConstraintDescription: Must be a valid IP range in x.x.x.x/x notation
    Default: 10.0.0.0/16
    Description: CIDR Block for the VPC
    Type: String
  PublicSubnet1CIDR:
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$
    ConstraintDescription: Must be a valid IP range in x.x.x.x/x notation
    Default: 10.0.1.0/24
    Description: CIDR Block for the public DMZ subnet 1 located in Availability Zone 1
    Type: String
  PublicSubnet2CIDR:
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$
    ConstraintDescription: Must be a valid IP range in x.x.x.x/x notation
    Default: 10.0.2.0/24
    Description: CIDR Block for the public DMZ subnet 2 located in Availability Zone 2
    Type: String
  PublicSubnet3CIDR:
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$
    ConstraintDescription: Must be a valid IP range in x.x.x.x/x notation
    Default: 10.0.3.0/24
    Description: CIDR Block for the public DMZ subnet 3 located in Availability Zone 3
    Type: String
  PrivateSubnet1CIDR:
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$
    ConstraintDescription: Must be a valid IP range in x.x.x.x/x notation
    Default: 10.0.10.0/24
    Description: CIDR block for private subnet 1 located in Availability Zone 1
    Type: String
  PrivateSubnet2CIDR:
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$
    ConstraintDescription: Must be a valid IP range in x.x.x.x/x notation
    Default: 10.0.11.0/24
    Description: CIDR block for private subnet 2 located in Availability Zone 2
    Type: String
  PrivateSubnet3CIDR:
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$
    ConstraintDescription: Must be a valid IP range in x.x.x.x/x notation
    Default: 10.0.12.0/24
    Description: CIDR block for private subnet 3 located in Availability Zone 3
    Type: String


Resources:
  myVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCIDR
      EnableDnsSupport: true
      EnableDnsHostnames: True
      InstanceTenancy: "default"
      Tags:
        - Key: Name
          Value: "Project X"

  IGW:
    Type: AWS::EC2::InternetGateway
    Properties: 
      Tags:
        - Key: Name
          Value: "Project X"

  EIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
  
  NatGW:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId:
        Fn::GetAtt:
          - EIP
          - AllocationId
      SubnetId: !Ref VPCPublicSubnet1
      Tags:
        - Key: Name
          Value: "Project X"

  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties: 
      InternetGatewayId: !Ref IGW
      VpcId: !Ref myVPC

  VPCPublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      MapPublicIpOnLaunch: True
      CidrBlock: !Ref PublicSubnet1CIDR
      AvailabilityZone: 
        Fn::Select: 
          - 0
          - Fn::GetAZs: ""
      Tags:
        - Key: Name
          Value: ProjectXPub1
      VpcId: !Ref myVPC
  
  VPCPublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      MapPublicIpOnLaunch: True
      CidrBlock: !Ref PublicSubnet2CIDR
      AvailabilityZone: 
        Fn::Select: 
          - 1
          - Fn::GetAZs: ""
      Tags:
        - Key: Name
          Value: ProjectXPub2
      VpcId: !Ref myVPC

  VPCPublicSubnet3:
    Type: AWS::EC2::Subnet
    Properties:
      MapPublicIpOnLaunch: True
      CidrBlock: !Ref PublicSubnet3CIDR
      AvailabilityZone: 
        Fn::Select: 
          - 2
          - Fn::GetAZs: ""
      Tags:
        - Key: Name
          Value: ProjectXPub3
      VpcId: !Ref myVPC

  VPCPrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: !Ref PrivateSubnet1CIDR
      AvailabilityZone: 
        Fn::Select: 
          - 0
          - Fn::GetAZs: ""
      Tags:
        - Key: Name
          Value: ProjectXPriv1
      VpcId: !Ref myVPC
  
  VPCPrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: !Ref PrivateSubnet2CIDR
      AvailabilityZone: 
        Fn::Select: 
          - 1
          - Fn::GetAZs: ""
      Tags:
        - Key: Name
          Value: ProjectXPriv2
      VpcId: !Ref myVPC
  
  VPCPrivateSubnet3:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: !Ref PrivateSubnet3CIDR
      AvailabilityZone: 
        Fn::Select: 
          - 2
          - Fn::GetAZs: ""
      Tags:
        - Key: Name
          Value: ProjectXPriv3
      VpcId: !Ref myVPC

Note that we have indicated default CIDR values for the subnets to avoid typing all parameters by hand in the web interface. The only thing we need to enter manually when creating the stack is a stack name.

It's time to get the routing part done. We will create a route table and a default route. Our route table only needs a reference to a VPC.

Resources:
[...]
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties: 
      VpcId: !Ref myVPC
      Tags:
        - Key: Name
          Value: ProjectXPubRT

Our default route will route any traffic to the internet gateway:

Resources:
[...]
  PublicDefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref IGW
      RouteTableId: !Ref PublicRouteTable

This public route table (containing the default route) needs to be associated with the public subnets.

Resources:
[...]
  PublicRouteSubnetAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties: 
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref VPCPublicSubnet1
  
  PublicRouteSubnetAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties: 
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref VPCPublicSubnet2

  PublicRouteSubnetAssociation3:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties: 
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref VPCPublicSubnet3

We will do the same for the private subnet except these subnet will have a default route routing to the NAT Gateway.

Our final VPC template is this:

AWSTemplateFormatVersion: 2010-09-09
Description: VPC template for Project X

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
    - Label:
        default: Network Configuration
      Parameters:
      - VPCCIDR
      - PublicSubnet1CIDR
      - PublicSubnet2CIDR
      - PublicSubnet3CIDR
      - PrivateSubnet1CIDR
      - PrivateSubnet2CIDR
      - PrivateSubnet3CIDR
    ParameterLabels:
      VPCCIDR:
        default: VPC CIDR
      PublicSubnet1CIDR:
        default: Public subnet 1 CIDR
      PublicSubnet2CIDR:
        default: Public subnet 2 CIDR
      PublicSubnet3CIDR:
        default: Public subnet 3 CIDR
      PrivateSubnet1CIDR:
        default: Private subnet 1 CIDR
      PrivateSubnet2CIDR:
        default: Private subnet 2 CIDR
      PrivateSubnet3CIDR:
        default: Private subnet 3 CIDR

Parameters:
  VPCCIDR:
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$
    ConstraintDescription: Must be a valid IP range in x.x.x.x/x notation
    Default: 10.0.0.0/16
    Description: CIDR Block for the VPC
    Type: String
  PublicSubnet1CIDR:
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$
    ConstraintDescription: Must be a valid IP range in x.x.x.x/x notation
    Default: 10.0.1.0/24
    Description: CIDR Block for the public DMZ subnet 1 located in Availability Zone 1
    Type: String
  PublicSubnet2CIDR:
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$
    ConstraintDescription: Must be a valid IP range in x.x.x.x/x notation
    Default: 10.0.2.0/24
    Description: CIDR Block for the public DMZ subnet 2 located in Availability Zone 2
    Type: String
  PublicSubnet3CIDR:
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$
    ConstraintDescription: Must be a valid IP range in x.x.x.x/x notation
    Default: 10.0.3.0/24
    Description: CIDR Block for the public DMZ subnet 3 located in Availability Zone 3
    Type: String
  PrivateSubnet1CIDR:
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$
    ConstraintDescription: Must be a valid IP range in x.x.x.x/x notation
    Default: 10.0.10.0/24
    Description: CIDR block for private subnet 1 located in Availability Zone 1
    Type: String
  PrivateSubnet2CIDR:
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$
    ConstraintDescription: Must be a valid IP range in x.x.x.x/x notation
    Default: 10.0.11.0/24
    Description: CIDR block for private subnet 2 located in Availability Zone 2
    Type: String
  PrivateSubnet3CIDR:
    AllowedPattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$
    ConstraintDescription: Must be a valid IP range in x.x.x.x/x notation
    Default: 10.0.12.0/24
    Description: CIDR block for private subnet 3 located in Availability Zone 3
    Type: String


Resources:
  myVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCIDR
      EnableDnsSupport: true
      EnableDnsHostnames: True
      InstanceTenancy: "default"
      Tags:
        - Key: Name
          Value: "Project X"

  IGW:
    Type: AWS::EC2::InternetGateway
    Properties: 
      Tags:
        - Key: Name
          Value: "Project X"

  EIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
  
  NatGW:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId:
        Fn::GetAtt:
          - EIP
          - AllocationId
      SubnetId: !Ref VPCPublicSubnet1
      Tags:
        - Key: Name
          Value: "Project X"

  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties: 
      InternetGatewayId: !Ref IGW
      VpcId: !Ref myVPC

  VPCPublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      MapPublicIpOnLaunch: True
      CidrBlock: !Ref PublicSubnet1CIDR
      AvailabilityZone: 
        Fn::Select: 
          - 0
          - Fn::GetAZs: ""
      Tags:
        - Key: Name
          Value: ProjectXPub1
      VpcId: !Ref myVPC
  
  VPCPublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      MapPublicIpOnLaunch: True
      CidrBlock: !Ref PublicSubnet2CIDR
      AvailabilityZone: 
        Fn::Select: 
          - 1
          - Fn::GetAZs: ""
      Tags:
        - Key: Name
          Value: ProjectXPub2
      VpcId: !Ref myVPC

  VPCPublicSubnet3:
    Type: AWS::EC2::Subnet
    Properties:
      MapPublicIpOnLaunch: True
      CidrBlock: !Ref PublicSubnet3CIDR
      AvailabilityZone: 
        Fn::Select: 
          - 2
          - Fn::GetAZs: ""
      Tags:
        - Key: Name
          Value: ProjectXPub3
      VpcId: !Ref myVPC

  VPCPrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: !Ref PrivateSubnet1CIDR
      AvailabilityZone: 
        Fn::Select: 
          - 0
          - Fn::GetAZs: ""
      Tags:
        - Key: Name
          Value: ProjectXPriv1
      VpcId: !Ref myVPC
  
  VPCPrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: !Ref PrivateSubnet2CIDR
      AvailabilityZone: 
        Fn::Select: 
          - 1
          - Fn::GetAZs: ""
      Tags:
        - Key: Name
          Value: ProjectXPriv2
      VpcId: !Ref myVPC
  
  VPCPrivateSubnet3:
    Type: AWS::EC2::Subnet
    Properties:
      CidrBlock: !Ref PrivateSubnet3CIDR
      AvailabilityZone: 
        Fn::Select: 
          - 2
          - Fn::GetAZs: ""
      Tags:
        - Key: Name
          Value: ProjectXPriv3
      VpcId: !Ref myVPC

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties: 
      VpcId: !Ref myVPC
      Tags:
        - Key: Name
          Value: ProjectXPubRT

  PublicDefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref IGW
      RouteTableId: !Ref PublicRouteTable

  PublicRouteSubnetAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties: 
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref VPCPublicSubnet1
  
  PublicRouteSubnetAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties: 
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref VPCPublicSubnet2

  PublicRouteSubnetAssociation3:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties: 
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref VPCPublicSubnet3

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties: 
      VpcId: !Ref myVPC
      Tags:
        - Key: Name
          Value: ProjectXPrivRT 

  PrivateDefaultRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: "0.0.0.0/0"
      NatGatewayId: !Ref NatGW
      RouteTableId: !Ref PrivateRouteTable

  PrivateRouteSubnetAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties: 
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref VPCPrivateSubnet1

  PrivateRouteSubnetAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties: 
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref VPCPrivateSubnet2
  
  PrivateRouteSubnetAssociation3:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties: 
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref VPCPrivateSubnet3

If you haven't uploaded the template to Cloudformation, here is the parameters section you'd get before launching it.

Stay tuned for Part 3 where we will deploy an EC2 bastion host in the public subnet and an EC2 instance in the private subnets to validate our configuration.