pyminizip を AWS Lambdaで使いたい

『pyminizipのライブラリをAWS Lambdaで使いたいけど何故かimportエラーが出る。』というお悩みの方向け。もしかして、共有ライブラリのCPUアーキテクチャがあってないかも?
2022.08.24

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

サーモン大好き横山です。

今回、Macから serverless framework + poetryを用いて、AWS Lambdaへdeployを行いパスワード付zipを作りたい!とおもってやってみましたところ、パッケージがimport出来ないと言われました。今回はその解決策を書きます。

実行環境

以下のMacの環境からやります。

$ sw_vers
ProductName:	macOS
ProductVersion:	12.4
BuildVersion:	21F79

$ uname -mprsv
Darwin 21.5.0 Darwin Kernel Version 21.5.0: Tue Apr 26 21:08:37 PDT 2022; root:xnu-8020.121.3~4/RELEASE_ARM64_T6000 arm64 arm

$ serverless --version
Running "serverless" from node_modules
Framework Core: 3.22.0 (local) 3.22.0 (global)
Plugin: 6.2.2
SDK: 4.3.2

$ python -V
Python 3.9.13

デプロイ準備

serverless create -t aws-python3 -n pyminizip で作成したプロジェクト配下に以下のファイルを編集・追加する。

handler.py

import logging
from typing import Any

import pyminizip

logger = logging.getLogger()
logger.setLevel(logging.INFO)


def hello(event: dict, context: Any) -> None:
    logger.info("hello world.")

serverless.yml

service: pyminizip
frameworkVersion: "3"
configValidationMode: error

provider:
  name: aws
  runtime: python3.9
  region: ap-northeast-1

plugins:
  - serverless-python-requirements

functions:
  pyminizip:
    handler: handler.hello

custom:
  pythonRequirements:
    usePoetry: true

package:
  patterns:
    - "!**/"
    - handler.py

pyproject.toml

[tool.poetry]
name = "aws_lambda_pyminizip"
version = "0.1.0"
description = ""
authors = ["yokoyama.fumihito <xxx@xxx.jp>"]

[tool.poetry.dependencies]
python = "^3.9"
pyminizip = "^0.2.6"

[tool.poetry.dev-dependencies]

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

編集後Python3.9でinstallを実行する。

$ poetry env use 3.9
$ poetry install

ここまでのディレクトリ構造

$ tree .
.
├── handler.py
├── poetry.lock
├── pyproject.toml
└── serverless.yml

0 directories, 4 files

serverless deployをして動作確認

pluginをインストールしてからdeployをします。

$ export AWS_PROFILE=xxx #Deploy先のAWSのProfile
$ serverless plugin install -n serverless-python-requirements
$ serverless deploy

AWS Lambdaの実行を確認

deployしたAWS Lambdaの関数を実行するとエラーになります。

{
  "errorMessage": "Unable to import module 'handler': No module named 'pyminizip'",
  "errorType": "Runtime.ImportModuleError",
  "requestId": "a4ca31b5-49a0-4a15-9d27-ad5ef41f2382",
  "stackTrace": []
}

pyminizipのパッケージがUnix/Linuxの共有ライブラリ形式(*.soファイル)でインストールされています。今回のエラーはMacの実行環境とAWS Lambda内の実行環境とでCPUアーキテクチャの違いにより、共有ライブラリ経由のimportが失敗しているからです。

対処方法

今回は、serverless plugin serverless-python-requirements の クロスコンパイル機能を使いAWS Lambdaへdeployし直します。 クロスコンパイルするために、AWSから用意されているdocker imageを使います。

事前に下記のdocker image public.ecr.aws/sam/build-python3.9:latest-x86_64 をlocalにpullしておきます。

$ docker pull public.ecr.aws/sam/build-python3.9:latest-x86_64

Dockerfileを新たに作成します。 下記記述し、 build/Dockerfile へ保存します。

FROM public.ecr.aws/sam/build-python3.9:latest-x86_64

serverless.ymlのpluginの設定を変更します。 クロスコンパイル機能は、poetryの機能がonのままだとうまく動かないので、 usePoetry: false にするのを忘れないようにしてください。

custom:
  pythonRequirements:
    usePoetry: false
    dockerizePip: true
    dockerFile: build/Dockerfile

ここまで対応したディレクトリ構成はこんな感じになります。

$ tree . -I node_modules
.
├── build
│   └── Dockerfile
├── handler.py
├── package-lock.json
├── package.json
├── poetry.lock
├── pyproject.toml
└── serverless.yml

修正版をserverless deploy をして動作確認

deployをする前に、Pythonのパッケージをダウンロードしたcacheが残っているとcacheからdeploy packageを作成しようとしてしまうので、一度cacheの方を削除します。

