🤖

PythonをLambda関数としてデプロイする方法

2024/07/07に公開

この方法を模索してきましたが、ついにすべてのピースを組み合わせることができました。

AWS Lambdaは、Pythonで書かれた関数をホストすることができます。これは「ゼロスケール」- 私の大好きなサーバーレスの定義です!- つまり、実際にトラフィックが発生した場合にのみ料金が発生し、トラフィックがないプロジェクトは運用コストがかかりません。

以前は、これらの関数をトリガーする動作するURLを取得するために多くの追加手順が必要でしたが、Lambda Function URLsがリリースされ、そのプロセスが劇的に簡素化されました。

それでもなお、まだ多くのステップがあります。ここでは、PythonウェブアプリケーションをLambda関数としてデプロイする方法を紹介します。

AWS CLIツールの設定

これを行ったのはずいぶん前のことで、方法を覚えていません。AWSアカウントが必要で、AWS CLIツールをインストールして設定する必要があります。

aws --versionコマンドは、バージョン番号1.22.90以上を返すべきです。というのも、そのバージョンで関数URLサポートが追加されたためです。

私のツールのバージョンが古すぎたため、以下の方法でアップグレードする方法を見つけました:

head -n 1 $(which aws)

出力:

#!/usr/local/opt/python@3.9/bin/python3.9

これにより、ツールが含まれているPython環境の場所が分かります。そのパスを編集して次のようにアップグレードしました:

/usr/local/opt/python@3.9/bin/pip3 install -U awscli

Pythonハンドラー関数の作成

以下は、Pythonハンドラー関数としての「Hello World」です。これをlambda_function.pyに入れます:

def lambda_handler(event, context): 
    return {
        "statusCode": 200,
        "headers": {
            "Content-Type": "text/html"
        },
        "body": "<h1>Hello World from Python</h1>"
    }

ZIPファイルに追加

これは最も直感的でない部分です。Lambda関数はZIPファイルとしてデプロイされます。このZIPファイルには、Pythonコードとそのすべての依存関係が含まれている必要があります - その詳細は後述します。

最初の関数には依存関係がないので、はるかに簡単です。以下のようにしてZIPファイルに変換し、デプロイの準備をします:

zip function.zip lambda_function.py

ポリシーを持つロールの作成

Lambda関数を初めてデプロイする際にのみこれを行う必要があります。他の手順に使用できるIAMロールが必要です。

このコマンドは、lambda-exという名前のロールを作成します:

aws iam create-role \
  --role-name lambda-ex \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [{
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"}
    ]}'

次にこれを実行する必要があります。なぜこれがcreate-roleコマンドの一部として処理できないのか分かりませんが、必要です:

aws iam attach-role-policy \
  --role-name lambda-ex \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

AWSアカウントIDの取得

次のステップに必要なAWSアカウントIDを確認する必要があります。

以下のコマンドを実行して確認します:

aws sts get-caller-identity \
  --query "Account" --output text

これを環境変数に割り当てて後で使用するようにします:

export AWS_ACCOUNT_ID=$(
  aws sts get-caller-identity \
  --query "Account" --output text
)

これが機能するか確認するために以下を実行します:

echo $AWS_ACCOUNT_ID

関数のデプロイ

今、ZIPファイルを新しいLambda関数としてデプロイできます!

一意の関数名を選択します - 私はlambda-python-hello-worldを選びました。

次に以下を実行します:

aws lambda create-function \
  --function-name lambda-python-hello-world \
  --zip-file fileb://function.zip \
  --runtime python3.9 \
  --handler "lambda_function.lambda_handler" \
  --role "arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda-ex"

ここでは、function.zipファイルをpython3.9ランタイムを使用してデプロイするよう指定しています。

Pythonファイルがlambda_function.pyと呼ばれ、関数がlambda_handlerと呼ばれていたため、lambda_function.lambda_handlerをハンドラーとしてリストしています。

すべてが順調に行けば、以下のような応答が返されるはずです:

