S3のイベント通知を完全に理解する🧘♀️
概要
今回はS3のイベント通知に関して掘り下げます。
よくあるユースケースとしては、S3に画像が置かれたのをトリガーにLambdaを起動してサムネイル画像を作成するとかでしょうか。
アーキテクチャはこんな感じ。
ターゲット
イベントの送信先として指定できるのは以下の3つのみです。
- Lambda
- SNS
- SQS
別のものを使いたい場合は、EventBridgeという選択肢があります。
とはいえSNS, SQSを経由すれば大体現実的なユースケースはカバーできると思います。
EventBridgeはCloudTrailを介するのでS3のイベント通知に比べると少し面倒です。
一つ注意点として、SQSで利用できるのは通常キューのみとなります。FIFOキューは2020年5月現在サポートされていません。仕組み的に今後サポートされることはなさそうな印象です。
イベントタイプ
大まかに分けると、オブジェクトの作成と削除です。
ただしS3のAPIは豊富なので、どのAPIを介して作成・削除されたのかを細かく絞り込むこともできます。
マネジメントコンソールだとわかりやすいです。
また、キーのプレフィックス・サフィックスを指定することで対象となるオブジェクトを絞り込むこともできます。
作成
例えば、オブジェクト作成イベントは以下の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のSourceArn
にRef
参照を使用すると循環依存になりスタックの作成に失敗します。
そのため、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
サンプルのソースコード
最後まで読んだ人は「SQS完全に理解した🧘♀️」って言っていいです。