tail my trail

作るのも使うのも、結局は、人なのだ

Lambda functions (Python) の 依存パッケージの保存場所を指定する

f:id:uorat:20170731120316p:plain

apex.run

Lambda w/ Apex 関連でもう一つ TIPs を備忘録しておく。

Function hooks のおさらい

Apex には Function hooks という機構があり、 Apex で管理する Lambda function のライフサイクルの中で、特定のステップで任意の shell commands を発火させることができる。

以下は Apex.run に記載されている Sample だ。

Shell commands を指定できるので、当然 golang に限らずなんでも実行できる。当然 pip install も可能。

hooks の種類は3種ある。

  • build
    • run before a function zip is built (use this to compile binaries or transform source)
  • deploy
    • run before a function is deployed (useful for testing, linting)
  • clean
    • run after a function is deployed (useful for cleaning up build artifacts)

なので、 Lamnbda Functions w/ Python の場合は以下のように build 時に pip install して functions を梱包して deploy する function.json と requirements.txt を用意することが多いと思う。

pip install したパッケージ群のインストール先を変えたい

ただ、上の方法だと function directory に各パッケージがインストールされてしまう。 function 開発時において重要なものは function.json, Lambda の handler となる python code, あとは依存モジュール記す requirements.txt 。 その他のファイル群が同層に展開されるのは美しくないし、 gitignore 依存パッケージを一つ一つ指定する形になり面倒。

$ tree -L 4
.
├── README.mkd
├── functions
│   └── helloworld-python
│       ├── certifi
│       ├── certifi-2017.7.27.1.dist-info
│       ├── chardet
│       ├── chardet-3.0.4.dist-info
│       ├── function.dev.json
│       ├── function.prd.json
│       ├── function.stg.json
│       ├── idna
│       ├── idna-2.5.dist-info
│       ├── main.py
│       ├── requests
│       ├── requests-2.18.2.dist-info
│       ├── requirements.txt
│       ├── urllib3
│       └── urllib3-1.22.dist-info
├── project.dev.json
├── project.prd.json
└── project.stg.json

例えばこうしたい。

$ tree -L 4
.
├── README.mkd
├── functions
│   └── helloworld-python
│       ├── function.dev.json
│       ├── function.prd.json
│       ├── function.stg.json
│       ├── main.py
│       ├── requirements.txt
│       └── site-packages
│           ├── certifi
│           ├── certifi-2017.7.27.1.dist-info
│           ├── chardet
│           ├── chardet-3.0.4.dist-info
│           ├── idna
│           ├── idna-2.5.dist-info
│           ├── requests
│           ├── requests-2.18.2.dist-info
│           ├── urllib3
│           └── urllib3-1.22.dist-info
├── project.dev.json
├── project.prd.json
└── project.stg.json

やりかた

ざっくり以下。

  1. function hooks を修正
  2. 環境変数 PYTHONPATH を指定

1. function hooks を修正

言うまでもないが、 pip install コマンドを微修正する。

- pip install -r requirements.txt -t ."
+ pip install -r requirements.txt -t ./site-packages"

2. 環境変数 PYTHONPATH を指定

site-packages を検索対象パスに追加する。

