好奇心の足跡

飽きっぽくすぐ他のことをしてしまうので、忘れないため・形にして頭に残すための備忘録。

AWS Lambda 上で pytest を動かす

AWS Lambda 上で pytest を動かす

サービスの死活監視(DeepHealthCheck)や日次でのFullFunctionTestなど、定期的なテスト実行のために Lambda 上で pytest を動かすためのApplication雛形。
それなりにハマったのでメモ。

動作環境

  • AWS Lambda: python3.6 runtime
  • local環境は特に依存ないはず

AWS Lambda構成

超シンプルな構成。

f:id:kusuwada:20180622041915p:plain

Lambdaの構築自体は特に記載しないが、今回のサンプルを動かすだけなら下記設定で問題なし。

f:id:kusuwada:20180622041910p:plain

※ただし、lambaのタイムアウトがUIからペッと作ったままだとDefault 3秒 なのでおそらく足りない。せめて60秒くらいに延ばしておいたほうが良い。

また、テストの中でDynamoやS3を直接見に行くなど、テスト内容やテスト結果の保存先などによって、Lambdaに IAM role の追加が必要。
あと今回はただのサンプルなのでトリガの設定もしていないけど、実際使う場合にはもちろん何かしらトリガの設定が必要。

Application構成

.
├── lambda_function.py  # AWS Lambdaのhandler
├── make_lambda_zip.sh  # Lambdaにdeployするzipを作成するスクリプト
├── pytest.ini          # pytestの設定 今回は固定optrion記載で使用
├── requirements.txt    # pytest実施に必要なpythonライブラリ
└── tests/               # 実施するテスト群
    └── sample_test.py

pytestの中身

上記treeの中で、pytest用のものは tests/ のみ。 (pytest.inirequirements.txt は関係あるが後述)
tests/ 配下は自由にpytestで使用するテストコードを書けば良い
今回は例として sample_test.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import requests


def test_simple_get_sample():
    "simple get page request test"
    res = requests.get('https://www.google.com/')
    assert res.status_code == 200

googleにアクセスしてstatus_codeが200かを確認するだけのテスト。
conftest.py や、他のテスト用src群などと組み合わせて大きめのtestを作ることも可能。

使用する python ライブラリたち

requirements.txt にテストで使用するライブラリを記載していることが多いと思いますが、lambda上では pip install が許可されていません。
なので、lambdaにdeployするzipに入れておく必要があります。

requirements.txt (参考までに今回向けのもの)

pytest
requests

zip作成スクリプト

上記のライブラリインストールの件を受けて、zip作成はこんな感じ

make_lambda_zip.sh

#!/bin/bash

pip install -r requirements.txt -t ./.requirements
zip -rv lambda_function.zip ./

ここでは ./.requirements pathにライブラリたちをインストールしている(他のコードとごちゃごちゃになるのを防ぐため)。ということは、ここにpathを通しておく必要がある。

また、毎回0からlib installを行う場合は、OSのtmp directoryコマンドなんかを使って書いておくと、冪等性が保てたり installしたlibを毎回削除できたりするので良いと思います。linuxだと mktemp コマンドとか。

Lambdaのhandler

lambda_function.py

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import subprocess


def lambda_handler(event, context):
    requirements_path = os.path.join(
        os.path.dirname(os.path.abspath(__file__)),
        ".requirements")
    os.environ["PYTHONPATH"] = requirements_path
    res = subprocess.run(["python", ".requirements/pytest.py", "./tests"],
                         env=os.environ, check=False, encoding="utf-8",
                         stdout=subprocess.PIPE)
    print(res.stdout)
    return res.check_returncode()

ということで、環境変数 "PYTHONPATH" に .requirements を追加するようにして、pytestを実行。
この書き方にしておくと、pytestの実行結果(success/failed)がlambda自体の成否に反映されます。
また、実行ログがlambdaのログとしてCloudWatchLogsに送信されるので、failした場合の解析もOK!

cacheの扱い

さて、ここまでの構成で Lambda を作成し、zipをdeployして動作させてみると、こんなエラーが発生。

OSError: [Errno 30] Read-only file system: '/var/task/.pytest_cache'

調査してみると、pytestは勝手に .pytest_cache というdirectoryを作成するらしく、ここに書き込み・参照を行うと。
cacheのpathを tmp/.pytest_cache とかに変更できればよいのですが、そういったoptionは見つからず。
※ tmp配下はlambda上で読み書きが許可されている

そこで、 cache を pytest 実行時に使用させないOptionを pytest.ini に記載。

pytest.ini

[pytest]
addopts = -p no:cacheprovider

ここまでで役者が全部出揃いました。

動作検証

lambdaを作成、上記 make_lambda_zip.sh で作成した zip を lambda に upload してテストしてみます。
(WebUI上の「テスト」で適当に実行。特に引数は不要なので、内容はなんでもOK)

f:id:kusuwada:20180622041918p:plain

更に、test失敗時のログ解析ができるかを検証するため、sample_test.py の期待するstatus_codeを 200 -> 500 に変更してテストしてみます。
失敗するので、「ログ」のリンクをたどってCloudWatchLogsのログを見に行きます。

f:id:kusuwada:20180622042210p:plain

失敗時のCloudWatchLogsから抜粋

============================= test session starts ==============================
platform linux -- Python 3.6.1, pytest-3.6.2, py-1.5.3, pluggy-0.6.0
rootdir: /var/task, inifile: pytest.ini
collected 1 item

tests/sample_test.py F

=================================== FAILURES ===================================
____________________________ test_simple_get_sample ____________________________

def test_simple_get_sample():
"simple get page request test"
res = requests.get('https://www.google.com/')
> assert res.status_code == 500
E assert 200 == 500
E + where 200 = <Response [200]>.status_code

tests/sample_test.py:9: AssertionError
=========================== 1 failed in 4.36 seconds ===========================

こんな感じで、ちゃんとテストの実行ログが残っていますね。これでテスト失敗時の解析もできそう!

参考リンク