Building Serverless Websites on AWS - Tutorial

JAN 31, 2016

Objective

This tutorial shows how to create a simple serverless application using the Serverless Framework 1.1. This tool supports code written in Node, Python and Java, but this tutorial uses only Node.js.

If you don't know yet what is this concept of Serverless and its benefits, you can read my previous blog post here.

The demo app is hosted at https://serverless-demo.zanon.io and the source code is available at https://github.com/zanon-io/aws-serverless-demo

This demo just shows a fake weather information when a button is clicked. To achieve this, we are using the following architecture:

lambda-web-apps

Note: this image was adapted from here. AWS likes to associate Lambda with DynamoDB, but I've used SimpleDB since that's the only serverless database offered by AWS. DynamoDB allows you to build more complex apps, but you need to provision capacity and you will pay for it even if no one is using your app. A true serverless app aims for infinite scalability, high availability and pay only for what you use, so DynamoDB lacks the last one.

Summary

This tutorial tries to cover everything to build a serverless site, including how to configure your domain and host your frontend code.

  1. Serverless Framework (Backend)
  2. SimpleDB (Database)
  3. Host your Website (DNS / S3)
  4. Frontend (Frontend)

Serverless Framework

The Serverless Framework is a tool that helps you to manage and deploy serverless projects that uses Lambda Functions, API Gateway and other AWS services. You can do everything manually using Amazon's console, but it'll be much harder to manage big projects.

The Serverless Framework is a Node.js module. So, install it using npm (or Yarn, if you prefer):

> npm install serverless -g

In this tutorial, I've used v1.1

Configuring

The Serverless Framework needs an AWS user account to manage your AWS resources. For production usage, this user must be configured with restricted access to just allow the services that will be used. However, if you are just learning, you can create an Admin account to get it up and running quickly.

To create a user, browse the IAM console and create a group first:

iam-group

Name it as serverless-group and attach the AdministratorAccess policy.

iam-group-admin

After that, create a new user named as serverless-admin, write down its Access Key ID and Secret Access Key, and add the user to the group that you have created.

iam-user

iam-user-group

As Serverless uses the AWS Node.js SDK, it looks for the user credentials in a credentials file located at ~/.aws/credentials on Mac/Linux or C:\Users\USERNAME\.aws\credentials on Windows.

Note: on Windows, using the file explorer to create a file named .aws returns an error because it starts with a dot. You need to create it with the name .aws. and the file explorer will rename it to .aws.

The file must have the following format: (replace with your access key and secret key)

[default]
aws_access_key_id = your_access_key
aws_secret_access_key = your_secret_key

Creating a Project

Create a folder for your project and execute:

> serverless create --template aws-nodejs --name my-serverless-demo

This command will create 3 files:

  • handler.js: the file where we will define our function.
  • event.json: it's just a file to help testing. The JSON data will be used as the input data for test invokes.
  • serverless.yml: it's our project configuration file.

handler.js

This file is our main function. That's where we'll write our Lambda code to accomplish a very specific task. In this tutorial, we'll create a service that will receive a locationId and read from a database the weather that matches this Id.

In the code below, we are just returning a hard-coded value as the temperature to simplify our first example. Additionally, we are returning the input as the locationId.

module.exports.currentTemperature = (event, context, callback) => {
  const response = {
    statusCode: 200,
    body: JSON.stringify({
      temperature: 30,
      locationId: event.id
    })
  };
  callback(null, response);
};

event.json

Below follows an example of the file event.json. It is useful for testing as it can be used to replace our event input parameter.

{
  "id": 5,
  "name": "Rio de Janeiro"
}

serverless.yml

This configuration file uses the YAML syntax that is very easy to use and readable. For our hello-world example, we can replace the file contents with the following:

service: my-serverless-demo
provider:
  name: aws
  runtime: nodejs4.3
functions:
  weather:
    handler: handler.currentTemperature

In this configuration, we have just one function, named as weather, that will be defined by a handler.js module that exports a currentTemperature function.

Deploying

Deploying is an easy task. Just run:

> serverless deploy

We have just deployed our function, but there is yet no public address to trigger it.

Invoking

We can test our Lambda function running the following:

> serverless invoke --function weather --path event.json

event.json is our input test file.

Endpoints

An endpoint is our RESTful interface implemented by the API Gateway which provides a public URL for our Lambda functions.

To create an endpoint, we need to add HTTP events to our serverless.yml file. See this example:

service: my-serverless-demo
provider:
  name: aws
  runtime: nodejs4.3
functions:
  weather:
    handler: handler.currentTemperature
    events:
      - http: GET weather/temperature

After modifying, deploy it again with serverless deploy.

Now our function is accessible from a public URL. In this test, I've got the following address: https://8w8ctjxkeh.execute-api.us-east-1.amazonaws.com/dev/weather/temperature

You can copy-paste your URL into your browser address to execute a GET request. You may receive the following JSON result:

{
  "temperature": 30
}

