0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

既存の Flask アプリを最短で AWS Lambda に載せる

0
Posted at

はじめに

「Flask で作った API がすでにある」

「でも、EC2 や ECS を立てるほどではない」

「とにかく早く AWS Lambda に載せたい」

そんなときに使える構成です。

この記事では、Flask アプリを大きく書き換えずに、AWS Lambda + API Gateway 上で動かす方法を紹介します。

ポイントは以下です。

  • Flask の @app.route() はそのまま使う
  • Lambda handler で API Gateway の event を Flask に渡す
  • Lambda はコンテナイメージとしてデプロイする
  • Serverless Framework で API Gateway と Lambda を定義する
  • Lambda 用ではない Docker image も使える

全体構成

構成はこんな感じです。

Client
  ↓
API Gateway
  ↓
Lambda
  ↓
aws-wsgi
  ↓
Flask app

Lambda ネイティブに全部作り直すのではなく、API Gateway のリクエストを WSGI に変換して、Flask アプリに渡します。

ディレクトリ構成

今回は最小構成として、以下のようにします。

.
├── app.py
├── requirements.txt
├── Dockerfile
├── serverless.yml
└── package.json

Flask アプリを書く

まずは普通の Flask アプリを書きます。

# app.py
from flask import Flask, jsonify, request
import awsgi

app = Flask(__name__)


@app.route("/health_check", methods=["GET"])
def health_check():
    return jsonify({"status": "ok"})


@app.route("/hello", methods=["POST"])
def hello():
    body = request.get_json(silent=True) or {}
    name = body.get("name", "world")

    return jsonify({
        "message": f"hello, {name}"
    })


def handler(event, context):
    return awsgi.response(app, event, context)


if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0", port=5000)

重要なのはここです。

def handler(event, context):
    return awsgi.response(app, event, context)

aws-wsgi を使うことで、API Gateway から渡される event / context を Flask が扱える形に変換できます。

つまり、Flask 側はいつも通り request@app.route() を使えます。

requirements.txt

Flask==3.0.3
aws-wsgi==0.2.7

パッケージ名は aws-wsgi ですが、Python 側では import awsgi します。

Dockerfile

まずは一番シンプルに、AWS 公式の Lambda Python ベースイメージを使う例です。

FROM public.ecr.aws/lambda/python:3.11

WORKDIR ${LAMBDA_TASK_ROOT}

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

CMD ["app.handler"]

AWS Lambda 用の Python ベースイメージを使うと、Lambda Runtime Interface Client が最初から入っているので楽です。

CMD ["app.handler"] は、

app.py の handler 関数を Lambda handler として使う

という意味です。

ここで大事なのは、Lambda コンテナでは Flask の HTTP サーバーを直接起動しないことです。

通常の Flask なら以下のように HTTP サーバーを起動します。

flask run
# or
gunicorn app:app

しかし Lambda コンテナでは、Lambda Runtime が handler を呼び出します。

そのため Dockerfile では、flask rungunicornCMD に書くのではなく、Lambda handler を指定します。

Serverless Framework を入れる

package.json は最小だと以下のようになります。

{
  "dependencies": {
    "serverless": "^3.39.0"
  },
  "devDependencies": {}
}

インストールします。

npm install

serverless.yml

service: flask-on-lambda

frameworkVersion: "3"

provider:
  name: aws
  region: ap-northeast-1
  stage: ${opt:stage, "dev"}
  timeout: 30
  memorySize: 512
  ecr:
    images:
      flaskApp:
        path: .

functions:
  api:
    image:
      name: flaskApp
    events:
      - http:
          path: /health_check
          method: get
      - http:
          path: /hello
          method: post

provider.ecr.imagespath: . を指定すると、Serverless Framework が Docker image を build して ECR に push し、その image を Lambda に設定してくれます。

API のエンドポイントは Dockerfile ではなく、serverless.ymlevents で決まります。

