How to create a thumbnail API service in 5 minutes

In this blog article, I would like to show you how to develop an API service for creating thumbnails with AWS Lambda in less than 5 minutes.

The service will accept pictures over a REST API and return the thumbnails using ImageMagick. In a second step, we are going to store the thumbnails directly in S3 and return their public accessible link.

We will use Chalice a new framework for Python from Amazon to create AWS Lambda apps. The framework takes care of routing, creating the lambda function and AWS API Gateway setup. With this new framework, you will be able to publish your own serverless function within a couple of minutes.

The beauty of AWS lambda for this kind of fire and forget services is, that it's easy to scale, cheap, and no infrastructure to maintain.

Setup AWS credentials

Before we start, let's setup everything needed to use Chalice. First, you need to configure your AWS credentials on your development machine. If you already have the AWS CLI installed then you can skip this step. Otherwise please execute the following command in a terminal:

$ mkdir ~/.aws
$ cat >> ~/.aws/config
[default]
aws_access_key_id=YOUR_ACCESS_KEY_HERE
aws_secret_access_key=YOUR_SECRET_ACCESS_KEY
region=YOUR_REGION (such as us-west-2, us-west-1, etc)

Take a look at the Chalice readme if you need more information on how to configure the credentials.

Install Chalice

Chalice is published as Python PyPI package and can be installed with:

$ pip install chalice

This will install the command line utility to create a new project and deploy your service.

To verify if Chalice is installed correctly run:

$ chalice --help

Create the service

Chalice can create us an empty project with all necessary template files for our service. We use that to get started quickly.

Run $ chalice new-project thumbnail-service to create our project files.

Chalice will create two files and a folder for us:

.chalice/
app.py
requirements.txt

The two main files app.py and requirements.txt will contain our application logic and dependency definitions. The .chalice folder contains state information and is managed by Chalice. No need to touch it.

Open app.py with the editor of your choice. The file should already contain a small sample service:

from chalice import Chalice

app = Chalice(app_name='thumbnail-service')


@app.route('/')
def index():
    return {'hello': 'world'}

Before we continue let's quickly test if we can run this sample service successfully. Execute the following command in your project folder:

$ chalice deploy

This will create an IAM service role, the Lambda service, and an AWS API Gateway definition.

Once the command is finished it returns the URL of our newly created service: https://<url>.amazonaws.com/dev/

Call this API endpoint with curl to see if the service returns {'hello': 'world'}.

$ curl https://<url>.amazonaws.com/dev/

Refer to the Chalice readme if you encounter any issues while deploying.

That's it. You just deployed an AWS Lambda service in record time. In the next chapter, we will implement the thumbnail service based on the existing app.py file.

Read request parameters

Our thumbnail service shall receive the picture data inside a JSON body. Not only because JSON is nice but also because Chalice currently only supports application/json requests.

The request can be accessed with app.current_request. We will extract the picture data and additional options like size (width, height), format and thumbnail mode from it.

Replace the existing index() function with the following code:

@app.route('/', methods=['POST'])
def index():
    body = app.current_request.json_body

    image = base64.b64decode(body['data'])
    format = {'jpg': 'jpeg', 'png': 'png'}[body.get('format', 'jpg').lower()]
    mode = {'max': '', 'min': '^', 'exact': '!'}[body.get('mode', 'max').lower()]
    width = int(body.get('width', 128))
    height = int(body.get('height', 128))

Make sure to also replace the @app.route definition to include methods=['POST'] otherwise the API endpoint doesn't accept POST requests.

Create thumbnail

The heavy lifting to create the actual thumbnail is done by ImageMagick's Convert program. Luckily AWS Lambda has already a couple of pre-installed programs and ImageMagick is one of them. Therefore there is no need to install any additional things.

ImageMagick can be called with Python's Popen command. This will pass our original picture to ImageMagick using the standard input pipe and retrieve the thumbnail with the standard output.

