Python サーバーレスフレームワークの比較 - Zappa vs Chalice

## はじめに

この記事は、[Serverless(2) Advent Calendar 2016](http://qiita.com/advent-calendar/2016/serverless2) 23日目の記事です。

## Python サーバーレスフレームワーク

Pythonでサーバレスなアプリケーションを開発するためのフレームワークとしては、以下の2つが人気のようです。

+ [awslabs/chalice: Python Serverless Microframework for AWS](https://github.com/awslabs/chalice)
+ [Miserlou/Zappa: Serverless Python Web Services](https://github.com/Miserlou/Zappa)


今回はこの2つをざっくり比較してみます。

ちなみにNode.jsを使用するのであれば、[Serverless Framework](https://github.com/serverless/serverless)や[Apex](https://github.com/apex/apex)が有名です。
他にどういったサーバーレスフレームワークがあるかについては、以下のスライドが参考になります。
[Unlimited Frameworks](http://www.slideshare.net/marcyterui/unlimited-frameworks)

## chalice
**Python Serverless Microframework for AWS**


+ Amazon製だけあってAWS専用のフレームワーク
+ 簡単に開発できるように設計されていて、FlaskやBottoleのようなデコレータベースの軽量フレームワーク
+ IAMポリシーを自動生成してくれる
+ GatewayとLambda全てのAPIを網羅していない
+ S3やDynamoDBなどの他のAWSリソースへのアクセスもできない
+ API Gatewayに特化している

用意してあるコマンドはこれだけ。

```
Commands:
  deploy
  gen-policy
  generate-sdk
  local
  logs
  new-project
  url
```

実際のchaliceを使ったソースコードを[How to create a thumbnail API service with AWS Lambda](https://www.mschweighauser.com/create-a-thumbnail-api-service/)を参考にみてみます。

写真をPOSTで受け取ってサムネイルを生成する処理を`app.py`が行なってます。

```python
import base64
import uuid
from subprocess import Popen, PIPE

import boto3
from chalice import BadRequestError, Chalice


app = Chalice(app_name='thumbnail-service')
app.debug = True  # TODO: Disable on production

S3 = boto3.client('s3')
S3_BUCKET = ''  # TODO: Replace with valid bucket name


@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))

    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')

    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)
    }
```

## Zappa

**Serverless Python Web Services**

+ WSGIアプリケーションをデプロイする
+ なのでchaliceと違って、AWS以外のクラウドにも対応可能
+ 多機能なフルスタックなフレームワーク
+ "ハイブリッド"アプリケーションに必要となる、AWSイベントソース全てを自動サポート

それではchalice同様に、POSTで受け取った写真のサムネイル生成を行うアプリケーションを[Building Serverless Microservices with Zappa and Flask - Gun.io](https://gun.io/blog/serverless-microservices-with-zappa-and-flask/)を参考にみてみます。

```python
import base64
import boto3
import calendar
import io

from datetime import datetime, timedelta
from flask import Flask, request, render_template
from PIL import Image

s3 = boto3.resource('s3')
BUCKET_NAME = 'your_public_s3_bucket'

@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        new_file_b64 = request.form['b64file']
        if new_file_b64:

            # Decode the image
            new_file = base64.b64decode(new_file_b64)

            # Crop the Image
            img = Image.open(io.BytesIO(new_file))
            img.thumbnail((200, 200))

            # Tag this filename with an expiry time
            future = datetime.utcnow() + timedelta(days=10)
            timestamp = str(calendar.timegm(future.timetuple()))
            filename = "thumb.%s.jpg" % timestamp

            # Send the Bytes to S3
            img_bytes = io.BytesIO()
            img.save(img_bytes, format='JPEG')
            s3_object = s3.Object(BUCKET_NAME, filename)
            resp = s3_object.put(
                Body=img_bytes.getvalue(),
                ContentType='image/jpeg'
                )

            if resp['ResponseMetadata']['HTTPStatusCode'] == 200:

                # Make the result public
                object_acl = s3_object.Acl()
                response = object_acl.put(
                    ACL='public-read')

                # And return the URL
                object_url = "https://{0}.s3.amazonaws.com/{1}".format(
                    BUCKET_NAME,
                    filename)
                return object_url, 200
            else:
                return "Something went wrong :(", 400

    return render_template('upload.html')
```

chaliceと違ってWSGIアプリケーションをデプロイできるので、写真アップロード画面なども一緒に用意できます。

またAWSイベントソースを利用することで、S3へのアップロードをフックにサムネイル処理をノンブロッキングに実行することもできます。

```
# zappa_settings.yml
---
dev:
  app_function: your_project.main.app
  events:
  - function: your_project.users.thumbnailer
    event_source:
      arn: arn:aws:s3:::your_public_s3_bucket
      events:
      - s3:ObjectCreated:*
```
```
# your_project/users.py
import Pillow

def thumbnailer(event, context):
    """ Upon PUT, thumbnail! """

    # Get the bytes from S3
    in_bucket = event['Records']['s3']['bucket']['name']
    key = event['Records']['s3']['object']['key']
    image_bytes = s3_client.download_file(in_bucket, key, '/tmp/' + key).read()

    # Thumbnail it
    size = (250, 250)
    thumb = ImageOps.fit(image_bytes, size, Image.ANTIALIAS)

    # Put it back on S3
    s3_client.put_object(
        ACL='public-read',
        Body=thumb,
        Key=key + 'thumbnail.jpg',
        Bucket='avatar-bucket')
```

詳しくは[Server-less Framework Comparison - Zappa Versus Chalice](https://blog.zappa.io/posts/comparison-zappa-verus-chalice)を参照してください。

## 参考にしたもの

+ [Server-less Framework Comparison - Zappa Versus Chalice](https://blog.zappa.io/posts/comparison-zappa-verus-chalice)
+ [How to create a thumbnail API service with AWS Lambda](https://www.mschweighauser.com/create-a-thumbnail-api-service/)
+ [Building Serverless Microservices with Zappa and Flask - Gun.io](https://gun.io/blog/serverless-microservices-with-zappa-and-flask/)
+ [AWS Lambda + API Gateway + Chalice(Python2)でLINE BOT - c-bata web](http://nwpct1.hatenablog.com/entry/aws-lambda-apigateway-line-bot)


## おわりに

[Top 10 Python libraries of 2016 - Tryolabs Blog](https://tryolabs.com/blog/2016/12/20/top-10-python-libraries-of-2016)でもZappaが1位に輝いているように、2016年12月の時点ではchaliceよりもZappaの方が人気があるようです。

しかしここで見てきたように2つのフレームワークは特徴がはっきりしていて差別化されています。
用途に合わせて使い分けることで、Pythonサーバーレスフレームワーク界隈が盛り上がっていくといいですね。