by Charlee Li

How to create a serverless service in 15 minutes

The word “serverless” has been popular for quite a while. When Amazon released the AWS Lambda service in 2015, many tools emerged to help people build serverless services with just a few commands. Compared to traditional always-on services, serverless services are very easy to develop, deploy and maintain. They’re also extremely cost effective, especially for those simple services which do not have too much traffic.

So what is serverless?

As its name implies, “serverless” means that you run a service without a server. Well, technically, there is still a server running the service, but you, as the service owner, do not need to worry about that server.

For example, AWS Lambda allows you to deploy a “function” to deal with the requests. AWS has a server to run all the functions when they are requested. But you don’t need to worry about how this server works or how to make your code work with this server. All you need to know is that you write a function then push it to the Lambda service.

And the sweetest part is that it’s very cheap. Amazon Lambda provides 1M free requests and 400,000 GB-seconds free compute time per month (which means your computation can use 1GB memory for 400,000 seconds), which is enough for most small services. Compared to EC2, where a nano instance would cost you $0.0058 per Hour (which is $0.14 per day), Lambda is way cheaper.

What we’ll do here

In this post, I will show you how to build a small serverless personal website using AWS. The website has the following characteristics:

  • Includes both front-end and back-end
  • Mostly static, or front-end heavy
  • API requests — these are rare but necessary
  • Back-end does not require too much memory or CPU (for example, a simple web counter that only requires one DB access)

Our service will be deployed at the following domains (I used fake domains in this post):

A serverless solution is perfect both technically and from a cost point of view. We will use the following AWS services:

  • Lambda + API Gateway + S3, for API server
  • DynamoDB, for data storage
  • S3, for static web hosting
  • Cloudfront, for distributed CDN
  • AWS Certificate Manager (ACM), for generating certificates for our https website

For the API server, we will use a Python + Flask combination, and Zappa as the serverless toolkit.

Setting up the AWS Environment

First we need to set up the AWS environment so that we can access AWS from our code and zappa. This takes two steps:

  1. We need to create an AWS user for programmatic access
  2. We need to set up a local AWS environment to use for that user

Create an AWS User

Log into AWS, and choose the “IAM” service to manage user credentials.

Create a user called “myservice-admin”(or any other username you would like to use), and don’t forget to check the “Programmatic access” option.

In the next screen, click the “Attach existing policies directly” button, then add “AdministratorAccess” to the user.

Note: From a security perspective this is not the best practice. But for demonstration purposes, this post will not cover the details of narrowing down permissions.

Click on the “next” button and then the “Create User” button, and the user myservice-admin will be created. On the last screen, the Access Key ID and Secret access key are displayed. Make sure to copy and paste them into a local file. These are the API credentials we are going to use in the next step.

Note: This is the only place you can view the secret access keys! If you fail to make a copy of them, you have to go to the user detail screen and generate a new pair of access keys and secret.

Setup your Local AWS Environment

We need to create a local environment in order to use AWS locally.

First, let’s install the awscli tool, which will help us configure the environment:

$ sudo apt install awscli

After installation, we will setup AWS by using the aws configurecommand:

$ aws configureAWS Access Key ID [None]: ******AWS Secret Access Key [None]: ******Default region name [None]: us-east-1Default output format [None]: json

Here we need to type in the Access Key ID and Secret Access Key we received from the last step. In terms of default region, I used us-east-1. You can choose any region you like, but other regions may cause some trouble when setting up CloudFront.

Create a Table in DynamoDB

In order to store the website visitor counter value in DynamoDB, we need a persistent store. So we need to create a table and populate an initial value within it.

Within the AWS console, choose DynamoDB service. Then click the “Create Table” button. In the “Create DynamoDB table” screen, fill the Table name with myservice-dev and the Primary key field with id, then click the Create Table button.

After a couple of seconds, the table should be created. Select the newly created table, choose the Items tab from the right pane, then click the Create item button, and create an item with id='counter' and counter_value=0.

Note: You need to click the plus sign on the left side to add the counter_value attribute, and don’t forget to set the type of counter_value to Number.

Create an API Service

Next, we’ll create the API service. For demonstration purposes, this API service will provide a counter API which will increase a counter value when clicked. The counter value will be stored in DynamoDB. The API endpoints are:

  • POST /counter/increase increases the counter and returns the counter value
  • GET /counter returns the current counter value