Did you miss the locationId property? As we are not passing parameters, event.id is undefined and the JSON object will omit the locationId property.

However, even if we use querystring parameters (adding ?id=5) to the end of the URL, we still won't see the locationId property. The reason is that the event object that the Lambda function receives is not exactly what we pass to API gateway. The querystring parameters will be available under the queryStringParameters property.

See the following Lambda function. In this example, it will be able to access the input variables and access the id property from a ?id=5 querystring.

module.exports.currentTemperature = (event, context, callback) => {
  const response = {
    statusCode: 200,
    body: JSON.stringify({
      temperature: 30,
      locationId: event.queryStringParameters.id
    })
  };
  callback(null, response);
};

Note: if you are using a POST/PUT/DELETE verb, parameters will be available under the body property and you need to parse it before using (example: JSON.parse(event.body)).

Setting Lambda limits

By default, your Lambda functions will be created with a reserved memory size of 1024 MB of RAM and a timeout setting of 6 seconds. You can change those values in the serverless.yml file.

functions:
  weather:
    handler: handler.currentTemperature
    events:
      - http: GET weather/temperature
    memorySize: 128
    timeout: 10

Serverless Architecture

Serverless fits well in a microservices architecture. In this architecture, each endpoint triggers just a single function that is responsible for a specific task. For example, if we added another function to retrieve the average temperature for a given interval, we would configure the serverless.yml as the following:

service: my-serverless-demo
provider:
  name: aws
  runtime: nodejs4.3
functions:
  weatherCurrent:
    handler: handler.currentTemperature
    events:
      - http: GET weather/current
  weatherAverage:
    handler: handler.averageTemperature
    events:
      - http: GET weather/average

In a Monolithic architecture, we would have just one big function that would handle all kind of requests.

service: my-serverless-demo
provider:
  name: aws
  runtime: nodejs4.3
functions:
  weather:
    handler: handler.temperature
    events:
      - http: GET weather/current
      - http: GET weather/average

You can read more about serverless architectures in this post.

Enable Cross-Origin Resource Sharing (CORS)

Since our API is deployed at the AWS domain and not inside our app domain, the browsers won't process AJAX requests due to security issues. To workaround this, you can use JSONP or enable CORS at Amazon's side. I prefer to enable CORS.

It's simple. You just need to set CORS headers inside your Lambda function.

module.exports.currentTemperature = (event, context, callback) => {
  const response = {
    statusCode: 200,
    headers: {
      'Access-Control-Allow-Origin': '*'
    },  
    body: JSON.stringify({
      temperature: 30,
      locationId: event.queryStringParameters || event.queryStringParameters.id
    })
  };
  callback(null, response);
};

SimpleDB

We have already configured and deployed our backend code. Let's make it more useful integrating it with SimpleDB, which is a serverless database.

Initialize your Data

Since there is no AWS Console for SimpleDB, you need to create your model using third-party tools or the AWS SDK. I like the SdbNavigator Chrome Extension. It's available here.

To connect, you need to set your AWS credentials (restrict access to SimpleDB only) and select a Region.

Click on "Add domain" and type "Weather" as the name. The domain is the equivalent of a table in the relational world.

sdb-domain

Click on "Add property" and add one property with the name "Value" and another with the name "ID".

sdb-properties

Now click on "Add record" to add a register with value 35 and ID 5.

sdb-record

As you may note, SimpleDB accepts only string fields which means that it is not suitable for complex data or aggregations.

Add permissions to your Lambda functions to read SimpleDB data

Lambda functions don't have access to AWS resources by default. You need to give explicitly access through iamRoleStatements in the serverless.yml file.

service: my-serverless-demo
provider:
  name: aws
  runtime: nodejs4.3
  region: us-east-1
  iamRoleStatements:
      - Effect: "Allow"
        Action:
          - 'sdb:Select'
        Resource: "arn:aws:sdb:${self:provider.region}:*:domain/Weather"
functions:
  weather:
    handler: handler.currentTemperature
    events:
      - http: GET weather/temperature
    memorySize: 128
    timeout: 10

Retrieve SimpleDB data using a Lambda function

As we are going to use the aws-sdk, it is already available for our Lambda functions without needing to install it locally. If you need to use another module, just npm-install and the Serverless Framework will zip the package with the node_modules.

Let's go back to our handle.js function and rewrite it to the following:

var AWS = require('aws-sdk');
// function that helps making queries
const queryData = (query, callback) => {
  const simpledb = new AWS.SimpleDB();
  simpledb.select({ SelectExpression: query }, callback);
};
// query temperature
const buildTemperatureQuery = (input) => {
  // sanitize the input confirming that it's a number
  const locationId = Number(input);
  return `select Value from Weather where ID = '${locationId}'`;
};
module.exports.currentTemperature = (event, context, callback) => {
  const param = event.queryStringParameters;
  const input = param ? param.id : 0;
  const query = buildTemperatureQuery(input);
  queryData(query, (err, resp) => {    
    const response = {
      statusCode: 200,
      headers: {
        'Access-Control-Allow-Origin': '*'
      },  
      body: JSON.stringify({
        temperature: resp.Items ? resp.Items[0].Attributes[0].Value : null,
        locationId: param ? param.id : undefined
      })
    };
    callback(err, response);
  });
};

