S3 Events
Jets supports S3 Events as a Lambda trigger. When objects are uploaded to s3, it triggers a Lambda function to run. You can access information about the uploaded object via event
, s3_event
, and s3_object
.
Usage
Here is an example connecting S3 events to a Lambda function in a Event.
Generate code.
jets generate:event upload --trigger s3
It looks something like this.
app/events/upload_event.rb
class UploadEvent < ApplicationEvent
# Please read the Considerations section before using s3_event
s3_event "my-bucket" # <= CHANGE ME: new or existing bucket
def handle
puts "event #{JSON.dump(event)}"
puts "s3_events #{JSON.dump(s3_events)}"
puts "s3_objects #{JSON.dump(s3_objects)}"
end
end
You can use existing or new buckets. New buckets get created as part of the jets deploy process.
The bucket is created outside of CloudFormation . This is a Jets design decision. This avoids coupling the bucket, which likely contains files, to CloudFormation.
How It Works
The s3_event
declaration does a few things. It configures the S3 bucket with a S3 Event Notification and sets up an SNS topic to fire when s3 objects are uploaded to the bucket. In turn, your Lambda functions are subscribed to the SNS topic. This is by design.
Though we could set up Bucket notifications to send the s3 event payload directly to a Lambda function, s3 bucket notifications only allow us to connect one Lambda function. So instead, Jets publishes the event to an SNS topic. This gives us the flexibility to fan out to multiple Lambda functions.
This design allows Jets code to do this:
class ExampleEvent < ApplicationEvent
s3_event "my-bucket" # new or existing bucket
def do_one_thing
# ...
end
s3_event "my-bucket" # new or existing bucket
def do_another_thing
# ...
end
s3_event "another-bucket" # new or existing bucket
def do_another_thing_on_another_bucket
# ...
end
end
Upload File to S3 Bucket
Here’s an example of uploading a file to a s3 bucket with the aws s3 cp CLI:
export S3_BUCKET=my-bucket # change to your bucket name
aws s3 cp file.txt s3://$S3_BUCKET/folder/file.txt
You can also upload a file with the S3 Console, sdk, etc also.
Tailing Logs
It helps to tail the logs and watch the event as it comes through.
jets logs -f -n s3_event-perform
Event Payloads
The event payload contains information about the object uploaded to the s3 bucket. The event
payload provides all the metadata. Within the payload, the Message
attribute contains the data relevant to the s3 upload as a JSON encoded string. The s3_objects
helper method unravels the data and provides the s3 objects information. Here’s an example to help:
{
"Records": [
{
"EventSource": "aws:sns",
"EventVersion": "1.0",
"EventSubscriptionArn": "arn:aws:sns:us-west-2:112233445566:my-bucket:bdc903bb-a51e-4799-b472-bd7293ce245b",
"Sns": {
"Type": "Notification",
"MessageId": "434252ae-b80f-58ad-9c11-067761c040dd",
"TopicArn": "arn:aws:sns:us-west-2:112233445566:my-bucket",
"Subject": "Amazon S3 Notification",
"Message": "{\"Records\":[{\"eventVersion\":\"2.1\",\"eventSource\":\"aws:s3\",\"awsRegion\":\"us-west-2\",\"eventTime\":\"2019-02-10T07:49:35.265Z\",\"eventName\":\"ObjectCreated:Put\",\"userIdentity\":{\"principalId\":\"AWS:AROAJBZT6HZEMHALPTL4E:botocore-session-1549784243\"},\"requestParameters\":{\"sourceIPAddress\":\"11.22.33.44\"},\"responseElements\":{\"x-amz-request-id\":\"4F2A57016225F633\",\"x-amz-id-2\":\"ek4gBwsxZtNeiXV8xUfxocSy3cZLR8ws/HcI8qIyDx75ID7hekUMuMMsa4DYltRsX3v0zJ9kl1c=\"},\"s3\":{\"s3SchemaVersion\":\"1.0\",\"configurationId\":\"NzFkOWNiMzQtZDI1Ni00N2U2LTlmYzgtODdhNWJkOWVkNjNm\",\"bucket\":{\"name\":\"my-bucket\",\"ownerIdentity\":{\"principalId\":\"AYC6O1A20R123\"},\"arn\":\"arn:aws:s3:::my-bucket\"},\"object\":{\"key\":\"myfolder/subfolder/test.txt\",\"size\":0,\"eTag\":\"d41d8cd98f00b204e9800998ecf8427e\",\"sequencer\":\"005C5FD78F373E174B\"}}}]}",
"Timestamp": "2019-02-10T07:49:35.417Z",
"SignatureVersion": "1",
"Signature": "nCgvhLBccqWkbCnmDk5t0RfIUkVo0toGDu13zkGxz7wkqCyv+10n2HO+Xy+qegfX2yB/yC3Vxzu9Sg/RbwLuVzqRWxtJ8YUSFx3UnUZEjrWLGTWrjyfkgrlDm8ovhT9hs1tSsoHm07lsUsxF+uhnBitFS8fKbe/PNBiTQKwLr2nUZxGy1zMrA75cdh/Ft7yDHy0S9bfcK/gosyfdSb1ggBQTQRoL2gZ46TAtciZ2eiGTUYW+PjSfAE9uKQcInSi2qvCIUTcWmRLXPDgRj3i5dJdjgqor8uCS+93kTF1OVURKJ5oMMmKVAabBGgJulZYQfaONd2si+H5Y8aUz5AzZSg==",
"SigningCertUrl": "https://sns.us-west-2.amazonaws.com/SimpleNotificationService-ac565b8b1a6c5d002d285f9598aa1d9b.pem",
"UnsubscribeUrl": "https://sns.us-west-2.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-west-2:112233445566:my-bucket:bdc903bb-a51e-4799-b472-bd7293ce245b",
"MessageAttributes": {
}
}
}
]
}
s3_events
The s3_events
helper method unravels the data and provides the Sns.Message
JSON data as an ActiveSupport::HashWithIndifferentAccess structure.
[
{
"eventVersion": "2.1",
"eventSource": "aws:s3",
"awsRegion": "us-west-2",
"eventTime": "2019-02-10T07:49:35.265Z",
"eventName": "ObjectCreated:Put",
"userIdentity": {
"principalId": "AWS:AROAJBZT6HZEMHALPTL4E:botocore-session-1549784243"
},
"requestParameters": {
"sourceIPAddress": "11.22.33.44"
},
"responseElements": {
"x-amz-request-id": "4F2A57016225F633",
"x-amz-id-2": "ek4gBwsxZtNeiXV8xUfxocSy3cZLR8ws/HcI8qIyDx75ID7hekUMuMMsa4DYltRsX3v0zJ9kl1c="
},
"s3": {
"s3SchemaVersion": "1.0",
"configurationId": "NzFkOWNiMzQtZDI1Ni00N2U2LTlmYzgtODdhNWJkOWVkNjNm",
"bucket": {
"name": "my-bucket",
"ownerIdentity": {
"principalId": "AYC6O1A20R123"
},
"arn": "arn:aws:s3:::my-bucket"
},
"object": {
"key": "myfolder/subfolder/test.txt",
"size": 0,
"eTag": "d41d8cd98f00b204e9800998ecf8427e",
"sequencer": "005C5FD78F373E174B"
}
}
}
]
s3_objects
The s3_objects
helper method unravels the data and provides the s3 object info.
[{
"key": "myfolder/subfolder/test.txt",
"size": 0,
"eTag": "d41d8cd98f00b204e9800998ecf8427e",
"sequencer": "005C5FD78F373E174B"
}]
s3_files
The s3_files
helper method downloads the files from s3 and provides them as a Jets NamedFile, a Ruby File-like handle. Example:
class UploadEvent < ApplicationEvent
s3_event "my-bucket"
def handle
s3_files.each do |file|
puts "file #{file}"
puts "file.filename #{file.filename}"
puts "file.content #{file.content}"
end
end
end
Notes:
- The
s3_files
method is lazy. It only downloads the files when it’s called. Files are downloaded only once. - The
NamedFile
provides.filename
and.content
methods. The RubyFile
class normally does not store this information. - The
s3_event
macro above the method automatically adds the AWS AmazonS3ReadOnlyAccess IAM Managed Policy so app has access to download the file to/tmp/s3_files/OBJECT_KEY
.
Considerations
When using s3_event
please consider:
- Jets configures and updates the s3 bucket event notification configuration on existing buckets. Jets wipes out the current bucket event notification configuration in doing this. You can disable this behavior with
config.s3_event.configure_bucket = false
. If you disable it, then you will need to set up the bucket notification manually. Most setups do not have any bucket configurations, but some do so please check your s3 bucket. You can find this under S3 “Properties / Events” in the S3 Console. - Jets does not clean up the bucket notification configuration upon deleting the Jets application. It will leave the s3 bucket notification configuration in place.
- Jets does not delete the s3 bucket upon deleting a Jets application, even if Jets created the s3 bucket.
- You may want to specify the s3 bucket name in .env files for multiple environments to use different buckets. If you use the same s3 bucket across multiple environments, each time you deploy it will change the s3 bucket notification to the SNS topic associated with the deployed environment, which is likely not what you want.
- Please be careful not to have your Lambda function write to the same bucket and cause an accidental infinite loop:
s3 event -> lambda function -> s3 event -> lambda function ...
. This is a quick way to exhaust the AWS Lambda free tier. Though you can apply bucket event filter rules to avoid an infinite loop, using a separate bucket to store processed results is probably a safer way to avoid the infinite loop possibility altogether.
S3 Event Configuration
There are two ways to set custom configigurations events. First is in the application configuration. This will affect the behavior of all s3_event
application-wide on all buckets that s3_event
uses. Here’s an example of the configuration options:
Jets.deploy.configure do
config.s3_event.configure_bucket = true # true by default
config.s3_event.notification_configuration = {
topic_configurations: [
{
events: ["s3:ObjectCreated:*"], # default: s3:ObjectCreated:*
topic_arn: "!Ref SnsTopic", # must use this logical id, dont change
# custom filter rule, by default there is no filter
filter: {
key: {
filter_rules: [
{
name: "prefix",
value: "images/",
},
{
name: "suffix",
value: ".png",
}
] # filter_rules
} # key
} # filter
},
],
}
end
The notification_configuration
setting maps to the ruby aws-sdk put_bucket_notification_configuration method’s options. With it, you can apply event filter rules on which s3 objects fire off a notification event.
The second option is to use method specific configurations to affect the way methods are subscribed to the events. Below is an example of 2 methods, each subscribed to events from the same bucket. The first method will only recieved bucket events from objects with the prefix test-prefix/
. The second method will recieve all object events in the bucket regardless of their prefix.
class EventJob < ApplicationJob
S3_BUCKET = "jets-s3-test-bucket-001".freeze
SUBSCRIPTION_PROPS = {
FilterPolicyScope: "MessageBody",
FilterPolicy: {
Records: {
s3: {
object: {
key: [
{ prefix: "test-prefix/" }
]
}
}
}
}.to_json
}.freeze
# Example of a job that is triggered by an S3 event with a filter policy
s3_event(S3_BUCKET, sns_subscription_properties: SUBSCRIPTION_PROPS)
def filtered
puts "Filtered event: #{JSON.dump(event)}"
end
# Example of a job that is triggered by an S3 event without a filter policy
s3_event(S3_BUCKET)
def unfiltered
puts "Unfiltered event: #{JSON.dump(event)}"
end
end