{
    "FunctionName": "lambda-python-hello-world",
    "FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:lambda-python-hello-world",
    "Runtime": "python3.9",
    "Role": "arn:aws:iam::123456789012:role/lambda-ex",
    "Handler": "lambda_function.lambda_handler",
    "CodeSize": 332,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2023-09-19T02:27:18.213+0000",
    "CodeSha256": "Y1nCZLN6KvU9vUmhHAgcAkYfvgu6uBhmdGVprq8c97Y=",
    "Version": "$LATEST",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "316481f5-7934-4e54-914f-6b075bb7d9dd",
    "State": "Pending",
    "StateReason": "The function is being created.",
    "StateReasonCode": "Creating",
    "PackageType": "Zip",
    "Architectures": [
        "x86_64"
    ],
    "EphemeralStorage": {
        "Size": 512
    }
}

実行許可の付与

すべてが動作するために必要なこの魔法のコマンドも実行します:

aws lambda add-permission \
  --function-name lambda-python-hello-world \
  --action lambda:InvokeFunctionUrl \
  --principal "*" \
  --function-url-auth-type "NONE" \
  --statement-id url

関数にURLを付与

ブラウザで関数をトリガーするためのURLが必要です。

以下のようにして、デプロイされた関数に新しい関数URLを追加します:

aws lambda create-function-url-config \
  --function-name lambda-python-hello-world \
  --auth-type NONE

この--auth-type NONEは、インターネット上の誰でもURLにアクセスして関数をトリガーできることを意味します。

これにより、以下のような結果が返されるはずです:

{
    "FunctionUrl": "https://example-lambda-url.lambda-url.us-east-1.on.aws/",
    "FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:lambda-python-hello-world",
    "AuthType": "NONE",
    "CreationTime": "2023-09-19T02:27:48.356967Z"
}

確かに、https://example-lambda-url.lambda-url.us-east-1.on.aws/ にアクセスすると「Hello World from Python」が表示されます。

関数の更新

関数をデプロイした後、更新するのは非常に簡単です。

新しいfunction.zipファイルを作成します - 私はこのようにします:

rm -f function.zip # 既に存在する場合は削除
zip function.zip lambda_function.py 

次に、以下のようにして更新をデプロイします:

aws lambda update-function-code \
  --function-name lambda-python-hello-world \
  --zip-file fileb://function.zip

純粋なPython依存関係の追加

プロジェクトに依存関係を追加することは、このプロセス全体で最も混乱する部分でした。

最終的に、良い方法を見つけました。これは、PixegamiのYouTubeビデオに付随する例コードとして公開されていました。

コツは、すべての依存関係をZIPファイルのルートに含めることです。

requirements.txtなどは無視します。実際の依存関係のコピーをインストールする必要があります。

私にとってうまくいったレシピは次のとおりです。まず、依存関係をリストしたrequirements.txtファイルを作成します:

cowsay

次に、pip install -tコマンドを使用してこれらの依存関係を特定のディレクトリにインストールします - 私はlibを使用します:

python3 -m pip install -t lib -r requirements.txt

libにファイルがあることを確認するためにls -lah libを実行します。

ls lib | cat
bin
cowsay
cowsay-5.0-py3.10.egg-info

次に、このレシピを使用してlib内のすべてをZIPファイルのルートに追加します:

(cd lib; zip ../function.zip -r .)

このコマンドを実行してZIPファイル内のファイルリストを確認できます:

unzip -l function.zip

lambda_function.pyを更新してcowsayライブラリを示します:

import cowsay


def lambda_handler(event, context): 
    return {
        "statusCode": 200,
        "headers": {
            "Content-Type": "text/plain"
        },
        "body": cowsay.get_output_string("pig", "Hello world, I am a pig")
    }

更新したlambda_function.pyを再びZIPファイルに追加します:

zip function.zip lambda_function.py

更新をデプロイします:

aws lambda update-function-code \
  --function-name lambda-python-hello-world \
  --zip-file fileb://function.zip

先ほどのURLをリフレッシュすると、次のようになります:

  _______________________
| Hello world, I am a pig |
  =======================
                       \
                        \
                         \
                          \
                                    ,.
                                   (_|,.
                                   ,' /, )_______   _
                               __j o``-'        `.'-)'
                               (")                 \'
                               `-j                |
                                 `-._(           /
                                    |_\  |--^.  /
                                   /_]'|_| /_)_/
                                       /_]'  /_]'

高度なPython依存関係

