重いライブラリを使用したPythonスプリプトをAPIとして使いたかったときの話 ①

· 2107 words · 5 minute read

Python製のパッケージをAPIとして使いたいときは、chaliceなりserverlessなりでaws Lambdaを使う方法が早いと思います。しかし、比較的容量の重いOpenCVのようなライブラリを用いて画像処理を行うような要件の場合、aws Lambdaでは何かと融通が効かず不便でした。今回はそのときのことを書いていきたいと思います。


ことのはじまり 🔗

メインサーバで動かしていたPythonによる画像処理がそれなりにサーバ負荷を与えていたので別サーバに移したくて。新しく別のサーバでAPIとして動かせばいいかなみたいな。画像処理とはいってもGPUを使用するようなメソッドを内部で使用していなかったのでLambdaで動けばそれが低コストでいいなっていうのもありました。

いざはじめてみるとpipで管理していたOpenCVパッケージが重く、そのままではLambdaレイヤーとしてzipでアップロードできませんでした。 aws Lambdaはクォータという割り当て量の制限が設けられていて、デプロイパッケージ(.zipファイルアーカイブ)のサイズは50MB、解凍後250MBまでと定められています。OpenCVは依存パッケージであるNumpyも含むとpipでは60MB程あり普通に圧縮してUPができません。

aws-lambda-quota

関数の設定、デプロイ、実行 - Lambda クォータ

ググると色々とLambdaでOpenCV動かしている記事を見つけましたが、一旦ローカルDocker環境でLambda立ち上げてzipレイヤーをオリジナルで作るか、serverlessの拡張ライブラリのようなものでレイヤーを自動生成するかみたいな方法で、それだったらサーバ立てた方がいいなってなりました。サーバ色々いじれた方が融通が効きやすいと思ったのと、そもそもあまり重くはないといえ画像処理の負荷的にLambdaでは不安。結局サーバ立てないとってなりそうなので。Lambdaサーバってレイヤーに依存パッケージを入れるのが基本のようで、Pythonライブラリを複数使ってあーだこーだしたり、重い処理を行ったりするのは適さないんだなと。


実装したこと 🔗

  • Pythonパッケージのアプリ化(API)
    • Flask + mod-wsgiライブラリを利用
  • aws EC2環境構築
    • 必要パッケージイストール
    • 依存ライブラリのインストール
    • Apache設定

環境 🔗

  • python 3.7.9

  • pip 21.2.4

  • amazon Linux 2

  • Apache 2.4.48

手順 🔗

1, FlaskでAPIを作成 🔗

APIを1つ用意するだけで事足りたので、今回はFlaskを使ってみることにしました。軽量なフレームワーク(というかライブラリな感覚)で低コストに開発したかったのもあります。

Flaskの簡単な使い方 - Qiita

自分の作成したディレクトリ構造は以下の通りです。(主要部のみ抜粋)

root/
 ├ config/
 │ ├ development.json
 │ ├ production.json
 │ └ staging.json
 ├ package/
 │ └ ... 画像処理Pythonパッケージ(これをAPIにしたかった)
 ├ src/
 │ ├ module/
 │ │ └ ... アプリケーションモジュール(コントローラ的な処理)記述用
 │ ├ script/
 │ │ └ ... ビジネスロジック記述用
 │ └ config.py
 ├ app.py
 ├ requirements.txt
 ├ staging.wsgi
 └ production.wsgi

主要ファイルごとに内容と詳細を載せていきます。ファイルやディレクトリの命名などなんとなくなので、気になる部分は個人で好きなようにつけてください。


app.py

from flask import Flask
from src.config import config
from src.module import index, execute_xxxxx

app = Flask(__name__)

app.register_blueprint(index.module)
app.register_blueprint(execute_ncode.module,
                       url_prefix=config('URL_PREFIX'))

if __name__ == '__main__':
    app.run()

Flaskのメインファイル。自分の場合は@app.route('/')などの記述を別ファイルで書いているため、ここではルーティング用のモジュールとconfig(環境変数)のインポートのみを行っています。

Flaskではregister_blueprintでアプリケーションをモジュール化できます。

Blueprintを使ったアプリケーションのモジュール化 — Flask Documentation 2.0.x

URL_PREFIXは環境変数として、後述するconfigモジュールで指定した値を引っ張ってきています。/api/v1的なやつを入れてます。


src / module / index.py

from flask import Blueprint

MODULE_NAME = 'index'

module = Blueprint(MODULE_NAME, __name__)


@module.route('/')
def execute():
    return 'This is root.'

とりあえずAPIのURLのルートを叩かれたときに何か表示しておくために書きました。Blueprintを使用するために必要なアプリケーションモジュールになります。書き方がとても単純。


src / module / execute_xxxxx.py

from flask import Blueprint, request, jsonify
from src.script import script_xxxxx

MODULE_NAME = 'execute_xxxxx'

module = Blueprint(MODULE_NAME, __name__)


@module.route(MODULE_NAME, methods=['POST'])
def execute():
    try:
        response = script_xxxxx.execute(request.form['data'])
        return jsonify(response)
    except Exception as e:
        raise ...

こちらがAPIの実装。POSTリクエストでリクエストボディにdataを渡したときの記述です。 script_xxxxxはビジネスロジックを書いているモジュールになります。内部で画像処理Pythonパッケージを呼び出して今回やりたかった処理を記述しています。

Flaskが用意しているrequestモジュールとjsonifyモジュールを使用することでJSON形式のAPIをこのように作成できます。


src / config.py

import os
import json


def config(key):
    env = os.environ.get('FLASK_ENV') or 'development'
    root_path = os.environ.get('APP_ROOT_PATH') or os.getcwd()

    with open('{}/config/{}.json'.format(root_path, env)) as f:
        return json.load(f)[key]

configの呼び出し関数を定義しています。これによってconfig/development.jsonの内容にアクセスでき、

from src.config import config

url = config('URL_PREFIX')

のような書き方で環境に応じた変数を取得可能です。


少し長くなってきたので、EC2へのデプロイ手順等の続きは次回にします。

以上ありがとうございました。