A serverless web application using Python, API Gateway and Lambda function

Creating a REST application using AWS Chalice and Peewee

Agenda

This post is inspired by Django polls app. We will build RESTful endpoints for a polls app using AWS Chalice.

We will use a Postgres database. We want to avoid raw sql queries and hence will use peewee as our ORM.

We will be creating the following apis in this post.

  • An api to create a poll question.
  • Api to list questions.
  • Api to get question detail.
  • Api to edit a question.
  • Api to delete a question.

Setup

Let’s create a virtualenv.

mkvirtualenv --python=/usr/local/bin/python3 use-chalice

We are using Python 3.7 through this post.

Install chalice

pip install chalice

Create a chalice project.

chalice new-project polls

We are assuming that your AWS credentials are properly configured in ~/.aws/config.

chalice new-project would create the following files:

drwxr-xr-x   .chalice
-rw-r--r--   app.py
-rw-r--r--   requirements.txt

Ignore .chalice and requirements.txt for now.

Issue following command to run the chalice server locally

chalice local

Navigate to http://localhost:8000/. You should see {"hello": "world"} on the page.

This confirms our setup is working properly. Let’s deploy our app using API Gateway and Lamda function. Run the following command

chalice deploy

The output would look similar to:

This assumes that the AWS credentials configured in ~/.aws/config has proper IAM policies assigned. The user should have the following policies assigned:

  • AWSLambdaFullAccess
  • AmazonAPIGatewayAdministrator
  • IAMFullAccess

Navigate to Rest API URL. The api url I got is https://baxnta8me9.execute-api.ap-south-1.amazonaws.com/api/

Polls apis

Our codebase isn’t going to have a single index route anymore. It will be a full-fledged web application having database connection code, an ORM, and several routes, and associated views for the routes.

We should be splitting this logic across multiple modules instead of keeping everything in app.py.

If we want any other file apart from app.py to be packaged by chalice during deployment, it needs to be in a chalicelib directory. Let’s create this directory.

mkdir chalicelib

We will keep four files in chalicelib namely __init__.py, settings.py, db.py and models.py.

touch chalicelib/__init__.py
touch chalicelib/settings.py
touch chalicelib/db.py
touch chalicelib/models.py

We don’t want to keep database credentials in code and hence will keep them in environment variables. We will read the environment variables in settings.py.

Add following code to settings.py.

# chalicelib/settings.py
import os

DATABASE = {
    'HOST': os.environ['DB_HOST'],
    'PORT': os.environ['DB_PORT'],
    'NAME': os.environ['DB_NAME'],
    'USER': os.environ['DB_USER'],
    'PASSWORD': os.environ['DB_PASSWORD'],
}

Let’s use db.py for handling database connection. Add following code to db.py.

# chalicelib/db.py
from chalicelib import settings
from peewee import PostgresqlDatabase


db = PostgresqlDatabase(
    settings.DATABASE['NAME'], user=settings.DATABASE['USER'], password=settings.DATABASE['PASSWORD'], host=settings.DATABASE['HOST'], port=settings.DATABASE['PORT']
)

We are relying on peewee library to connect to the database. Also peewee will need psycopg2 since we are connecting to a PostgreSQL database.

pip install peewee
pip install psycopg2

Let’s define our Peewee model in models.py

# chalicelib/models.py
from peewee import Model, CharField, DateField

from chalicelib.db import db

class Question(Model):
    question_text = CharField()
    pub_date = DateField()

    class Meta:
        database = db


if db.table_exists('question') is False:
    db.create_tables([Question])

In models.py we have also written code to create the question table if it doesn’t exist. We will take a different approach in production environment but for demonstration purpose, this will do.

Let’s write the view to handle POST and GET for questions. Add following code in app.py

@app.route('/polls/questions', methods=['GET', 'POST'])
def questions():
    request = app.current_request
    if request.method == 'POST':
        question_text = request.json_body['question_text']
        pub_date = datetime.strptime(request.json_body['pub_date'], '%Y-%m-%d')
        question = Question(question_text=question_text, pub_date=pub_date)
        question.save()
        rep = {'question_text': question.question_text, 'id': question.id, 'pub_date': question.pub_date.strftime('%Y-%m-%d')}
        return rep
    elif request.method == 'GET':
        questions = Question.select()
        l = []
        for question in questions:
            l.append({'question_text': question.question_text, 'id': question.id, 'pub_date': question.pub_date.strftime('%Y-%m-%d')})
        return l