上記のレシピは、純粋にPythonで書かれた依存関係には問題なく動作します。

より複雑になるのは、ネイティブコードを含む依存関係を使用したい場合です。

私はMacを使用しています。pip install -t lib -r requirements.txtを実行すると、これらの依存関係のMacバージョンが取得されます。

しかし、AWS Lambda関数はAmazon Linuxで実行されます。そのため、Amazon Linux用にビルドされたパッケージをZIPファイルに含める必要があります。

初めてこの問題に直面したのは、python3.9ランタイムが非常に古いSQLiteバージョンを含んでいることに気づいたときでした - 2013年5月20日のバージョン3.7.17です。

pysqlite3-binaryパッケージは、より新しいSQLiteを提供し、Datasetteはそれがインストールされている場合自動的にそれを使用します。

これを行う最善の方法は、Amazon Linux Dockerコンテナ内でpip installコマンドを実行することだと判断しました。多くの試行錯誤の末、次のようにするレシピを考え出しました:

docker run -t -v $(pwd):/mnt \
  public.ecr.aws/sam/build-python3.9:latest \
  /bin/sh -c "pip install -r /mnt/requirements.txt -t /mnt/lib"
  • -v $(pwd):/mntフラグは現在のディレクトリをコンテナ内の/mntとしてマウントします。
  • public.ecr.aws/sam/build-python3.9:latestイメージは公式のAWS Lambda Python 3.9イメージです。
  • /bin/sh -c "pip install -r /mnt/requirements.txt -t /mnt/lib"はコンテナ内でpip installを実行しますが、ファイルはlibフォルダに書き込まれるようにします。

このレシピは機能します!結果はAmazon Linux Pythonパッケージでいっぱいのlib/フォルダであり、これをZIPしてデプロイする準備が整います。

ASGIアプリケーションの実行

私はDatasetteをデプロイしたいと思っています。

DatasetteはASGIアプリケーションです。

しかし、AWS Lambda関数にはHTTPに対する独自のインターフェースがあり、上記のeventcontextパラメータがそれを示しています。

Mangumは、このギャップを埋めるよく知られたライブラリです。

以下は、DatasetteとMangumを動作させる方法です。驚くほど簡単でした!

requirements.txtファイルに以下を追加しました:

datasette
pysqlite3-binary
mangum

libフォルダを削除しました:

rm -rf lib

次に上記の魔法の呪文を実行しました:

docker run -t -v $(pwd):/mnt \
  public.ecr.aws/sam/build-python3.9:latest \
  /bin/sh -c "pip install -r /mnt/requirements.txt -t /mnt/lib"

依存関係を新しいfunction.zipファイルに追加しました:

rm -rf function.zip
(cd lib; zip ../function.zip -r .)

次にlambda_function.pyに以下を追加しました:

import asyncio
from datasette.app import Datasette
import mangum


ds = Datasette(["fixtures.db"])
# Handler wraps the Datasette ASGI app with Mangum:
lambda_handler = mangum.Mangum(ds.app())

それをZIPファイルに追加しました:

zip function.zip lambda_function.py

最後に、標準のDatasette fixtures.dbデータベースファイルのコピーを取得し、それをZIPファイルに追加しました:

wget https://latest.datasette.io/fixtures.db
zip function.zip fixtures.db

完成したfunction.zipは7.1MBです。デプロイします:

aws lambda update-function-code \
  --function-name lambda-python-hello-world \
  --zip-file fileb://function.zip

これでうまくいきました!Lambda上でDatasetteインスタンスが稼働しています:https://example-lambda-url.lambda-url.us-east-1.on.aws/

デフォルトのLambda構成では128MBのRAMしか提供されておらず、時折タイムアウトエラーが発生しました。256MBに増やすことで問題が解決しました:

aws lambda update-function-configuration \
  --function-name lambda-python-hello-world \
  --memory-size 256

StarletteやFastAPIにも対応可能

MangumはASGIアプリに対応しているため、StarletteFastAPIで作成されたアプリも同様に動作します。

フリーランスエンジニア必見!

最後に、フリーランスエンジニアの方にご案内です。
あなたに今だけご紹介できる限定の案件があります!

気になる方は公式ラインの追加をお願いします👇
https://bit.ly/3xLrLGw

Discussion