ちょっと話題の記事

AWS Lambda Pythonをローカル環境で実行

2015.11.04

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

AWS Lambda を開発する際には

  1. コードを書く
  2. Zip で固めてアップロードする
  3. サンプルイベントをインプットに Lambda 関数をテスト実行する
  4. CloudWatch Logs でログを確認してデバッグ

というフローが発生します。

コード修正のたびにこのフローをたどるのはなかなか手間です。

そこで今回は python-lambda-local を使ってローカル環境で AWS Lambda Python の実行環境をエミュレートし、Lambda 関数を実行する方法について解説します。

以下のようにライブラリーパス(-l)、ハンドラー(-f)、タイムアウト(-t)、プログラム、(test.py)、入力イベント(events.json) を指定すると、Lambda 関数をローカル環境で実行できます。

$ python-lambda-local -l lib/ -f handler -t 5 test.py event.json
[INFO 2015-10-16 18:21:14,774] Event: {'key': 'value'}
[INFO 2015-10-16 18:21:14,774] START RequestId: 324cb1c5-fa9b-4f39-8ad9-01c95f7d5744
16
25
36
[INFO 2015-10-16 18:21:14,775] END RequestId: 324cb1c5-fa9b-4f39-8ad9-01c95f7d5744
[INFO 2015-10-16 18:21:14,775] RESULT: None

コード修正後に即座に実行結果を得られるため、Lambda 関数の開発スピードがかなり向上します。

インストールについて

virtualenv を使って閉じた Python 環境を用意します。

$ virtualenv ~/test
$ source ~test/bin/activate

pip で python-lambda-local をインストールします。 ヘルプコマンドを叩いて、インストールできていることを確認します。

$ pip install python-lambda-local
$ python-lambda-local -h
usage: python-lambda-local [-h] [-l LIBRARY_PATH] [-f HANDLER_FUNCTION]
                           [-t TIMEOUT]
                           FILE EVENT

Run AWS Lambda function written in Python on local machine.

positional arguments:
  FILE                  Lambda function file name
  EVENT                 Event data file name.

optional arguments:
  -h, --help            show this help message and exit
  -l LIBRARY_PATH, --library LIBRARY_PATH
                        Path of 3rd party libraries.
  -f HANDLER_FUNCTION, --function HANDLER_FUNCTION
                        Lambda function handler name. Default: "handler".
  -t TIMEOUT, --timeout TIMEOUT
                        Seconds until lambda function timeout. Default: 3

例1)ブループリントの hello-world-python を動かしてみる

AWS Lambda ブループリント hello-world-python は 入力イベントにあるキー "key1" の値を返すだけの単純な関数です。 3rdパーティーライブラリには依存していません。

lambda_function.py という名前で保存します。

# lambda_function.py
import json

print('Loading function')


def lambda_handler(event, context):
    #print("Received event: " + json.dumps(event, indent=2))
    print("value1 = " + event['key1'])
    print("value2 = " + event['key2'])
    print("value3 = " + event['key3'])
    return event['key1']  # Echo back the first key value
    #raise Exception('Something went wrong')

次にサンプルイベントテンプレートにある Hello World を event.json という名前で保存します。

{
  "key3": "value3",
  "key2": "value2",
  "key1": "value1"
}

ディレクトリ構造は次のようになっています。

$ tree .
.
|-- event.json
`-- lambda_function.py

python-lambda-local からハンドラー(-f lambda_handler)と入力イベント(event.json)を指定して実行してみましょう。

$ python-lambda-local -f lambda_handler lambda_function.py event.json
Loading function
[root - INFO - 2015-11-03 12:30:20,494] Event: {u'key3': u'value3', u'key2': u'value2', u'key1': u'value1'}
[root - INFO - 2015-11-03 12:30:20,494] START RequestId: f2a08429-3583-4ab4-9d16-fbdda458bca0
value1 = value1
value2 = value2
value3 = value3
[root - INFO - 2015-11-03 12:30:20,494] END RequestId: f2a08429-3583-4ab4-9d16-fbdda458bca0
[root - INFO - 2015-11-03 12:30:20,495] RESULT:
value1
[root - INFO - 2015-11-03 12:30:20,495] REPORT RequestId: f2a08429-3583-4ab4-9d16-fbdda458bca0	Duration: 0.25 ms

無事実行できました。

例2)ブループリントの lambda-canary を動かしてみる

AWS Lambda ブループリント Lambda-canary はスケジュール起動の Lambda 関数で、サイトの死活チェックを行います。 3rdパーティーライブラリには依存していません。 lambda_function.py という名前で保存します。

# lambda_function.py
from datetime import datetime
from urllib2 import urlopen

SITE = 'https://www.amazon.com/'  # URL of the site to check
EXPECTED = 'Online Shopping'  # String expected to be on the page


def validate(res):
    '''Return False to trigger the canary

    Currently this simply checks whether the EXPECTED string is present.
    However, you could modify this to perform any number of arbitrary
    checks on the contents of SITE.
    '''
    return EXPECTED in res


def lambda_handler(event, context):
    print('Checking {} at {}...'.format(SITE, event['time']))
    try:
        if not validate(urlopen(SITE).read()):
            raise Exception('Validation failed')
    except:
        print('Check failed!')
        raise
    else:
        print('Check passed!')
        return event['time']
    finally:
        print('Check complete at {}'.format(str(datetime.now())))

次にサンプルイベントテンプレートにある Scheduled Event を event.json という名前で保存します。

{
  "account": "123456789012",
  "region": "us-east-1",
  "detail": {},
  "detail-type": "Scheduled Event",
  "source": "aws.events",
  "time": "1970-01-01T00:00:00Z",
  "id": "cdc73f9d-aea9-11e3-9d5a-835b769c0d9c",
  "resources": [
    "arn:aws:events:us-east-1:123456789012:rule/my-schedule"
  ]
}

ディレクトリ構造は次のようになっています。

$ tree .
.
|-- event.json
`-- lambda_function.py

