akiyoko blog

akiyoko の IT技術系ブログです

はじめての Python for Lambda (Python Functions)

この投稿は 「今年もやるよ!AWS Lambda縛り Advent Calendar 2015 - Qiita」 の 2日目の記事です。

はじめに

10月の re:Invent 2015 にて AWS Lambda の大幅アップデートが発表され、Node.js と Java のみが対応していた Lambda Function が Python に対応したことで、世界の Pythonista が「これで勝つる!」と色めき立ったのは記憶に新しいところです。


aws.typepad.com



さて、それから約2ヶ月が経ち、そろそろ試してみようかと思ったところ、意外と Python for Lambda (Python Functions) の記事が少なかったので *1、今回、自分でやってみたという次第です。



やりたいこと

S3オブジェクトの追加や変更を AWS Lambda で検知して、Amazon SNS でメール通知する


はじめての Python for Lambda (Python Functions) ということで、シンプルなパターンをやってみます。

まず、AWS Lambda では、S3 の状態変更などをトリガーとして Lambda Function を発火させることができます。今回は、S3オブジェクトの追加や変更を Event source として Python Function を発火し、実行結果を Amazon SNS 経由でメール通知させてみようと思います。

CloudWatch ログでも実行結果を確認することができるのですが、ログを確認できるのが(デフォルトだと)5分間隔となっていて、デバッグには少し不便です。そこで Amazon SNS を使うと、AWS の非同期タスクが完了したタイミングやエラーが発生したタイミングで通知を出すことができ、容易にデバッグができるようになります。


Amazon SNS については、以下の過去記事を参照のこと。

<過去記事>akiyoko.hatenablog.jp

 

利用条件

  • Python 2.7(現時点では 3系は利用できません)
  • Boto3(Boto は AWS Lambda に対応していません)

 

利用手順

Python for Lambda (Python Functions) の利用方法は、概ね以下の通りです。




 

1. Event source を設定する

AWS Management Console から AWS Lambda を選択し、「Get Started Now」をクリックします。
f:id:akiyoko:20151106003335p:plain


Blueprint(Lambda のテンプレート)を選択します。
今回は、「s3-get-object-python」を選びます。
f:id:akiyoko:20151106003423p:plain


Event source の設定を行います。

項目 設定例 備考
Event source type S3  
Bucket boto3-test 作成済みの S3バケットを指定
Event type Object Created (All) 「Put / Post / Copy / Complete Multipart Upload」など細かな指定ができるが、オブジェクトの追加・変更を一括で定義したいときは「Object Created (All)」とすればよい
Prefix  
Suffix mp4 この設定をしておくと、例えば、拡張子が「mp4」以外のファイルをアップロードしても Lambda Function が発火しなくなる

f:id:akiyoko:20151106003442p:plain


 

2. Lamdba Function を登録する

Lamdba Function の登録画面が開くので、まずは Role の設定を行います。

Role で「S3 execution role」を選択すると、新しいウィンドウが開きます。*2

f:id:akiyoko:20151106003503p:plain


「S3 execution role」には、S3 にアクセスする権限しか記述されていないため、Amazon SNS にアクセスできるように、この Role のポリシーに追記をします。
f:id:akiyoko:20151106003519p:plain
f:id:akiyoko:20151106003534p:plain


「lambda_s3_exec_role」に Amazon SNS へのアクセス許可ポリシー(ただし、必要最低限の「sns:CreateTopic」と「sns:Publish」のみ)を加えます。

<編集前>(lambda_s3_exec_role)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": [
        "arn:aws:s3:::*"
      ]
    }
  ]
}

<編集後>

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": [
        "arn:aws:s3:::*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "sns:CreateTopic",
        "sns:Publish"
      ],
      "Resource": "*"
    }
  ]
}


f:id:akiyoko:20151106003551p:plain


Lambda Function名(ARN の一部になります)、および Pythonコードを以下のように修正します。

f:id:akiyoko:20151106003608p:plain

