less is more

心のフルスタックエンジニア👨‍💻バイブスでコードを書いています🤘

S3のイベント通知を完全に理解する🧘‍♀️

概要

今回はS3のイベント通知に関して掘り下げます。
よくあるユースケースとしては、S3に画像が置かれたのをトリガーにLambdaを起動してサムネイル画像を作成するとかでしょうか。

アーキテクチャはこんな感じ。

f:id:bluepixel:20200504145655p:plain

ターゲット

イベントの送信先として指定できるのは以下の3つのみです。

  • Lambda
  • SNS
  • SQS

別のものを使いたい場合は、EventBridgeという選択肢があります。
とはいえSNS, SQSを経由すれば大体現実的なユースケースはカバーできると思います。

EventBridgeはCloudTrailを介するのでS3のイベント通知に比べると少し面倒です。

一つ注意点として、SQSで利用できるのは通常キューのみとなります。FIFOキューは2020年5月現在サポートされていません。仕組み的に今後サポートされることはなさそうな印象です。

イベントタイプ

大まかに分けると、オブジェクトの作成削除です。
ただしS3のAPIは豊富なので、どのAPIを介して作成・削除されたのかを細かく絞り込むこともできます。

マネジメントコンソールだとわかりやすいです。

f:id:bluepixel:20200504160918p:plain

また、キーのプレフィックスサフィックスを指定することで対象となるオブジェクトを絞り込むこともできます。

作成

例えば、オブジェクト作成イベントは以下の4つのAPIからトリガーされます。

  • s3:ObjectCreated:Put
  • s3:ObjectCreated:Post
  • s3:ObjectCreated:Copy
  • s3:ObjectCreated:CompleteMultipartUpload

これらを問わずに全ての作成イベントを通知したければワイルドカードを使います s3:ObjectCreated:*

ちなみに通知はリクエストベースではなく、ステータスベースで送信されます。つまり失敗したAPI呼び出しに対してイベントは発行されません。

削除

以下の2つです。

  • s3:ObjectRemoved:Delete
  • s3:ObjectRemoved:DeleteMarkerCreated

バージョニングが有効なオブジェクトの場合、削除マーカーが作成された時に通知を送信するためにはObjectRemoved:DeleteMarkerCreatedを使います。

例によってs3:ObjectRemoved:*で一括指定が可能です。

削除に関して注意しておきたいのは、ライフサイクルポリシーによる自動削除はイベントを発行しない点です。

復元

Glacierにアーカイブしたオブジェクトを復元する時に通知されます。
結果的にバケット内にオブジェクトが作成されるわけですが、使うAPIが異なるので作成のイベントは通知されません。
あくまでAPIベースでイベントが管理されているためです。
ファイルが置かれたら、みたいな直感的な感覚で捉えているとこのあたりハマる可能性があります。

復元が開始された時にs3:ObjectRestore:Postが、完了した時にs3:ObjectRestore:Completedが通知されます。

消失

冗長化ストレージ(RRS)を使用している場合に限るイベントです。
通常のストレージがイレブンナインのデータの耐久性を保証するのに対し、RRSではフォーナイン(99.99%)まで下がるため、1万個のオブジェクトがある場合、1年間に1つくらいのデータロストが発生する可能性があります。

それを捕捉するイベントです。
s3:ReducedRedundancyLostObject

レプリケーション

別のバケットにオブジェクトをレプリケーションする設定が有効になっている場合に限るイベントです。

  • レプリケート失敗時 s3:Replication:OperationFailedReplication
  • 15分の閾値を超えてもレプリケートされていない時s3:Replication:OperationMissedThreshold
  • 閾値を超えた後にレプリケートされた時s3:Replication:OperationReplicatedAfterThreshold
  • メトリクスによって追跡されなくなった時s3:Replication:OperationNotTracked

サンプル

ターゲットにLambdaを指定する場合

サフィックス.lambdaのオブジェクトが作成されたらLambdaを起動してパラメータをログに出力します。

AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  Name:
    Description: identifier
    Type: String
    Default: test-s3-event20200504
