Bojan Djurdjevic

Web Development Blog
Blog About Contact
16 May 2017

Integrating AWS Lambda into a static site

How to integrate dynamic functionality into static site. Contact form example from my blog.

I guess you have built a shiny new website using a static site generator like Jekyll, Hugo, Hexo, or similar, but you want more.
For example, you want your own form handling or other custom functionality. I will demonstrate how I built my contact form using AWS Lambda, SES, and API Gateway, and Serverless.js to do that. My plan is to use this approach to add a section with the most popular posts, and maybe to add a full content search. There are other ways to implement these, but I think this is the most cost effective.

You will need an Amazon Web Services account, and Node.js, NPM, and Serverless.js installed.

Install Serverless and create you projects

1
2
3
npm install serverless -g
mkdir sls-api && cd sls-api
serverless create -t aws-nodejs

You will need SU privileges to install serverless globally if you are using Linux.
If everything goes fine, you will have two files at your current working dir - handler.js, and serverless.yml.

Configure your new projects

I will paste my configuration. You will need to change your AWS user name, service name, roles, etc. - everything in square brackets.

serverless.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
service: [service name]

provider:
name: aws
runtime: nodejs6.10
role: [user role]

# you can overwrite defaults here
# stage: dev
# region: us-east-1

functions:
sendEmail:
handler: handler.sendEmail
events:
- http:
path: contact/send
method: post
cors:
origins:
- '[your website url]'
- '[maybe your website url with www.]'
- 'http://localhost:4000' # comment out in production
environment:
MY_FORM_EMAIL: "Contact Form <form@[your website].com>"
MY_EMAIL: [your email]

resources:
Resources:
[user role]:
Type: AWS::IAM::Role
Properties:
Path: /
RoleName: [user role]
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action: sts:AssumeRole
# note that these rights are needed if you want your function to be able to communicate with resources within your vpc
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
Policies:
- PolicyName: default
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow # note that these rights are given in the default policy and are required if you want logs out of your lambda(s)
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource:
- 'Fn::Join':
- ':'
-
- 'arn:aws:logs'
- Ref: 'AWS::Region'
- Ref: 'AWS::AccountId'
- 'log-group:/aws/lambda/*:*:*'
- Effect: "Allow"
Action:
- "s3:PutObject"
Resource:
Fn::Join:
- ""
- - "arn:aws:s3:::"
- "Ref" : "ServerlessDeploymentBucket"
- Effect: "Allow"
Action:
- ses:sendEmail
- ses:SendRawEmail
Resource: "*"

SES will throw errors if you don’t verify your domain and email address. Use this guide for SES email and domain verification.

Set up your AWS credentials

To give Serverless permission to create AWS services, you will need to set up an AWS user and grant him administrative permissions and configure serverless CLI with the users AWS key and secret.
I found this video very helpful.

Edit your AWS Lambda function

handler.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
'use strict';
const AWS = require('aws-sdk');
const ses = new AWS.SES();


module.exports.sendEmail = (event, context, callback) => {
console.log(event);
var mailer = new Mailer(ses);
var data = JSON.parse(event.body);

var origins = [
'[your website url]',
'[maybe your website url with www.]',
'http://localhost:4000' //comment out in production
];
var origin = origins.find(org => {
return org === event.headers.origin;
});

if(!origin) {
return callback(new Error('Origin not known'));
}

//used to prevent spam bots
if(data._sd) {
return callback(new Error('Possible spam'));
}

var response = {
statusCode: 200,
headers: {
"Access-Control-Allow-Origin" : origin
},
};


mailer.send(data.name, data.email, data.message).then(() => {
callback(null, Object.assign(response, {
body: JSON.stringify({
success: true,
message: 'Message sent successfully!'
})
}));
}).catch(callback);
};


class Mailer {
constructor(ses) {
this.ses = ses;
}

send(name, email, message) {
return new Promise((resolve, reject) =>{
if(!name || !email || !message) {
throw new Error('Missing parameters');
}
if(!this._isValidEmail(email)) {
throw new Error('Invalid email address');
}

var params = {
Destination: {
ToAddresses: [
process.env.MY_EMAIL
]
},
Message: {
Body: {
Text: {
Data: message,
Charset: 'UTF-8'
}
},
Subject: {
Data: 'Message from ' + name,
Charset: 'UTF-8'
}
},
Source: process.env.MY_FORM_EMAIL,
ReplyToAddresses: [ email ]
};

this.ses.sendEmail(params, err => {
if(err) return reject(err);
resolve();
});
});
}

_isValidEmail(email) {
const re = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/;
return re.test(email);
}
}

Deploy to AWS

Deploy your project with the following command:

1
serverless deploy

Save the function URL for later use to submit your form.

Form with AJAX call

Add the form to your website:

1
2
3
4
5
6
7
8
9
<form class="contact-form">
<input type="text" name="name" placeholder="Name" required/>
<input type="email" name="email" placeholder="Email address" required/>
<input type="text" name="_sd">

<textarea name="message" placeholder="Your message" required></textarea>

<button type="submit">Send</button>
</form>

Make sure to hide input with name “_sd” with CSS.

JavaScript code to submit the form:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$('.contact-form').submit(function(evt) {
var form = this;
evt.preventDefault();
var data = {
name: $('[name="name"]').val(),
email: $('[name="email"]').val(),
message: $('[name="message"]').val(),
_sd: $('[name="_sd"]').val()
};

$.ajax({
method: 'POST',
url: [AWS_URL],
data: JSON.stringify(data),
success: function(res) {
alert('hooray!');
},
error: function() {
alert("Don't blame the blog post author");
}
});
});

AWS_URL should be changed with endpoint URL which is printed on successful deploy. You can always use “serverless info” command to get the URL.

You can test the function with cURL:

1
curl 'https://ibmsxw1iq3.execute-api.us-east-1.amazonaws.com/dev/contact/send' -H 'origin: http://localhost:4000' -d'{"name":"Name","email":"you@yourmail.com","message":"test","_sd":""}'

I would appreciate your suggestions, so please leave me a comment. My current concern is that someone who really hates me (thou I can’t think of anyone), trying to flood me with a huge number of emails. I would have to modify my function to do more security checks if that happens.

share

Hello, I'm Bojan, 30 year old full stack web developer, based in Niš, Serbia

© 2018 Bojan Djurdjevic Powered by Hexo