AWS VPC With Public and Private Subnets

January 22, 2020
[AWS] [VPC] [EC2] [Architecture]

Overview

Virtual Private Clouds (or VPC) as you all probably know, is one of those services, which would be a lifesaver when you know how to use them. The isolation and service integrations provided by VPCs, suppose to reduce the common cloud management hassles and help you when you plan to scale your application. However, covering all those heavy liftings, it's not that easy to abstract everything into a simple point and clicks user interface. In a couple of my last projects, I have faced some problems on applying a generic architecture based on VPCs (usually because I forgot a small detail somewhere). So I decided to automate it the solution to be able to reuse it in future.

What We Need

Architecture schema for VPC with public and private subnets

To share my solution, I guess, it's better to describe the problem, in a high-level manner. In web-based projects, we usually, need a database, and on the AWS platform, RDS is the product we typically use. To lunch RDS, we need a VPC with subnets in at least two availability zones. Also, if you want to have access to this database from your local machine, you must make sure that it's launched on a public subnet. A quick side note here: To have direct access to your RDS instance, you must also make sure:

  1. The database is configured to have public access, so it gets an IP address assigned to it. Otherwise, the database would only be accessible from services, inside the VPC.
  2. The VPC's security group should allow incoming packets from your desired IP addresses.
  3. Always limit access to your databases to the minimum required machines.