python-lambda-local からハンドラー(-f lambda_handler)と入力イベント(event.json)を指定して実行してみましょう。

$ python-lambda-local -f lambda_handler lambda_function.py event.json
[root - INFO - 2015-11-03 12:34:24,874] Event: {u'account': u'123456789012', u'region': u'us-east-1', u'detail': {}, u'detail-type': u'Scheduled Event', u'source': u'aws.events', u'time': u'1970-01-01T00:00:00Z', u'id': u'cdc73f9d-aea9-11e3-9d5a-835b769c0d9c', u'resources': [u'arn:aws:events:us-east-1:123456789012:rule/my-schedule']}
[root - INFO - 2015-11-03 12:34:24,874] START RequestId: 4b6f36e2-0d8c-4ec8-b895-60d34d70d2ca
Checking https://www.amazon.com/ at 1970-01-01T00:00:00Z...
Check passed!
Check complete at 2015-11-03 12:34:27.730889
[root - INFO - 2015-11-03 12:34:27,731] END RequestId: 4b6f36e2-0d8c-4ec8-b895-60d34d70d2ca
[root - INFO - 2015-11-03 12:34:27,731] RESULT:
1970-01-01T00:00:00Z
[root - INFO - 2015-11-03 12:34:27,731] REPORT RequestId: 4b6f36e2-0d8c-4ec8-b895-60d34d70d2ca	Duration: 2856.46 ms

"Check passed!" と成功系のメッセージがかえってきていますが、ネットワークを介しているため、最後の Duration からわかるように 実行に 2.8 秒かかっています。

Lambda 関数のタイムアウトをデフォルトの3秒から1秒に変えて実行してみましょう。

$ python-lambda-local -f lambda_handler -t 1 lambda_function.py event.json
[root - INFO - 2015-11-03 12:35:01,806] Event: {u'account': u'123456789012', u'region': u'us-east-1', u'detail': {}, u'detail-type': u'Scheduled Event', u'source': u'aws.events', u'time': u'1970-01-01T00:00:00Z', u'id': u'cdc73f9d-aea9-11e3-9d5a-835b769c0d9c', u'resources': [u'arn:aws:events:us-east-1:123456789012:rule/my-schedule']}
[root - INFO - 2015-11-03 12:35:01,807] START RequestId: 59e25865-9209-46ef-8fea-92c538e74fbd
Checking https://www.amazon.com/ at 1970-01-01T00:00:00Z...
Check failed!
Check complete at 2015-11-03 12:35:02.807572
[root - INFO - 2015-11-03 12:35:02,807] END RequestId: 59e25865-9209-46ef-8fea-92c538e74fbd
[root - ERROR - 2015-11-03 12:35:02,807] RESULT:
Timeout after 1 seconds.
[root - INFO - 2015-11-03 12:35:02,808] REPORT RequestId: 59e25865-9209-46ef-8fea-92c538e74fbd	Duration: 1000.51 ms

"Timeout after 1 seconds." というエラー系メッセージが表示され、"Duration: 1000.51 ms" と1秒きっかりでタイムアウトしています。

例3)ブループリントの lambda-canary を requests ライブラリに書き換えて動かしてみる

先ほどの lambda-canary に対して requests ライブラリを利用するように書き換えます。

3rd パーティーの requests ライブラリを

  • デフォルトとは異なるパス
  • デフォルトのライブラリパス

の2通りでインストールしてローカル実行します。

Lambda 関数プログラムの Zip アップロード時には、3rd パーティーライブラリを含めなくてはいけません。 Zip パッケージ化の方法やコード管理のポリシーに応じて、好ましい方法でライブラリをインストールしてください。

デフォルトとは異なるパスに requests ライブラリをインストール

