AWS LambdaでLambda Layersを使う場合、環境変数のPYTHONPATHは使っちゃダメという話

AWS Lambda × Lambda Layers × Pythonの環境で発生した一風変わったエラーについてご紹介します。
2019.04.05

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

サーバーレス開発部@大阪の岩田です。 先日遭遇したAWS Lambdaの一風変わったエラーについてご紹介します。

結論

まず結論から。 LambdaのランタイムにPythonを選択し、かつLambda Layersの機能を利用する場合Lambdaの環境変数にPYTHONPATHを設定しないで下さい。 レイヤー内に配置したPythonのモジュールが読み込めなくなります。

処理概要と発生したエラー

今回エラーが発生したコードです。

import os
import sys
import Crypto
import json


def lambda_handler(event, context):
    
    ...略

マネジメントコンソールからテストを実行するとこんなエラーが発生しました。

{
  "errorMessage": "Unable to import module 'lambda_function'"
}

どうもLambdaの初期化処理の中でCryptoモジュールが読み込めていないようです。 Cryptoモジュールはレイヤーの中に詰め込んでおり、レイヤーとLambda Functionの紐付けも正しく行えています。

なぜエラーが・・・??

原因切り分け

ここからエラーの原因を切り分けていきます。

レイヤーの作成ミスを疑う

まずレイヤーの作成ミスを疑いました。 原因切り分けのために新しく1からLambda Functionを作成し、同じレイヤーを紐付けてCryptoモジュールをimportしましたが、こちらは問題ありませんでした。 レイヤーではなくLambda Functionの方に問題がありそうです。

PYTHONPATHを疑う

Lambda Functionの設定を隅々まで見渡したところモジュールのimportに影響しそうな設定として、環境変数PYTHONPATHが指定されているのを発見しました。 試しに環境変数PYTHONPATHを削除してからテストイベントを実行すると、モジュールのimportエラーが解消されました!!

元々PYTHONPATHには/var/task/vendor:/var/runtimeが指定されていたのですが、なぜこれがレイヤーに影響を??

PYTHONPATHの確認

Lambda実行環境に自動で設定されるPYTHONPATHの値を自分で上書いているのが原因と考えました。本来はPYTHONPATH/var/runtime:/opt/python:/opt/python/lib/python3.6/site-packagesのように設定されるべきところLambda Functionの設定で明示的に指定したことでPYTHONPATHから

  • /opt/python
  • /opt/python/lib/python3.6/site-packages

が欠落したのでは??という仮説です。 確認のためにLambda Functionの設定から環境変数PYTHONPATHを削除した状態だとPYTHONPATHの値がどうなるのか確認してみます。

import os
import sys
print(os.environ['PYTHONPATH'])
import Crypto
import json


def lambda_handler(event, context):
    
    ...略

環境変数PYTHONPATHの中身を出力するコードを追加しています。このコードを実行したところ、環境変数PYTHONPATHには/var/runtimeが設定されていました。正常に実行できるパターンでもPYTHONPATHにレイヤーのパスが設定されている訳ではないようです。

環境変数PYTHONPATHの有無によるsys.pathの比較

このままだと何故環境変数PYTHONPATHを削除することでエラーが解消したのか分かりません。 もう少し詳細を調査してみます。

import os
import sys
print(sys.path)
import Crypto
import json


def lambda_handler(event, context):
    
    ...略

Cryptoモジュールをimportする直前にprintでsys.pathの中身を出力するコードを追加しました。 上記のコードを実行し、環境変数PYTHONPATHありの場合と無しの場合それぞれでsys.pathの 中身を比較してみます。

PYTHONPATH無し

まずはエラーが発生しないPYTHONPATH無しバージョンです。

['/var/task', '/opt/python/lib/python3.6/site-packages', '/opt/python', '/var/runtime', '/var/runtime/awslambda', '/var/lang/lib/python36.zip', '/var/lang/lib/python3.6', '/var/lang/lib/python3.6/lib-dynload', '/var/lang/lib/python3.6/site-packages', '/opt/python/lib/python3.6/site-packages']
PYTHONPATH有り

次にエラーが発生したPYTHONPATHありバージョンのログです。

['/var/task', '/var/runtime/awslambda', '/var/runtime', '/var/lang/lib/python36.zip', '/var/lang/lib/python3.6', '/var/lang/lib/python3.6/lib-dynload', '/var/lang/lib/python3.6/site-packages']

sys.pathから

  • /opt/python/lib/python3.6/site-packages
  • /opt/python

が消えてしまいました!!

考察

これまでの調査結果から推測するとPythonで書いたLambda Functionの初期化処理は、環境変数PYTHONPATHがセットされていない場合に限りsys.pathのモジュール検索パスに/opt以下のパスを追加するようです。この結果レイヤーに配置したモジュールがLambda Functionから読み込めるようになるのです。 と、いうわけで、、、

Lambda Layersの機能を使う場合Lambda Functionの環境変数にPYTHONPATHを設定するのはやめましょう!!

まとめ

Lambda Layers利用時の一風変わったエラーについてご紹介しました。 なぜ環境変数PYTHONPATHをセットするとsys.pathのモジュール検索パスに/opt/以下のパスが追加されないのでしょうか? 興味のある方はLambda実行環境をハックしてLambdaのbootstrap処理を追いかけてみると面白いかもしれません。 Lambda実行環境をハックする方法は自己責任の元Googleさんにお尋ね下さい。