When Less is More: Serverless NAT Gateway - Part 1

Vishnu Prasad
January 13, 2022
Contents

Serverless architecture offers developers a variety of advantagesthat prove attractive in the development of large, scalable applications. Let's go over 3 top benefits:

  1. It offers the ability to write code and deploy to the cloud without worrying about the infrastructure.
  2. It enhances the economic sense of paying for what you use or execution-only billing.
  3. The ability to write an application in a language/framework of your choosing with a fast turnaround to production-ready setup.

Integration of third-party services is an inevitable part of the development lifecycle. If you do work with security-conscious third-party services then a common requirement that arises is whitelisting of an IP to avail these services.

In this two-part tutorial series, we will walk through the creation of an AWS lambda function with some additional AWS resources that will allow you to provide third-party services with a static IP for whitelisted communication.

Let’s begin with Part 1 of this tutorial where you will:

  • Use the serverless framework with webpack to create a serverless application and all the necessary AWS resources that go with it.
  • Integrate a NAT Gateway with Elastic IP for static IP.

In the next part (i.e. Part 2 ) of this series, you will,

  • Use GitHub Actions as a CD pipeline to verify and deploy to AWS.

The Architecture

This tutorial assumes you have an expert level understanding of the following AWS services:

We will also be using the serverless framework to create, setup, test locally and deploy the application. The serverless framework is a great tool to get started with serverless architecture and systems.

Please visit the link https://www.serverless.com/framework/docs to learn more.

Our setup will look like this:

Lambda function

In this tutorial, we will help you get through the deployment of a Lambda function with the proper connections to have an elastic IP associated.

Let's start building

Starter Project

Just a quick intro to what all we have. The most important part of the setup is the serverless.yml file. In it you will find:

1. Service name: Currently, it reads from the env file. Feel free to use one of your choosing.

2. Plugins:

We’ll break this part of the tutorial into two sections

  • Create the AWS resources
  • Addition of Lambda functions

Creating the AWS Resources

Step 1 - Update the serverless file

Add the following lines below the last lines of the serverless.yml file:

 
...
functions: ${file(./resources/functions.yml)}
resources:
  - ${file(./resources/iam.yml)}
  - ${file(./resources/vpc.yml)}
  - ${file(./resources/security-groups.yml)}
  - ${file(./resources/internet-gateway.yml)}
  - ${file(./resources/elastic-ip.yml)}
  - ${file(./resources/nat-gateway.yml)}
  - ${file(./resources/route-private.yml)}
  - ${file(./resources/route-public.yml)}

Here we are pointing to the functions (lambdas) and the resources (AWS infrastructure) that we will need to setup all this. We will add these files along the way. Exciting much?

 
YAML syntax maybe problematic for some people.
Please take the help of lint plugin or a service 
like http://www.yamllint.com/
Step 2 - Adding the VPC
 
vi resources/vpc.yml

Let's add the resources.

  • First, create a vpc.yml file in the resources folder. This is where you will create an AWS vpc resource.
  • Copy and paste the following code into the vpc.yml.
  • Don’t forget to check the indentation and change the names, tags as you want.

Resources:

 
Resources:
  ServerlessVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: "10.0.0.0/16"
      Tags:
        - Key: 'Name'
          Value: 'ServerlessVPC'

Pretty basic stuff, right? We have a resource type and a CIDR block (a range of IP addresses).

We will need to come back to this file in a bit. Let's move on.

Step 3 - Adding the Elastic IP and Internet Gateway

We will create two files called internet-gateway.yml and elastic-ip.yml in the resources folder.

Add the below resources to the files as mentioned in the elastic-ip.yml

 
vi resources/elastic-ip.yml
 
## Elastic IP
Resources:
  ElasticIpLambda:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

Now let's create the internet-gateway.yml file.

 
vi resources/internet-gateway.yml
 
## Internet GW
Resources:
  SlsTutorialIGW:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: SlsTutorialIGW

We created two more resources. An Internet Gateway that allows us to connect to the outside internet from AWS VPC and an Elastic IP is the pubic static IP that will be given to third parties as our service IP address. The domain is a field that indicates whether the Elastic IP address is for use with instances in a VPC (which it is!).

