Featured image of post Lambdaをローカルで動かす「python-lambda-local」の使い方【AWS】

Lambdaをローカルで動かす「python-lambda-local」の使い方【AWS】

AWS LambdaのPythonスクリプトをローカルで動作確認できる「python-lambda-local」の使い方。pipでのインストールから、外部依存なし・リクエスト依存・外部ライブラリ依存の3パターンの実行方法とJSONエラーの対処まで♪

3710文字

ローカルでのLambdaスクリプト開発の課題

サーバーレスアーキテクチャの代名詞

皆さんはAWSのLambdaを使っているでしょうか?

Lambdaはサーバーレスアーキテクチャを実現する上で、欠かせない存在となっており、サーバーレスの流行りのおかげでとてもメジャーなAWSサービスの一つとなっています。

AWS Lambda を使用することで、サーバーのプロビジョニングや管理をすることなく、コードを実行できます。課金は実際に使用したコンピューティング時間に対してのみ発生し、コードが実行されていないときには料金も発生しません。

Lambda を使用すれば、実質どのようなタイプのアプリケーションやバックエンドサービスでも管理を必要とせずに実行できます。コードさえアップロードすれば、高可用性を実現しながらコードを実行およびスケーリングするために必要なことは、すべて Lambda により行われます。コードは、他の AWS のサービスから自動的にトリガーされるよう設定することも、ウェブやモバイルアプリケーションから直接呼び出すよう設定することもできます。 AWS Lambda (サーバーレスでコードを実行・自動管理) | AWS

また、Lambdaは言語を問わずJava、Node.js、Pythonなど様々な言語を用いて実装出来るのも魅力の一つです。
最近はPythonでLambdaを実装するのが多いですね。

ローカル開発がちょっと大変

この便利なLambdaですが、ローカルで動作確認をしたい場合にちょっと厄介です。

外部のライブラリを使用している場合はZipに固める必要があったり、関数のトリガーもLambdaに依存しているため、ローカルで同じように動かすにはローカル様にちょっとソースを変える必要があったりと差分が発生してしまいます。

python-lambda-localが便利過ぎる

そんな中、PythonでLambdaをローカル環境で実装・動作確認をする際にとてもpython-lambda-local と言う便利なツールを見つけました。

こちらを使えば、ローカルでのLambdaスクリプトを簡単に実装・動作確認することが出来ます。

また、インストールもpipコマンドでワンタッチで出来たのでインストールから使い方までご紹介致します。

手順

pipでpython-lambda-localをインストール

今回はpipコマンドでpython-lambda-localをインストールします。
pip install python-lambda-localを実行すると以下のようにインストールが完了すると思います。

pip install python-lambda-local
Collecting python-lambda-local
  Downloading https://files.pythonhosted.org/packages/d1/87/f54ade7dea162f501451340ec6ce1f906d7ef5ed09f310c6a9dd468caaac/python-lambda-local-0.1.7.tar.gz
Collecting boto3 (from python-lambda-local)
  Downloading https://files.pythonhosted.org/packages/0b/3b/925c7aded1c54c12d5be964ab543420b01d95759c5756a574588d49444ad/boto3-1.9.71-py2.py3-none-any.whl (128kB)
    100% |████████████████████████████████| 133kB 2.9MB/s
Collecting botocore<1.13.0,>=1.12.71 (from boto3->python-lambda-local)
  Downloading https://files.pythonhosted.org/packages/50/c0/cd4f8bec8a10876f0ce34f0cf264fda04e09df41d1a473a43f890c71fffa/botocore-1.12.71-py2.py3-none-any.whl (5.2MB)
    100% |████████████████████████████████| 5.2MB 1.8MB/s
Collecting jmespath<1.0.0,>=0.7.1 (from boto3->python-lambda-local)
  Downloading https://files.pythonhosted.org/packages/b7/31/05c8d001f7f87f0f07289a5fc0fc3832e9a57f2dbd4d3b0fee70e0d51365/jmespath-0.9.3-py2.py3-none-any.whl
