github.com/mweagle/Sparta@v1.15.0/docs_source/content/reference/eventsources/ses.md (about) 1 --- 2 date: 2016-03-09T19:56:50+01:00 3 title: SES 4 weight: 10 5 --- 6 7 In this section we'll walkthrough how to trigger your lambda function in response to inbound email. This overview is based on the [SpartaApplication](https://github.com/mweagle/SpartaApplication/blob/master/application.go) sample code if you'd rather jump to the end result. 8 9 # Goal 10 11 Assume that we have already [verified our email domain](http://docs.aws.amazon.com/ses/latest/DeveloperGuide/verify-domains.html) with AWS. This allows our domain's email to be handled by SES. 12 13 We've been asked to write a lambda function that logs inbound messages, including the metadata associated with the message body itself. 14 15 There is also an additional requirement to support [immutable infrastructure](http://radar.oreilly.com/2015/06/an-introduction-to-immutable-infrastructure.html), so our service needs to manage the S3 bucket to which message bodies should be stored. Our service cannot rely on a pre-existing S3 bucket. The infrastructure (and associated security policies) together with the application logic is coupled. 16 17 ## Getting Started 18 19 We'll start with an empty lambda function and build up the needed functionality. 20 21 ```go 22 import ( 23 spartaSES "github.com/mweagle/Sparta/aws/ses" 24 ) 25 func echoSESEvent(ctx context.Context, sesEvent spartaSES.Event) (*spartaSES.Event, error) { 26 logger, _ := ctx.Value(sparta.ContextKeyRequestLogger).(*logrus.Entry) 27 configuration, configErr := sparta.Discover() 28 logger.WithFields(logrus.Fields{ 29 "Error": configErr, 30 "Configuration": configuration, 31 }).Info("Discovery results") 32 } 33 ``` 34 35 ## Unmarshalling the SES Event 36 37 At this point we would normally continue processing the SES event, using Sparta types until the 38 official [events](https://godoc.org/github.com/aws/aws-lambda-go/events) are available. 39 40 However, before moving on to the event processing, we need to take a detour into [dynamic infrastructure](/reference/dynamic_infrastructure/) 41 because of the immutable infrastructure requirement. 42 43 This requirement implies that our service must be self-contained: we can't assume that 44 the S3 bucket already exists. How can our locally compiled code access AWS-created resources? 45 46 ## Dynamic Resources 47 48 The immutable infrastructure requirement makes this lambda function a bit more complex. Our service needs to: 49 50 * Provision a new S3 bucket for email message body storage 51 - SES will not provide the message body in the event data. It will only store 52 the email body in an S3 bucket, from which your lambda function can later consume it. 53 * Wait for the S3 bucket to be provisioned 54 - As we need a new S3 bucket, we're relying on AWS to generate a [unique name](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket.html#cfn-s3-bucket-name). But this means that our lambda function doesn't know the S3 bucket name during provisioning. 55 - In fact, we shouldn't even create an AWS Lambda function if the S3 bucket can't be created. 56 * Include an IAMPrivilege so that our **go** function can access the dynamically created bucket 57 * Discover the S3 Bucket at lambda execution time 58 59 ### Provision Message Body Storage Resource 60 61 Let's first take a look at how the SES lambda handler provisions a new S3 bucket via the [MessageBodyStorage](https://godoc.org/github.com/mweagle/Sparta#MessageBodyStorage) type: 62 63 ```go 64 65 func appendSESLambda(api *sparta.API, 66 lambdaFunctions []*sparta.LambdaAWSInfo) 67 []*sparta.LambdaAWSInfo { 68 69 // Our lambda function will need to be able to read from the bucket, which 70 // will be handled by the S3MessageBodyBucketDecorator below 71 lambdaFn, _ := sparta.NewAWSLambda(sparta.LambdaName(echoSESEvent), 72 echoSESEvent, 73 sparta.IAMRoleDefinition{}) 74 // Setup options s.t. the lambda function has time to consume the message body 75 lambdaFn.Options = &sparta.LambdaFunctionOptions{ 76 Description: "", 77 MemorySize: 128, 78 Timeout: 10, 79 } 80 81 // Add a Permission s.t. the Lambda function automatically manages SES registration 82 sesPermission := sparta.SESPermission{ 83 BasePermission: sparta.BasePermission{ 84 // SES only supports wildcard ARNs 85 SourceArn: "*", 86 }, 87 InvocationType: "Event", 88 } 89 // Store the message body 90 bodyStorage, _ := sesPermission.NewMessageBodyStorageResource("Special") 91 sesPermission.MessageBodyStorage = bodyStorage 92 93 ``` 94 95 The `MessageBodyStorage` type (and the related [MessageBodyStorageOptions](https://godoc.org/github.com/mweagle/Sparta#MessageBodyStorageOptions) type) 96 cause our SESPermission handler to add an [S3 ReceiptRule](http://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-action-s3.html) 97 at the head of the rules list. This rule instructs SES to store the message body in the supplied bucket before invoking our lambda function. 98 99 The single parameter `"Special"` is an application-unique literal value that is used to create a stable CloudFormation 100 resource identifier so that new buckets are not created in response to stack update requests. 101 102 Our SES handler then adds two [ReceiptRules](http://docs.aws.amazon.com/ses/latest/APIReference/API_ReceiptRule.html): 103 104 ```go 105 106 sesPermission.ReceiptRules = make([]sparta.ReceiptRule, 0) 107 sesPermission.ReceiptRules = append(sesPermission.ReceiptRules, 108 sparta.ReceiptRule{ 109 Name: "Special", 110 Recipients: []string{"sombody_special@gosparta.io"}, 111 TLSPolicy: "Optional", 112 }) 113 sesPermission.ReceiptRules = append(sesPermission.ReceiptRules, 114 sparta.ReceiptRule{ 115 Name: "Default", 116 Recipients: []string{}, 117 TLSPolicy: "Optional", 118 }) 119 ``` 120 121 ### Dynamic IAMPrivilege Arn 122 123 Our lambda function is required to access the message body in the dynamically created `MessageBodyStorage` resource, but 124 the S3 resource Arn is only defined _after_ the service is provisioned. The solution to this is to reference the 125 dynamically generated `BucketArnAllKeys()` value in the `sparta.IAMRolePrivilege` initializer: 126 127 ```go 128 129 // Then add the privilege to the Lambda function s.t. we can actually get at the data 130 lambdaFn.RoleDefinition.Privileges = append(lambdaFn.RoleDefinition.Privileges, 131 sparta.IAMRolePrivilege{ 132 Actions: []string{"s3:GetObject", "s3:HeadObject"}, 133 Resource: sesPermission.MessageBodyStorage.BucketArnAllKeys(), 134 }) 135 ``` 136 137 The last step is to register the `SESPermission` with the lambda info: 138 139 ```go 140 // Finally add the SES permission to the lambda function 141 lambdaFn.Permissions = append(lambdaFn.Permissions, sesPermission) 142 ``` 143 144 145 At this point we've implicitly created an S3 bucket via the `MessageBodyStorage` value. Our lambda function now needs to dynamically determine the AWS-assigned bucket name. 146 147 ### Dynamic Message Body Storage Discovery 148 149 Our `echoSESEvent` function needs to determine, at execution time, the `MessageBodyStorage` S3 bucket name. This is done via `sparta.Discover()`: 150 151 ```go 152 configuration, configErr := sparta.Discover() 153 logger.WithFields(logrus.Fields{ 154 "Error": configErr, 155 "Configuration": configuration, 156 }).Info("Discovery results") 157 158 // The message bucket is an explicit `DependsOn` relationship, so it'll be in the 159 // resources map. We'll find it by looking for the dependent resource with the "AWS::S3::Bucket" type 160 bucketName := "" 161 for _, eachResourceInfo := range configuration.Resources { 162 if eachResourceInfo.ResourceType == "AWS::S3::Bucket" { 163 bucketName = eachResourceInfo.Properties["Ref"] 164 } 165 } 166 if "" == bucketName { 167 return nil, errors.Errorf("Failed to discover SES bucket from sparta.Discovery: %#v", configuration) 168 } 169 ``` 170 171 The `sparta.Discover()` function returns a [DiscoveryInfo](https://godoc.org/github.com/mweagle/Sparta#DiscoveryInfo) structure. This 172 data is published into the Lambda's environment variables to enable it to discover other 173 resources published in the same Stack. 174 175 The structure includes the stack's [Pseudo Parameters](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/pseudo-parameter-reference.html) 176 as well information about any _immediate_ resource dependencies. 177 Eg, those that were explicitly marked as `DependsOn`. See the [discovery documentation](/reference/discovery/) for more details. 178 179 As we only have a single dependency, our discovery filter is: 180 181 ```go 182 // The message bucket is an explicit `DependsOn` relationship, so it'll be in the 183 // resources map. We'll find it by looking for the dependent resource with the "AWS::S3::Bucket" type 184 bucketName := "" 185 for _, eachResourceInfo := range configuration.Resources { 186 if eachResourceInfo.ResourceType == "AWS::S3::Bucket" { 187 bucketName = eachResourceInfo.Properties["Ref"] 188 } 189 } 190 if "" == bucketName { 191 return nil, errors.Errorf("Failed to discover SES bucket from sparta.Discovery: %#v", configuration) 192 } 193 ``` 194 195 # #Sparta Integration 196 197 The rest of `echoSESEvent` satisfies the other requirements, with a bit of help from the SES [event types](https://godoc.org/github.com/mweagle/Sparta/aws/ses): 198 199 ```go 200 // Get the metdata about the item... 201 svc := s3.New(session.New()) 202 for _, eachRecord := range sesEvent.Records { 203 logger.WithFields(logrus.Fields{ 204 "Source": eachRecord.SES.Mail.Source, 205 "MessageID": eachRecord.SES.Mail.MessageID, 206 "BucketName": bucketName, 207 }).Info("SES Event") 208 209 if "" != bucketName { 210 params := &s3.HeadObjectInput{ 211 Bucket: aws.String(bucketName), 212 Key: aws.String(eachRecord.SES.Mail.MessageID), 213 } 214 resp, err := svc.HeadObject(params) 215 logger.WithFields(logrus.Fields{ 216 "Error": err, 217 "Metadata": resp, 218 }).Info("SES MessageBody") 219 } 220 } 221 return &sesEvent, nil 222 ``` 223 224 # Wrapping Up 225 226 With the `lambdaFn` fully defined, we can provide it to `sparta.Main()` and deploy our service. The workflow below is shared by all SES-triggered lambda function: 227 228 * Define the lambda function (`echoSESEvent`). 229 * If needed, create the required [IAMRoleDefinition](https://godoc.org/github.com/mweagle/Sparta*IAMRoleDefinition) with appropriate privileges if the lambda function accesses other AWS services. 230 * Provide the lambda function & IAMRoleDefinition to `sparta.NewAWSLambda()` 231 * Add the necessary [Permissions](https://godoc.org/github.com/mweagle/Sparta#LambdaAWSInfo) to the `LambdaAWSInfo` struct so that the lambda function is triggered. 232 233 Additionally, if the SES handler needs to access the raw email message body: 234 235 * Create a new `sesPermission.NewMessageBodyStorageResource("Special")` value to store the message body 236 * Assign the value to the `sesPermission.MessageBodyStorage` field 237 * If your lambda function needs to consume the message body, add an entry to `sesPermission.[]IAMPrivilege` that includes the `sesPermission.MessageBodyStorage.BucketArnAllKeys()` Arn 238 * In your **go** lambda function definition, discover the S3 bucketname via `sparta.Discover()` 239 240 # Notes 241 242 * The SES message (including headers) is stored in the [raw format](http://stackoverflow.com/questions/33549327/what-is-the-format-of-the-aws-ses-body-stored-in-s3) 243 * More on Immutable Infrastructure: 244 * [Subbu - Automate Everything](https://www.subbu.org/blog/2014/10/automate-everything-but-dont-ignore-drift) 245 * [Chad Fowler - Immutable Deployments](http://chadfowler.com/2013/06/23/immutable-deployments.html) 246 * [The Cloudcast - What is Immutable Infrastructure](http://www.thecloudcast.net/2015/09/the-cloudcast-213-what-is-immutable.html) 247 * [The New Stack](http://thenewstack.io/a-brief-look-at-immutable-infrastructure-and-why-it-is-such-a-quest/)