こんにちは。beaglesoftの真鍋です。
先日ユーザー初期画像生成APIを公開しました。
blog.beaglesoft.net
AWS LambdaでAPIを作ることが楽しかったので、ソースコードとその手順をまとめたいと思います。なお、Pythonについては初心者なので何かお気づきの点があれば指摘いただけるとうれしいです。
今回のゴール
今回作ろうと思うものは、アルファベット二文字を表示する画像を作成しAPI Gatewayのレスポンスとして返却する処理です。よくあるユーザーアイコンの初期画像のようなものですね。今回はAWS Lambdaへバックエンドの処理を作成するところまでまとめたいと思います。API Gatewayの設定は次回に行います。
前提条件
今回実行した環境は以下の通りとなっています。
- 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点です。
- フォントファイルについて
フォントファイルをソースコードと同じディレクトリに保存してdraw_text_at_center
内で設定します。
- パッケージのインポートについて
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関数ができていることを確認してください。
API Gatewayの設定
ここまででAWS Lambdaをデプロイすることができました。このあと実際にAPIからレスポンスを返却するためにAPI Gatewayの設定を行い今回作成したLambda関数と連携することになります。この設定は次のエントリーでまとめたいと思います。