Deploy and test the modified code.

Host your Website

Register a Domain

Buying a domain name is not required for this demo to work. If you want to go beyond testing to make something useful, the first step is to buy a domain with a nice name. As you can see, this is hard! The best ones are already taken.

Create Amazon S3 buckets

Amazon S3 buckets is the service where you will manually host your frontend code (HTML/CSS/JavaScript and images) and also where the Serverless Framework will automatically host (in a different bucket) the backend code.

If you've bought a domain named as example.com, you need to create a bucket with the same example.com name and configure it to enable website hosting.

s3-hosting

If you want to support old people that still like to type www for every site, you can also create a bucket with the name www.example.com. For that one, configure it to redirect all requests for the main address.

s3-www

Configure Amazon Route 53 Hosted Zone

After buying your domain, you need to change the default Name Servers. The Name Servers are a set of server addresses (more than one for redundancy) that helps the DNS queries to translate the domain name to the correspondent address of the machine that is hosting your site. Since we want to host our app on Amazon, we need to give them this control. So, the name server will be changed from your domain registrar to Amazon's addresses. The Amazon service that is responsible for this kind of control is the Route 53.

Go to Amazon Route 53 and click to create a Hosted Zone for your domain. After creating it, click at your hosted zone and write down the given Name Server (NS).

route53-nameservers

Create Amazon Route 53 Aliases

You need to create some aliases to map the incoming Amazon Route 53 requests to your S3 buckets. These aliases are created clicking to Create a Record Set.

You need to create a record set of the A-type for your example.com domain and CNAME-type for your www.example.com domain.

For the A-type record set, just select the S3 bucket within the available options.

route53-a-recordset

Regarding the CNAME-type, you need to provide an address. It should be the bucket endpoint address available at the bucket properties tab.

route53-cname-recordset

Changing the Name Servers

After preparing your Amazon Host, you need to give them the domain control accessing your registrar website and changing the name servers configuration.

I've bought my zanon.io domain at GoDaddy.com, so this tutorial uses their control panel to show how to configure your domain. However, since all domain sellers let you change your name server values, the control panels are different, but you need to make the same configuration.

1) Log into your domain registrar's control panel and click to manage your domains.

godaddy-control-panel

2) Click at your domain name settings and select the Manage DNS option.

godaddy-manage-dns

3) View your configured name servers options and edit them with the addresses that are provided by AWS.

godaddy-nameservers

Testing

Due to cache, you need to wait some hours until this name server changing takes effect. While you wait, create a simple web page in a file named as index.html and upload it to your example.com bucket. When the cache is refreshed, you can see your webpage up and running typing your domain name.

Frontend

For our demo, we'll create just one file, named as index.html, with a simple button to retrieve the weather info. Nothing fancy. For a real-world website, you would upload all HTML/CSS/JavaScript and images to your S3 bucket.

index.html contents:

<div class="container">
  <div class="row">
    <div class="col-md-offset-5 col-md-2 text-center">
      <h3>Daily Weather</h3>
      <input id="btn-show" type="button" class="btn btn-primary" value='Show Current'>
    </div>
  </div>
  <div class="row">
    <div class="col-md-offset-5 col-md-2">
      <p>Value: <span id="weather-value"></span></p>
      <a href="https://zanon.io/posts/building-serverless-websites-on-aws-tutorial"><p>source</p></a>
    </div>
  </div>
</div>

JavaScript code:

$(document).ready(function() {
  $('#btn-show').on('click', function() {
      $.ajax({
        url: "https://8w8ctjxkeh.execute-api.us-east-1.amazonaws.com/dev/weather/temperature?id=5",
        success: function(json) {
          $("#weather-value").text(json.temperature + ' ÂșC').fadeOut('slow').fadeIn('slow');
        }
      });
  });
});

Our Ajax call is using https://8w8ctjxkeh.execute-api.us-east-1.amazonaws.com/dev/weather/temperature?id=5 as the URL parameter. This is the address of our Lambda function that we have deployed early.

Result:

demo

And... that's it! The app is working at https://serverless-demo.zanon.io, where the HTML/CSS/JS is hosted on S3, the backend code is a service that runs only on demand with AWS Lambda and the data is stored in SimpleDB, which generate costs only while processing queries.

UPDATE: Aug 06, 2016

Updated the tutorial due to changes on the Serverless Framework (v0.2 => v0.5)

UPDATE: Nov 04, 2016

Updating the Serverless Framework code (v0.5 => v1.1)

UPDATE: Mar 25, 2017

If you want to know more, I've found a nice website that may help you to go deeper into this subject: http://serverless-stack.com. It offers a detailed guide and a nice demo using Lambda, API Gateway, Cognito and DynamoDB.