DEV Community

Cover image for When Less is More: Serverless NAT Gateway - Part 1
Wednesday Solutions
Wednesday Solutions

Posted on

When Less is More: Serverless NAT Gateway - Part 1

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

  • It offers the ability to write code and deploy to the cloud without worrying about the infrastructure.
  • It enhances the economic sense of paying for what you use or execution-only billing.
  • 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, set up, 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:

Setup

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:

  3. serverless-webpack plugin for bundling the functions, the dependencies and more. Check out the webpack config here.

  4. serverless-offline plugin for testing locally.

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

  5. Create the AWS resources

  6. 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)}
Enter fullscreen mode Exit fullscreen mode

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/`
Enter fullscreen mode Exit fullscreen mode

Step 2 - Adding the VPC


vi resources/vpc.yml
Enter fullscreen mode Exit fullscreen mode

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:
  ServerlessVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: "10.0.0.0/16"
      Tags:
        - Key: 'Name'
          Value: 'ServerlessVPC'
Enter fullscreen mode Exit fullscreen mode


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
Enter fullscreen mode Exit fullscreen mode

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

 vi touch resources/internet-gateway.yml
Enter fullscreen mode Exit fullscreen mode
## Elastic IP
Resources:
  ElasticIpLambda:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
Enter fullscreen mode Exit fullscreen mode


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:

folder structure

Step 4 - Adding a NAT Gateway Resource and Subnets

vi touch resources/nat-gateway.yml
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
#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

Enter fullscreen mode Exit fullscreen mode

Now the route public file and resources

vi resources/route-public.yml
Enter fullscreen mode Exit fullscreen mode
#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
Enter fullscreen mode Exit fullscreen mode

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.

route table

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

Enter fullscreen mode Exit fullscreen mode
#vpc.yml
.
.
.
ServerlessVPCGA:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId:
        Ref: ServerlessVPC
      InternetGatewayId:
        Ref: SlsTutorialIGW
Enter fullscreen mode Exit fullscreen mode

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:

code

Step 7 - Adding an IAM resource

vi resources/iam.yml
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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.

lambda logs

Step - 8: Add a security group

We will add a security group for our architecture setup.

vi resources/security-groups.yml
Enter fullscreen mode Exit fullscreen mode
#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'
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
#functions.yml
slsNatTutorialFunction:
  handler: functions/tutorial-function/index.handler
  role: TestRoleForSLSNATGateway
  events:
    - http
        method: GET
        path: /say-hello
        cors: true

Enter fullscreen mode Exit fullscreen mode

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 touch functions/tutorial-function/index.js
Enter fullscreen mode Exit fullscreen mode
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);
    }
};
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Image description

You can see a GET method exposed by the server. To verify the API’s you could now just go to a 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' 

Enter fullscreen mode Exit fullscreen mode

The result should be something like this.

Image description

Nice right? Now let’s get this deployed to AWS so that the whole world can say hello to your API. 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!

Liked what you see? Found it helpful? Feel Free to share it.
We'd love to hear what you think, Tweet at us here.

‍Originally appeared on https://www.wednesday.is/writing-tutorials/when-less-is-more-serverless-nat-gateway-part-1

About the Author
Vishnu Prasad is a Software Engineer at Wednesday Solutions. If not thinking about creating great
experiences on the web he is probably re-watching episodes of the Office or listening to 90's Malayalam Music

Top comments (0)