役割を分けると、以下のようになります。

  • Dockerfile: Lambda が呼ぶ handler を指定する
  • serverless.yml: API Gateway の URL/path を指定する
  • Flask: path に対応する処理を書く

デプロイする

npx serverless deploy --stage dev

デプロイが終わると、API Gateway の URL が表示されます。

例:

endpoints:
  GET - https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/health_check
  POST - https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello

動作確認

curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/health_check
{"status":"ok"}

POST も試します。

curl -X POST \
  https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/hello \
  -H "Content-Type: application/json" \
  -d '{"name":"lambda"}'
{"message":"hello, lambda"}

これで Flask アプリが Lambda 上で動きました。

Lambda 用ではない Docker image も使える

Lambda コンテナというと、まず AWS 公式の Lambda ベースイメージを使う方法があります。

FROM public.ecr.aws/lambda/python:3.11

これは一番簡単です。

ただ、実際のアプリでは OS パッケージやネイティブライブラリの都合で、Ubuntu などの独自ベースイメージを使いたいことがあります。

その場合でも、Lambda コンテナとして動かすことはできます。

必要なのは Lambda Runtime Interface Client です。

RUN pip install awslambdaric

そして ENTRYPOINT で awslambdaric を起動します。

ENTRYPOINT ["python", "-m", "awslambdaric"]
CMD ["app.handler"]

これで、Lambda がコンテナを起動したときに Runtime Interface Client が立ち上がり、app.handler を呼んでくれます。

つまり、ベースイメージ自体が Lambda 専用でなくても、以下を満たせば Lambda コンテナとして動かせます。

  • Lambda Runtime Interface Client が入っている
  • ENTRYPOINT で Runtime Interface Client を起動している
  • CMD で handler を指定している

例えば Ubuntu 系の image でも、次のようにできます。

FROM ubuntu:22.04

WORKDIR /var/task

RUN apt-get update && apt-get install -y \
    python3 \
    python3-pip

COPY requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt
RUN pip3 install awslambdaric

COPY . .

ENTRYPOINT ["python3", "-m", "awslambdaric"]
CMD ["app.handler"]

この構成のメリットは、Lambda 用ベースイメージに縛られず、アプリに必要な OS パッケージやネイティブライブラリを入れやすいことです。

例えば以下のようなケースでは便利です。

  • apt install で OS パッケージを入れたい
  • C/C++ 系の依存ライブラリがある
  • 画像処理・数値計算・機械学習系のライブラリを使う
  • 既存の Dockerfile をなるべく流用したい
  • Lambda 公式イメージでは環境構築が面倒

import エラーが出る場合は、どの Python が呼ばれているか確認する

独自ベースイメージを使う場合、awslambdaric を起動する Python と、依存ライブラリをインストールした Python がズレることがあります。

例えば、Dockerfile で以下のように書いたとします。

RUN pip install -r requirements.txt
RUN pip install awslambdaric

ENTRYPOINT ["python", "-m", "awslambdaric"]
CMD ["app.handler"]

一見よさそうに見えます。

しかし環境によっては、python/usr/bin/python を指していて、実際にライブラリを入れた venv や pyenv の Python とは別物になっていることがあります。

この状態で Lambda を起動すると、以下のような import エラーが出ることがあります。

Runtime.ImportModuleError: Unable to import module 'app'
ModuleNotFoundError: No module named 'xxx'

この場合は、まずどの Python が呼ばれているか確認します。

RUN which python
RUN python --version
RUN python -m pip list

また、venv や pyenv を使っている場合は、その Python を明示的に指定します。

ENTRYPOINT ["/path/to/venv/bin/python", "-m", "awslambdaric"]
CMD ["app.handler"]

自分の環境では、python だけで指定すると /usr/bin/python が呼ばれてしまい、依存ライブラリを入れた Python とズレて import エラーになりました。

そのため、venv 側の Python を直接指定しました。

ENTRYPOINT ["/opt/pysetup/.venv/bin/python", "-m", "awslambdaric"]
CMD ["app.handler"]

