もっとスマートなやり方がないのかどなたか教えていただきたく。
きっかけ
AWS Lambda から RDS (mysql / aurora) に繋げようと、mysqlclient を import したわけです。
ローカルでそつなく動いたので serverless でデプロイ。AWS 管理コンソールからテスト実行してみると、なんと動かない。
どうやら mysqlclient にネイティブコードが含まれているせいで、Mac でビルドしたイメージを linux で動かそうとしてエラーが出ているご様子。
この辺を解決するために色々してみた、というお話です。
まずそのままデプロイしてみる
ちなみに python の外部パッケージは serverless-python-requirements で管理しています。
こんな感じの俺サービスが、
ore-service
├── handler.py
├── requirements.py
├── requirements.txt
└── serverless.yml
こんな内容だったとします。
mysqlclient
#!/use/bin/env python
# -*- coding: utf-8 -*-
import requirements
import MySQLdb
def lambda_handler(event, context):
con = MySQLdb.connect(host='〜', db='〜', user='〜', passwd='〜', charset='utf8')
cur = con.cursor()
cur.execute("SELECT * FROM なんとか〜")
....(略)
sls deploy して AWS コンソールから AWS Lambda をテスト実行してみるとエラー。
Log output には以下のような出力があります。
Unable to import module 'handler': /var/task/_mysql.so: invalid ELF header
ああそうですか。
Mac でビルドした _mysql.so を linux で動かそうとしてますからね。
serverless-python-requirements の dockerizePip を使ってみる
serverless-python-requirements には dockerizePip なるオプションがありまして、true にすると docker-lambda イメージを使ってクロスコンパイルしてくれるとのこと。
早速やってみます。
serverless.yml は以下のような感じ。
...(略)
plugins:
- serverless-python-requirements
custom:
pythonRequirements:
dockerizePip: true
...(略)
で、デプロイすると残念ながらめっちゃエラー。
$ sls deploy
Serverless: Installing required Python packages...
Error --------------------------------------------------
Command "python setup.py egg_info" failed with error
code 1 in /tmp/pip-build-4MzA_g/mysqlclient/
これ、mysqlclient のビルドに必要な python-devel と mysql-devel が docker イメージに入ってないのが原因らしい。
lambci/lambda でクロスコンパイルする
「EC2 でビルドしなきゃだめなの?」とテンションだだ下がりつつ serverless-python-requirements のソース 見てみたら、なんとなくやってることが理解できた。
コンテナ起動して pip install してるだけっぽい。
そしたら自分でコンテナ起動して、bash で入って必要ライブラリインストールすればいいんですよね。
以下のような感じ。
$ docker run -it --rm -v "$PWD":/var/task "lambci/lambda:build-python2.7" bash
bash-4.2# cd /var/task
bash-4.2# yum -y install python-devel mysql-devel
bash-4.2# pip install mysqlclient -t .
bash-4.2# cp /usr/lib64/mysql/libmysqlclient.so.18 .
コンテナの /var/task をローカルのカレントにマウントしておく。
この辺はさっき見てたソースの installRequirements() あたりの処理をまるパクリ。
で、必要ライブラリをインストールして pip。
ついでに libmysqlclient.so.18 も必要なのでコピーしておく。
serverless は serverless.yml がある場所にあるファイル、フォルダはまるっと zip にしてアップロードしてくれるので、あとはこのまま sls deploy すればよい。
(さっき serverless.yml に追記した dockerizePip: true は削除しておくこと)
これで lambda 内から linux 用にビルドした mysqlclient がロードされ、無事 RDS にアクセスできます。
実行環境別にロードするパッケージを使い分ける
ここで問題発生。
ローカルに linux イメージの mysqlclient があるせいでローカル実行ができなくなってしまいます。
$ python handler.py
Traceback (most recent call last):
File "handler.py", line 18, in <module>
import MySQLdb
File "./MySQLdb/__init__.py", line 19, in <module>
import _mysql
ImportError: dlopen(./_mysql.so, 2): no suitable image found. Did find:
./_mysql.so: unknown file type, first eight bytes: 0x7F 0x45 0x4C 0x46 0x02 0x01 0x01 0x00
よってロードする mysqlclient バイナリを切り替える仕組みをととのえます。
「linux ELF は ./third-party/ 配下に置き、実行環境が mac 以外だったらそちらを先にロードする」という作戦。
プロジェクトルートが外部パッケージフォルダで汚れることもない。
ディレクトリ構成は以下の通り。
ore-service
├── third-party
│ ├── MySQLdb
│ ├── _mysql.so
│ ├── libmysqlclient.so.18
│ └── その他いろいろ linux 用ファイル群
├── handler.py
├── requirements.py
├── requirements.txt
└── serverless.yml
クロスコンパイルは以下のようにする。
(マウントポイントを third-party/ にしただけ)
$ mkdir third-party
$ cd $_
$ docker run -it --rm -v "$PWD"/third-party/:/var/task "lambci/lambda:build-python2.7" bash
(以下同文)
で、python 側で OS を判定してロード先を切り替えます。
sys.path の先頭に third-party を差し込むのと、あと python は LD_LIBRARY_PATH 的な設定がないらしいので libmysqlclient.so.18 を直接ロードしてあげる。
import platform
if platform.system() != "Darwin":
import sys
import ctypes
sys.path.insert(0, './third-party/')
ctypes.CDLL("./third-party/libmysqlclient.so.18")
泥臭いですが、これでなんとか目的は達成できました。
あとは third-party ディレクトリを .gitignore しておいて、クロスコンパイル処理を Dockerfile とかにまとめてしまえば構成管理的にも大丈夫かなというところです。
まとめ
こんなんせなあかんのん?