$ serverless requirements cleanCache

クロスコンパイル機能を動かすために、deploy直前に requirements.txt を手動で出力します。

$ poetry export --without-hashes -f requirements.txt -o requirements.txt --with-credentials

docker imageを利用してdeploy packageを作成しているか確認するために --verbose の引数を使用してserverless deployをしていきます。 Running docker run --rm 〜 というログが出ていればうまく実行できてます。

$ serverless deploy --verbose
Running "serverless" from node_modules

Deploying pyminizip to stage dev (ap-northeast-1)

Packaging
Generated requirements from /path/to/requirements.txt in /path/to/.serverless/requirements.txt
Installing requirements from "/Users/user/Library/Caches/serverless-python-requirements/97a77931bd1f047a2b8a5c20db8ef50459973e46acdb97ddee8842ecce71c43b_x86_64_slspyc/requirements.txt"
Docker Image: sls-py-reqs-custom
Using download cache directory /Users/user/Library/Caches/serverless-python-requirements/downloadCacheslspyc
Running docker run --rm -v /Users/user/Library/Caches/serverless-python-requirements/97a77931bd1f047a2b8a5c20db8ef50459973e46acdb97ddee8842ecce71c43b_x86_64_slspyc\:/var/task\:z -v /Users/user/Library/Caches/serverless-python-requirements/downloadCacheslspyc\:/var/useDownloadCache\:z -u 0 sls-py-reqs-custom python3.9 -m pip install -t /var/task/ -r /var/task/requirements.txt --cache-dir /var/useDownloadCache...
Excluding development dependencies for service package
Injecting required Python packages to package
Retrieving CloudFormation stack

...(snip)...

AWS Lambdaの実行を確認

importエラーが解消されて、ハンドラーが正常に実行できました。

まとめ

共有ライブラリを含むPythonのパッケージをAWS Lambdaへdeployする場合の一助となれば幸いです。


補足:importの挙動のあれこれ

ここからは、CPUアーキテクチャの違う共有ライブラリのimportの挙動に関しての補足です。

前述の通り、pyminizipはC言語のコードをコンパイルし、共有ライブラリとして利用しています。 → GitHub
serverless deployのコマンドを叩いたときに作成するzipファイルを解凍して中を見てみると、pyminizipの共有ライブラリが含まれています。

## Deployのための生成物が .serverless ディレクトリに集められている
$ cd .serverless/

## pyminizip.zipを解凍
$ mkdir dist
$ cd dist
$ unzip ../pyminizip.zip

## 共有ライブラリを検索
$ ls *.so
pyminizip.cpython-39-darwin.so
$ file pyminizip.cpython-39-darwin.so
pyminizip.cpython-39-darwin.so: Mach-O 64-bit bundle arm64

共有ライブラリのファイルはpathが通っている場所にあるとimportすることが可能です。 python実行するディレクトリ直下においてもimportすることが出来ます。

## 実行するPython環境にはpyminizipはインストールされていない
$ python3 -mpip freeze | grep pyminizip

## ディレクトリ直下にpyminizipの共有ライブラリが存在する場合
## importエラーが出ない
$ ls
pyminizip.cpython-39-darwin.so
$ python3 -c "import pyminizip"
$ 

## ディレクトリ直下にpyminizipの共有ライブラリを削除した場合
## importエラーが発生する
$ rm pyminizip.cpython-39-darwin.so
$ python3 -c "import pyminizip"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
ModuleNotFoundError: No module named 'pyminizip'

Macとは違うCPUアーキテクチャの環境に共有ライブラリを持っていきpyminizipをimportしようとしても、importエラーになります。 下記はCloudShell上に共有ライブラリを転送し、コマンドを叩いた結果です。

[cloudshell-user@ip-10-0-x-x tmp]$ uname -a
Linux ip-10-0-127-227.ap-northeast-1.compute.internal 4.14.287-215.504.amzn2.x86_64 #1 SMP Wed Jul 13 21:34:43 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux

## ディレクトリ直下に存在することを確認
[cloudshell-user@ip-10-0-x-x tmp]$ ls
pyminizip.cpython-39-aarch64-linux-gnu.so

## pyminizipをimportしようとしてもエラーになる
[cloudshell-user@ip-10-0-x-x tmp]$ python3 -c "import pyminizip"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
ModuleNotFoundError: No module named 'pyminizip'

ですので、最初にdeployした方法ですと、MacのCPUアーキテクチャ用に作成された共有ライブラリが封入されます。そして、AWS Lambdaの環境で動かそうとしてimportに失敗した、という感じでエラーで動きませんでした。