At this point your folder structure will look like this:

Step 4 - Adding a NAT Gateway Resource and Subnets
 
vi resources/nat-gateway.yml

Create a nat-gateway.yml file in the resources. Add the following resources.

 
#nat-gateway.yml
Resources:
  ServerlessNatGateway:
    Type: AWS::EC2::NatGateway
    Properties: 
      AllocationId:
        Fn::GetAtt:
         - ElasticIpLambda
         - AllocationId
      SubnetId:
        Ref: ServerlessPublicSubnet1
  ServerlessPublicSubnet1: 
    DependsOn: 
      - ServerlessVPC
    Type: AWS::EC2::Subnet
    Properties:
      VpcId:
        Ref: ServerlessVPC
      CidrBlock: '10.0.2.0/24'
      AvailabilityZone: ${self:provider.region}a
      Tags:
        - Key: Name
          Value: ServerlessPublicSubnet1
  ServerlessPrivateSubnet1:
    DependsOn: 
      - ServerlessVPC
    Type: AWS::EC2::Subnet
    Properties:
      VpcId:
        Ref: ServerlessVPC
      CidrBlock: '10.0.1.0/24'
      AvailabilityZone: ${self:provider.region}a
      Tags:
        - Key: Name
          Value: ServerlessPrivateSubnet1

The NAT gateway is the star of the show.

NAT is a service that allows instances inside your vpc to connect to outside resources or systems but external connections to internal vpc systems are prohibited. AllocationId is a function that gets the AllocationId of the Elastic IP resource we created.

The Nat has a public subnet that it connects to. Look at the figure for the architecture.

The other resources are subnets. A private one that connects to resources in the vpc. A public one that will reroute and connect to Internet Gateway. Read more about the subnet here.

Step 5 - Route Tables

We will have two route tables as part of this setup.

One for the private subnet and another for the public subnet. Create two files route-private.yml and route-public.yml and add the following resources correctly. Let's work on the route-private first

 
vi resources/route-private.yml
 
#route-private.yml
Resources:
  DefaultPrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: ServerlessVPC
      Tags:
        - Key: Name
          Value: DefaultPrivateRouteTable
  DefaultPrivateRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId:
        Ref: DefaultPrivateRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId:
        Ref: ServerlessNatGateway
  SubnetRouteTableLambdaAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: DefaultPrivateRouteTable
      SubnetId:
        Ref: ServerlessPrivateSubnet1

Now the route public file and resources

 
vi  resources/route-public.yml
 
#route-public.yml
Resources:
  DefaultPublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId:
        Ref: ServerlessVPC
      Tags:
        - Key: Name
          Value: DefaultPublicRouteTable
  DefaultPublicRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId:
        Ref: DefaultPublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: 
        Ref: SlsTutorialIGW
  IGWRouteTableLambdaAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId:
        Ref: DefaultPublicRouteTable
      SubnetId:
        Ref: ServerlessPublicSubnet1

A route table is a set of rules that establishes where network traffic is directed. It's can be associated with a subnet. It has a destination and target gateway. For the private route table, we add a route table rule that routes all traffic through the NAT Gateway. We also create an association between the route table and our private subnet.

This is how a route table looks after creation. Don't worry we will reach there.

The public route table also follows mostly the same pattern. The only difference is that its association is with the IG we created in Step 2.

Step 6 - VPC Gateway attachment

Now let's get back to the vpc resource down the line. It's that time. Go back to the vpc.yml file and add the following lines

 
vi resources/vpc.yml
 
#vpc.yml
.
.
.
ServerlessVPCGA:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId:
        Ref: ServerlessVPC
      InternetGatewayId:
        Ref: SlsTutorialIGW

This attaches the Internet Gateway to the vpc enabling communication with the internet through the vpc.

Remember this is a new resource within the vpc file. I know some people don’t like code pictures, but I wanted to try carbon. So vpc.yml will look like this:

Step 7 - Adding an IAM resource
 
vi resources/iam.yml
 
  Resources:
  TestRoleForSLSNATGateway:
    Type: AWS::IAM::Role
    Properties:
      Description: This is an example role for SLS NAT Gateway
      RoleName: ${self:service.name}-nat-gateway-role
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
          - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole

We are adding an IAM role that will allow us to access the lambda and the associated lambda logs in CloudWatch. Now that we have all the resources. All the file structures should look like this.

Step - 8: Add a security group

We will add a security group for our architecture setup.

 
vi resources/security-groups.yml
 
#security-groups.yml
Resources:
  ServerlessSecurityGroup:
    DependsOn:
      - ServerlessVPC
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: SecurityGroup for Serverless Functions
      VpcId:
        Ref: ServerlessVPC
      Tags:
        - Key: 'Name'
          Value: 'sls-tutorial-sg'

We add a security group and add it to the VPC we created using the VpcId key in the template. This is important to control traffic to and from the subnets. By default, it has a set of rules associated with the traffic that flows through the group.

Adding the functions

Step 1: Add a function resource

We will add a function or a lambda in this step. Create a file called functions.yml in the resources folders and add the following code to it. This just points to a function that we will add, nothing fancy.

 
vi  resources/functions.yml
 	
#functions.yml
slsNatTutorialFunction:
  handler: functions/tutorial-function/index.handler
  role: TestRoleForSLSNATGateway
  events:
    - http
        method: GET
        path: /say-hello
        cors: true

We have the name of the function and the handler it points to.

Step 2 Add the function

Inside the functions folder, create a folder called tutorial-function and an index.js. Add the following function to the handler

 	
mkdir -p functions/tutorial-function
vi  functions/tutorial-function/index.js
 	
import { apiSuccess, apiFailure } from '@utils';
import axios from 'axios';

exports.handler = async (event, context, callback) => {
	console.log(JSON.stringify(event));
	try {
		const response = await axios.get('https://httpbin.org/ip');
		const data = response.data;
		console.log(data);
		return apiSuccess(callback, data);
	} catch (error) {
		return apiFailure(callback, error);
	}
};

This function is very basic with the idea being to just hit an outside service that returns the IP of the server from which the request was made. This will help us see that we have assigned a NAT Gateway Elastic IP to the lambda.

Step 3 - Attaching the resources to the function

This is where it all comes together. We have created a lot of resources, we need to piece it all together so that the lambda we created has these resources attached. We do that in the serverless.yml file.

 	
vi serverless.yml
 	
.
.
.
versionFunctions: false
vpc:
    securityGroupIds:
      - Fn::GetAtt:
          - ServerlessSecurityGroup
          - GroupId
    subnetIds:
      - Ref: ServerlessPrivateSubnet1

We should add the lines starting from the vpc to the file. Make sure you get the indent correct. We are attaching our Lambda functions with the vpc, security group, and the private subnet. Remember the Lambda rests in the private subnet.

Step 4 - Testing this locally

Now as part of this set up, we do have a very interesting way to test our functions locally. We have added a plugin called serverless-offline to get this started up locally rather easily.

To get started, go to your working directory with your setup and run the following command.

 	
yarn start-offline


This should start up a server using a webpack, that exposes the following API’s.

You can see a GET method exposed by the server. To verify the API’s you could now just go to an API testing resource like postman and try to hit this endpoint.

 	
#here is a cURL for you to copy paste.
curl --location --request GET 
'http://localhost:3000/local/say-hello' 

The result should be something like this.

Nice right? Now let’s get this deployed to AWS so that the whole world can say hello to your AP That was bad. Onwards we go. 🚀

Where to go from here?

This got a bit long, didn’t it ? If at any point you get stuck or want to refer to the resources, please feel free to refer to the finished setup available here

We have certainly made great strides in creating all these resources and linking all of them. If you reached all the way to the end here, great job!

Let’s wrap this up and deploy all the resources we created using GitHub Actions in Part 2 of this tutorial series. Will see you there!

We hope you enjoyed reading this article as much as we enjoyed piecing it together! While we work on Part 2, please feel free to join the conversation over on our Twitter.

More Tutorials

Schedule a meeting with Wednesday. Tell us about your product development journey and let's figure out a way to work together.
Talk to us
The Wednesday Show - A show about the design, development, and business of digital products. Every week the founders of Wednesday talk about their learnings of building digital products.
Psst! Listen to our podcast The Wednesday Show here
Close Icon