requests ライブラリを通常とは異なる CWD/lib 以下にインストールします。

$ pip install requests --target=`pwd`/lib
$ tree -d .
.
`-- lib
    |-- requests
    |   `-- packages
    |       |-- chardet
    |       `-- urllib3
    |           |-- contrib
    |           |-- packages
    |           |   `-- ssl_match_hostname
    |           `-- util
    `-- requests-2.8.1.dist-info

lambda_function.py を requests ライブラリを使うように書き換えます。

修正は次の2点です。

1つ目。 プログラムの先頭部分で import requests を追加

2つ目。 HTML 取得で if not validate(urlopen(SITE).read()) となっていた箇所を if not validate(requests.get(SITE).text) に変更。

from datetime import datetime
from urllib2 import urlopen
import requests # XXX added

SITE = 'https://www.amazon.com/'  # URL of the site to check
EXPECTED = 'Online Shopping'  # String expected to be on the page


def validate(res):
    '''Return False to trigger the canary

    Currently this simply checks whether the EXPECTED string is present.
    However, you could modify this to perform any number of arbitrary
    checks on the contents of SITE.
    '''
    return EXPECTED in res


def lambda_handler(event, context):
    print('Checking {} at {}...'.format(SITE, event['time']))
    try:
        #if not validate(urlopen(SITE).read()):
        if not validate(requests.get(SITE).text): # XXX changed
            raise Exception('Validation failed')
    except:
        print('Check failed!')
        raise
    else:
        print('Check passed!')
        return event['time']
    finally:
        print('Check complete at {}'.format(str(datetime.now())))

python-lambda-local コマンド実行時に -l ./lib とライブラリパスを指定すると、Lambda 関数実行時のライブラリパスに ./lib も追加されます。

$ python-lambda-local -f lambda_handler -l ./lib lambda_function.py event.json
[root - INFO - 2015-11-03 12:50:13,316] Event: {u'account': u'123456789012', u'region': u'us-east-1', u'detail': {}, u'detail-type': u'Scheduled Event', u'source': u'aws.events', u'time': u'1970-01-01T00:00:00Z', u'id': u'cdc73f9d-aea9-11e3-9d5a-835b769c0d9c', u'resources': [u'arn:aws:events:us-east-1:123456789012:rule/my-schedule']}
[root - INFO - 2015-11-03 12:50:13,316] START RequestId: fbad8bee-44eb-499f-9d26-a805d9e7a81d
Checking https://www.amazon.com/ at 1970-01-01T00:00:00Z...
[requests.packages.urllib3.connectionpool - INFO - 2015-11-03 12:50:13,320] Starting new HTTPS connection (1): www.amazon.com
[requests.packages.urllib3.connectionpool - INFO - 2015-11-03 12:50:14,050] Starting new HTTP connection (1): www.amazon.com
Check passed!
Check complete at 2015-11-03 12:50:15.803431
[root - INFO - 2015-11-03 12:50:15,803] END RequestId: fbad8bee-44eb-499f-9d26-a805d9e7a81d
[root - INFO - 2015-11-03 12:50:15,803] RESULT:
1970-01-01T00:00:00Z
[root - INFO - 2015-11-03 12:50:15,803] REPORT RequestId: fbad8bee-44eb-499f-9d26-a805d9e7a81d	Duration: 2486.68 ms

requests ライブラリ固有のログが出力されており、"Check passed!" と成功系のメッセージがかえってきています。

通常のパスに requests ライブラリをインストール

pip に何もオプションを指定せず、素直に requests ライブラリをインストールします。

$ pip install requests
$ python -c 'import requests;print requests.__path__'
['/home/ec2-user/test/local/lib/python2.7/site-packages/requests']

requests ライブラリはデフォルトのライブラリパス以下にインストールされているため、 python-lambda-local 実行時に追加のライブラリパスを指定する必要はありません。

$ rm -r ./lib # 念のため lib 以下を削除
$ python-lambda-local -f lambda_handler -t 5 lambda_function.py event.json

python-lambda-local ができないこと

python-lambda-local はできたてホヤホヤのプロダクトです。 当然ながら、制限事項もあります。

AWS Lambda へのデプロイはできません。 プログラムの Zip 可やアップロードは別のツールを使ってください。

メモリ使用量の制限はできません。

Lambda に実行ロールを assume-role できません。 Lambda と同じロールの Instance Profile を作成し、IAM Role を設定した EC2 から実行すると、近いことができます。

まとめ

最新版の python-lambda-local 0.1.1 でも、基本機能は一通りそろっており、開発に使えます。

AWS Lambda Python はまだデビューしてから1ヶ月程度しか経過していないため、開発周辺ツールが発展途上です。 ローカル環境での開発ツールや AWS へのデプロイツールが整い、生産性が上がると、より多くの人が Python Lambda を Lambda 関数開発の第一候補に考えるのではないでしょうか。

参考