Collecting s3transfer<0.2.0,>=0.1.10 (from boto3->python-lambda-local)
  Downloading https://files.pythonhosted.org/packages/d7/14/2a0004d487464d120c9fb85313a75cd3d71a7506955be458eebfe19a6b1d/s3transfer-0.1.13-py2.py3-none-any.whl (59kB)
    100% |████████████████████████████████| 61kB 532kB/s
Collecting docutils>=0.10 (from botocore<1.13.0,>=1.12.71->boto3->python-lambda-local)
  Downloading https://files.pythonhosted.org/packages/50/09/c53398e0005b11f7ffb27b7aa720c617aba53be4fb4f4f3f06b9b5c60f28/docutils-0.14-py2-none-any.whl (543kB)
    100% |████████████████████████████████| 552kB 2.0MB/s
Collecting python-dateutil<3.0.0,>=2.1; python_version >= "2.7" (from botocore<1.13.0,>=1.12.71->boto3->python-lambda-local)
  Downloading https://files.pythonhosted.org/packages/74/68/d87d9b36af36f44254a8d512cbfc48369103a3b9e474be9bdfe536abfc45/python_dateutil-2.7.5-py2.py3-none-any.whl (225kB)
    100% |████████████████████████████████| 235kB 2.2MB/s
Requirement already satisfied: urllib3<1.25,>=1.20; python_version == "2.7" in /usr/local/lib/python2.7/site-packages (from botocore<1.13.0,>=1.12.71->boto3->python-lambda-local) (1.23)
Collecting futures<4.0.0,>=2.2.0; python_version == "2.6" or python_version == "2.7" (from s3transfer<0.2.0,>=0.1.10->boto3->python-lambda-local)
  Downloading https://files.pythonhosted.org/packages/2d/99/b2c4e9d5a30f6471e410a146232b4118e697fa3ffc06d6a65efde84debd0/futures-3.2.0-py2-none-any.whl
Collecting six>=1.5 (from python-dateutil<3.0.0,>=2.1; python_version >= "2.7"->botocore<1.13.0,>=1.12.71->boto3->python-lambda-local)
  Downloading https://files.pythonhosted.org/packages/73/fb/00a976f728d0d1fecfe898238ce23f502a721c0ac0ecfedb80e0d88c64e9/six-1.12.0-py2.py3-none-any.whl
Building wheels for collected packages: python-lambda-local
  Running setup.py bdist_wheel for python-lambda-local ... done
  Stored in directory: /Users/blogenist/Library/Caches/pip/wheels/12/e2/66/adee4094d943f6647227ccc5feeca73ddafe31aac3351b6c84
Successfully built python-lambda-local
Installing collected packages: jmespath, docutils, six, python-dateutil, botocore, futures, s3transfer, boto3, python-lambda-local
Successfully installed boto3-1.9.71 botocore-1.12.71 docutils-0.14 futures-3.2.0 jmespath-0.9.3 python-dateutil-2.7.5 python-lambda-local-0.1.7 s3transfer-0.1.13 six-1.12.0

確認

では、python-lambda-local --versionコマンドで動作確認をしてみましょう。
以下のようにバージョンが表示されればインストール成功です。

python-lambda-local --version
python-lambda-local 0.1.7

外部依存無しの場合

では、実際にスクリプトを書いてみましょう。

まずはお決まりのHello World!!!を出力してみます。
こちらはライブラリなどには何も依存していないスクリプトとなります。

lambda_function.py
print('Start Function.')

def lambda_handler(event, context):
    print("Hello World!!")

次に、event.jsonを用意します。

こちらは実行する際のリクエスト情報を定義します。

今回は特にリクエスト内容は不要ですが、必須で指定する必要があるようなので以下のような空のJsonのファイルを用意しておきます

event.json
{}

ValueError: No JSON object could be decoded

ファイルは用意したが、中身が空の場合は以下のようなエラーが出てしまいます。

