S3イベント通知からSNS経由でLambdaを起動し、イベントが発生したバケット名とオブジェクトキーを取得する

S3バケットへのファイルのPUTをトリガーにしてLambdaを起動する仕組みを実装するとき、その後のシステムの拡張性などを考慮すると、S3イベント通知とLambdaの間でSNSトピックを経由させた方が都合がいい場合があります。今回は、S3バケットにファイルがPUTされた際に、S3イベント通知からSNS経由でLambdaを起動し、イベントが発生したバケット名とオブジェクトキーを取得する方法を確認してみました。
2020.02.27

こんにちは、CX事業本部の若槻です。

S3バケットへのファイルのPUTをトリガーにしてLambdaを起動する仕組みを実装するとき、その後のシステムの拡張性などを考慮すると、S3イベント通知とLambdaの間でSNSトピックを経由させた方が都合がいい場合があります。

今回は、S3バケットにファイルがPUTされた際に、S3イベント通知からSNS経由でLambdaを起動し、イベントが発生したバケット名とオブジェクトキーを取得する方法を確認してみました。

環境準備

S3バケット、SNSトピック、Lambdaから成る以下のようなAWS環境を準備していきます。 image.png

CloudFormationで環境を構築します。テンプレートは以下のようになります。

template.yml

Resources:
  myTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: myTopic
      Subscription: 
        - Endpoint: !GetAtt myFunc.Arn
          Protocol: lambda

  myTopicPolicy:
    Type: AWS::SNS::TopicPolicy
    Properties:
      Topics:
        - !Ref myTopic
      PolicyDocument:
        Id: !Ref myTopic
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: s3.amazonaws.com
            Action: SNS:Publish
            Resource: !Ref myTopic
            Condition:
              ArnLike:
                aws:SourceArn: !Sub arn:aws:s3:::mybucket-${AWS::AccountId}

  myBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub mybucket-${AWS::AccountId}
      NotificationConfiguration:
        TopicConfigurations:
          - Event: s3:ObjectCreated:Put
            Topic: !Ref myTopic

  myRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: myRole
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

  lambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref myFunc
      Principal: sns.amazonaws.com
      SourceArn: !Ref myTopic

  myFunc:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: myFunc
      Code:
        ZipFile: |
          import json
          
          def lambda_handler(event, context):
            # 1. SNSトピックのeventデータ
            print(json.dumps(event, ensure_ascii=False))
          
            # 2. S3イベント通知のeventデータ
            s3_event = json.loads(event['Records'][0]['Sns']['Message'])
            print(json.dumps(s3_event, ensure_ascii=False))
          
            s3_event_record = s3_event['Records'][0]['s3']
          
            # 3. S3イベントが発生したバケット名
            bucket = s3_event_record['bucket']['name']
            print(bucket)
          
            # 4. S3イベントが発生したオブジェクトキー
            key = s3_event_record['object']['key']
            print(key)

      Handler: index.lambda_handler
      MemorySize: 128
      Timeout: 30
      Role: !GetAtt myRole.Arn
      Runtime: python3.7

上記テンプレートを用いて、デプロイコマンドでスタックのデプロイを行います。

$ aws cloudformation create-stack \
   --stack-name myStack \
   --template-body file://template.yml \
   --capabilities CAPABILITY_NAMED_IAM

バケット名とオブジェクトキーの取得

結論から言いますと、SNSトピックをサブスクライブするLambdaでは以下のようなコードを用いれば、イベントが発生したバケット名とオブジェクトキーを取得することができます。

import json

def lambda_handler(event, context):
  # 1. SNSトピックのeventデータ
  print(json.dumps(event, ensure_ascii=False))

  # 2. S3イベント通知のeventデータ
  s3_event = json.loads(event['Records'][0]['Sns']['Message'])
  print(json.dumps(s3_event, ensure_ascii=False))

  s3_event_record = s3_event['Records'][0]['s3']

  # 3. S3イベントが発生したバケット名
  bucket = s3_event_record['bucket']['name']
  print(bucket)

  # 4. S3イベントが発生したオブジェクトキー
  key = s3_event_record['object']['key']
  print(key)

ここでは、ファイルdemo.pngをバケットmybucket-123456789012にプレフィクスraw_data/としてPUTした際の、以下情報の内容と取得方法についてそれぞれ確認していきます.

  • 1.SNSトピックのeventデータ
  • 2.S3イベント通知のeventデータ
  • 3.S3イベントが発生したバケット名
  • 4.S3イベントが発生したオブジェクトキー

1. SNSトピックのeventデータ

SNSトピックからLambdaへはeventデータとして以下のようなデータが渡されてきます。トピックメッセージはMessageキーの値に含まれます。

>> # 1. SNSトピックのeventデータ
>> print(json.dumps(event, ensure_ascii=False))

