AWS Lambda Pythonをlambda-uploaderでデプロイ

AmazonLambda

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

AWS Lambda を開発する際には

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

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

今回は lambda-uploader を使い、 Step2の手順、つまり、コードのZIP化と AWS Lambda へのアップロードをコマンド一発で実行する方法について解説します。AWS Lambda では依存ライブラリも含めてZIP化しないといけないため、多くの人が一度は頭を悩まし、効率化を追求したくなるステップかと思います。

python-lambda-local と組み合わせることで

  1. python-lambda-local を使ってローカル環境で開発
  2. lambda-uploader で AWS Lambda にデプロイ

という AWS Lambda Python に特化した開発フローの出来上がりです。

lambda-uploader ができること

lambda-uploader は AWS Lambda Python 向けのツールです。

  • コードの ZIP 化
  • コードの AWS Lambda へのアップロード
  • AWS Lambda 設定の更新

などが可能です。

IAM 周りの事前準備

Lambda 関数作成時に IAM ロールを指定します。そのため、デプロイするユーザーは iam:PassRole 権限が必要です。

また、Lambda 関数用のロールも事前に作成しておいてください。

  • Lambda 関数が利用するリソースの操作権限
  • プルモデル Lambda の場合はイベントソースをポーリングする権限
  • プッシュモデル Lambda の場合はイベントソースが Lambda 関数を呼び出す権限

などが必要です。

Lambda の権限周りの詳細は公式ドキュメントをご確認ください。

http://docs.aws.amazon.com/lambda/latest/dg/intro-permission-model.html

インストール方法

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

$ virtualenv uploader
$ source  uploader/bin/activate

pip で lambda-uploader をインストールします。

$ pip install lambda-uploader # 最新のリリース版をインストール
$ pip install git+https://github.com/rackerlabs/lambda-uploader # git main branch をインストール

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

$ lambda-uploader --version
version 0.2.1

デプロイしてみる

ライブラリ requests を利用した Lambda 関数をデプロイしてみます。

Lambda 関数の定義ファイルの用意

まずは lamda-uploder を使ったデプロイでコアとなる lambda.json という JSON 形式の Lambda 関数の定義ファイルを用意します。

{
  "name": "Canary",
  "description": "lambda-uploader w/ 3rd party packages",
  "region": "ap-northeast-1",
  "handler": "canary.lambda_handler",
  "role": "arn:aws:iam::123456789012:role/lambda_canary_execution",
  "timeout": 300,
  "memory": 128
}
  • name は Lambda 関数名です。関数名をキーに関数の新規・更新処理が行われます。
  • handler には ファイル名.ハンドラー関数名 を指定しましょう。
  • role は Lambda 関数の Execution Role  の arn です。事前に定義しておきましょう。

あとは項目名の通りで迷うことはないかと思います。

Lambda 関数の定義

関数(canary.py)を用意します。blueprint-canary をベースに 3rd パーティーの requests ライブラリを使うように一部書き換えているだけです。

from datetime import datetime
import requests

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(requests.get(SITE).text):
            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())))

依存ライブラリは requirements.txt で管理し、pip 経由でインストールします。

$ cat requirements.txt
requests
$ pip install -r requirements.txt

requests モジュールが必要という情報はレポジトリ管理しますが、requests モジュールそのものはソースコード管理しない戦略です。

サンプルイベントの用意