Resources:
  Lambda:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Ref Name
      Handler: index.lambda_handler
      Role: !GetAtt LambdaRole.Arn
      Runtime: nodejs12.x
      Code:
        ZipFile: |
          exports.lambda_handler = async (event, context) => {
            const util = require('util');
            console.log(util.inspect(event,false,null));
            console.log(util.inspect(context,false,null));
            return 'hello';
          }

  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns: 
        - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess

  LambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !GetAtt Lambda.Arn
      Principal: s3.amazonaws.com
      SourceArn: !Sub arn:aws:s3:::${Name}

  S3:
    Type: AWS::S3::Bucket
    DependsOn:
      - LambdaPermission
    Properties:
      BucketName: !Ref Name
      NotificationConfiguration:
        LambdaConfigurations:
          - Event: s3:ObjectCreated:*
            Filter:
              S3Key:
                Rules:
                  - Name: suffix
                    Value: .lambda
            Function: !GetAtt Lambda.Arn

ハマりどころとしては2つあって、

1つはLambda::Permissionです。これがないとS3がLambdaを起動できる権限がなくイベントを通知できません。

これはスタックの作成時にすでにバリデーションされます。 権限がない場合は以下のようなエラーメッセージが表示されます。

Unable to validate the following destination configurations

2つ目のハマりどころはリソースの依存関係です。

S3のイベント設定にLambda関数を指定する+権限のバリデーションがあるため、先にLambdaとLambdaPermissonを作る必要があります。

LambdaPermissionの方はDependsOnキーワードで制御できます。 関数の方はRefで参照するので特に依存関係を明示する必要はありません。

ただし、LambdaPermissionのSourceArnRef参照を使用すると循環依存になりスタックの作成に失敗します。 そのため、Parametersなどをうまく使って直接バケット名を埋め込んだArnを指定する必要があります。

実行するとeventにはこんな感じの値が含まれてきます。

{
    "Records": [
        {
            "eventVersion": "2.1",
            "eventSource": "aws:s3",
            "awsRegion": "ap-northeast-1",
            "eventTime": "2020-05-04T11:11:03.944Z",
            "eventName": "ObjectCreated:Put",
            "userIdentity": {
                "principalId": "xxxxxxxxxxxxxxxxxxxx"
            },
            "requestParameters": {
                "sourceIPAddress": "xxx.xxx.xxx.xxx"
            },
            "responseElements": {
                "x-amz-request-id": "xxxxxxxxxxxxxxxxxxxx",
                "x-amz-id-2": "xxxxxxxxxxxxxxxxxxxx"
            },
            "s3": {
                "s3SchemaVersion": "1.0",
                "configurationId": "23982aac-f2ca-40a6-8979-277afab383a6",
                "bucket": {
                    "name": "test-s3-event20200504",
                    "ownerIdentity": {
                        "principalId": "xxxxxxxxxxxxxxxxxxxx"
                    },
                    "arn": "arn:aws:s3:::test-s3-event20200504"
                },
                "object": {
                    "key": "sample.lambda",
                    "size": 18,
                    "eTag": "f0a94df54850f1f740e1c2ed6c78ee49",
                    "sequencer": "005EAFF84B50DBAEE4"
                }
            }
        }
    ]
}

Records[0]["s3"]["bucket"]["name"]バケット名が、
Records[0]["s3"]["object"]["key"]でオブジェクトのキーが取得できます。

ターゲットにSNSを指定する場合

トピックを作成してそこに飛ばします。動作確認のためにメールでのサブスクリプションも作っています。

AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  Name:
    Description: identifier
    Type: String
    Default: test-s3-event20200504
  Email:
    Description: email address
    Type: String
Resources:
  Topic:
    Type: AWS::SNS::Topic
    Properties: 
      DisplayName: !Ref Name
      TopicName: !Ref Name

  TopicPolicy:
    Type: AWS::SNS::TopicPolicy
    Properties: 
      PolicyDocument:
        Id: 1
        Version: "2012-10-17"
        Statement:
          - Sid: 1
            Effect: Allow
            Action: 
              - sns:Publish
            Resource: !Ref Topic
            Principal:
              Service: s3.amazonaws.com
            Condition:
              ArnLike:
                aws:SourceArn: !Sub arn:aws:s3:::${Name}
      Topics: 
        - !Ref Topic

  TopicSubscription:  
    Type: AWS::SNS::Subscription
    Properties: 
      Endpoint: !Ref Email
      Protocol: email
      TopicArn: !Ref Topic

  S3:
    Type: AWS::S3::Bucket
    DependsOn:
      - LambdaPermission
      - TopicPolicy
    Properties:
      BucketName: !Ref Name
      NotificationConfiguration:
        LambdaConfigurations:
          # 中略
        TopicConfigurations:
          - Event: s3:ObjectCreated:*
            Filter:
              S3Key:
                Rules:
                  - Name: suffix
                    Value: .sns
            Topic: !Ref Topic 

SNSの場合は、LambdaPermissionにあたるTopicPolicyを作る必要があります。勘所はほとんど一緒です。

唯一異なるのがポリシードキュメントの指定です。
IAMポリシーとかでおなじみのやつですが、 Principal: s3.amazonaws.comと書くことはできません。正しくは以下。

Principal:
  Service: s3.amazonaws.com

あとは例によってバリデーションがあるので、TopicPolicyをS3バケットより先に作るように依存関係を定義します。

ターゲットにSQSを指定する場合

キューの場合です。 SNSとほとんど一緒です。

AWSTemplateFormatVersion: "2010-09-09"
Parameters:
  Name:
    Description: identifier
    Type: String
    Default: test-s3-event20200504
Resources:
  Queue:
    Type: AWS::SQS::Queue
    Properties: 
      QueueName: !Ref Name

  QueuePolicy:
    Type: AWS::SQS::QueuePolicy
    Properties: 
      PolicyDocument:
        Id: 1
        Version: "2012-10-17"
        Statement:
          - Sid: 1
            Effect: Allow
            Action: 
              - sqs:SendMessage
            Resource: !GetAtt Queue.Arn
            Principal: 
              Service: s3.amazonaws.com
            Condition:
              ArnLike:
                aws:SourceArn: !Sub arn:aws:s3:::${Name}
      Queues: 
        - !Ref Queue

  S3:
    Type: AWS::S3::Bucket
    DependsOn:
      - LambdaPermission
      - QueuePolicy
      - TopicPolicy
    Properties:
      BucketName: !Ref Name
      NotificationConfiguration:
        LambdaConfigurations:
          # 中略
        QueueConfigurations:
          - Event: s3:ObjectCreated:*
            Filter:
              S3Key:
                Rules:
                  - Name: suffix
                    Value: .sqs
            Queue: !GetAtt Queue.Arn
        TopicConfigurations:
          # 中略

動作確認用のスクリプト

#!/bin/sh
echo "hello from lambda" >> sample.lambda
echo "hello from sns" >> sample.sns
echo "hello from sqs" >> sample.sqs
aws s3 cp ./sample.lambda s3://$BUCKET/
aws s3 cp ./sample.sns s3://$BUCKET/
aws s3 cp ./sample.sqs s3://$BUCKET/

その他覚えておきたいこと

  • 通知は数秒で配信されるが1分以上かかることもある。
  • バージョニングされていない場合、同じオブジェクトを同時に書き込むと1つしかイベントが発行されないことがある。
  • ごくまれにイベントが配信されない、二重で配信されることがある(ググると色々記事が出てきますが、100万回に1回以下とかそのくらいのレベルっぽい)。
  • トリガーされるLambdaで同じバケットにオブジェクトを配置する処理とかを書いているとループする。プレフィックスの設定でうまく処理する。

公式ドキュメント

Amazon S3 イベント通知の設定 - Amazon Simple Storage Service

サンプルのソースコード

github.com

最後まで読んだ人は「SQS完全に理解した🧘‍♀️」って言っていいです。