実行時エラー
Process Process-1:
Traceback (most recent call last):
  File "/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/multiprocessing/process.py", line 267, in _bootstrap
    self.run()
  File "/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/multiprocessing/process.py", line 114, in run
    self._target(*self._args, **self._kwargs)
  File "/usr/local/lib/python2.7/site-packages/lambda_local/main.py", line 49, in run
    e = event.read_event(args.event)
  File "/usr/local/lib/python2.7/site-packages/lambda_local/event.py", line 11, in read_event
    data = json.load(event)
  File "/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/__init__.py", line 291, in load
    **kw)
  File "/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/__init__.py", line 339, in loads
    return _default_decoder.decode(s)
  File "/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/decoder.py", line 364, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
  File "/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7/json/decoder.py", line 382, in raw_decode
    raise ValueError("No JSON object could be decoded")
ValueError: No JSON object could be decoded

LambdaはJson形式で待ち構えているので、ファイルが空の場合にJsonオブジェクトをデコード出来ずにエラーになってしまうので気をつけましょう。

確認

では、実行してみましょう。

実行コマンドはpython-lambda-local --function {関数名} --timeout {タイムアウト秒} {スクリプトファイル名} {イベントファイル名}のように組み立てます。

今回だと以下のようになります。

コマンド
python-lambda-local --function lambda_handler --timeout 5 lambda_function.py event.json

実行すると以下のように出力されます。

実行ログ
Start Function.
[root - INFO - 2018-12-27 14:21:04,297] Event: {}
[root - INFO - 2018-12-27 14:21:04,298] START RequestId: c4dc58b6-29e9-47df-b800-9f51ea96abbe
Hello World!!
[root - INFO - 2018-12-27 14:21:04,299] END RequestId: c4dc58b6-29e9-47df-b800-9f51ea96abbe
[root - INFO - 2018-12-27 14:21:04,299] RESULT:
None
[root - INFO - 2018-12-27 14:21:04,299] REPORT RequestId: c4dc58b6-29e9-47df-b800-9f51ea96abbe	Duration: 0.36 ms

正常にHello World!!!が出力されましたね♪

リクエスト依存の場合

では、POSTで送られてくるデータを操作するケースを想定してみます。
データはevent.jsonに定義する事でスクリプト内で取得出来るようになります。

event.jsonを以下のように変えてみます。

event.json
{
  users:[
    {
      "name": "Taro",
      "age": 22
    },{
      "name": "Jiro",
      "age": 20
    },{
      "name": "Hanako",
      "age": 98
    }
  ]
}

そして、lambda_function.pyは以下のように修正します。

lambda_function.py
print('Start Function.')

def lambda_handler(event, context):
    for user in event['users']:
        print(user['name'])
        print(user['age'])
        print("================")

確認

では、先ほどと同じコマンドで実行してみましょう。

python-lambda-local --function lambda_handler --timeout 5 lambda_function.py event.json
Start Function.
[root - INFO - 2018-12-27 14:37:48,434] Event: {u'users': [{u'age': 22, u'name': u'Taro'}, {u'age': 20, u'name': u'Jiro'}, {u'age': 98, u'name': u'Hanako'}]}
[root - INFO - 2018-12-27 14:37:48,435] START RequestId: c401f427-415b-4d1a-be8a-c65a2c1d87cd
Taro
22
================
Jiro
20
================
Hanako
98
================
[root - INFO - 2018-12-27 14:37:48,435] END RequestId: c401f427-415b-4d1a-be8a-c65a2c1d87cd
[root - INFO - 2018-12-27 14:37:48,435] RESULT:
None
[root - INFO - 2018-12-27 14:37:48,436] REPORT RequestId: c401f427-415b-4d1a-be8a-c65a2c1d87cd	Duration: 0.29 ms

正常にJsonの情報を読み取れていますね♪

実際にPOSTでリクエストを投げられる場合も、Json形式で来るはずなのでローカルで正常に動いていれば、実際のAWSのLambda上でもJson構造が正しければ問題無く動くはずです。

外部ライブラリに依存する場合

次に外部ライブラリに依存するケースを試してみましょう。

Lambdaでは依存するライブラリはZip内に含めないといけない、と言う制約があります。
ローカルで動作確認するだけなら特に意識せずに通常通りに記述すれば良いです。
※実際にAWSのLambdaに載せる際はライブラリ毎Zipに含める必要があります