最後に、ローカル環境から Lambda 関数を invoke できるようにサンプルイベントファイル(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 .
.
|-- canary.py
|-- event.json
|-- lambda.json
`-- requirements.txt

0 directories, 4 files

AWS Lambda にデプロイ

カレントディレクトリで $ Lambda-uploader コマンドを叩けば、デプロイ完了です。

$ lambda-uploader
λ Building Package
λ Uploading Package
λ Fin

requirements.txt で指定した 3rd パーティライブラリ(や Lambda 関数実行には不要なライブラリ)も含めて ZIP 化&アップロードされます。

親ディレクトリにいる時は $ lambda-uploader ./canary のようにコマンド実行することもできます。

デプロイした Lambda 関数を確認

マネージメントコンソールからデプロイした Lambda 関数を確認しましょう。

aws-lambda-function

API でデプロイした関数を確認しましょう。

$ aws lambda get-function --function-name Canary
{
    "Code": {
        "RepositoryType": "S3",
        "Location": "https://awslambda-ap-ne-1-tasks.s3-ap-northeast-1.amazonaws.com/snapshots/123456789012/Canary-6b7ee826-45a8-4904-8a96-b94721d7eeba?x-amz-security-token=..."
    },
    "Configuration": {
        "Version": "$LATEST",
        "CodeSha256": "sALDX0piM8OddamwnbUDVDWfDjjA7IAEVK3I1iqj2X8=",
        "FunctionName": "Canary",
        "MemorySize": 128,
        "CodeSize": 3752046,
        "FunctionArn": "arn:aws:lambda:ap-northeast-1:123456789012:function:Canary",
        "Handler": "canary.lambda_handler",
        "Role": "arn:aws:iam::123456789012:role/lambda_basic_execution",
        "Timeout": 300,
        "LastModified": "2015-11-07T08:28:20.160+0000",
        "Runtime": "python2.7",
        "Description": "lambda-uploader w/ 3rd party packages"
    }
}

デプロイした Lambda 関数を実行

デプロイした Lambda 関数 Canary を invoke してみましょう。 サンプルイベントとして、先ほど用意した event.json ファイルを指定します。

$ aws lambda invoke \
    --invocation-type RequestResponse \
    --function-name Canary \
    --payload file://event.json \
    outputfile.txt
{
    "StatusCode": 200
}
$ cat outputfile.txt
"1970-01-01T00:00:00Z"

成功です。

Lambda 関数の更新の場合

Lambda-uploader は関数名をキーにして

  • 存在しなければ、新規登録処理(create-function)
  • 存在すれば、コードと設定の更新処理(update-function-code & update-function-configuration)

を行います。

コードと Lambda 関数設定を変更して再デプロイしてみます。

$ aws lambda get-function --function-name Canary
{
    "Code": {
        "RepositoryType": "S3",
        "Location": "https://awslambda-ap-ne-1-tasks.s3-ap-northeast-1.amazonaws.com/snapshots/123456789012/Canary-895d00d9-2a63-41b7-9711-2f6261faaacf?x-amz-security-token=..."
    },
    "Configuration": {
        "Version": "$LATEST",
        "CodeSha256": "4LOuCyfuKoS+vyECl0FkerJElCG6EKjprs8tJBbzmHc=",
        "FunctionName": "Canary",
        "MemorySize": 128,
        "CodeSize": 3752529,
        "FunctionArn": "arn:aws:lambda:ap-northeast-1:123456789012:function:Canary",
        "Handler": "canary.lambda_handler",
        "Role": "arn:aws:iam::123456789012:role/lambda_basic_execution",
        "Timeout": 180,
        "LastModified": "2015-11-07T08:57:10.772+0000",
        "Runtime": "python2.7",
        "Description": "updated description"
    }
}
$ aws lambda invoke \
  --invocation-type RequestResponse \
  --function-name Canary \
  --payload file://event.json \
  outputfile.txt
{
    "StatusCode": 200
}
$ cat outputfile.txt
"Check passed!"

デプロイ時の細かい設定について

バージョニングを有効にする

  • lambda-uploader 時に -p/--publish オプションを有効にする
  • lambda.json ファイルで属性 "publish" : true を追加する

と Lambda 関数がバージョニングされます。

エイリアス設定する

lambda-uploader 時に

  • エイリアス名(--alias)
  • エイリアスの説明(--alias-description)

を指定すると、デプロイと同時にエイリアス更新もできます。

エイリアス機能はバージョニングされているのが前提のため、--publish オプションもつけてください。

$ lambda-uploader --publish --alias test --alias-description "my first test alias"
λ Building Package
λ Uploading Package
λ Fin
$ aws lambda list-aliases --function-name Canary
{
    "Aliases": [
        {
            "AliasArn": "arn:aws:lambda:ap-northeast-1:123456789012:function:Canary:test",
            "FunctionVersion": "1",
            "Name": "test",
            "Description": "my first test alias"
        }
    ]
}

エイリアス名をキーにして

  • 存在しなければ、新規登録処理(create-alias)
  • 存在すれば、更新処理(update-alias)

を行います。

試しにエイリアスを更新してみましょう。

$ lambda-uploader --publish --alias test --alias-description "my 2nd test alias"
λ Building Package
λ Uploading Package
λ Fin
$ aws lambda list-aliases --function-name Canary
{
    "Aliases": [
        {
            "AliasArn": "arn:aws:lambda:ap-northeast-1:123456789012:function:Canary:test",
            "FunctionVersion": "2",
            "Name": "test",
            "Description": "my 2nd test alias"
        }
    ]
}

デプロイパイプラインをもう少し詳しく見てみる

ここからは若干趣味の領域です。 興味のない方は読み飛ばしてください。

  1. 作業ディレクトリを作成し、コードをまとめる
  2. コードを ZIP 化
  3. 作業ディレクトリの削除
  4. AWS Lambda にデプロイ

という処理から成り立っています。

このパイプライン処理を少し深追いしてみます。

コードのZIP化だけをする

  • --no-upload オプションをつけると、ZIP 化までの処理を行い、AWS Lambda へのアップロードは行いません。
  • --no-clean オプションをつけると、作業ディレクトリを削除しません。

verbose オプション(-V) を有効にして実行してみましょう。

$ lambda-uploader --no-upload --no-clean -V
λ Building Package
INFO:lambda_uploader.package:Copying site packages
INFO:lambda_uploader.utils:Copying source files
INFO:lambda_uploader.package:Creating zipfile
λ Fin
$ ls -al
total 3700
drwxrwxr-x  3 ec2-user ec2-user    4096 Nov  7 08:51 .
drwx------ 18 ec2-user ec2-user    4096 Nov  7 08:38 ..
drwxrwxr-x  4 ec2-user ec2-user    4096 Nov  7 08:51 .lambda_package
-rw-rw-r--  1 ec2-user ec2-user     934 Nov  7 07:42 canary.py
-rw-rw-r--  1 ec2-user ec2-user     300 Nov  3 06:43 event.json
-rw-rw-r--  1 ec2-user ec2-user     251 Nov  7 07:15 lambda.json
-rw-rw-r--  1 ec2-user ec2-user 3752380 Nov  7 08:51 lambda_function.zip
-rw-rw-r--  1 ec2-user ec2-user      22 Nov  7 08:29 outputfile.txt
-rw-rw-r--  1 ec2-user ec2-user       9 Nov  7 07:04 requirements.txt

lambda_function.zip が AWS Lambda にアップロードする ZIP ファイルです。4MB 近くあります。 .lambda_package ディレクトリ以下が作業ディレクトリです。

依存ライブラリの指定方法

デプロイ時に依存パッケージを何も明示的に指定しなければ requirements.txt が利用されます。

Lambda.json ファイルの "requirements" で依存パッケージを指定することもできます。

{
  "name": "canary",
  ...
  "requirements": ["requests"],
  ...
}

2重管理を避けるために、requests.txt に一本化したほうが良いでしょう。

何が ZIP 化されるのか

$ ls -l --block-size=MB *zip
-rw-rw-r-- 1 ec2-user ec2-user 4MB Nov  7 08:32 lambda_function.zip

4MB と思ったより大きいですね。 ZIP ファイルの中身をのぞいてみてみましょう。

$ unzip -l lambda_function.zip
Archive:  lambda_function.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
      315  11-07-2015 08:32   easy_install.pyc
       22  11-07-2015 08:32   outputfile.txt
      126  11-07-2015 08:32   easy_install.py
        9  11-07-2015 08:32   requirements.txt
      300  11-07-2015 08:32   event.json
      934  11-07-2015 08:32   canary.py
      251  11-07-2015 08:32   lambda.json
    69120  11-07-2015 08:32   setuptools/cli-arm-32.exe
     1274  11-07-2015 08:32   setuptools/windows_support.pyc
...
     3686  11-07-2015 08:32   wheel/signatures/__init__.pyc
     3779  11-07-2015 08:32   wheel/signatures/__init__.py
    13985  11-07-2015 08:32   wheel/tool/__init__.pyc
    13310  11-07-2015 08:32   wheel/tool/__init__.py
     5063  11-07-2015 08:32   _markerlib/markers.pyc
     1097  11-07-2015 08:32   _markerlib/__init__.pyc
      552  11-07-2015 08:32   _markerlib/__init__.py
     3979  11-07-2015 08:32   _markerlib/markers.py
---------                     -------
 10066874                     833 files
  • site-packages 以下
  • requirements.txt または labmda.json で指定したライブラリ
  • カレントディレクトリ以下

をひとまとめにして ZIP 化しているようです。

作業ディレクトリ(.lambda_package)を覗いてみましょう。

$ tree -d -L 2 .lambda_package/
.lambda_package/
|-- lambda_package
|   |-- _markerlib
|   |-- pip
|   |-- pip-7.1.2.dist-info
|   |-- pkg_resources
|   |-- requests
|   |-- requests-2.8.1.dist-info
|   |-- setuptools
|   |-- setuptools-18.2.dist-info
|   |-- wheel
|   `-- wheel-0.24.0.dist-info
`-- venv
    |-- bin
    |-- include
    |-- lib
    |-- lib64 -> lib
    `-- local

17 directories

コードを斜め読みすると、以下の様なことをやっているようです。

  1. 作業ディレクトリ以下に別途 virtualenv 環境を作る
  2. その中で pip install やファイルのコピーを .lambda_package/lambda_package 以下に向けて行う
  3. .lambda_package/lambda_package 以下を ZIP ファイル(lambda_function.zip)にする

ZIP ファイルには pip/setuptools のような Lambda 関数実行に関係のないライブラリが含まれていたり、pyc ファイルも含まれていたりするので ZIP ファイルをスリム化する余地はまだまだ残っています。改善したいですね。

まとめ

今年の9月から10月にかけては Lambda 関数を node.js/Python である程度書く機会がありました。開発で一番面倒に感じたのがコードの ZIP パッケージ化です。今回紹介した lambda-uploader はその処理を担ってくれます。

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

参考