Serverless Frameworkを使ってAWS LambdaにQRコードを返すAPIをデプロイする

サーバレスアーキテクチャAWS Lambda, Azure Functions, Google CloudFunctionsなど)の構成管理ができるServerless Frameworkを使ってみたかったので、任意の文字列をQRコード化して返すAPIを作ってデプロイしてみました。

環境は下記のとおりです。

本記事でわかること

Serverless Frameworkのインストール

$ npm install -g serverless
$ sls create --template aws-python --path qrcode-service

python環境の構築

$ cd qrcode-service
$ pyenv install 2.7
$ pyenv virtualenv 2.7 qrcode-service
$ pyenv local qrcode-service
$ pip install -r requirements.txt

requirements.txtは下記の通り。

olefile==0.44
Pillow==4.0.0
qrcode==5.3
six==1.10.0

QRコードを返す関数の作成

受け取った文字列をQRコード化して、base64エンコードした結果を返す関数を作成します。

handler.py

import base64
from io import BytesIO
import os
import sys

sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'vendor'))
sys.path.append(os.path.join(os.path.abspath(os.path.dirname(__file__)), 'vendor/PIL'))

import qrcode

def create(event, context):
    params = event['query']

    img = qrcode.make(params['text'])
    bufferd = BytesIO()
    stdout_buffer = getattr(sys.stdout, 'buffer', None)
    img.save(bufferd)
    image_base64 = base64.b64encode(bufferd.getvalue()).decode('utf-8')

    return image_base64

serverless.ymlを書く

基本的にはこちらのブログの記事の構成を再現する内容です。

Binary Support for API Integrations with Amazon API Gateway | AWS Compute Blog

serverless.yml

service: qrcode-service

provider:
  name: aws
  runtime: python2.7
  stage: dev
  region:  ap-northeast-1

package:
 exclude:
   - build_packages.sh
   - Dockerfile
   - requirements.txt

functions:
  qrcode:
    handler: handler.create
    events:
     - http:
         path: qrcode
         method: get
         integration: lambda
         request:
            parameters:
              querystrings:
                text: true
            passThrough: WHEN_NO_TEMPLATES
            template:
              image/png: |
                #define( $loop )
                  {
                  #foreach($key in $map.keySet())
                      #set( $k = $util.escapeJavaScript($key) )
                      #set( $v = $util.escapeJavaScript($map.get($key)).replaceAll("\\'", "'") )
                      "$k":
                        "$v"
                        #if( $foreach.hasNext ) , #end
                  #end
                  }
                #end

                {
                    #set( $map = $input.params().querystring )
                    "query": $loop
                }

ローカル実行で動作確認

Serverless Frameworkにはローカル実行する機能があるので、デプロイしなくても下記のようにして動作確認ができます。

$ sls invoke local -f qrcode -d '{ "query": { "text": "Kappa" } }' | sed 's/"//g' | base64 -d > kappa.png
$ open kappa.png

デプロイ用に外部ライブラリパッケージをまとめる

Lambdaで外部ライブラリを使う場合は外部ライブラリも含めてzipにする必要があります。 しかし、使用するライブラリにネイティブバイナリが含まれている場合は、MacでビルドしてしまうとDarwin用のモジュールになってしまってLambda上では動作しません。 今回は、MacもEC2もCPUは同じx64系ということで、amazonlinuxのDocker Imageを使ってMac上でビルドしてしまうことにします。

Docker Imageのビルド

Dockerfileは下記の通りです。

FROM amazonlinux

WORKDIR /work

RUN set -x && \
    yum install -y python27-devel python27-pip gcc gcc-c++ cmake && \
    yum install -y libtiff-devel libjpeg-devel libzip-devel freetype-devel \
      lcms2-devel libwebp-devel tcl-devel tk-devel && \
    pip install --upgrade pip

ビルドします。

$ docker build . -t qrcode-service

外部ライブラリのビルド

外部ライブラリをdocker container上でビルドします。

$ mkdir vendor
$ cp requirements.txt vendor
$ docker run -v "$(pwd)"/vendor:/work qrcode-service pip install -r requirements.txt -t .

AWSにデプロイする

Serverless Frameworkのコマンド一発で完了……といきたいところですが、Serverless Frameworkがバックエンドで使用しているCloudFormationがバイナリサポートの設定に対応していないらしく、コマンド一発で完了とはいきませんでした。 しかたがないので一部の設定は別途行います。

1. まずはデプロイする

下記のコマンドでデプロイを実行します。

$ sls deploy
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
.....
Serverless: Stack create finished...
Serverless: Packaging service...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading service .zip file to S3 (6.26 MB)...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
..............................
Serverless: Stack update finished...
Service Information
service: qrcode-service
stage: dev
region: ap-northeast-1
api keys:
  None
endpoints:
  GET - https://u5uko0b1ne.execute-api.ap-northeast-1.amazonaws.com/dev/qrcode
