REST API with Chalice + Pynamo

In this post, I am going to deploy a sample REST API on AWS API Gateway, Lambda, and DynamoDB using Chalice and PynamoDB.


API spec

The following methods will be served for CRUD operations:

/users/
 POST:
  param: id name

/users/{id}
 GET:
  response: id name
 
 PUT:
  param: name
 
 DELETE


create project

% chalice new-project user_api

% cd ./user_api


prepare files

% tree
.
├── app.py
├── chalicelib
│   ├── __init__.py
│   └── user.py
├── docker-compose.yml
└── requirements.txt


# app.py

from chalice import Chalice

from chalice import ForbiddenError, NotFoundError

from chalicelib.user import User


app = Chalice(app_name='user_api')


@app.route('/users', methods=['POST'])

def create_user():

    user_as_json = app.current_request.json_body

    user_id = user_as_json['id']

    user_name = user_as_json['name']


    if User.count(user_id) == 0:

        User(user_id, name=user_name).save()

        return True


    raise ForbiddenError('Forbidden')


@app.route('/users/{user_id}') # GET

def load_user(user_id):

    try:

        user_name = User.get(user_id).name

        return {'id': user_id, 'name': user_name}

    except User.DoesNotExist:

        raise NotFoundError('User Not Found')


@app.route('/users/{user_id}', methods=['PUT'])

def update_user(user_id):

    user_as_json = app.current_request.json_body

    user_name = user_as_json['name']


    try:

        user_name = User.get(user_id).name

        User(user_id, name=user_name).save()

        return True

    except User.DoesNotExist:

        raise NotFoundError('User Not Found')


@app.route('/users/{user_id}', methods=['DELETE'])

def delete_user(user_id):

    try:

        User.get(user_id).delete()

        return True

    except User.DoesNotExist:

        raise NotFoundError('User Not Found')

ref: error handling: https://aws.github.io/chalice/topics/views.html


# chalicelib/__init__.py (empty file)


Note: "You can create a chalicelib/ directory, and anything in that directory is recursively included in the deployment package" (https://aws.github.io/chalice/topics/multifile.html)


# chalicelib/user.py

from pynamodb.attributes import UnicodeAttribute

from pynamodb.models import Model


class User(Model):

  class Meta:

    table_name = 'user'

    host = 'http://localhost:8001' # to be removed later

    write_capacity_units = 1 

    read_capacity_units = 1 


  id = UnicodeAttribute(hash_key=True)

  name = UnicodeAttribute(null=True)


# docker-compose.yml

Use port=8001 this time.

version: '3.8'

services:

  dynamodb-local:

    command: "-jar DynamoDBLocal.jar -sharedDb -optimizeDbBeforeStartup -dbPath ./data"

    image: "amazon/dynamodb-local:latest"

    container_name: dynamodb-local

    ports:

      - "8001:8000"

    volumes:

      - "./docker/dynamodb:/home/dynamodblocal/data"

    working_dir: /home/dynamodblocal


# requirements.txt

pynamodb==5.0.3


run locally

% docker-compose up -d
% python
>>> from chalicelib.user import User
>>> User.create_table()
>>> User.exists()
True
>>>
% chalice local
# insert
% curl -X POST localhost:8000/users -H "Content-Type: application/json" -d '{"id":"1", "name":"foo"}'
true
% curl -X POST localhost:8000/users -H "Content-Type: application/json" -d '{"id":"2", "name":"bar"}'
true
% curl -X POST localhost:8000/users -H "Content-Type: application/json" -d '{"id":"2", "name":"foobar"}'
{"Code":"ForbiddenError","Message":"ForbiddenError: Forbidden"}

# select
% curl localhost:8000/users/1
{"id":"1","name":"foo"}
% curl localhost:8000/users/3
{"Code":"NotFoundError","Message":"NotFoundError: User Not Found"}

# update
% curl -X PUT localhost:8000/users/2 -H "Content-Type: application/json" -d '{"id":"2", "name":"foobar"}'
true

# delete
% curl -X DELETE localhost:8000/users/2                                                               
true


deploy

Remove host definition from user.py to access DynamoDB on the cloud

vim chalicelib/user.py
- host = 'http://localhost:8001'


% python
>>> from chalicelib.user import User
>>> User.create_table()
>>> User.exists()
True
>>>
% chalice deploy


run

% BASE_URL=$(chalice url)

% curl -X POST ${BASE_URL}/users -H "Content-Type: application/json" -d '{"id":"1", "name":"foo"}'
true

% curl ${BASE_URL}/users/1
{"id":"1","name":"foo"}


troubleshoot (AccessDeniedException)

issue

internal error when POST /users

cause

% chalice logs

2021-05-03 00:57:59 ... AccessDeniedException ... assumed-role/user_api-dev/user_api-dev is not authorized to perform: dynamodb:DescribeTable ...

root cause

Chalice analyzes the code to detect necessary permissions, but sometimes fails like when boto3 is not used.

resolution

aws console > IAM > Roles > user_api-dev > Attach policies > add "AmazonDynamoDBFullAccess"

Note: manually added policies must be detached before chalice delete

prevention

  • A: define permissions in .chalice/config.json
  • B: use boto3 instead of pynamodb


cleanup

% chalice delete

% python
>>> from chalicelib.user import User
>>> User.delete_table()
>>> User.exists()
False


Comments

Popular posts from this blog

Selenide: Quick Start

Minikube Installation for M1 Mac

Three.js Quick Start - Run Spinning-Cube Example