ユーザー初期画像生成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を使ったサーバーレスアプリケーション開発ガイド

ユーザーの初期画像に便利なAPIを作りました

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

いま構築しているシステムで欲しい機能だけれどもなかったから作ってみたAPIにユーザー名に該当する文字列を設定すると特定のサイズの画像を生成するAPIを作りました。今回のAPIは引数に設定された文字を埋め込んで適当な背景の画像を作成します。

f:id:beaglesoft:20180519094745p:plain

Auth0とかどこかのOAuthとかを利用しているなら特に自分で用意する必要はないのですが、組み込みで認証を自分で用意しているとちょっと欲しいけど作るには面倒だなぁというものではないでしょうか。(私はASP.NET Securityを利用しています。)

利用するには

以下の通り実行すれば画像を取得できます。(Windowsの方はWSLなどでcurlを利用できるようにして実行してみてください。)

$ curl "https://api.camello.info/uc-ri/img?s=BS" -H 'x-api-key:gR8u8SlQj71huEjxn1RmF9PmsbVyrDhf4gzVDURC' -o ~/sample.png

f:id:beaglesoft:20180519092015p:plain

日本語もURLエンコードすると取得できます。

$ curl "https://api.camello.info/uc-ri/img?s=%E7%9C%9F%E9%8D%8B" -H 'x-api-key:gR8u8SlQj71huEjxn1RmF9PmsbVyrDhf4gzVDURC' -v -o ~/sample.png

f:id:beaglesoft:20180519094745p:plain

パラメーター

パラメーターは以下の通りとなります。注意点として、日本語はURLエンコードするようにしてください。

引数名 必須 データ型 内容 備考
s String 設定する文字 2文字程度のアルファベットまたは日本語を想定しています。

APIキー

APIGatewayを利用して公開していますが、APIキーは以下の通りとなります。

gR8u8SlQj71huEjxn1RmF9PmsbVyrDhf4gzVDURC

このキーは利用者全体で共有していますが、月間リクエストが10,000件まで利用可能としています。

画像サイズなど

画像サイズは今のところ150x150に固定しています。また、フォントはipag.ttfを利用して48ポイント固定です。背景色は以下の通り10種類作成してその中からランダムに利用しています。

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)
    print(f"r:{r}")
    return color_list[r]

システムの構成

このAPIはAWS LambdaでPythonを利用してバックエンドを作成し、API GatewayでWebAPIとして公開しています。Serverless Fraemworkを利用してます。

最後に

Serverlessというか、マイクロサービスというか、そういうものは仕組みが整っていると対象とする用途に強い言語で開発ができることはとても素晴らしいのではと思います。最初はとっつきづらい感じがあっても慣れれば便利なので是非試してみて楽しんでもらえればと思います。

追記

画像のフォントを変更しました。こちらを利用させていただきました。ありがとうございます。

jikasei.me

追記2

このAPIのソースコードや構成をまとめました!

blog.beaglesoft.net

Serverless FrameworkがAWS Lambdaを使いやすくしてくれる

Serverless Framework

Serverless Framework - Build applications on AWS Lambda, Google CloudFunctions, Azure Functions, AWS Flourish and moreはあまたあるクラウドベンダーのサーバーレスアプリケーションを効率よく開発するためのスキャフォールド(足場)やワークフローの自動化などを提供するCLIツールです。イメージとしてはDockerHubのLambda版という感じだと思います。

serverless/serverless: Serverless Framework – Build web, mobile and IoT applications with serverless architectures using AWS Lambda, Azure Functions, Google CloudFunctions & more! –

インストール

インストールはnpmコマンドで実行します。

npm install -g serverless

インストールが完了したらバージョンを確認します。

$ sls -v
1.27.2

createコマンド

Serverless Frameworkでは以下の通りServerlessアプリケーションの雛形を生成することができます。こうしてみるといろいろな雛形があります。

sls create --help
Plugin: Create
create ........................ Create new Serverless service
    --template / -t .................... Template for the service. Available templates: "aws-nodejs", "aws-nodejs-typescript", "aws-nodejs-ecma-script", "aws-python", "aws-python3", "aws-groovy-gradle", "aws-java-maven", "aws-java-gradle", "aws-kotlin-jvm-maven", "aws-kotlin-jvm-gradle", "aws-kotlin-nodejs-gradle", "aws-scala-sbt", "aws-csharp", "aws-fsharp", "aws-go", "aws-go-dep", "azure-nodejs", "fn-nodejs", "fn-go", "google-nodejs", "kubeless-python", "kubeless-nodejs", "openwhisk-java-maven", "openwhisk-nodejs", "openwhisk-php", "openwhisk-python", "openwhisk-swift", "spotinst-nodejs", "spotinst-python", "spotinst-ruby", "spotinst-java8", "webtasks-nodejs", "plugin" and "hello-world"
    --template-url / -u ................ Template URL for the service. Supports: GitHub, BitBucket
    --template-path .................... Template local path for the service.
    --path / -p ........................ The path where the service should be created (e.g. --path my-service)
    --name / -n ........................ Name for the service. Overwrites the default name of the created service.

最初のプロジェクト

今回は試しにAWS LabmdaへPython3.6を利用したサンプルを作成してみたいと思います。なお、利用する環境ではAWSのアクセスキーとアクセスキーIDはあらかじめ設定されていてaws cli でAWSへアクセスできることを確認してください。