Add the following code below the request parameters.

    cmd = [
        'convert',  # ImageMagick Convert
        '-',  # Read original picture from StdIn
        '-auto-orient',  # Detect picture orientation from metadata
        '-thumbnail', '{}x{}{}'.format(width, height, mode),  # Thumbnail size
        '-extent', '{}x{}'.format(width, height),  # Fill if original picture is smaller than thumbnail
        '-gravity', 'Center',  # Extend (fill) from the thumbnail middle
        '-unsharp',' 0x.5',  # Un-sharpen slightly to improve small thumbnails
        '-quality', '80%',  # Thumbnail JPG quality
        '{}:-'.format(format),  # Write thumbnail with `format` to StdOut
    ]

    p = Popen(cmd, stdout=PIPE, stdin=PIPE)
    thumbnail = p.communicate(input=image)[0]

    if not thumbnail:
        raise BadRequestError('Image format not supported')

This code will call ImageMagick and create a thumbnail based on the given with and format. The picture size is extended if necessary to meet the given size.

ImageMagick offers a lot of different command line arguments to adjust the thumbnail creation. Check out the ImageMagick documentation if needed.

In case the thumbnail couldn't be created an error is thrown. This may happen if the input file is not supported or corrupt.

To see the thumbnail you can add the following code which returns the picture as base64 encoded string.

return {
    'data': base64.b64encode(thumbnail),
}

Unfortunately, Chalice doesn't support yet any other response format than application/json so you can't just return the actual raw thumbnail data.

Deploy the service with $ chalice deploy for a first test.

The following snippet executes curl to call the service and includes the input picture as base64 encoded string. Replace <picture-path> and <url> first before running it.

$ curl -X POST -H "Content-Type: application/json" -d '{
    "width": 64,
    "height": 64,
    "data": "'"$( base64 -i <picture-path> )"'"
}' "https://<url>.amazonaws.com/dev"

If all goes well the service returns the thumbnail as base64 encoded string. Copy the base64 encoded response to the clipboard and run $ pbpaste | base64 -D -o test.jpg to see the thumbnail.

I recommend to add the following line below app = Chalice(..) to activate the debug mode in case you get an error while calling the API. This will include the exceptions detail inside the error response. Don't forget to deactivate it for productive usage.

app.debug = True

Store on S3

It would be great to store the thumbnail directly in S3 for further processing or to use it inside a website.

Insert the following code on top of app.py and replace the bucket name with a valid bucket name.

import boto3
S3 = boto3.client('s3')
S3_BUCKET = '<name>'

Also add the line boto3==1.3.1 to the requirements.txt file.

Then replace the return code with the following code:

    filename = '{}_{}x{}.{}'.format(uuid.uuid4(), width, height, format)
    S3.put_object(
        Bucket=S3_BUCKET,
        Key=filename,
        Body=thumbnail,
        ACL='public-read',
        ContentType='image/{}'.format(format),
    )

    return {
        'url': 'https://s3.amazonaws.com/{}/{}'.format(S3_BUCKET, filename)
    }

This will upload the thumbnail with a random filename to S3 and make it public accessible. The URL is then returned as the response from the service.

Deploy the change with $ chalice deploy.

Tip: Chalice will automatically extend the IAM service role with S3 permissions. If you still get an S3 permission denied exception then you need to manually add the S3 permission to the Chalice IAM role in the AWS security console.

Congratulations! The thumbnail service is now up and running waiting to accept trillions of requests :)

Outlook

The complete code can be found on my Github page Schweigi/thumbnail-service.

Two more things before I let you go.

As you probably already noticed we didn't handle authentication. For production usage, you need to implement something because the AWS Lambda function is public accessible. The AWS API Gateway supports multiple authentication methods which you can use for that.

Currently, Chalice is in beta and doesn't support a production stage for the API Gateway. I hope this will be implemented soon. Until then you can take a look at similar but more advanced frameworks like Zappa.