ユーザー初期画像生成APIをつくってみる(AWS Lambda編)

こんにちは。beaglesoftの真鍋です。

先日ユーザー初期画像生成APIを公開しました。

blog.beaglesoft.net

AWS LambdaでAPIを作ることが楽しかったので、ソースコードとその手順をまとめたいと思います。なお、Pythonについては初心者なので何かお気づきの点があれば指摘いただけるとうれしいです。

今回のゴール

今回作ろうと思うものは、アルファベット二文字を表示する画像を作成しAPI Gatewayのレスポンスとして返却する処理です。よくあるユーザーアイコンの初期画像のようなものですね。今回はAWS Lambdaへバックエンドの処理を作成するところまでまとめたいと思います。API Gatewayの設定は次回に行います。

f:id:beaglesoft:20180519092015p:plain

前提条件

今回実行した環境は以下の通りとなっています。

  • OS macOS Sierra 10.12.6
  • Python 3.6.5

雛形を作成する

Serverless Frameworkを利用して開発する雛形を用意したいと思います。Serverless FrameworkのインストールがまだのときにはServerless FrameworkがAWS Lambdaを使いやすくしてくれる - blog.beaglesoft.netを参考にインストールと概要を確認してください。

$ sls create -t aws-python3 -p user_image_creator-op
Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "/Users/ymanabe/projects/user_image_creator-op"
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.27.2
 -------'

Serverless: Successfully generated boilerplate for template: "aws-python3"

$ cd user_image_creator-op
$  user_image_creator-op ls -al
total 24
drwxr-xr-x   5 ymanabe  staff   170  5 16 13:36 .
drwxr-xr-x  46 ymanabe  staff  1564  5 16 13:36 ..
-rw-r--r--   1 ymanabe  staff   192  5 16 13:36 .gitignore
-rw-r--r--   1 ymanabe  staff   497  5 16 13:36 handler.py
-rw-r--r--   1 ymanabe  staff  2854  5 16 13:36 serverless.yml

プラグインを追加する

次にPythonのライブラリを管理するためserverless-python-requirements - npmをインストールします。

$ sls plugin install -n serverless-python-requirements
Serverless: Creating an empty package.json file in your service directory
Serverless: Installing plugin "serverless-python-requirements@latest" (this might take a few seconds...)
Serverless: Successfully installed "serverless-python-requirements@latest"

次にrequirements.txtを追加し、利用するPythonのライブラリを記述します。

numpy
Pillow

user_image_creator_ret_image/requirements.txt at master · beaglesoftjp/user_image_creator_ret_image · GitHub

以上で準備は完了です。

実装

今回は画像ファイルを作成してS3へ保存する処理を作成します。実装するソースコードは以下の通りとなります。

import base64
import json
import random
import urllib
import uuid

try:
    import unzip_requirements
except ImportError:
    pass

import numpy
from PIL import Image, ImageDraw, ImageFont

import logging

