Build a Serverless Email Collector For Your Site in AWS using Lambdas

Mailchimp is cool, but if you aren’t going to send email newsletters using services that require opt-in, the platform can be very cumbersome. That’s because the can-spam act requires a complicated opt-in workflow to make it compliant. It is a pain if all you want is to collect emails for your launch.

Making it easy for you to tell people your ready is important for launches.

Building up and deploying a microservices feels like an overkill. Setting up an instance, monitoring it, operations, yuck. I just want to host my website in s3 and collect some emails without a database. I will warn you though, configuring Lambdas a bit complicated, so I think I am giving most of the steps required to you here but there is room for error.

Lambdas To The Rescue

Lambda use a function as a service FaaS concept, which means that custom code is executed in ephemeral containers. Lambdas have a lot of capabilities including an infrastructure for authorization of your APIs with JWT.

Lambdas when used with API Gateway can provide full microservices capabilities.

In this model individual functions implement specific endpoints, managed by an API Gateway:

API Gateway is about decomposing services.

Getting Your IAM Policy Right

You will need to assign a role to the Lambda with an IAM policy on what it can do. Well I will be honest, this could be tighter, but I am not willing to spend the time to fine tooth comb the minimum policy required for the Lambda to execute. This is safe though, it is just sending one email. If you have an improvement put it in the comments and I will update this article.

{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "s3:*",
"Resource": "*"
}, {
"Action": [
"autoscaling:Describe*",
"cloudwatch:*",
"logs:*",
"sns:*"
],
"Effect": "Allow",
"Resource": "*"
}
]
}

We also need a trust policy to allow API gateway to talk to the Lambda.

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"lambda.amazonaws.com",
"apigateway.amazonaws.com"
]
},
"Action": "sts:AssumeRole"
}
]
}

Swagger is as Swagger Does

The swagger document describes how the API Gateway will send requests to the lambdas, this is a super simple one so it only has one endpoint. We are going to use a proxy type so everything gets passed to the Lambda’s request model.

---
swagger: "2.0"
info:
version: "2016-12-17T20:35:56Z"
title: "dronze_website"
host: "1wffia5k9c.execute-api.us-west-2.amazonaws.com"
basePath: "/prod"
schemes:
- "https"
paths:
/email/{proxy+}:
options:
consumes:
- "application/json"
produces:
- "application/json"
responses:
200:
description: "200 response"
schema:
$ref: "#/definitions/Empty"
headers:
Access-Control-Allow-Origin:
type: "string"
Access-Control-Allow-Methods:
type: "string"
Access-Control-Allow-Headers:
type: "string"
x-amazon-apigateway-any-method:
produces:
- "application/json"
parameters:
- name: "proxy"
in: "path"
required: true
type: "string"
responses:
200:
description: "200 response"
securityDefinitions:
api_key:
type: "apiKey"
name: "x-api-key"
in: "header"
definitions:
Empty:
type: "object"
title: "Empty Schema"

As you can see in this API definition the OPTIONS method has Access-Control-Allow-Origin, Access-Control-Allow-Methods and Access-Control-Allow-Headers, but what isn’t intuitive that the last mile that allows the browser to actually make calls needs to be handled in the Lambda implementation (see below)

Then once you have the document defined you can import it to API Gateway after you have created a new API.

You will also need to read the docs for some subtle concepts like “stages” and “stage variables”. In this example I only have one stage: PROD.

You can then upload your swagger document to API Gateway, and that is what your API will do.

CORS Enabled Clients From Your Browser

Since this is a s3 only website, we are going to let the web browser collect and send the email addresses directly to our API using ajax. Here is our simple zurb.foundation based web form.

<form id="signup-form">
<div class="row">
<div class="small-12 medium-12 large-12 columns">
<div class="row collapse">
<div class="small-12 medium-8 large-8 columns">
<input id="signup-email" type="text" style="font-size: 1.3em; font-weight:700; height: 60px;" placeholder="Please enter your email...">
</div>
<div class="small-12 medium-4 large-4 columns text-left">
<a href="#" id="signup-button" class="button" style="width:100%; font-size: 1.3em; font-weight:700; height: 60px;">Sign me up!</a>
</div>
<span style="font-size:0.9em; font-weight:300;">
We hate spam as much as you do. You'll never get any from us, just instructions on how to get started.
</span>
</div>
</div>
</div>
</form>

Now we need a script that will make an ajax call to our lambda using CORS. Making CORS work is always a pain. I dealt with it by forcing the form to use a GET method. If you figure out how to do this with a POST ac†ion please show us how.

$(document).ready(function() {
// click on button submit
$("#signup-button").on('click', function() {
//validate
var value = $('#signup-email').val();
var valid = validateEmail(value);
if (!valid) {
$('#email-status').html('Please enter a valid email address.');
$('#signup-email').css('color', '#8A1111');
$('#signup-email').css('background', '#FFD1D1');
} else {
// send ajax
$('#email-status').html('Looks great. We are adding you.');
$('#signup-email').css('color', '#30781C');
$('#signup-email').css('background', '#CFFFC2');
$.ajax({
url: 'https://1wffia5k9c.execute-api.us-west-2.amazonaws.com/prod/email/collect',
type: "GET", // type of action POST || GET
data: {
'email': $("#signup-email").val()
},
success: function(result) {
// you can see the result from the console
// tab of the developer tools
console.log("thank you for signing up!");
window.location.replace("/thanks.html");
},
error: function(xhr, resp, text) {
console.log("response:" + resp + " text:" + text);
}
});
}
});
});

The Big Reveal, The Lambda Implementation

So we want to write our collected info as json documents in s3, and since our execution role has the ability to write s3 documents we don’t need a database. Finally we have locked down what clients can call with the Access-Control-Allow-Origin set to our website.

import json
import boto3
import botocore
import uuid
s3 = boto3.client('s3')
def lambda_handler(event, context):
print boto3.__version__
#check for params
if 'queryStringParameters' in event:
print json.dumps(event['queryStringParameters'], indent=4)
s3_put_model(
str(uuid.uuid4()),
'collect/email.dronze.com',
event['queryStringParameters'],
'dronze.2b67')
body = { "message":"SUCCEED" }
response = {
'statusCode': 200,
'headers': {
"Access-Control-Allow-Origin" : "https://dronze.com"
},
'body': json.dumps(body, indent=4)
}
return response
def s3_put_model(fid, prefix, model, bucketName):
"""
send model to bucket
"""
try:
bodydata = json.dumps(model, indent=4)
s3.put_object(Bucket='dronze.2b67',
Key='{0}/{1}.json'.format(prefix,fid),
Body=b'{0}'.format(bodydata))
except Exception as e:
print(e)
raise e

And now we have email documents saving in s3 when our users submit a form.

Writing s3 documents from lambda means we don’t need a database.

So was it worth it?

Let’s Do The Math

Duration: 192.94 ms Billed Duration: 200 ms Memory Size: 128 MB Max Memory Used: 30 MB

That’s one email collected. So if we go to the handy AWS Lambda calculator and

Collect 1M emails each month for 62 cents

1M invocations using 128M at 200ms = 62 cents. Well I think that will do just fine.