With a little knowledge of peewee the above code should be self explanatory.

To avoid hassle of writing export DB_HOST=localhost etc. while testing, I use a basic shell script. I have saved it in env.sh

#!/bin/sh
export DB_HOST=localhost
export DB_PORT=5432
export DB_NAME=peewee_polls
export DB_USER=akshar
export DB_PASSWORD=akshar

Change env.sh to populate it with your local postgres db credentials.

Execute this script.

. ./env.sh

Run chalice local if it’s not already running.

chalice local

Navigate to http://localhost:8000/polls/questions. You will get an empty list in response.

Let’s post a question. We will use requests to post a question. You could use curl or any other tool you prefer.

In [1]: import requests

In [2]: url = 'http://localhost:8000/polls/questions/'

In [3]: data = {'question_text': 'What is the color of sky?', 'pub_date': '2019-09-22'}

In [4]: resp = requests.post(url, json=data)

In [5]: resp
Out[5]: <Response [200]>

In [7]: resp.json()
Out[7]:
{u'id': 2,
 u'pub_date': u'2019-09-22',
 u'question_text': u'What is the color of sky?'}

Navigate to http://localhost:8000/polls/questions again. You should be seeing this question’s detail in response.

Deploying

Our project relies on peewee, so add it to requirements.txt. requirements.txt will have following content.

peewee

We cannot straightaway add psycopg2 to requirements because of reasons described here. psycopg2 from this repository needs to be added to our project.

chalice has a requirement that vendor libraries be added to a directory called vendor. Take the following steps

mkdir vendor
git clone [email protected]:jkehler/awslambda-psycopg2.git /tmp/awslambda-psycopg2
cp -r /tmp/awslambda-psycopg2/psycopg2-3.7 vendor/psycopg2

The environment variables need to be defined in config.json. You should have a .chalice/config.json. Edit it to add environment variables.

{
  "version": "2.0",
  "app_name": "polls",
  "stages": {
    "dev": {
      "api_gateway_stage": "api",
      "environment_variables": {
        "DB_HOST": "yourdb.c7saowmwi1we.ap-south-1.rds.amazonaws.com",
        "DB_PORT": "5432",
        "DB_USER": "postgres",
        "DB_PASSWORD": "yourpassword",
        "DB_NAME": "yourdbname"
      }
    }
  }
}

We are assuming that you have a publically available database. You lambda code would connect to this database.

Run chalice deploy

chalice deploy

You will get an output which would look like:

Creating deployment package.
Updating policy for IAM role: polls-dev
Updating lambda function: polls-dev
Updating rest API
Resources deployed:
  - Lambda ARN: arn:aws:lambda:ap-south-1:117635876922:function:polls-dev
  - Rest API URL: https://baxnta8me9.execute-api.ap-south-1.amazonaws.com/api/

Navigate to https://baxnta8me9.execute-api.ap-south-1.amazonaws.com/api/polls/questions. You will see an empty list because we haven’t created any question yet.

Let’s create a question now.

In [2]: url = 'http://localhost:8000/polls/questions/'

In [3]: data = {'question_text': 'What is the color of sky?', 'pub_date': '2019-09-22'}

In [4]: resp = requests.post(url, json=data)

In [5]: resp
Out[5]: <Response [200]>

In [7]: resp.json()
Out[7]:
{u'id': 2,
 u'pub_date': u'2019-09-22',
 u'question_text': u'What is the color of sky?'}

Navigate to https://baxnta8me9.execute-api.ap-south-1.amazonaws.com/api/polls/questions again. You will see the just created question in the response.

In a similar way, we could add view which handles editing and deleting a question.

You can see fully functional code on Github.

Thank you for reading the Agiliq blog. This article was written by Akshar on Sep 26, 2019 in ServerlessAWSLambda .

You can subscribe ⚛ to our blog.

We love building amazing apps for web and mobile for our clients. If you are looking for development help, contact us today ✉.

Would you like to download 10+ free Django and Python books? Get them here