Serverlessルートディレクトリの外にある自作モジュールを読み込ませる

概要

repo-root
├ functions
│ ├ func_a
│ │ ├ main.py
│ │ └ serverless.yml
│ └ func_b
│   ├ main.py
│   └ serverless.yml
├ libs
│ └ mod_c
│   └ foo.py
└ tox.ini

とあるServerlessのLamdaを管理するリポジトリがあります。
このリポジトリは(歴史的背景により)、それぞれ serverless.yml を持つ複数のFunction(func_a, func_b)を持っています。

という条件の中で、func_a, func_bで共通の自作モジュール mod_c/foo.py を読み込みたくていろいろやったメモです。

なにゆえ?

普通にこのままデプロイをかけると、 serverless.yml があるディレクトリの外側にある libs はzipされません=Lambdaが libs を持たない状態になります。
そのため、Lambdaは

Unable to import module 'main': No module named 'libs'

というエラーを出すことになります。
本稿の目的はこれを回避することです。

本来なら /repo-rootserverless.yml を置くのがたぶん最適解です。
が、歴史的背景が色々あってその変更を掛けることができませんでした。
というお断りをしておきます…

説明すること

  1. Serverlessデプロイするまで
  2. PyCharm上で読み込めるようにする方法
  3. tox/pytestでテストできるようにする方法

1. Serverlessデプロイするまで

serverless-python-requirementsというプラグインを使用します。

github.com

serverless.yml があるディレクトリで↓を実行してインストール。

$ sls plugin install -n serverless-python-requirements

インストール後、 serverless.ymlplugins 内に vendor オプションを追記することで libs を含めることが可能になります。

GitHub - UnitedIncome/serverless-python-requirements: ⚡️🐍📦 Serverless plugin to bundle Python packages

service: xxxx-yyyy-zzzz
frameworkVersion: "^1.xx.x"

provider:
  ...(省略)...

plugins:
  - serverless-python-requirements
custom:
  pythonRequirements:
    vendor: ../../libs
  ...(省略)...

functions:
  ...(省略)...

ただ、デプロイ後、 vendor に指定したディレクトリ階層が1階層浅くなるトラップがあります。

デプロイ後、Lambdaの中は↓のようになります。

func_a
├ /bin
├ /redis
├ /requests
├ __init__.py
├ main.py
├ /mod_c
└ ...

serverlessルートディレクトリ直下にimportしているライブラリが展開されます。
それによって何が起きるかというと、func_aおよびfunc_bの main.py

from libs.mod_c.foo import bar

のようにimportしている場合、
libs が見つからないことによってLambdaの中で結局 Unable to import module が出ます。
ソースコード上での解決方法は簡単で、 libs を除けば動作します。

from mod_c.foo import bar

ただこの状態だとローカルでテストが動作しなくなったり、toxが回らなくなったりするので、
importに合わせて各種設定を変更することが必要になります。次節へ。

2. PyCharm上で読み込めるようにする方法

1の手順に沿ってimportを書き換えるとPyCharmが警告を出すので、Project Structureから libs をSource Foldersとして登録します。

  1. PyCharmのProject Structure画面を開きます。
  2. [File] -> [Settings] -> [Project] -> [Project Structure]
  3. [Mark as:] から [Sources] をクリックしてから、 /libs をクリックします。
  4. /libsディレクトリが青色になったら [Apply] -> [OK]

これでPyCharm上では正しくimportができる状態になります。

3. tox/pytestでテストできるようにする方法

toxを使っている場合、まだimportが失敗するので修正する必要があります。
(本稿ではCircleCI+tox+pytestの使用を前提としています)

原因は libs にpathが通っていないことなので、
テスト実行時の環境変数PYTHONPATH を追記します。

tox.ini

[testenv:py36]
deps = pipenv
commands =
  - pipenv install --dev
  pipenv run pytest
setenv =
  PYTHONPATH = {toxinidir}/functions{:}{toxinidir}/libs
...

この例では、 /functions/libs の2つのディレクトリにパスを通しています。

setenvについての詳細はこちら。名前の通り環境変数をセットするだけです。

tox.readthedocs.io

{toxinidir} はtox.iniがあるディレクトリを指す変数、
{:} は複数変数の区切り文字(環境依存)を環境に合わせて変更してくれるものです。

toxの置換変数の詳細はこちらを参照。

tox.readthedocs.io

{:} でちょっとハマりました。
Windowsローカル環境で作業しているときに ; で書いて、
それをCircleCI(つまりLinux)に出したら動かなくなって、区切り文字が環境依存であることに気づくまでに数十分かけてしまった…
直接 : , ; とか書かずにちゃんと {:} を使いましょう。