Serverless for Web Developers

Robert Sebescen
symphonyis

--

Why should you read this?

If you are a web developer who wants to achieve faster time to market for new products and features, cut (almost eliminate) time to provision and manage infrastructure, minimize downtime and security risks, along with costs — this is the place for you. This article will teach you how easy it is to make a simple serverless app that is scalable and highly available.

We will cover this topic using Serverless Framework for managing AWS services with a small amount of coding since you already know how to do that. Previous AWS experience or knowledge isn’t required for understanding this article.

What is Serverless?

Serverless characteristics:

Serverless is generally defined as an execution model in cloud computing where a cloud provider allocates and bills machine resources on demand. This means that you do not care about the underlying infrastructure, have flexible scaling and high availability while focusing on solving the problem at hand. You only care about the what, not the how.

The Serverless Framework allows you to manage resources with ease and without any previous knowledge of the cloud platform (in our case, AWS).

How is web development different when using Serverless?

When we develop an API, we usually choose:

  • Programming language
  • Web framework
  • Database

When developing an API as a Serverless we choose:

  • Programming language
  • Cloud platform
  • Database

The list of services is usually more comprehensive in real-world scenarios (Message broker, email service, etc.), but for the sake of simplicity, we will stick with the aforementioned services. As you can see, cloud platforms can be considered a development framework, where you use cloud services as building blocks of your application. While having a vendor-locking as a potential downside, you substantially benefit from the well-connected ecosystem. You are not “reinventing the wheel”, but focusing on your business case.

Lambda function that handles REST request

AWS Lambda is what it ultimately promises — A LAMBDA, a.k.a., a function.

Prerequisites

Lambda in a nutshell

Let’s write some “code”

You can find an example code on GitHub to follow through, or write the code along the way.

Create folder structure

Update file ‘handler.js’ with the following code

module.exports.hello = async () => {
return {
statusCode: 200,
body: 'Hello there!'
};
};

Code part — DONE.

Update file ‘serverless.yaml’ with the following configuration

service: myapp
frameworkVersion: '2'
provider:
name: aws
runtime: nodejs12.x
functions:
hello:
handler: handler.hello

Configuration part — DONE.

  • service: the name of the service we are writing (it can contain multiple Lambda functions).
  • provider: the provider-specific configuration
  • functions: all the functions

In this example, the “hello” function handler is in “handler.hello” which means a file named “handler.js” and “hello” being the name of the exported property.

We can now execute sls deploy and have our first Lambda function created.

Right now, our Serverless environment looks like this:

At this point, we still haven’t put the Lambda function we created into use. What Serverless framework did in the background:

  • Create myapp Stack — (Stack = collection of AWS resources)
  • Create IAM Role — Lambda runs under a specific execution role and communicates with the rest of the AWS services as an assigned execution role.
  • Create hello Lambda; By default, Lambda will create an execution IAM role with permissions to upload logs to Amazon CloudWatch Logs.

How can we use our Lambda function?

Since we want to create an HTTP endpoint, we need to subscribe our hello Lambda function to an HTTP event.

Create an HTTP trigger for our Lambda function

functions:
hello:
handler: handler.hello
events:
- http:
path: /hello
method: get

Again: sls deploy

What did the “sls” do for us now:

  • Create an API Gateway resource
  • Expose HTTP API endpoint and invoke hello Lambda function on GET /hello request.
  • Adds permission to invoke our Lambda function

Create a database and endpoints to manipulate the data

Every web application works with some data. We will create the following endpoints:

  • POST /tasks — create a new task
  • GET /tasks/:id — get created tasks

When creating a database, we need to consider what kind of database we need. Next, since the Lambda function will consume the database, we should consider the risk of losing the Serverless characteristics due to the database being a bottleneck.

Our example does not require a relational database, so we choose DynamoDB.

Update configuration to create DynamoDB. Details of the DynamoDB configuration are a separate topic, and we will not cover it in this post. In short, the table will have an “id” that is a string as the primary key. You can find details about workload (provisioned throughput) here.

resources:
Resources:
TasksTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: tasksTable
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1

Lambda function (its underlying role) must have appropriate permissions for the DynamoDB table (no username/password/connection string in the configuration, just specific permissions) in order to use the DynamoDB table. When enabling permissions, try to be as conservative as possible. We do not want a role to have particular permission if it does not need it.

We will now add permissions for database access:

...
provider:
...
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
Resource: !GetAtt TasksTable.Arn
...

!GetAtt logicalNameOfResource.attributeName is used to pull the “arn” (resource identifier) of the DynamoDB table we created.

Now we will create two lambdas to create and fetch records from the database.

module.exports.createTask = async (event) => {
// the most elegant ID generator
const id = new Date().getTime().toString();

var params = {
TableName: "tasksTable",
Item: {
id,
data: JSON.parse(event.body),
},
};
await documentClient.put(params).promise();
return {
statusCode: 201,
headers: {
Location: `/tasks/${id}`,
},
};
};

module.exports.getTaskById = async (event) => {
const id = event.pathParameters.id;
var params = {
TableName: "tasksTable",
Key: {
id,
},
};

const item = await documentClient.get(params).promise();
return {
statusCode: 200,
body: JSON.stringify(item.Item.data),
};
};

We also need a configuration to make this code a lambda function.

...
functions:
...
createTask:
handler: handler.createTask
events:
- http:
path: /tasks
method: post
getTaskById:
handler: handler.getTaskById
events:
- http:
path: /tasks/{id}
method: get
...

Aaaaand we sls deploy once again.

Now sls created:

  • DynamoDB table
  • createTask and getTaskById Lambdas
  • Permissions to write logs into the CloudWatch — implicit
  • Permissions to Get/Put items in DynamoDB table — explicit
  • Updated API Gateway with new handlers
  • Permissions for API Gateway to invoke new Lambdas

We can start creating records in the database and pulling those records using record ID.

And that’s it! With zero effort in designing the infrastructure (VMs, networks, load balancers, etc.) we now have a scalable and highly available application that is billed per usage — this is the true power of serverless.

--

--

Robert Sebescen
symphonyis

Curious problem solver. Engineer who loves to learn and share knowledge. Passionate about software architecture, coding and a casual game of football.