Network with Public and Private Subnets

AWSTemplateFormatVersion: 2010-09-09
Description: >
  A VPC with two public subnets and two private subnets. Each public
  subnet includes a NAT Gateway which provides outbound connectivity
  from the private subnets to the internet.

Overview

Network-Public-Private.svg

This CloudFormation template creates the "standard" AWS network: a VPC with two public subnets and two private subnets. AWS documents this architecture here, and AWS provides a CloudFormation example here.

This is the go-to VPC architecture; it is a good starting place for AWS network design. For example, when using the AWS CDK, this is the infrastrucure that is created when you use the VPC construct with its default options.

All that having been said, using NAT Gateways is expensive, and there are often ways around it. Some possibilities are:

  • Use only public subnets
  • Even with private subnets, you can access AWS services through VPC Endpoints.

Parameters

Parameters:
Param Value
DeploymentName test

DeploymentName

  DeploymentName:
    Type: String
    Description: A name for this deployment

A deployment is a deployed application, potentially comprised of many CloudFormation stacks. This is sometimes called an "environment", but that is an overloaded and confusing term. Use the DeploymentName to indicate which logical deployment a stack belongs to.

If a deployment is completely specified by exactly one CloudFormation template, the DeploymentName and the AWS::StackName refer to the same things. In that case, consider not using a DeploymentName parameter.

  VPCCIDR:
    Description: CIDR range for this VPC
    Type: String
    Default: 10.192.0.0/16

  PublicSubnet1CIDR:
    Description: CIDR range for public subnet in 1st AZ
    Type: String
    Default: 10.192.10.0/24

  PublicSubnet2CIDR:
    Description: CIDR range for public subnet in 2nd AZ
    Type: String
    Default: 10.192.11.0/24

  PrivateSubnet1CIDR:
    Description: CIDR range for private subnet in 1st AZ
    Type: String
    Default: 10.192.20.0/24

  PrivateSubnet2CIDR:
    Description: CIDR range for private subnet in 2nd AZ
    Type: String
    Default: 10.192.21.0/24

Resources

Resources:

VPC

  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCIDR
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Ref DeploymentName

Internet Gateway

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Ref DeploymentName

  InternetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC

Public Subnets

  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [0, !GetAZs ""]
      CidrBlock: !Ref PublicSubnet1CIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub "${DeploymentName} Public (AZ1)"

  PublicSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet1
  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [1, !GetAZs  ""]
      CidrBlock: !Ref PublicSubnet2CIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub "${DeploymentName} Public (AZ2)"

  PublicSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet2

Private Subnets

There are two differences between "public" and "private" subnets:

  1. Public subnets must have a route to an Internet Gateway.

    This allows internet traffic to flow between the internet and the instance. Note: this isn't actually a quality of the subnet itself; rather, it's a matter of what routes are in the Route Table that the subnet is associated with.

  2. Public subnets often specify MapPublicIpOnLaunch: true.

    This tells AWS to assign public IP addresses to EC2 instances in the subnet. This is required if you want to be able to ssh user@$INSTANCE_IP. Which is usually what you want, but one could imagine a situation when public IP addresses wouldn't be necessary.

    Private subnets can also specify MapPublicIpOnLaunch: true. Internet-valid IP addresses would be assigned to instances, but there would be no route from the internet to those intstances. That could be useful if, say, you want to be able to make a subnet temporarily public by temporarily adding a route to the Internet Gateway.

  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [0, !GetAZs  ""]
      CidrBlock: !Ref PrivateSubnet1CIDR
      Tags:
        - Key: Name
          Value: !Sub "${DeploymentName} Private (AZ1)"

  PrivateSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable1
      SubnetId: !Ref PrivateSubnet1
  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select [1, !GetAZs  ""]
      CidrBlock: !Ref PrivateSubnet2CIDR
      Tags:
        - Key: Name
          Value: !Sub "${DeploymentName} Private (AZ2)"

  PrivateSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable2
      SubnetId: !Ref PrivateSubnet2

Public Route Table

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${DeploymentName} Public"
  DefaultPublicRoute:
    Type: AWS::EC2::Route
    DependsOn: InternetGatewayAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

Private Route Tables

  PrivateRouteTable1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${DeploymentName} Private (AZ1)"
  DefaultPrivateRoute1:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable1
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway1
  PrivateRouteTable2:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub "${DeploymentName} Private (AZ1)"
  DefaultPrivateRoute2:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable2
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway2

NAT Gateways

  NatGateway1EIP:
    Type: AWS::EC2::EIP
    DependsOn: InternetGatewayAttachment
    Properties:
      Domain: vpc

  NatGateway1:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatGateway1EIP.AllocationId
      SubnetId: !Ref PublicSubnet1
  NatGateway2EIP:
    Type: AWS::EC2::EIP
    DependsOn: InternetGatewayAttachment
    Properties:
      Domain: vpc

  NatGateway2:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatGateway2EIP.AllocationId
      SubnetId: !Ref PublicSubnet2

Outputs

Outputs:

  VPCID:
    Description: A reference to the created VPC
    Value: !Ref VPC
    Export:
      Name: !Sub "${DeploymentName}-VPCID"

  VPCCIDR:
    Description: The VPC CIDR range
    Value: !GetAtt VPC.CidrBlock
    Export:
      Name: !Sub "${DeploymentName}-VPCCIDR"

  PublicSubnet1:
    Description: The public subnet in the 1st AZ
    Value: !Ref PublicSubnet1
    Export:
      Name: !Sub "${DeploymentName}-PublicSubnet1"

  PublicSubnet2:
    Description: The public subnet in the 2nd AZ
    Value: !Ref PublicSubnet2
    Export:
      Name: !Sub "${DeploymentName}-PublicSubnet2"

  PrivateSubnet1:
    Description: The private subnet in the 1st AZ
    Value: !Ref PrivateSubnet1
    Export:
      Name: !Sub "${DeploymentName}-PrivateSubnet1"

  PrivateSubnet2:
    Description: The private subnet in the 2nd AZ
    Value: !Ref PrivateSubnet2
    Export:
      Name: !Sub "${DeploymentName}-PrivateSubnet2"