今回はrequestsモジュールを利用して、POSTされたurlにアクセスして、HttpStatusを出力する、と言うシンプルなスクリプトを書いてみましょう。

requestsをまだインストールしていない場合はpip install requestsコマンドでインストールをしてください。

pip install requests
 Downloading https://files.pythonhosted.org/packages/7d/e3/20f3d364d6c8e5d2353c72a67778eb189176f08e873c9900e10c0287b84b/requests-2.21.0-py2.py3-none-any.whl (57kB)
    100% |████████████████████████████████| 61kB 435kB/s
Requirement already satisfied: urllib3<1.25,>=1.21.1 in /usr/local/lib/python2.7/site-packages (from requests) (1.23)
Collecting chardet<3.1.0,>=3.0.2 (from requests)
  Downloading https://files.pythonhosted.org/packages/bc/a9/01ffebfb562e4274b6487b4bb1ddec7ca55ec7510b22e4c51f14098443b8/chardet-3.0.4-py2.py3-none-any.whl (133kB)
    100% |████████████████████████████████| 143kB 1.3MB/s
Collecting idna<2.9,>=2.5 (from requests)
  Downloading https://files.pythonhosted.org/packages/14/2c/cd551d81dbe15200be1cf41cd03869a46fe7226e7450af7a6545bfc474c9/idna-2.8-py2.py3-none-any.whl (58kB)
    100% |████████████████████████████████| 61kB 992kB/s
Collecting certifi>=2017.4.17 (from requests)
  Downloading https://files.pythonhosted.org/packages/9f/e0/accfc1b56b57e9750eba272e24c4dddeac86852c2bebd1236674d7887e8a/certifi-2018.11.29-py2.py3-none-any.whl (154kB)
    100% |████████████████████████████████| 163kB 896kB/s
Installing collected packages: chardet, idna, certifi, requests
Successfully installed certifi-2018.11.29 chardet-3.0.4 idna-2.8 requests-2.21.0

lambda_function.pyの中身は以下のように修正します。

lambda_function.py
import requests

print('Start Function.')

def lambda_handler(event, context):
    response = requests.get(event['url'])
    return 'Status : %d' % response.status_code

確認

では、こちらも実行してみてください。

python-lambda-local --function lambda_handler --timeout 5 lambda_function.py event.json
Start Function.
[root - INFO - 2018-12-27 17:54:34,702] Event: {u'url': u'https://www.google.co.jp/'}
[root - INFO - 2018-12-27 17:54:34,704] START RequestId: 60cb4394-12f3-463f-beba-d4f623dcb49b
[root - INFO - 2018-12-27 17:54:35,527] END RequestId: 60cb4394-12f3-463f-beba-d4f623dcb49b
[root - INFO - 2018-12-27 17:54:35,528] RESULT:
Status : 200
[root - INFO - 2018-12-27 17:54:35,528] REPORT RequestId: 60cb4394-12f3-463f-beba-d4f623dcb49b	Duration: 820.28 ms

こちらも正常に処理が実行されましたね♪

AWSサービスのトリガーに依存する場合

では、最後により業務に近づいたケースを試してみましょう。

今回はAWSのS3バケットにファイルがPUTされたタイミングで動くスクリプトを書いてみます。

まずは、event.jsonにPUTされた際に送られてくるデータを定義します。

公式 に記載されていたサンプルイベントデータをベースに{バケット名}と{キー名}だけを修正します。