sampleプロジェクトを作成するためcreateコマンドを利用します。

$ sls create -t aws-python3 -p sample

Serverless: Generating boilerplate...
Serverless: Generating boilerplate in "/Users/ymanabe/projects/sandbox/sample"
 _______                             __
|   _   .-----.----.--.--.-----.----|  .-----.-----.-----.
|   |___|  -__|   _|  |  |  -__|   _|  |  -__|__ --|__ --|
|____   |_____|__|  \___/|_____|__| |__|_____|_____|_____|
|   |   |             The Serverless Application Framework
|       |                           serverless.com, v1.27.2
 -------'

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

これでサンプルプロジェクトが作成されましたので、サンプルプロジェクトへ移動します。

$ cd sample
$ ls -al

total 24
drwxr-xr-x  5 ymanabe  staff   170  5 13 16:27 .
drwxr-xr-x  4 ymanabe  staff   136  5 13 16:27 ..
-rw-r--r--  1 ymanabe  staff   192  5 13 16:27 .gitignore
-rw-r--r--  1 ymanabe  staff   497  5 13 16:27 handler.py
-rw-r--r--  1 ymanabe  staff  2839  5 13 16:27 serverless.yml

このようにhandler.pyserverless.ymlというファイルが作成されています。

  • handler.py:Lambda関数として処理を記述するファイル
  • serverless.yml:Serverless FrameworkでLambdaを利用するための設定を記述するファイル

Lambda関数を作成する

ここまでで作成された内容をそのままAWS LambdaへLambda関数として作成してみたいと思いますが、このまま実行するとus-east-1にLambda関数が作成されます。できれば最初はap-northeast-1がいいという場合には以下の通り設定を変更してください。

@@ -23,7 +23,7 @@ provider:

 # you can overwrite defaults here
 #  stage: dev
-#  region: us-east-1
+  region: ap-northeast-1

 # you can add statements to the Lambda function's IAM Role here
 #  iamRoleStatements:

実際に実行してみます。実行はdeployコマンドで実行します。(下記はus-east-1で実行したログです。)

$ sls deploy -v -s dev
Serverless: Packaging service...
Serverless: Excluding development dependencies...
Serverless: Creating Stack...
Serverless: Checking Stack create progress...
CloudFormation - CREATE_IN_PROGRESS - AWS::CloudFormation::Stack - sample-dev
CloudFormation - CREATE_IN_PROGRESS - AWS::S3::Bucket - ServerlessDeploymentBucket
CloudFormation - CREATE_IN_PROGRESS - AWS::S3::Bucket - ServerlessDeploymentBucket
CloudFormation - CREATE_COMPLETE - AWS::S3::Bucket - ServerlessDeploymentBucket
CloudFormation - CREATE_COMPLETE - AWS::CloudFormation::Stack - sample-dev
Serverless: Stack create finished...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Uploading service .zip file to S3 (390 B)...
Serverless: Validating template...
Serverless: Updating Stack...
Serverless: Checking Stack update progress...
...
Serverless: Stack update finished...
Service Information
service: sample
stage: dev
region: us-east-1
stack: sample-dev
api keys:
  None
endpoints:
  None
functions:
  hello: sample-dev-hello

Stack Outputs
HelloLambdaFunctionQualifiedArn: arn:aws:lambda:us-east-1:123456789:function:sample-dev-hello:1
ServerlessDeploymentBucketName: sample-dev-serverlessdeploymentbucket-123456789

上記の通り

Serverless: Stack update finished...

と出力されればリリースは完了となります。

コンソールで確認する

正しくLambda関数が作成されたかをAWS コンソールで確認してみます。

image.png (259.4 kB)

Pythonのライブラリも含められる

Pythonを利用するときにはたいていNumPy — NumPyPillow — Pillow (PIL Fork) 5.1.1 documentationを利用します。これらはAWS Lambdaの実行環境には含められていないため、Lambda関数を作成するときにパッケージとして含める必要があります。

このような処理についてもServerless Frameworkは対応していてDockerコンテナを利用してAWS Lambdaで動作するパッケージを作成してデプロイまで自動化することができます。

この辺はまた別な機会にまとめたいと思いますが、とても便利なツールです。

さいごに

マイクロサービスの一環としてServerlessなAPIを構築することが結構多くなってきました。対象とする機能ごとにそれぞれ得意な言語を利用してAPIから呼び出せる仕組みは適材適所でありより一層利用する機会が増えてくると思います。

そんな中でServerless FrameworkのようなCLIツールが有ることはとてもありがたいことです。このお陰で処理を開発することに注力できるようになります。素晴らしいことですね。

補足

AWS LambdaがよくわからないときはいきなりServerless Frameworkとか利用しないでこちらの書籍を読むといいと思います。

実践AWS Lambda ~「サーバレス」を実現する新しいアプリケーションのプラットフォーム~

実践AWS Lambda ~「サーバレス」を実現する新しいアプリケーションのプラットフォーム~

言語ごとの説明もいくつかあるので実際にはNodeかPythonのサンプルをみて試してみて、実際に幾つかコンソールへ貼り付けたりS3からデプロイしてみてからのほうがServerless Frameworkの便利さはわかると思います。いずれにしてもAWS LambdaやAzure Functionsはとてもおもしろいので食わず嫌いせずに試してみてください!