Let's go back to our main story. So far, our requirements are:

  • 1 VPC
  • 2 Public Subnets (at least)
  • 1 Internet Gateway (to route connection's through public subnet)

We have our database, and we now want a computing engine to implement our business logic. Our computing engine of choice, here would be a Lambda. As we said, for a service, to be able to access our database, it needs to either get placed inside our VPC or have a public IP address and Lambdas doesn't have any of them by default. So our option here is to put it inside the VPC which can help our Lambda to access to the RDS instance.

However, this would also remove our Lambda function's internet access. If you need to access an external web resource or a public AWS service like AWS Cognito, you need to follow these rules:

  1. The function should only be associated with private subnets.
  2. Your VPC should contain a NAT gateway or instance in a public subnet.

So basically, our list of requirements grows to this:

  • 1 VPC
  • 2 Public Subnets
  • 2 Private Subnets
  • 1 Internet Gateway (to route connection's through public subnet)
  • 1 Nat Gateway (to route connection's through private subnet)
  • And an IP address (which is required by the NAT gateway)

If you've ever tried to do this manually either through the web interface, or CLI, you've probably noticed that it's easy to make a mistake in one step of the setup, and end up with a hard to debug situations (at least I experienced that several times). Having a quite happy experience with provisioning automation tools like Ansible in recent years, I find out AWS CloudFormation, would help me almost the same. I was even happier when I found it has a quite capable teardown functionality, which would be very helpful to reduce clutter inside the account (especially, when you use a single account to experiment different ideas). So we are going to set up a ready to use VPC for this scenario using CloudFormation. But first, some terminology:

Terminology

  • Public Subnet: Means the subnet's traffic is routed through an internet gateway so instances in these subnets, can send their traffic directly to the internet.
  • Private Subnet: Unlike public subnets, to access web resources, private subnets need to send their traffic through a NAT gateway that resides inside a public subnet.
  • Internet Gateway: It allows instances with a public IP address (e.g. EC2) inside your VPC to access the internet.
  • Nat Gateway: Allows instances without a public IP address (e.g. Lambda), inside your VPC to access the internet.

Implementation

We are going to create a CloudFormation template to take care of this setup. First, we define all of the resources we need:

AWSTemplateFormatVersion: 2010-09-09
Description: Deploy a VPC with public/private subnets and NAT/IGW access

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.1.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
      - Key: Name
        Value: !Sub ${AWS::StackName}-vpc

  NATGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt ElasticIPAddress.AllocationId
      SubnetId: !Ref PublicSubnetA
      Tags:
      - Key: Name
        Value: !Sub NAT-${AWS::StackName}

  ElasticIPAddress:
    Type: AWS::EC2::EIP
    Properties:
      Domain: VPC

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    DependsOn: VPC

  PublicSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.1.10.0/24
      AvailabilityZone: !Select [ 0, !GetAZs ]
      Tags:
      - Key: Name
        Value: !Sub ${AWS::StackName}-public-a
  PublicSubnetB:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.1.20.0/24
      AvailabilityZone: !Select [ 1, !GetAZs ]
      Tags:
      - Key: Name
        Value: !Sub ${AWS::StackName}-public-b
  PrivateSubnetA:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.1.30.0/24
      AvailabilityZone: !Select [ 0, !GetAZs ]
      Tags:
      - Key: Name
        Value: !Sub ${AWS::StackName}-private-a
  PrivateSubnetB:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.1.40.0/24
      AvailabilityZone: !Select [ 1, !GetAZs ]
      Tags:
      - Key: Name
        Value: !Sub ${AWS::StackName}-private-b

The YAML structure for this template is quite self-explanatory. I'm going to explain some special syntax or choices here, but if you want, you can learn more about each type and available properties using this reference.

First, what we are creating here is:

  • 1 VPC
  • 1 Nat Gateway
  • 1 Elastic IP
  • 1 Internet Gateway
  • 4 subnets

These are all of the services we need. However, we need to connect them together to form our architecture design. But before that, let's examine some of the configurations in detail.

The syntax !Sub ${AWS::StackName}* is a form of string interpolation, which I've used for naming the resources. This way, I can select a custom stack name every time I invoke this template, and all the resources would get adequately namespaced in the format I've defined here.

The !Select [ 0, !GetAZs ], means get all availability zones which I can use (!GetAZs), and use its first item. So basically, I'm selecting the first item of my availability zones for the given resource. So as you've probably already noticed, we tend to create the subnets inside two different availability zones.

And finally, !Ref * is a way to mention the resources available in the current template. So !Ref VPC, means I'm specifying the VPC instance created in the current template.

So let's continue with mapping these resources together; first, we attach the internet gateway to the VPC

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

Next, we need to create our routing tables. A public one which routes 0.0.0.0/0 through our defined internet gateway:

PublicRouteTable:
  Type: AWS::EC2::RouteTable
  Properties:
    VpcId: !Ref VPC
    Tags:
    - Key: Name
      Value: !Sub ${AWS::StackName}-public
PublicRoute:
  Type: AWS::EC2::Route
  DependsOn: AttachGateway
  Properties:
    RouteTableId: !Ref PublicRouteTable
    DestinationCidrBlock: 0.0.0.0/0
    GatewayId: !Ref InternetGateway

And a private one which uses the NAT gateway for the same purpose:

PrivateRouteTable:
  Type: AWS::EC2::RouteTable
  Properties:
    VpcId: !Ref VPC
    Tags:
    - Key: Name
      Value: !Sub ${AWS::StackName}-private
PrivateRoute:
  Type: AWS::EC2::Route
  Properties:
    RouteTableId: !Ref PrivateRouteTable
    DestinationCidrBlock: 0.0.0.0/0
    NatGatewayId: !Ref NATGateway

Now that our routing tables are ready, we can map our subnets to use them:

PublicSubnetARouteTableAssociation:
  Type: AWS::EC2::SubnetRouteTableAssociation
  Properties:
    SubnetId: !Ref PublicSubnetA
    RouteTableId: !Ref PublicRouteTable
PublicSubnetBRouteTableAssociation:
  Type: AWS::EC2::SubnetRouteTableAssociation
  Properties:
    SubnetId: !Ref PublicSubnetB
    RouteTableId: !Ref PublicRouteTable
PrivateSubnetARouteTableAssociation:
  Type: AWS::EC2::SubnetRouteTableAssociation
  Properties:
    SubnetId: !Ref PrivateSubnetA
    RouteTableId: !Ref PrivateRouteTable
PrivateSubnetBRouteTableAssociation:
  Type: AWS::EC2::SubnetRouteTableAssociation
  Properties:
    SubnetId: !Ref PrivateSubnetB
    RouteTableId: !Ref PrivateRouteTable

That's it. Save the template in a file, and you can invoke it to create a CloudFormation stack using AWS CLI as follows:

$ aws cloudformation create-stack --stack-name DC-MyProject --template-body file://vpc.yaml

As you can see, I used DC-MyProject as my stack name, which would get used by the template to name my resources correctly. That's it, and now you can follow the creation process from your web console.

comments powered by Disqus
Made with + in Amsterdam