Adding Dynamic Features to a Basic S3 Hosted Website

If you've ready my previous posts on configuring Amazon S3 to host a static web application and configuring a CloudFront distribution to deliver application content and building a basic website with S3 and javascript, then you're going to fit right in with this discussion which focuses on adding a contact form to your website without breaking the bank. In fact it's probably going to be so cheap as to be essentially free.

I'm going to start this by say that the most frustrating part of building this was working with the API Gateway - as an AWS service, I think it has a way to go before it's polished.

So to start, we're going to cover:

  1. Building a small contact form addition to our website.
  2. Creating an SNS queue.
  3. Writing a Lambda function to send an email.
  4. Wiring it all together with the API Gateway service.
  5. Results!

Building the contact form!

I'm going to make an assumption here, and assume that the vast majority of my audience knows how to make a contact form. Here's the code:

<form action="TODO" method="post">
        <div class="form-group">
            <label for="from">Name</label>
            <input type="text" class="form-control" id="from" name="from" placeholder="Jane Doe">
        </div>
        <div class="form-group">
            <label for="fromemail">Email address</label>
            <input type="email" class="form-control" id="fromemail" name="fromemail" placeholder="user@domain.xyz">
        </div>
        <div class="form-group">
            <label for="subject">Subject</label>
            <input type="text" class="form-control" id="subject" name="subject" placeholder="Great Article Idea XYZ">
        </div>
        <div class="form-group">
            <label for="message">Message</label>
            <textarea id="message" name="message" class="form-control" rows="3"></textarea>
        </div>
        <button type="submit" class="btn btn-default">Send</button>
    </form>

Not to over simplify, but standard form tags, a few inputs, and a submit button is all there is to this example. On to the more interesting parts!

SNS Queues

Using the CLI this is just as easy as: aws sns create-topic --name my-website-messages. This should display a topic ARN - grab it for the next step!

Then with the topic ARN, run this command: aws sns subscribe --topic-arn [ARN] --protocol email --notification-endpoint my-email@example.com.

You can now go to your email and confirm the subscription! Easy right?

Lambda Function

Since doing this on the command line is a bit more advanced, we're going to cover the initial set up of Lambda in the AWS console.