Coding the API Service with Python and Flask

We will start with creating a Python virtual environment and install the necessary packages:

$ mkdir myservice && cd myservice$ python3 -m venv .env$ source .env/bin/activate(.env)$ pip install flask boto3 simplejson

flask is the web framework and the boto3 package is required for accessing DynamoDB. simplejson can help us deal with some JSON conversion issues. Let’s create the service by creating a file myservice.py with the content below:

import boto3from flask import Flask, jsonify
app = Flask(__name__)
# Initialize dynamodb accessdynamodb = boto3.resource('dynamodb')db = dynamodb.Table('myservice-dev')
@app.route('/counter', methods=['GET'])def counter_get():  res = db.get_item(Key={'id': 'counter'})  return jsonify({'counter': res['Item']['counter_value']})
@app.route('/counter/increase', methods=['POST'])def counter_increase():  res = db.get_item(Key={'id': 'counter'})  value = res['Item']['counter_value'] + 1  res = db.update_item(    Key={'id': 'counter'},    UpdateExpression='set counter_value=:value',    ExpressionAttributeValues={':value': value},  )  return jsonify({'counter': value})

Create a run.py file to test this API service locally:

from myservice import appif __name__ == '__main__':  app.run(debug=True, host='127.0.0.1', port=8000)

Now run the service:

(.env)$ python run.py

And we can test this service with the following commands (open another terminal to type these commands):

$ curl localhost:8000/counter{  "counter": 0}$ curl -X POST localhost:8000/counter/increase{  "counter": 1}$ curl -X POST localhost:8000/counter/increase{  "counter": 2}$ curl localhost:8000/counter{  "counter": 2}

We can see that our code is working and it is successfully increasing the counter!

Deploying our code to Lambda with Zappa

Deploying our API to Lambda is extremely easy with zappa. First, we need to install zappa:

(.env)$ pip install zappa

Then initialize the zappa environment with zappa init. It will ask you some questions, but generally you can use default answers for all the questions:

(.env)$ zappa init...What do you want to call this environment (default 'dev'): ...What do you want to call your bucket? (default 'zappa-ab7dd70x5'):
It looks like this is a Flask application.What's the modular path to your app's function?This will likely be something like 'your_module.app'.We discovered: myservice.appWhere is your app's function? (default 'myservice.app'): ...
Would you like to deploy this application globally? (default 'n') [y/n/(p)rimary]:
Okay, here's your zappa_settings.json:
{    "dev": {        "app_function": "myservice.app",        "aws_region": "us-east-1",        "profile_name": "default",        "project_name": "myservice",        "runtime": "python3.6",        "s3_bucket": "zappa-ab7dd70x5"    }}
Does this look okay? (default 'y') [y/n]: ...

After initialization, we can see the generated zappa_settings.json file. Then we can start to deploy our service:

(.env)$ zappa deploy devCalling deploy for stage dev.....Deployment complete!: https://2ks1n5nrxh.execute-api.us-east-1.amazonaws.com/dev

Great! Our service is online. You can test this service with curl as well:

(.env)$ curl https://2ks1n5nrxh.execute-api.us-east-1.amazonaws.com/dev/counter{"counter":2}(.env)$ curl -X POST https://2ks1n5nrxh.execute-api.us-east-1.amazonaws.com/dev/counter/increase{"counter":3}(.env)$ curl https://2ks1n5nrxh.execute-api.us-east-1.amazonaws.com/dev/counter{"counter":3}

Setup a Custom Domain for API Service

However, there is one issue with the API service. The auto generated API endpoint 2ks1n5nrxh.execute-api.us-east-1.amazonaws.com is very difficult to read or use for human consumption. Fortunately, we can bind a custom domain name to this API endpoint.

We will use the custom domain https://myservice-api.example.com for this API service. Since we want to serve it with https, we need to get a certificate first. AWS provides a free certificate with the “Certificate Manager” service, and it is very easy to use.

After the certificate is generated, we can use it to setup a custom domain for our service in the AWS API Gateway service.

Apply for a Certificate

Switch to ACM service in the AWS management console (the service is actually called Certificate Manager, but you can type “ACM” to search for it). Click Request a certificate button, then choose Request a public certificate option in the next screen. The certificate is free as long as you choose a public certificate here.