logging.basicConfig(
    format='%(asctime)s - %(threadName)s - %(module)s:%(funcName)s(%(lineno)d) - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

logger.info('Loading function')


def handle(event, context):
    logger.info("event:{event}".format(event=event))
    logger.info("context:{context}".format(context=context))

    body = event['queryStringParameters']
    s = body['s']

    if len(s) > 20:
        logger.error('Parameter error. Parameter s length is too long.')
        return response(400, {'message': "Parameter error. Parameter s length is too long."})

    display_str = urllib.parse.unquote(body['s'])
    logger.info(f"display_str:{display_str}")

    color_hash = get_color_hash()

    # サイズが150の正方形を生成する
    im = Image.new('RGB', (150, 150), color_hash['bg'])

    # テキストを中心に出力します
    draw_text_at_center(im, display_str, color_hash['font'])

    save_file_path = f"/tmp/{uuid.uuid4()}.png"
    im.save(save_file_path)

    return {
        "statusCode": 200,
        "headers": {
            "Content-Type": "image/png",
        },
        "body": base64.b64encode(open(save_file_path, 'rb').read()).decode('utf-8'),
        "isBase64Encoded": True}


def response(status_code, message):
    return {
        "isBase64Encoded": False,
        "statusCode": status_code,
        "body": json.dumps(message)
    }


# テキストを画像の中心に描画します
def draw_text_at_center(img, text, font_color):
    draw = ImageDraw.Draw(img)
    draw.font = ImageFont.truetype('GenJyuuGothicX-P-Bold.ttf', 48)
    img_size = numpy.array(img.size)
    txt_size = numpy.array(draw.font.getsize(text))
    pos = (img_size - txt_size) / 2

    draw.text(pos, text, font_color)


def get_color_hash():
    color_list = [
        {'font': (255, 255, 255), 'bg': (248, 4, 6)},
        {'font': (255, 255, 255), 'bg': (204, 0, 10)},
        {'font': (255, 255, 255), 'bg': (229, 72, 0)},  # 緋色
        {'font': (255, 255, 255), 'bg': (39, 38, 114)},  # 藍色
        {'font': (255, 255, 255), 'bg': (57, 3, 124)},  # 紺藍
        {'font': (255, 255, 255), 'bg': (11, 43, 21)},  # セルリアンブルー
        {'font': (255, 255, 255), 'bg': (251, 231, 9)},  # 卵色
        {'font': (255, 255, 255), 'bg': (228, 176, 55)},  # ブロンド
        {'font': (255, 255, 255), 'bg': (228, 162, 11)},  # 山吹色
        {'font': (255, 255, 255), 'bg': (51, 96, 69)},  # 深緑
    ]

    r = random.randrange(10)
    return color_list[r]


if __name__ == '__main__':
    subscriber_id = uuid.uuid4()
    user_id = uuid.uuid4()

    # ObjectKeyは /{subscriber_id}/user_images/{user_id}.png に保存する
    event = {'resource': '/create', 'path': '/create', 'httpMethod': 'GET', 'headers': None,
             'queryStringParameters': {'s': 'BS'}, 'pathParameters': None, 'stageVariables': None,
             'requestContext': {'path': '/create', 'resourceId': 'mmgrks',
                                'stage': 'test-invoke-stage', 'requestId': 'ae0367fe-5904-11e8-a178-259f37ad7e5e',
                                'resourcePath': '/create', 'httpMethod': 'GET'},
             'body': None, 'isBase64Encoded': False}

    handle(event, None)

user_image_creator_ret_image/handler.py at master · beaglesoftjp/user_image_creator_ret_image · GitHub

ポイントは以下の2点です。

  1. フォントファイルについて
    フォントファイルをソースコードと同じディレクトリに保存してdraw_text_at_center内で設定します。
  2. パッケージのインポートについて
    PILやNumpyを利用するためserverless-python-requirements - npmの設定に沿ってインポート部分に記述を追加します。

それぞれまとめます。

フォントファイルについて

今回フォントファイルを利用しています。そのためフォントファイルが適切に配置されていないとエラーになります。適宜フォントをダウンロードしてソースコードと同じディレクトリに保存してください。

なお、今回フォントは源柔ゴシック (げんじゅうゴシック) | 自家製フォント工房を利用させていただきました。ありがとうございます。

jikasei.me

また、フォントを画像の中心に配置する処理はこちらのエントリーを参考にいたしました。こちらもありがとうございます。

d.hatena.ne.jp

パッケージのインポートについて

PILやNumpyを利用していますが、インポートは以下の通り記述する必要があります。

try:
    import unzip_requirements
except ImportError:
    pass

import numpy
from PIL import Image, ImageDraw, ImageFont

これはserverless-python-requirements - npmでAWS Lambdaのサイズ制限を回避するためサイズの大きいパッケージを圧縮して管理しているためです。

www.npmjs.com

この記述がないとnumpyやPILのインポートエラーになりますので注意してください。

ローカルで動作させてみる

早速ローカルで動作させてみます。今回は画像の作成先ディレクトリを/tmpとしているためWindowsユーザーの方はWSLなどを利用してLinux環境上で動作するようにしてください。

$ python handler.py

2018-05-19 09:24:01,507 - MainThread - handler:<module>(22) - INFO - Loading function
2018-05-19 09:24:01,507 - MainThread - handler:handle(26) - INFO - event:{'resource': '/create', 'path': '/create', 'httpMethod': 'GET', 'headers': None, 'queryStringParameters': {'s': 'BS'}, 'pathParameters': None, 'stageVariables': None, 'requestContext': {'path': '/create', 'resourceId': 'mmgrks', 'stage': 'test-invoke-stage', 'requestId': 'ae0367fe-5904-11e8-a178-259f37ad7e5e', 'resourcePath': '/create', 'httpMethod': 'GET'}, 'body': None, 'isBase64Encoded': False}
2018-05-19 09:24:01,507 - MainThread - handler:handle(27) - INFO - context:None
2018-05-19 09:24:01,507 - MainThread - handler:handle(37) - INFO - display_str:BS

これで/tmpに画像が作成されます。

$ ls -al /tmp | grep png

# macの場合はこちら
$ ls -al /private/tmp | grep png
-rw-r--r--   1 ymanabe  wheel   2291 May 19 09:24 6a5cae15-ccd5-4e76-bc26-e304545a2b0e.png

画像ファイルが正しく表示されればOKです。

デプロイ

最後にデプロイを行います。が、その前に追加したフォントをAWS Lambdaへアップロードするパッケージに含めるためserverless.ymlに追記を行います。

package:
  include:
    - GenJyuuGothicX-P-Bold.ttf

user_image_creator_ret_image/serverless.yml at master · beaglesoftjp/user_image_creator_ret_image · GitHub

何か追加で含めたいファイルや逆に除外したいファイルについては上記のようにserverless.ymlに追加することで対応できます。これは結構便利ですね。

続いてデプロイを行います。デプロイはsls deployで実行できます。今回はproductionモードでデプロイしたいと思いますのでs- prodをつけます。

$ sls deploy -v -s prod

...
Serverless: Stack update finished...
...

Serverless: Stack update finished...と出れば正常に終了しています。早速AWS コンソールでLambda関数ができていることを確認してください。

f:id:beaglesoft:20180519094507p:plain

API Gatewayの設定

ここまででAWS Lambdaをデプロイすることができました。このあと実際にAPIからレスポンスを返却するためにAPI Gatewayの設定を行い今回作成したLambda関数と連携することになります。この設定は次のエントリーでまとめたいと思います。

Amazon Web Servicesを使ったサーバーレスアプリケーション開発ガイド

Amazon Web Servicesを使ったサーバーレスアプリケーション開発ガイド