Steps:

  1. Log into the AWS Console.
  2. Browse to the Lambda console.
  3. Click "Get Started" or "Create a Lambda function".
  4. For step 1 (select blueprint) click "Skip".
  5. Assign an appropriate name for your function, preferably something descriptive like: websitename-api-lambda-contact.
  6. Optionally, add a description.
  7. Ensure the runetime is set to "Node.js" (unless you're working with another language not covered here).
  8. Add the following basic code by editing inline:
    var AWS       = require('aws-sdk');
    

    exports.handler = function(event, context) { console.log(“Request received:\n”, JSON.stringify(event)); console.log(“Context received:\n”, JSON.stringify(context));

    context.done(null, event);
    

    };

  9. Select or create an IAM role - If you’re creating a role, you’ll get a pop up to walk you through the process.
  10. Leave the rest as default, and click “Next”
  11. Review your settings.
  12. Click “Create Function”!

Now that we have a working Lambda function (that essentially is just logging requests) we’re going to take a step back and modify our IAM role to grant us the permission to run, log, and send messages. Here’s the different IAM policies you need:

{
“Effect”: “Allow”,
“Action”: [
“sns:Publish”
],
“Resource”: [
“”
]
},{
“Effect”: “Allow”,
“Action”: [
“logs:CreateLogGroup”,
“logs:CreateLogStream”,
“logs:PutLogEvents”
],
“Resource”: “arn:aws:logs:::”
}

With that policy change in place, we can go back and modify our Lambda function to send email:

var AWS       = require(‘aws-sdk’);

exports.handler = function(event, context) { console.log(“Request received:\n”, JSON.stringify(event)); console.log(“Context received:\n”, JSON.stringify(context));

var sns = new AWS.SNS();
sns.publish(event, function(err,data){
    if (err) {
        console.log('Error sending a message: ', err);
        context.done(null, {"Status":"Error","Message":err});
    } else {
        console.log('Sent message: ', data.MessageId);
        context.done(null, {"Staus":"Success", "Id": data.MessageId});
    }
});

};

For additional mods (ex. make the email pretty), checkout this repo.

API Gateway

I have to say, when I first started this I thought that this should be super easy with this service - it was not. Hopefully this part of my article helps to save someone else the frustration I felt trying to get this to work originally.

Steps:

  1. Log into the AWS console.
  2. Browse to the API Gateway console.
  3. If this is your first time here, click “Getting started”. Otherwise click “Create API” or use an existing API on your account.
  4. Give your API a relevant name and description.
  5. Click “Create API”.
  6. Click “Create Resource” (top right).
  7. Give the resource an appropriate name. Ex: contact.
  8. Click “Create Resource”.
  9. Ensure our new resource is selected. Click “Create Method”.
  10. A selector will appear below our resource. Select the “POST” method, and click the checkmark.
  11. Change the integration type to Lambda Function.
  12. Select a Lambda region.
  13. Type in your function name.
  14. Click “Save”.
  15. Click “OK” when prompted.
  16. Clicking on POST will now give you a menu with “Method Request”, “Integration Request”, “Integration Response”, and “Method Response”.
  17. Click “Integration Request”.
  18. Expand “Mapping Templates”.
  19. Click “Add mapping template”.
  20. Enter application/x-www-form-urlencoded.
  21. On the right, click the pencil to edit the mapping template.
  22. Change from “Input passthrough” to “Mapping template”.
  23. Set the template to:
    #set($allParams = $input.params())
    {
    “body-json” : $input.json('$'),
    “params” : {
    #foreach($type in $allParams.keySet())
    #set($params = $allParams.get($type))
    “$type” : {
    #foreach($paramName in $params.keySet())
    “$paramName” : “$util.escapeJavaScript($params.get($paramName))”
    #if($foreach.hasNext),#end
    #end
    }
    #if($foreach.hasNext),#end
    #end
    },
    “stage-variables” : {
    #foreach($key in $stageVariables.keySet())
    “$key” : “$util.escapeJavaScript($stageVariables.get($key))”
    #if($foreach.hasNext),#end
    #end
    },
    “context” : {
    “account-id” : “$context.identity.accountId”,
    “api-id” : “$context.apiId”,
    “api-key” : “$context.identity.apiKey”,
    “authorizer-principal-id” : “$context.authorizer.principalId”,
    “caller” : “$context.identity.caller”,
    “cognito-authentication-provider” : “$context.identity.cognitoAuthenticationProvider”,
    “cognito-authentication-type” : “$context.identity.cognitoAuthenticationType”,
    “cognito-identity-id” : “$context.identity.cognitoIdentityId”,
    “cognito-identity-pool-id” : “$context.identity.cognitoIdentityPoolId”,
    “http-method” : “$context.httpMethod”,
    “stage” : “$context.stage”,
    “source-ip” : “$context.identity.sourceIp”,
    “user” : “$context.identity.user”,
    “user-agent” : “$context.identity.userAgent”,
    “user-arn” : “$context.identity.userArn”,
    “request-id” : “$context.requestId”,
    “resource-id” : “$context.resourceId”,
    “resource-path” : “$context.resourcePath”
    },
    “urlencoded” : {
    #if ( $context.httpMethod == “POST” )
    #set( $requestBody = $input.path('$') )
    #else
    #set( $requestBody = "" )
    #end
    #set( $keyValuePairs = $requestBody.split("&") )
    #set( $params = [] )

    Filter empty key-value pairs

    #foreach( $kvp in $keyValuePairs ) #set( $operands = $kvp.split("=") ) #if( $operands.size() == 1 || $operands.size() == 2 ) #set( $success = $params.add($operands) ) #end #end #foreach( $param in $params ) #set( $key = $util.urlDecode($param[0]) ) #if( $param.size() > 1 ) #set( $value = $param[1] ) #else #set( $value = "" ) #end “$key”: “$value”#if( $foreach.hasNext ),#end #end } }

    }

  24. Click the checkmark when done.
  25. Next move to “Method Response”.
  26. Click “Add Response”.
  27. Add the “302” response and click the checkmark.
  28. Expand the “302” HTTP Status header.
  29. Click “Add Header”.
  30. Type “Location” and click the checkmark.
  31. Now go back and select “Integration Response”.
  32. Click “Add integration response”.
  33. Select the “302” method response status.
  34. Save.
  35. Expand the “302” response.
  36. Expand the “Header Mappings” section.
  37. Set the “Location” header to a fully qualified domain name location or URL and click the checkmark.
  38. Now click deploy to save your API and make it available.

Results!

On page load:

Fill out the form:

Peaking into CloudWatch Logs:

Resulting email: