AWS Lambda の PythonでShared Library(*.so)を含む外部ライブラリを扱う方法

仕事でPython / Lambdaを触る機会があり、Shared Library(*.so) を扱うライブラリの取扱でめちゃくちゃはまったので、メモっておく。

あと最近エモい記事ばっかり書いていたので、たまにはエンジニアアピールしておく。

Python初心者なので、それxxxでできるよ的な話があれば教えてください。

TL;DR

  • Lambdaで外部ライブラリを扱うときにはZIPファイルに固めてアップロードする必要があるが、これに .so ファイルも一緒に固めておく
  • LD_LIBRARY_PATH はLambda実行時に指定できないので ctypes.cdll.LoadLibraryPython実行時に指定して読み込む
  • MacでビルドするとLambdaで動かなかったりするので、Dockerを使ってAWSAmazon Linuxなどでpip install など行う

何がやりたかったのか

Lambdaでとある形式のファイルから、画像を抜き出すという処理をPythonでやろうとしていた。Lambdaでは依存する外部ライブラリは実行したいファイルと一緒にZIPで固めてアップロードすればよいらしい(Pythonの場合)。

よく紹介されているのは以下の方法だ。

pip install -r requirements.txt -t .

しかしPythonのライブラリの中には、純粋なPythonだけでなくて、Cなどで作られたライブラリをWrapしてPythonから使えるようにしているものがある。その場合は .py ファイルだけZIPにつめても実行時に以下のようなエラーになってしまう。

ImportError: libgdcmMEXD.so.2.8: cannot open shared object file: No such file or directory

しかし、 *.so を単純にZIPに詰めれば解決するという問題ではない。Lambdaでは *.so の読み込み先を決める LD_LIBRARY_PATH をいじれないからだ。

どうするのか

Lambdaでの実行用にZIPに固める際に、以下のように必要なLibraryをlib配下にまとめてしまう。

# 依存関係を./distにインストール
pip install -r requirements.txt -t ./dist
# gdcm関連パッケージを./distにコピー
cp -r /opt/conda/lib/python3.6/site-packages/*gdcm* ./dist
mkdir -p ./dist/lib
cp -r /opt/conda/lib/libgdcm* ./dist/lib
cp -r /opt/conda/lib/libsocketxx* ./dist/lib

# 容量削減のためtestsファイルを削除
rm -rf ./dist/**/tests

# メインファイルを./distにコピー
cp ./main.py ./dist

その上で、Lambdaで実行するファイルで、明示的に lib 配下のファイルを読み込んでやればよい。

import os
import ctypes

for d, dirs, files in os.walk('lib'):
    for f in files:
        if f.endswith('.a'):
            continue
        ctypes.cdll.LoadLibrary(os.path.join(d, f))

TIPSとして、Mac環境でこれをやってもAWSのLambdaが動くLinuxの環境では動作しなかったりするので、LinuxのDockerイメージを用意してそこでZIPファイル作成や依存関係の解決を行うのがおすすめ。自分は雑に公式のAmazon Linuxは使わずに miniconda3 のイメージを使ってパッケージングしたけど問題なくLambda上でも動いた。

まとめと感想

Lambdaまともに使ったのは実は初めてだけど便利だった。Shared Library依存ののものがあると結構面倒だけど、気合でなんとなかる。 サーバーレスで自分のDockerImageをシュッと動かせるようになるとめっちゃ楽になりそうなのでGCPに期待します

参考

Using Scikit-Learn in AWS Lambda -- Serverless Code