TLDR; This post explains how to use claudia.js to send emails via AWS Lambda using AWS Simple Email Service.
In a previous post I explained how integrate webtask.io into a static website in order to be able to send emails after submitting a contact-me form. As many people are already running on AWS I figured it makes sense to take a look at their serverless architecture as well.
First, their services are composed of two parts. There is AWS Lambda, the infrastructure needed to execute code. However in order to trigger, one needs to use the API Gateway, which maps URLs to lambda calls.
This blog post will not cater for setting up a website like the last post, it will simply focus on getting up and running.
Setup
First and foremost, get an AWS account. Next install the awesome awscli tools and make sure they are working. Also checking out the quick introduction into claudia.js might help.
Create a new node project with this package json and run npm install
{
"name": "ses-mailer",
"version": "0.0.1",
"private": true,
"files": [
"*.js",
"package.json"
],
"scripts": {
"test": "node_modules/.bin/standard && node_modules/.bin/claudia test-lambda test-event.json",
"tail": "node_modules/.bin/smoketail -f /aws/lambda/ses-mailer",
"start": "node_modules/.bin/claudia create --name ses-mailer --region us-east-1
--api-module ses-mailer --policies policies",
"deploy": "node_modules/.bin/claudia update"
},
"devDependencies": {
"claudia": "1.1.2",
"smoketail": "0.1.0",
"standard": "6.0.8"
},
"dependencies": {
"aws-sdk": "2.3.0",
"aws-sdk-promise": "0.0.2",
"claudia-api-builder": "1.1.0"
}
}
The dependencies are going to be part of the packaged zip file, where as the dev dependencies are just needed for local development phase.
The last part of the setup is verify the email address we are going to use.
aws ses verify-email-identity --email-address YOUR@EMAIL.ADDRESS
The code
The code is basically just two files. The first is required to add additional rights to the lambda execution, as emails should be sent. This file should be put into policies/send-email.json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [ "ses:SendEmail" ],
"Resource": [ "arn:aws:ses:us-east-1:*:*" ]
}
]
}
Please be aware what the above does! Check the ARN syntax and its implications when you give permissions!
The second file is the code to be executed, named ses-mailer.js
var ApiBuilder = require('claudia-api-builder')
var api = new ApiBuilder()
var fs = require('fs')
var AWS = require('aws-sdk-promise')
var SES = new AWS.SES()
var sender = 'Spinscale Form Mailer <YOUR@EMAIL.ADDRESS>'
var recipient = 'Alexander Reelsen <YOUR@EMAIL.ADDRESS>'
var subject = 'Form mailer for example.org: New enquiry'
api.get('/version', function (req) {
var packageJson = JSON.parse(fs.readFileSync('package.json'))
return packageJson.version
})
api.post('/mail', function (req) {
var msg = ''
for (var key in req.post) {
msg += key + ': ' + req.post[key] + '\n'
}
var email = { Source: sender, Destination: { ToAddresses: [ recipient ] },
Message: { Subject: { Data: subject }, Body: { Text: { Data: msg } } } }
return SES.sendEmail(email).promise()
.then(function (data) {
return { 'status': 'OK' }
})
.catch(function (err) {
console.log('Error sending mail: ' + err)
return { 'status': 'ERROR' }
})
})
module.exports = api
Not too much code given that we added two HTTP endpoints here - even though one just returns the version from the package.json
and is merely an easy way to get the currently deployed version instead of using the AWS API.
As you can see, configuring endpoints is as simple as in any express app. From this code it is hard to spot, that this is going to be deployed in AWS - I really like that fact.
The other endpoint is more interesting though. A nice feature of claudia is the fact, that a posted form will automatically put into req.post
, without you having to fiddle with the velocity template in API Gateway. That is something I really like about claudia.js. It tries to abstract away the AWS API, because as a developer you do not really care about that.
So the form parameters are simply appended to a string, then the email object is build and handed off to SES.sendEmail
. However, the regular AWS SDK does not support promises, even though you can return one from a Lambda, which then will wait until the promise is completed and return back. However the package aws-sdk-promise extends the sdk with promise support, even though it states it is a terrible hack.
Last but not least, note the console.log()
statement, which is perfectly fine, as this is going to be logged in case of an error, but we do not want to return the full error message to the user.
Deployment
Time to go live! Basically all the steps are already outlined in the package.json
file. If the lambda has never been deployed before, you need to create it first
node_modules/.bin/claudia create --name ses-mailer --region us-east-1 --api-module ses-mailer --policies policies
This will upload everything and create the correct permissions. Also, a local file called claudia.json
will be created. This file includes the API id, which can be used to address your endpoints. So check out that file and call
curl -v -X GET https://<<ID>>.execute-api.us-east-1.amazonaws.com/latest/version
You should see the same version, that is stated in the package.json
. Now let’s send an email
curl -v -X POST https://<<ID>>.execute-api.us-east-1.amazonaws.com/latest/mail \
--data 'email=user@example.org&message=my message&subject=My subject'
If you verified your email address, you should have gotten an email now. However, what if you did not? How can we debug things?
Logging
Again, the package.json
file got you covered, as it mentions smoketail, a CLI tool for tailing AWS cloudwatch logs. Running npm run tail
runs
node_modules/.bin/smoketail -f /aws/lambda/ses-mailer
You should see three log lines for each lambda invocation (or more, if there is an actual error, like missing permissions, or you just added more log statements)
2016-04-03T11:07:26.220Z START RequestId: 3f088a3c-f98c-11e5-abe5-d322f6420a5c Version: $LATEST
2016-04-03T11:07:27.109Z END RequestId: 3f088a3c-f98c-11e5-abe5-d322f6420a5c
2016-04-03T11:07:27.109Z REPORT RequestId: 3f088a3c-f98c-11e5-abe5-d322f6420a5c Duration: 869.36 ms Billed Duration: 900 ms Memory Size: 128 MB Max Memory Used: 33 MB
You can see here also the duration and memory size, which are two factors for the billing. If you did not add the policy to send email, you should see something like this
2016-04-03T10:00:06.585Z d6fd308d-f982-11e5-b259-6366087c5746 {"errorMessage":"User
`arn:aws:sts::451105476305:assumed-role/ses-mailer-executor/awslambda_579_20160403085258120' is not authorized
to perform `ses:SendEmail' on resource `arn:aws:ses:us-east-1:451105476305:identity/YOUR@EMAIL.ADDRESS'",
"errorType":"AccessDenied","stackTrace":["Request.extractError (/var/task/node_modules/aws-sdk/lib/protocol/query.js:40:29)",
"Request.callListeners (/var/task/node_modules/aws-sdk/lib/sequential_executor.js:105:20)",
"Request.emit (/var/task/node_modules/aws-sdk/lib/sequential_executor.js:77:10)",
"Request.emit (/var/task/node_modules/aws-sdk/lib/request.js:615:14)",
"Request.transition (/var/task/node_modules/aws-sdk/lib/request.js:22:10)",
"AcceptorStateMachine.runTo (/var/task/node_modules/aws-sdk/lib/state_machine.js:14:12)",
"/var/task/node_modules/aws-sdk/lib/state_machine.js:26:10",
"Request.<anonymous> (/var/task/node_modules/aws-sdk/lib/request.js:38:9)",
"Request.<anonymous> (/var/task/node_modules/aws-sdk/lib/request.js:617:12)",
"Request.callListeners (/var/task/node_modules/aws-sdk/lib/sequential_executor.js:115:18)"]}
If you apply changes to your app, just run ./node_modules/.bin/claudia update
- be aware however that this does not update the policies file.
Testing
You can run claudia test-lambda
, where you can feed different events into your lambda, see this blog post by Marcus Hammarberg
In our example we could simply create an test-event.json
like this
{
"context" : {
"path": "/version",
"method" : "GET"
}
}
and then run claudia test-lambda --event test-event.json
, which should show a 200 status code and the version as payload.
Obviously I could have written all my tests as with any JS app, meaning I have an own mailer class, which I can use sinon to create a spy, so emails do not get send to AWS (or fail because of missing authorization). In addition I would love to have a small test express JS app, that brings up a small server, once could run tests against locally, kind of an offline mode. I think this is what serverless-offline is actually doing, just using hapi instead. I am sure this will come.
Summary
claudia.js is impressive. The ability to hide the fact that API Gateway and Lambda are different services is pretty awesome. Also the simplicity of deploying is really nice. As the big three competitors Amazon, Google and Microsoft all have their own implementations, which do not have significant differences, people will either choose by price, existing infrastructure or by the available tooling and frameworks. The latter part is going to be more significant then, and so far I do not see any frameworks popping up for other platforms, where there is serverless and claudia.js for node, kappa and zappa for python (supporting flask and django) and even lambadaframework for Java.
Also of course the example here is minimal, you should add stuff like captcha support, which is also mentioned in this nice blog post by Ryler Hockenbury. If you need to store sensitive data like access tokens, you should not put it into the code, but use something like AWS Key Management.
Last but not least, claudia.js itself has not only support for lambdas triggered by the API gateway, you can also have lambdas executed as a scheduled event (yay, free cron jobs!) or based on S3 or SNS events, including permissions. You should check out the documentation for more infos.
The future looks interesting, it is just a question of time, until the first open source lambda backend will be published, IBM got the ball rolling with openwhisk.