import json
import urllib
import boto3

print('Loading function')

s3 = boto3.client('s3')
sns = boto3.resource('sns', 'ap-northeast-1')


def lambda_handler(event, context):
    print("Received event: " + json.dumps(event, indent=2))

    # Get the object from the event and show its content type
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = urllib.unquote_plus(event['Records'][0]['s3']['object']['key']).decode('utf8')
    print("bucket={}, key={}".format(bucket, key))

    try:
        response = s3.get_object(Bucket=bucket, Key=key)
    except Exception as e:
        print(e)
        print('Error getting object {} from bucket {}. Make sure they exist and your bucket is in the same region as this function.'.format(key, bucket))
        # Publish a message
        sns.Topic(sns.create_topic(Name='test-complete').arn).publish(
            Subject="Error!",
            Message="Failed to get object. bucket={}, key={}, {}".format(bucket, key, e),
        )
        raise e

    # Publish a message
    sns.Topic(sns.create_topic(Name='test-complete').arn).publish(
        Subject="Success!",
        Message="Completed to get object. bucket={}, key={}".format(bucket, key),
    )
    print("CONTENT TYPE: " + response['ContentType'])
    return response['ContentType']



最後に、この Event の検知を ON にするかどうかを選択して、「Create function」をクリックします。今回は、「Enable later」を選択しておき、テストが完了した後に設定を変更することにします。
f:id:akiyoko:20151106003624p:plain



 

3. Lamdba Function のテストを行う

Lamdba Function のテストをします。


まずは、テストをする前に、テスト用のダミーファイルを置いておきます。
(「Sample event template」と呼ばれるテスト用のサンプルイベントに、CreateObject のテストが用意されておらず、PutObject しか用意されていないので、先にオブジェクトが存在しないとエラーになってしまうためです。)

f:id:akiyoko:20151106003640p:plain


テストしてみます。

f:id:akiyoko:20151106003701p:plain


Sample event template で「S3 Put」を選択します。
最低限、以下のようにバケット名だけを修正すれば、テストが実行できます。

f:id:akiyoko:20151106003718p:plain


テストが成功しました。

Execution result: succeeded

"image/jpeg"

Summary

Request ID
c3e359d6-83c9-11e5-a817-8ff289613676
Duration
626.36 ms
Billed duration
700 ms
Resources configured
128 MB
Max memory used
46 MB

Log output

START RequestId: c3e359d6-83c9-11e5-a817-8ff289613676 Version: $LATEST
Received event: {
  "Records": [
    {
      "eventVersion": "2.0", 
      "eventTime": "1970-01-01T00:00:00.000Z", 
      "requestParameters": {
        "sourceIPAddress": "127.0.0.1"
      }, 
      "s3": {
        "configurationId": "testConfigRule", 
        "object": {
          "eTag": "0123456789abcdef0123456789abcdef", 
          "key": "HappyFace.jpg", 
          "sequencer": "0A1B2C3D4E5F678901", 
          "size": 1024
        }, 
        "bucket": {
          "ownerIdentity": {
            "principalId": "EXAMPLE"
          }, 
          "name": "boto3-test", 
          "arn": "arn:aws:s3:::mybucket"
        }, 
        "s3SchemaVersion": "1.0"
      }, 
      "responseElements": {
        "x-amz-id-2": "EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH", 
        "x-amz-request-id": "EXAMPLE123456789"
      }, 
      "awsRegion": "us-east-1", 
      "eventName": "ObjectCreated:Put", 
      "userIdentity": {
        "principalId": "EXAMPLE"
      }, 
      "eventSource": "aws:s3"
    }
  ]
}
bucket=boto3-test, key=HappyFace.jpg
CONTENT TYPE: image/jpeg
END RequestId: c3e359d6-83c9-11e5-a817-8ff289613676
REPORT RequestId: c3e359d6-83c9-11e5-a817-8ff289613676  Duration: 626.36 ms Billed Duration: 700 ms     Memory Size: 128 MB Max Memory Used: 46 MB  