event.json
{
   "Records":[
      {
         "eventVersion":"2.0",
         "eventSource":"aws:s3",
         "awsRegion":"us-west-2",
         "eventTime":"1970-01-01T00:00:00.000Z",
         "eventName":"ObjectCreated:Put",
         "userIdentity":{
            "principalId":"AIDAJDPLRKLG7UEXAMPLE"
         },
         "requestParameters":{
            "sourceIPAddress":"127.0.0.1"
         },
         "responseElements":{
            "x-amz-request-id":"C3D13FE58DE4C810",
            "x-amz-id-2":"FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD"
         },
         "s3":{
            "s3SchemaVersion":"1.0",
            "configurationId":"testConfigRule",
            "bucket":{
               "name":"{バケット名}",
               "ownerIdentity":{
                  "principalId":"A3NL1KOZZKExample"
               },
               "arn":"arn:aws:s3:::{バケット名}"
            },
            "object":{
               "key":"{キー名}",
               "size":1024,
               "eTag":"d41d8cd98f00b204e9800998ecf8427e",
               "versionId":"096fKKXTRTtl3on89fVO.nfljtsv6qko"
            }
         }
      }
   ]
}

boto3モジュールをインストールしていない方はpip install boto3でインストールしてください。

lambda_function.pyは以下のように修正します。

lambda_function.py
from boto3.session import Session
import urllib

print('Start Function.')

session = Session(profile_name='{profile名}')
s3 = session.client('s3')

def lambda_handler(event, context):
    # Get Bucket Name
    bucket = event['Records'][0]['s3']['bucket']['name']

    # Get Key Name
    key = urllib.unquote_plus(event['Records'][0]['s3']['object']['key'].encode('utf8'))
    try:
        # Get Object
        response = s3.get_object(Bucket=bucket, Key=key)

        # Print Object ContentType
        print("CONTENT TYPE: " + response['ContentType'])
        return response['ContentType']
    except Exception as e:
        print(e)
        raise e

このファイルの中でS3にアクセスする際にAWS CLIが利用されます。
session = Session(profile_name='{profile名}')としています。

確認

では、実際に実行してみましょう。

python-lambda-local --function lambda_handler --timeout 5 lambda_function.py event.json
Start Function.
[botocore.credentials - INFO - 2018-12-27 18:43:56,374] Found credentials in shared credentials file: ~/.aws/credentials
[root - INFO - 2018-12-27 18:43:56,545] Event: {u'Records': [{u'eventVersion': u'2.0', u'eventTime': u'1970-01-01T00:00:00.000Z', u'requestParameters': {u'sourceIPAddress': u'127.0.0.1'}, u's3': {u'configurationId': u'testConfigRule', u'object': {u'versionId': u'096fKKXTRTtl3on89fVO.nfljtsv6qko', u'eTag': u'd41d8cd98f00b204e9800998ecf8427e', u'key': u'{キー名}', u'size': 1024}, u'bucket': {u'arn': u'arn:aws:s3:::{バケット名}', u'name': u'{バケット名}', u'ownerIdentity': {u'principalId': u'A3NL1KOZZKExample'}}, u's3SchemaVersion': u'1.0'}, u'responseElements': {u'x-amz-id-2': u'FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD', u'x-amz-request-id': u'C3D13FE58DE4C810'}, u'awsRegion': u'us-west-2', u'eventName': u'ObjectCreated:Put', u'userIdentity': {u'principalId': u'AIDAJDPLRKLG7UEXAMPLE'}, u'eventSource': u'aws:s3'}]}
[root - INFO - 2018-12-27 18:43:56,545] START RequestId: b56de8ee-067d-471d-87a0-01e671f895cf
CONTENT TYPE: image/jpeg
[root - INFO - 2018-12-27 18:43:59,139] END RequestId: b56de8ee-067d-471d-87a0-01e671f895cf
[root - INFO - 2018-12-27 18:43:59,139] RESULT:
image/jpeg
[root - INFO - 2018-12-27 18:43:59,140] REPORT RequestId: b56de8ee-067d-471d-87a0-01e671f895cf	Duration: 2593.92 ms

正常に存在するファイルのContentTypeが取得出来ましたね。
AWS上のLambdaに乗せればリアルなPUTイベントを取得出来るので様々な操作が可能になります♪

終わりに

今まではローカルで動作確認する際と実際にLambdaに上げる際にソースの修正やZip化をしたりと、差分が出てしまっていましたが、このツールを利用することでソースを変えること無くよりリアルな動作確認が出来るようになりました。

Lambdaを使っている方はとても便利なので是非使ってみてください♪