UNPKG

serverless-sam

Version:

Serverless framework plugin to export AWS SAM templates for a service

277 lines (232 loc) 9.9 kB
<!-- title: AWS Serverless REST API with DynamoDB store and presigned URLs example in Python 3.6. description: This example demonstrates how to setup a RESTful Web Service allowing you to create, list, get, update and delete Assets. DynamoDB is used to store the data. layout: Doc --> # Serverless REST API This example demonstrates how to setup a [RESTful Web Service](https://en.wikipedia.org/wiki/Representational_state_transfer#Applied_to_web_services) using [Presigned URLs](http://boto3.readthedocs.io/en/latest/guide/s3.html?highlight=presigned#generating-presigned-urls) to manage asset uploads and downloads. The initial POST creates an asset entry in dynamo and returns a presigned upload URL. This is used to upload the asset without needing any credentials. An s3 event triggers another lambda method to mark the asset as "RECEIVED". One can then initiate a PUT to the asset's REST path to mark it as "UPLOADED" To download an asset, do a GET to the asset path to get a presigned download URL with an optional TTL. This URL can be used to retrieve the asset with no additional credentials. Also provides "LIST" and "DELETE" methods. DynamoDB is used to store the index and tracking data referring to the assets on s3. This is just an example and of course you could use any data storage as a backend. ## Structure This service has a separate directory for all the assets operations. For each operation exactly one file exists e.g. `assets/delete.py`. In each of these files there is exactly one function defined. ### Model The idea behind the `assets` directory is that in case you want to create a service containing multiple resources e.g. users, notes, comments you could do so in the same service. While this is certainly possible you might consider creating a separate service for each resource. It depends on the use-case and your preference. ### API GW Integration model All methods use `lambda` integration as that reduces the API GW interference in the payload. ### Logging The log_cfg.py is an alternate way to setup the python logging to be more friendly wth AWS lambda. The lambda default logging config is to not print any source file or line number which makes it harder to correleate with the source. Adding the import: ```python from log_cfg import logger ``` at the start of every event handler ensures that the format of the log messages are consistent, customizable and all in one place. Default format uses: ```python '%(asctime)-15s %(process)d-%(thread)d %(name)s [%(filename)s:%(lineno)d] :%(levelname)8s: %(message)s' ``` ### Notes Initial scaffold copied from the aws-python-rest-api-with-pynamodb example. The PUT method to mark the asset as UPLOADED is somewhat redundant as the S3 event that marks uploads as RECEIVED should be sufficient for most cases. However the goal was to use a PUT method to mark it received, so the PUT marks a RECEIVED asset as UPLOADED. That said, there is no distinction between UPLOADED vs RECEIVED anywhere in the example. The DELETE method does a `soft delete` which marks the asset as deleted without removing the s3 key. If the file on s3 is deleted, an event is generated which does fully delete the asset in dynamo as well. ## Setup ```bash npm install ``` ## Deploy In order to deploy the endpoint simply run ```bash serverless deploy ``` The expected result should be similar to: ```bash %> sls deploy Serverless: Parsing Python requirements.txt Serverless: Installing required Python packages for runtime python3.6... Serverless: Linking required Python packages... Serverless: Packaging service... Serverless: Excluding development dependencies... Serverless: Unlinking required Python packages... Serverless: Uploading CloudFormation file to S3... Serverless: Uploading artifacts... Serverless: Uploading service .zip file to S3 (7.14 MB)... Serverless: Validating template... Serverless: Updating Stack... Serverless: Checking Stack update progress... ............................................ Serverless: Stack update finished... Service Information service: aws-python-pynamodb-s3-sigurl stage: dev region: us-east-1 stack: aws-python-pynamodb-s3-sigurl-dev api keys: None endpoints: POST - https://1xith51inb.execute-api.us-east-1.amazonaws.com/dev/asset GET - https://1xith51inb.execute-api.us-east-1.amazonaws.com/dev/asset GET - https://1xith51inb.execute-api.us-east-1.amazonaws.com/dev/asset/{asset_id} PUT - https://1xith51inb.execute-api.us-east-1.amazonaws.com/dev/asset/{asset_id} DELETE - https://1xith51inb.execute-api.us-east-1.amazonaws.com/dev/asset/{asset_id} functions: create: aws-python-pynamodb-s3-sigurl-dev-create bucket: aws-python-pynamodb-s3-sigurl-dev-bucket list: aws-python-pynamodb-s3-sigurl-dev-list get: aws-python-pynamodb-s3-sigurl-dev-get update: aws-python-pynamodb-s3-sigurl-dev-update delete: aws-python-pynamodb-s3-sigurl-dev-delete ``` ## Usage You can create, retrieve, update, or delete assets with the following commands: The $URL is the base URL specified in the POST endpoint above. `jsonpp` used to format the output for visibility but is not required for use. ### Get an asset pre-signed upload URL ```bash %> curl -sX POST $URL | jsonpp { "statusCode": 201, "body": { "upload_url": "<SIGNED-URL>", "id": "1a5ea69a-d30c-11e7-90d0-129b5a655d2d" } } ``` ### Upload a file to the URL ```bash %> curl -sX PUT --upload-file file.txt "<SIGNED_URL>" ``` ### Upload a file after pre-signed URL has expired ```bash %> curl -sX PUT --upload-file file.txt "<SIGNED-URL>" <?xml version="1.0" encoding="UTF-8"?> <Error> <Code>AccessDenied</Code> <Message>Request has expired</Message> <Expires>2017-11-27T01:03:04Z</Expires> <ServerTime>2017-11-27T01:05:41Z</ServerTime> <RequestId>D4EFA3C1A8DDD525</RequestId> <HostId>vS12oM24ZidzjG0JZon/y/8XD8whCKD/0JZappUNOekOJ3Eqp10Q5ne0emPVM/Mx6K1lYr0bi6c=</HostId> </Error> ``` ### Mark asset as uploaded ```bash %> curl -sX PUT "$URL/1a5ea69a-d30c-11e7-90d0-129b5a655d2d" | jsonpp { "statusCode": 202, "body": { "status": "UPLOADED" } } ``` ### Get download URL for asset: ```bash %> curl -sX GET "$URL/1a5ea69a-d30c-11e7-90d0-129b5a655d2d" | jsonpp { "statusCode": 202, "body": { "download_url": "<SIGNED-URL>" } } ``` ### List all Assets ```bash %> curl -sX GET "$URL" | jsonpp { "statusCode": 200, "body": { "items": [ { "asset_id": "312aba96-d30f-11e7-b004-129b5a655d2d", "createdAt": "2017-11-27T01:05:21.830944+0000", "state": "UPLOADED", "updatedAt": "2017-11-27T01:07:05.311962+0000" }, { "asset_id": "0add1bcc-d30f-11e7-b004-129b5a655d2d", "createdAt": "2017-11-27T01:05:21.830944+0000", "state": "UPLOADED", "updatedAt": "2017-11-27T01:06:19.413445+0000" }, { "asset_id": "57226e20-d30e-11e7-bda4-129b5a655d2d", "createdAt": "2017-11-27T01:00:20.296693+0000", "state": "CREATED", "updatedAt": "2017-11-27T01:02:57.750625+0000" } ] } } ``` ### Get one Asset download URL ```bash %> curl -sX GET "$URL/57226e20-d30e-11e7-bda4-129b5a655d2d" | jsonpp { "statusCode": 202, "body": { "download_url": "<SIGNED-URL>" } } ``` The returned URL is all that's needed to retrieve the asset file. ### Download asset Use the `download_url` returned from the above GET ```bash %> %> curl -sX GET "<SIGNED_URL>" --output file.txt ``` ### Download asset with expired URL Use the `download_url` returned from the above GET ```bash %> curl -sX GET "<SIGNED_URL>" <?xml version="1.0" encoding="UTF-8"?> <Error> <Code>AccessDenied</Code> <Message>Request has expired</Message> <Expires>2017-11-27T03:15:54Z</Expires> <ServerTime>2017-11-27T03:17:25Z</ServerTime> <RequestId>09B6B5DD49895A40</RequestId> <HostId>mlN7TDikYBhehryCiGXtROuNCZL+/50kfvA0Ui2NP2JPVyTCY9hIbQxlsayB2rdxefhHfKn77mI=</HostId> </Error> ``` ### Delete an asset ```bash %> curl -sX DELETE "$URL/312aba96-d30f-11e7-b004-129b5a655d2d" | jsonpp { "statusCode": 204 } ``` ## Scaling ### AWS Lambda By default, AWS Lambda limits the total concurrent executions across all functions within a given region to 100. The default limit is a safety limit that protects you from costs due to potential runaway or recursive functions during initial development and testing. To increase this limit above the default, follow the steps in [To request a limit increase for concurrent executions](http://docs.aws.amazon.com/lambda/latest/dg/concurrent-executions.html#increase-concurrent-executions-limit). ### AWS Lambda Edge Lambda is constrained to one region. The One can configure AWS cloudfront to pass events to lambda instances in the closest region using [Lambda Edge](https://aws.amazon.com/about-aws/whats-new/2017/07/lambda-at-edge-now-generally-available/). ### DynamoDB When you create a table, you specify how much provisioned throughput capacity you want to reserve for reads and writes. DynamoDB will reserve the necessary resources to meet your throughput needs while ensuring consistent, low-latency performance. You can change the provisioned throughput and increasing or decreasing capacity as needed. This is can be done via settings in the `serverless.yml`. ```yaml ProvisionedThroughput: ReadCapacityUnits: 1 WriteCapacityUnits: 1 ``` Dynamo now also supports an auto-scaling option to eliminate the need for manual capacity scaling. In case you expect a lot of traffic fluctuation we recommend to checkout this guide on how to auto scale DynamoDB [https://aws.amazon.com/blogs/aws/auto-scale-dynamodb-with-dynamic-dynamodb/](https://aws.amazon.com/blogs/aws/auto-scale-dynamodb-with-dynamic-dynamodb/)