f:id:akiyoko:20151106003734p:plain



SNS へのメール通知も成功していました。

<メール件名>

Success!

<メール本文>

Completed to get object. bucket=boto3-test, key=HappyFace.jpg


CloudWatch にもログが吐き出されています。
ただし先述したように、CloudWatch のログはデフォルト(無料枠)では 5分ごとに更新されるので、何度もテストするには少し不便です。

Event Data
Loading function
START RequestId: c3e359d6-83c9-11e5-a817-8ff289613676 Version: $LATEST 
Received event: { 
 "Records": [ 
 { 
 "eventVersion": "2.0",  
 "eventTime": "1970-01-01T00:00:00.000Z",  
 "requestParameters": { 
 "sourceIPAddress": "127.0.0.1" 
 },  
 "s3": { 
 "configurationId": "testConfigRule",  
 "object": { 
 "eTag": "0123456789abcdef0123456789abcdef",  
 "key": "HappyFace.jpg",  
 "sequencer": "0A1B2C3D4E5F678901",  
 "size": 1024 
 },  
 "bucket": { 
 "ownerIdentity": { 
 "principalId": "EXAMPLE" 
 },  
 "name": "boto3-test",  
 "arn": "arn:aws:s3:::mybucket" 
 },  
 "s3SchemaVersion": "1.0" 
 },  
 "responseElements": { 
 "x-amz-id-2": "EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH",  
 "x-amz-request-id": "EXAMPLE123456789" 
 },  
 "awsRegion": "us-east-1",  
 "eventName": "ObjectCreated:Put",  
 "userIdentity": { 
 "principalId": "EXAMPLE" 
 },  
 "eventSource": "aws:s3" 
 } 
 ] 
} 
bucket=boto3-test, key=HappyFace.jpg 
CONTENT TYPE: image/jpeg 
END RequestId: c3e359d6-83c9-11e5-a817-8ff289613676 
REPORT RequestId: c3e359d6-83c9-11e5-a817-8ff289613676  Duration: 626.36 ms Billed Duration: 700 ms Memory Size: 128 MB Max Memory Used: 46 MB  


f:id:akiyoko:20151106003807p:plain



 

4. 実環境で試す

テストが成功したので、Event の検知を ON にしておきます。

f:id:akiyoko:20151106003832p:plain

f:id:akiyoko:20151106003857p:plain

f:id:akiyoko:20151106003912p:plain


最後に、実際に S3 に mp4ファイルをアップロードしてみて、Lambda Function が発火するかテストしてみます。

f:id:akiyoko:20151106003936p:plain



実行結果

<メール件名>

Success!

<メール本文>

Completed to get object. bucket=boto3-test, key=test/sample.mp4


メール通知されました。



 

まとめ

AWS Lambda をはじめて使った感想ですが、IAM の設定やら GUI画面のインタフェースに少し取っ付きづらいところはあるものの、Python のコードが実行でき、Boto3 から AWSの各種サービスを呼び出せるということで、「もう何でもできる!」という印象です。


サーバレスな 2-Tier アーキテクチャを考慮するなら、AWS Lambda は各サービスを連携させるためのコアとなるサービスになるので、ある程度のパターンを習得しておくのはアリかと思います。


今回は「S3オブジェクトの追加や変更を AWS Lambda で検知して、Amazon SNS でメール通知する」というシンプルなパターンでしたが、次回は応用パターンとして、「S3 に mp4ファイルが追加されたら、Amazon Elastic Transcoder を起動して HLS形式の動画ファイルにトランスコードする」というのをやってみたいと思います。


ということで明日は引き続き、私 akiyoko の 3日目の記事になります。
(連チャンですいません。。)

*1: Python の「Lambda式」が検索のノイズになっていたのかな?とも考えられますが、、いずれにせよ、個人的な感覚です。

*2: Chrome のウィンドウブロックに引っ掛かっているのに気付かず、ドハマリしてしまったのは今となってはいい思い出です。