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.
- Chalice: https://web-quickstart.blogspot.com/2021/03/aws-chalice.html
- Pynamo: https://web-quickstart.blogspot.com/2021/05/crud-dynamodb-from-python-pynamodb.html
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
Post a Comment