+  },
+   "environment": {
+       "PYTHONPATH": "/var/runtime:/var/task/site-packages"

両方をまとめるとこうなる。

これで apex build すれば、依存パッケージは ./site-packages/ 以下に保存されるようになり、かつ 依存パッケージにパスが通るようになる。

肝は

  • 依存パッケージの path を /var/task/site-packages とすること
  • /var/runtime も指定しておくこと

理由は以下に記す。

Lambda Functions を覗いてみる

pip install 時に指定した ./site-packages 以下のパッケージ群を検索対象するべくパスを通すために、展開先の挙動を把握しておく。

簡単な function を実行してみよう。

環境変数や実行ファイルのパスを除くだけのプログラムである。 この function のログはこんな感じとなる。

[INFO]  2017-08-31T04:29:53.519Z        08d61df6-8e05-11e7-a88f-4d9fe5cb6d15    $ENV: environ({'APEX_FUNCTION_NAME': 'python-sample','LAMBDA_FUNCTION_NAME': 'hello-apex_python-sample', 'PATH': '/var/lang/bin:/usr/local/bin:/usr/bin/:/bin', 'LANG': 'en_US.UTF-8', 'TZ': ':UTC', 'LD_LIBRARY_PATH': '/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib', 'LAMBDA_TASK_ROOT': '/var/task', 'LAMBDA_RUNTIME_DIR': '/var/runtime', 'AWS_REGION': 'ap-northeast-1', 'AWS_DEFAULT_REGION': 'ap-northeast-1', 'AWS_LAMBDA_LOG_GROUP_NAME': '/aws/lambda/hello-apex_python-sample', 'AWS_LAMBDA_LOG_STREAM_NAME': '2017/08/31/[12]xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 'AWS_LAMBDA_FUNCTION_NAME': 'hello-apex_python-sample', 'AWS_LAMBDA_FUNCTION_MEMORY_SIZE': '128', 'AWS_LAMBDA_FUNCTION_VERSION': '1', '_AWS_XRAY_DAEMON_ADDRESS': '169.254.79.2', '_AWS_XRAY_DAEMON_PORT': '2000', 'AWS_XRAY_DAEMON_ADDRESS': '169.254.79.2:2000', 'AWS_XRAY_CONTEXT_MISSING': 'LOG_ERROR', '_X_AMZN_TRACE_ID': 'Root=1-xxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx;Parent=xxxxxxxxxxxxxxxx;Sampled=0', 'AWS_EXECUTION_ENV': 'AWS_Lambda_python3.6', '_HANDLER': 'main.handle', 'PYTHONPATH': '/var/runtime', 'AWS_ACCESS_KEY_ID': 'ASIAXXXXXXXXXXXXXXXX', 'AWS_SECRET_ACCESS_KEY': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', 'AWS_SESSION_TOKEN': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx==', 'AWS_SECURITY_TOKEN': 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=='})
[INFO]  2017-08-31T04:29:53.519Z        08d61df6-8e05-11e7-a88f-4d9fe5cb6d15    __file__ : /var/task/main.py
[INFO]  2017-08-31T04:29:53.519Z        08d61df6-8e05-11e7-a88f-4d9fe5cb6d15    $PYTHONPATH: /var/runtime
[INFO]  2017-08-31T04:29:53.519Z        08d61df6-8e05-11e7-a88f-4d9fe5cb6d15    sys.path: ['/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']
[INFO]  2017-08-31T04:29:53.519Z        08d61df6-8e05-11e7-a88f-4d9fe5cb6d15    list /var/task/: ['site-packages', 'function.dev.json', 'main.py', 'requirements.txt', 'function.stg.json']
[INFO]  2017-08-31T04:29:53.519Z        08d61df6-8e05-11e7-a88f-4d9fe5cb6d15    list /var/runtime/: ['s3transfer-0.1.10.dist-info', 'liblambdalog.so', 'dateutil', 'botocore', 's3transfer', 'boto3', 'six-1.10.0.dist-info', '__pycache__', 'botocore-1.5.89.dist-info', 'six.py', 'python_dateutil-2.6.1.dist-info', 'liblambdaio.so', 'liblambdaipc.so', 'boto3-1.4.4.dist-info', 'jmespath', 'awslambda', 'liblambdaruntime.so', 'docutils-0.13.1.dist-info', 'docutils', 'jmespath-0.9.3.dist-info']
[INFO]  2017-08-31T04:29:53.519Z        08d61df6-8e05-11e7-a88f-4d9fe5cb6d15    Received event: {
  "region": "ap-northeast-2"
}
END RequestId: 08d61df6-8e05-11e7-a88f-4d9fe5cb6d15
REPORT RequestId: 08d61df6-8e05-11e7-a88f-4d9fe5cb6d15  Duration: 17.21 ms      Billed Duration: 100 ms         Memory Size: 128 MB    Max Memory Used: 20 MB
null

ここから分かるのは、

  • handler となる main.py の path は /var/task/main.py である
  • /var/task に Upload した ZIP が展開される
  • $PYTHONPATH には /var/runtime が元々セットされている
  • /var/runtime には boto3 をはじめとした util 群 が存在する

つまり、 自分で pip install したパッケージの保存先 ./site-packages に path を通したければ PYTHONPATH に /var/task/site-packages を追加すれば良い。 ただ、デフォルトで PYTHONPATH にセットされている /var/runtime には boto3 をはじめとした util 群が含まれるので、このパスも有効にしておく。

なので、先程紹介した function.json のような形に行き着く。