{
    "Records": [
        {
            "EventSource": "aws:sns",
            "EventVersion": "1.0",
            "EventSubscriptionArn": "arn:aws:sns:ap-northeast-1:123456789012:myTopic:af809669-1234-5678-9012-9c75d42eb040",
            "Sns": {
                "Type": "Notification",
                "MessageId": "c5284e6e-1234-5678-9012-18bef4f0b3ab",
                "TopicArn": "arn:aws:sns:ap-northeast-1:123456789012:myTopic",
                "Subject": "Amazon S3 Notification",
                "Message": "{\"Records\":[{\"eventVersion\":\"2.1\",\"eventSource\":\"aws:s3\",\"awsRegion\":\"ap-northeast-1\",\"eventTime\":\"2020-02-26T14:20:43.189Z\",\"eventName\":\"ObjectCreated:Put\",\"userIdentity\":{\"principalId\":\"A3TB...80N\"},\"requestParameters\":{\"sourceIPAddress\":\"11.22.33.44\"},\"responseElements\":{\"x-amz-request-id\":\"F8DB3...48CB\",\"x-amz-id-2\":\"reVXEnUIYHAIM5vk5WJpeTC/1W1ekbi8...=\"},\"s3\":{\"s3SchemaVersion\":\"1.0\",\"configurationId\":\"76d49dfc-1234-5678-9012-065324629fec\",\"bucket\":{\"name\":\"mybucket-123456789012\",\"ownerIdentity\":{\"principalId\":\"A3TB...80N\"},\"arn\":\"arn:aws:s3:::mybucket-123456789012\"},\"object\":{\"key\":\"raw_data/demo.png\",\"size\":15719,\"eTag\":\"d83d0...2cd823\",\"sequencer\":\"005E...16B2\"}}}]}",
                "Timestamp": "2020-02-26T14:20:45.309Z",
                "SignatureVersion": "1",
                "Signature": "BBTc5+4XirDlkcHlC+MWRyptM4EfyFf4yFmJGQic7shKrS...==",
                "SigningCertUrl": "https://sns.ap-northeast-1.amazonaws.com/SimpleNotificationService-a86cb...128f7b6.pem",
                "UnsubscribeUrl": "https://sns.ap-northeast-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:ap-northeast-1:123456789012:myTopic:af809669-1234-5678-9012-9c75d42eb040",
                "MessageAttributes": {}
            }
        }
    ]
}

図中で示しますと、SNSトピックからLambdaに受け渡される赤枠部分のデータです。 image.png

2. S3イベント通知のeventデータ

S3イベント通知のeventデータは前述のSNSトピックのeventデータのMessageキーの値にString形式で含まれています。よってMessageキーの値に対してjson.loadsによるデシリアライズ(JSON→辞書オブジェクト)を行うことにより、以下のようなS3イベント通知のeventデータを得ることができます。

>> # 2. S3イベント通知のeventデータ
>> s3_event = json.loads(event['Records'][0]['Sns']['Message'])
>> print(json.dumps(s3_event, ensure_ascii=False))

{
    "Records": [
        {
            "eventVersion": "2.1",
            "eventSource": "aws:s3",
            "awsRegion": "ap-northeast-1",
            "eventTime": "2020-02-26T14:20:43.189Z",
            "eventName": "ObjectCreated:Put",
            "userIdentity": {
                "principalId": "ABCDEFGH123456"
            },
            "requestParameters": {
                "sourceIPAddress": "11.22.33.44"
            },
            "responseElements": {
                "x-amz-request-id": "F8DB3...48CB",
                "x-amz-id-2": "reVXEnUIYHAIM5vk5WJpeTC/1...="
            },
            "s3": {
                "s3SchemaVersion": "1.0",
                "configurationId": "76d49dfc-1234-5678-9012-065324629fec",
                "bucket": {
                    "name": "mybucket-123456789012",
                    "ownerIdentity": {
                        "principalId": "A3T...Q80N"
                    },
                    "arn": "arn:aws:s3:::mybucket-123456789012"
                },
                "object": {
                    "key": "raw_data/demo.png",
                    "size": 15719,
                    "eTag": "d83d0...cd823",
                    "sequencer": "005E...16B2"
                }
            }
        }
    ]
}

図中で示しますと、S3バケットからSNSトピックに受け渡される赤枠部分のデータです。 image.png

3. S3イベントが発生したバケット名

前述のS3イベント通知のeventデータから、イベントが発生したバケット名を以下のようにして取得できます。

>> # 3. S3イベントが発生したバケット名
>> bucket = s3_event_record['bucket']['name']
>> print(bucket)

mybucket-123456789012

4. S3イベントが発生したオブジェクトキー

前述のS3イベント通知のeventデータから、イベントが発生したオブジェクトキーを以下のようにして取得できます。

>> # 4. S3イベントが発生したオブジェクトキー
>> key = s3_event_record['object']['key']
>> print(key)

raw_data/demo.png

これでS3イベントが発生したバケット名とオブジェクトキーを取得することができました。

おわりに

「S3イベント通知のeventデータ」が「SNSトピックのeventデータ」にラッピングされているため、JSONデシリアライズしながら上手く取り出してあげることがミソとなります。もちろん、バケット名とオブジェクトキーが取得できれば、同じLambdaの中でPUTされたファイル自体のデータを取得することもできます。

以上