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 that heavy lifting, 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 applying a generic architecture based on VPCs (usually because I forgot a small detail somewhere). So I decided to automate the solution to be able to reuse it in the future.
What We Need
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 launch an RDS instance, 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:
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.
The VPC’s security group should allow incoming packets from your desired IP addresses.
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 access 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:
The function should only be associated with private subnets.
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)
You probably noticed that it’s easy to make a mistake in one phase of the setup and end up with a hard-to-debug situation if you’ve ever tried to do this manually using the web interface or CLI (at least, that’s what I encountered multiple times). Having a quite happy experience with provisioning automation tools like Ansible in recent years, I find out AWSCloudFormation, 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 with 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: This 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 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 it 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 an 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 that 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.