In the next screen, enter the domain name you want to apply the certificate for, then click Next. Here I applied for *.example.com which means the certificate can be used by all the sub-domains under example.com. In this way, we can use the same certificate for our front end at myfrontend.example.com without having to apply for a new one.

In the next step, we need to prove that we own this domain name. Since I applied for this domain name from Google Domains, I will choose DNS validation. Click the Review button then click Confirm and Request.

The certificate request will be created, and a validation screen will be displayed. The instructions show how to validate this domain name:

Based on the instructions, we need to add a CNAME record and assign it the given value. In my case, I will open Google Domains, find my domain name example.com, and add the specified CNAME record:

Note: I only added the random string _2adee19a0967c7dd5014b81110387d11 in the Name field, without typing the .example.com part. This is to avoid the suffix .example.com part getting duplicated.

Now, we need to wait for about 10 minutes until AWS Certificate Manager validates this domain name. Once validated, the “Status” column in the certificate will display “Issued” in green.

Now that we have the certificate ready, we can start binding our custom domain name to our API.

Setuping up a Custom Domain for our API Service

Go to the “API Gateway” service. From the “APIs” in the left pane, we can see that our API myservice-dev has already been created by zappa.

Click on “Custom Domain Names” from the left pane, then click the Create Custom Domain Name button on the right pane and fill in the necessary fields.

Here I want my API service to be exposed via CloudFront so that it can be accessed with optimal speed all around the world. So I chose Edge Optimized in this configuration. You can choose Regional if you don’t need CloudFront.

Click the “Add mapping” link below, then select our API myservice-dev as the Destination, and choose dev for the right most box. In this way, our API will not expose the environment name dev in the URL. Leave the Path field empty.

After clicking the Save button, our custom domain binding will be created. The actual domain binding requires up to 40 minutes to initialize, but we can configure the DNS settings now.

From the above screenshot, we can see that the actual domain name is dgt9opldriaup.cloudfront.net. We need to setup a CNAME in our DNS, pointing myservice-api.example.com to the CloudFront subdomain dgt9opldriaup.cloudfront.net.

Go to Google Domains and add the CNAME to the DNS settings:

After this step, wait for about 40 minutes until the “Initializing…” in the API Gateway service disappears.

Now try our new API service!

(.env)$ curl https://myservice-api.example.com/counter{"counter":3}(.env)$ curl -X POST https://myservice-api.example.com/counter/increase{"counter":4}(.env)$ curl https://myservice-api.example.com/counter{"counter":4}

Static Website for Front end

For the next task, we will be creating a front end for our brand new API service. For demonstration purposes, we will create a simple page with a button that triggers the /counter/increase API call.

Coding the front end

Let’s create a new directory called myfrontend:

$ mkdir myfrontend && cd myfrontend

Then make a simple HTML file index.html:

<html><body>  <h1>Welcome to my homepage!</h1>  <p>Counter: <span id="counter"></span></p>  <button id="increase">Increase Counter</button>  <script>    const setCounter = (counter_value) => {      document.querySelector('#counter').innerHTML = counter_value;    };
    const api = 'https://myservice-api.example.com';    fetch(api + '/counter')      .then(res => res.json())      .then(result => setCounter(result.counter));
document.querySelector('#increase')      .addEventListener('click', () => {        fetch(api + '/counter/increase', { method: 'POST' })          .then(res => res.json())          .then(result => setCounter(result.counter));        }      );  </script></body></html>

Publish the Front end to AWS S3

To create a static web site with S3, we need to create a bucket with the same name as our domain name.

Note: If you’ve been following along with this tutorial, the bucket name myfrontend.example.com may not be available, as bucket names are globally unique. Also, you’ll need to create a bucket name based on your public domain. For example,myfrontend.[yourdomain].com

Switch to the S3 service in the AWS management console. Since we want to host the static website on myfrontend.example.com, we will create a bucket with that name. Click the Create Bucket button, and fill in the bucket name, then keep clicking Next until the bucket is created.

Next, we need to enable static web hosting from this bucket. Open this bucket, then choose the Properties tab, and choose Static Web Hosting. In the dialog, select Use this bucket to host a website, then type index.html in the “Index document” field. Click Save when finished.

Note: The “Endpoint” link shown in the dialog above. We will test our static website with this URL later.

The last thing we need to do is to enable public access on the bucket. This can be done by adding a bucket policy. Open this bucket and choose the Permissions tab, then click the Bucket Policy button.