functions:
  qrcode: qrcode-service-dev-qrcode

上記の例だとu5uko0b1neAPI IDになるので控えておきます。

デプロイが完了した時点でで下記のようにリクエストを送るとbase64エンコードされたまま結果が返ってくるのが確認できます。

$ curl -X GET -H "Accept: image/png" -H "Content-Type: image/png" "https://u5uko0b1ne.execute-api.ap-northeast-1.amazonaws.com/dev/qrcode?text=Kappa"
"iVBORw0KGgoAAAANSUhEUgAAASIAAAEiAQAAAAB1xeIbAAABhklEQVR4nO2YTW6DMBCFv6m9d26Qo5gb9Ky9AT5KDhAJ9kbThW3kkkrthvDnWSBiPoknZzR+PFH+rvDxDwga1ahGNapRe6ckl0W60QJjWek21XUJyquq6gDaA9JhVFVVf1Lv13UJaiw9Hu4x/wEidntdZ6bs4rfgJvviO/eq/mSUf1ike+cbL0uVvncKjACYKAB18+9V/bGpvPdBADDz+iRrvbFRpWQx2wVMXEItU1iRkg6QbpTU7dnb3/KDvas/KkXy8T1GtXdaXL1TxQ/z072qPzaV5r3gnoJ/2Ejo5pnjYjmJ96r+2FTaXWW06bOKfMxmky++30bXFahq5uQhM0CaPr2LbeasSeXdnQs/QH1pe78aRR2ZYbRea33/FqrOMY0SbpPgB6PSbavr1FRxMuMN/BcIgOCMEm7PkqrtVf25KFWN4B8idaq2va4zUssMmfA5QLgrkvK1jXRdiXKq2qfbSSqfs7WuM1OvPsfFdOBmy9l8zmrUMsf8tVqO2ahGNapRh6e+AZkLzhog49P2AAAAAElFTkSuQmCC"

2. バイナリサポートの有効化

API Gatewayがリクエストの送受信でバイナリデータを扱えるようにバイナリサポートを有効にします。

$ aws apigateway update-rest-api \
--rest-api-id u5uko0b1ne \
--patch-operations op=add,path=/binaryMediaTypes/image~1png

u5uko0b1ne はそれぞれのAPI IDで置き換えるようにしてください。

3. API GatewayにLambda関数を呼び出す権限を与える

手動で作ると問題ないのに、CloudFormationで構成したときに上手く動かないので悩んでいたのですが、次の記事によるとCloudFormationで構成した場合は、AWS Consoleから権限を与える必要があるそうです。(他に方法はないのでしょうか?)

krisgholson/serverless-thumbnail: Recreate the thumbnail service described here .. https://aws.amazon.com/blogs/compute/binary-support-for-api-integrations-with-amazon-api-gateway/ .. using the serverless.com framework (and document some gotchas).

4. 再度のデプロイ

再度、デプロイします。

$ sls deploy
Serverless: Packaging service...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading service .zip file to S3 (6.26 MB)...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
..............
Serverless: Stack update finished...
Service Information
service: qrcode-service
stage: dev
region: ap-northeast-1
api keys:
  None
endpoints:
  GET - https://u5uko0b1ne.execute-api.ap-northeast-1.amazonaws.com/dev/qrcode
functions:
  qrcode: qrcode-service-dev-qrcode

APIを叩いてみると今度はバイナリデータが返ってくることが確認できました。

$ curl -X GET -H "Accept: image/png" -H "Content-Type: image/png" "https://u5uko0b1ne.execute-api.ap-northeast-1.amazonaws.com/dev/qrcode?text=Kappa"
�PNG

IHDR""u���IDATx��Mn�0����wn�����>J        �FӅm�J���Y b>�'g4~<Q���ըF5�Q{�$�E��cY�6�u        ʫ����aTUU�R��u        j,=�1�"v{]g����&��;��d��X�{�K��w
��(u��U��A0��$k��Q�d1�L\B-SX���n������򃽫?*E��=F�wZ\�S��ӽ�?6�������H���b9����T�]e�鳊|�f�/��F��j��!3@�>��m�I�ݝ
                                                                                                           ?@}i{�E�a�^k}���1�n����nϒ��U�(U��"u����3R�
                     ��9@�+��t]�r�ڧ�I*����3S�>��t�f��|�j�2��Z�٨F5�Q����
                                                                       � ���IEND�B`�i

以上です。

リポジトリ

完成品が下記になります。

bagpack/serverless-framework-qrcode-service: Sample for Binary Support using API Gateway REST API with Serverless Framework.

参考

Binary Support for API Integrations with Amazon API Gateway | AWS Compute Blog

krisgholson/serverless-thumbnail: Recreate the thumbnail service described here .. https://aws.amazon.com/blogs/compute/binary-support-for-api-integrations-with-amazon-api-gateway/ .. using the serverless.com framework (and document some gotchas).