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/)