Type in the following content as the policy, then click the Save button (don’t forget to replace myservice.example.com with your domain name).

{    "Version": "2012-10-17",    "Statement": [        {            "Sid": "PublicReadGetObject",            "Effect": "Allow",            "Principal": "*",            "Action": "s3:GetObject",            "Resource": "arn:aws:s3:::myfrontend.example.com/*"        }    ]}

After saving, we should be able to see an orange “public” sign on the Bucket Policy button and the Permissions tab, which indicates that our bucket is publicly accessible.

Now the bucket is created but it is still empty. We need to upload our front end code files to this bucket. Make sure we are in the newly created myfrontend directory and type the following command:

# Make sure you are in the `myfrontend` directory...$ aws s3 sync . s3://myfrontend.example.com

The above command copies all files fromt he current . directory to S3.

All done! Now we can test this static web site with the URL displayed earlier. Open that URL with any browser (in my case, http://myfrontend.example.com.s3-website-us-east-1.amazonaws.com/) and see the result!

Oops! The counter is not displayed at all. ?

And it looks like we got some JavaScript error. We can see the following error in the console:

Failed to load https://myservice-api.example.com/counter: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://myfrontend.example.com.s3-website-us-east-1.amazonaws.com' is therefore not allowed access. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

Apparently we need to set the CORS header in order to make this script work, since the backend API is located on another domain. But since we’re going to set up a custom domain for the frontend, the URL will change, so we will worry about CORS later.

Setting up CloudFront for our Static Web site

The last step is to setup CloudFront for our front end. Since we have already created a certificate for *.example.com, this step will be very easy.

Switch to CloudFront service in the AWS management console. Click Create Distribution button, then click the Start button in the “Web” section.

In the “Create Distribution” screen, we need to make five changes:

  • Click the Origin domain name input box and select our S3 bucket myfrontend.example.com.s3.amazonaws.com.
  • Then change the Viewer Protocol Policy to “Redirect HTTP to HTTPS” in order to force https access.
  • In the Alternate Domain Names box, type in our custom domain. In this case we type in myfrontend.example.com.
  • Scroll down to the SSL Certificate section, choose “Custom SSL Certificate”, then select our *.example.com certificate.
  • Change Default Root Object to index.html.

After the distribution is created, we can see the CloudFront domain in the distribution list.

Although the status is still “In Progress”, we can setup our DNS record now. Go to Google Domains and add a CNAME for this domain:

Then wait until the distribution status change to “Deployed”. Now open your browser and try to access myfrontend.example.com. We can see the exact same static web site!

Fix the CORS issue

Now the only issue left is CORS. Since we are using a different domain name on the backend and frontend, we need to add CORS support.

Cross-origin resource sharing (CORS) is a mechanism that allows restricted resources (e.g. fonts) on a web page to be requested from another domain outside the domain from which the first resource was served. — Wikipedia

Go back to our API directory (myservice) and activate the Python environment. Then install the flask_cors package.

$ cd myservice$ source .env/bin/activate(.env)$ pip install flask_cors

Then edit myservice.py and add the following lines (in bold):

import boto3from flask import Flask, jsonifyfrom flask_cors import CORS
app = Flask(__name__)CORS(app, origins=['https://myfrontend.example.com'])

Push the updated service to AWS Lambda:

(.env)$ zappa update dev

Now try to refresh our browser. We can see the counter is displayed correctly. Clicking the “Increase Counter” button can increase the counter as well.

Conclusion

In this post we explored various AWS services required to create a simple serverless service. You may feel there are too many AWS services if you are not familiar with AWS, but most AWS services we used here are for one-time use. Once they are setup, we don’t need to touch them at all in further development. All you need to do is to run zappa update and aws s3 sync.

Besides, this is way easier than setting up a private VPS, installing web servers, and writing a Jenkins job for continuous deployment.

As a summary, here are the key takeaways from this post:

  • Lambda can run a simple service. This service can be exposed by API Gateway.
  • zappa is a great tool if you want to write serverless services in Python.
  • S3 bucket can be used for static hosting.
  • Apply for a certificate from AWS ACM if you want to use https.
  • API Gateway and CloudFront both support custom domain names.

Hope you like this post and don’t hesitate to clap ? for me if you did! Follow me if you want to read more about web development.