pyenv を使っている場合は、例えば以下のようになります。

ENTRYPOINT ["/root/.pyenv/shims/python", "-m", "awslambdaric"]
CMD ["app.handler"]

ポイントは、pip install した先の Python と、awslambdaric を起動する Python を一致させることです。

Lambda コンテナの import エラーは、コードの問題ではなく「起動している Python が違う」だけのことがあります。

import エラーが出たときは、アプリのコードを疑う前に、

which python
python --version
python -m pip list

で、Lambda コンテナ内で実際に使われている Python を確認すると原因に近づきやすいです。

この構成のうれしいところ

この構成の一番のメリットは、Flask アプリを Lambda 用に大きく書き換えなくていいことです。

例えば、すでにこういう Flask アプリがある場合でも、

@app.route("/users", methods=["POST"])
def create_user():
    ...

基本的にはそのまま使えます。

Lambda handler を1つ用意して、awsgi.response() に Flask app を渡せば、API Gateway からのリクエストを Flask に流せます。

また、コンテナイメージとしてデプロイするので、依存関係が多少重い場合でも ZIP デプロイより扱いやすいです。

さらに、Lambda 公式ベースイメージ以外の Docker image も使えます。

Ubuntu などに OS パッケージを入れて、そのまま Lambda コンテナにできるのは、既存の Dockerfile 資産を流用したい場合にかなり便利です。

デメリット

もちろん、この構成は万能ではありません。

「最短で Flask を Lambda に載せる」ための構成であって、「Lambda に最適化された設計」ではありません。

注意点はあります。

  • コールドスタートが重くなりやすい
  • 複数 API を1つの Lambda にまとめると、API ごとの timeout / memory 調整がしづらい
  • 小さな修正でもコンテナイメージを build / push する必要がある
  • API Gateway の制約を受ける
  • 長時間処理には向かない
  • Flask/Gunicorn でのローカル実行と Lambda 実行で差が出ることがある

特に依存ライブラリが重い場合、import 時間がそのままコールドスタートに効いてきます。

小話: Numba の AOT

Python で重い数値計算をしている場合、Numba を使うことがあります。

Numba の @njit は便利ですが、JIT なので初回実行時にコンパイルが走ります。

Lambda ではこの初回コンパイルが、コールドスタートや初回リクエストの遅さとして見えることがあります。

その場合、処理によっては AOT、つまり Ahead-of-Time compile を検討できます。

from numba.pycc import CC
from numba import njit

cc = CC("my_math")


@cc.export("add", "int64(int64, int64)")
@njit
def add(a, b):
    return a + b


if __name__ == "__main__":
    cc.compile()

これをビルドすると、.so のような compiled extension が生成されます。

from my_math import add

print(add(1, 2))

Lambda コンテナイメージにこの compiled extension を含めておけば、実行時に毎回 JIT コンパイルする必要がなくなります。

ただし、numba.pycc は将来的な非推奨・置き換え対象として扱われているため、新規採用する場合は注意が必要です。

「こういう逃がし方もある」くらいの小話として覚えておくとよさそうです。

まとめ

Flask アプリを AWS Lambda に載せるとき、必ずしも Lambda 用に全面的に書き換える必要はありません。

aws-wsgi を使えば、API Gateway の event を Flask に渡せます。

さらに Lambda コンテナイメージを使えば、依存関係がある程度重い Flask アプリでも載せやすくなります。

この構成は、次のようなケースに向いています。

  • 既存の Flask アプリをまず Lambda に載せたい
  • Lambda ネイティブに作り直す時間がない
  • ZIP デプロイでは依存関係がつらい
  • Ubuntu などの既存 Docker image を流用したい
  • まず動くものを早く作りたい

一方で、コールドスタートやデプロイ単位、API ごとの細かい最適化には注意が必要です。

最初の一歩としてはかなり便利ですが、本格運用するなら Lambda らしい設計に寄せるかどうかも後で検討するとよさそうです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?