

Have you ever wanted to add a ‘contact us’ form to your static website but didn’t want to pay for a backend server, running 24/7, to handle the requests? So did I. That’s why I built this handy CloudFormation stack that creates a single API endpoint via AWS API Gateway & Lambda.
The backend validates the Google reCAPTCHA and sends out the email via SNS.
Simply launch the CloudFormation stack and add your ‘contact us’ form! It’s that easy.
You can see the entire stack here.
There are 2 input parameters:
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
-
Label:
default: "Configuration"
Parameters:
- ToEmailAddress
- ReCaptchaSecret
Parameters:
ToEmailAddress:
Type: String
Description: Email address you want contact form submittions to go to
ReCaptchaSecret:
Type: String
Description: Your Google reCAPTCHA secret
The Metadata section describes which parameters will show up on the AWS CloudFormation ‘create new stack’ wizard when you use this template. This makes it easy for users to just click the button above and create their stack.
We create a new SNS Topic with a single inline subscription. The subscription is the email address you provided. Our Lambda that handles the ‘contact us’ form submissions will post the form to this SNS topic which will then send an email to you.
ContactUsSNSTopic:
Type: AWS::SNS::Topic
Properties:
DisplayName:
Fn::Join:
- ''
- - Ref: AWS::StackName
- ' Topic'
Subscription:
- Endpoint: !Ref ToEmailAddress
Protocol: email
TopicName:
Fn::Join:
- ''
- - Ref: AWS::StackName
- '-topic'
We need to create an IAM Role for our Lambda that gives our Lambda permission to access other AWS resources.
#
# Role that our Lambda will assume to provide access to other AWS resources
#
IamRoleLambdaExecution:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
Path: '/'
#
# Create a Policy and attach it to our Lambda Role.
#
IamPolicyLambdaExecution:
Type: AWS::IAM::Policy
Properties:
PolicyName: IamPolicyLambdaExecution
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
Resource: arn:aws:logs:us-east-1:*:*
- Effect: Allow
Action:
- logs:PutLogEvents
Resource: arn:aws:logs:us-east-1:*:*
Resource: '*'
- Effect: Allow
Action:
- sns:Publish
Resource: !Ref ContactUsSNSTopic
Roles:
- Ref: IamRoleLambdaExecution
Our Lambda will need access to AWS CloudWatch Logs so it can write logs. It will also need Publish access to our SNS Topic so it can publish to the topic.
Our lambda receives the ‘contact us’ form submission via API Gateway and then posts the form details to our SNS Topic.
We have our Lambda code, inline, within our CloudFormation template. We did this so we could have a simple deployment process. Users only need to click the button above to deploy the entire stack.
We also wanted to references CloudFormation parameters within our code. See ${ReCaptchaSecret}
and ${ContactUsSNSTopic}
.
If the Lambda is greater than 4096 characters you can’t include it inline and will have to upload the lambda to S3 first, then include a reference.
ContactUsFunction:
Type: AWS::Lambda::Function
Properties:
Handler: index.handler
Timeout: 5
Role:
Fn::GetAtt:
- IamRoleLambdaExecution
- Arn
Code:
ZipFile: !Sub |
<inline code>
Runtime: nodejs6.10
Lambda code:
Take a look at the CORS headers we added. These are required when POST’ing to the API Gateway endpoint.
headers: {
"Access-Control-Allow-Origin" : "*", // Required for CORS support to work
"Access-Control-Allow-Credentials" : true // Required for cookies, authorization headers with HTTPS
},
The AWS::ApiGateway::RestApi
resource contains a collection of Amazon API Gateway resources and methods that can be invoked through HTTPS endpoints.
ApiGatewayContactUs:
Type: AWS::ApiGateway::RestApi
Properties:
Name: ApiGatewayContactUs
The AWS::ApiGateway::Resource
resource creates a resource in an Amazon API Gateway (API Gateway) API.
ApiGatewayResource:
Type: AWS::ApiGateway::Resource
Properties:
ParentId:
Fn::GetAtt:
- ApiGatewayContactUs
- RootResourceId
PathPart: api
RestApiId:
Ref: ApiGatewayContactUs
Now we need to add 2 Method’s Options
and Post
. The Options
method will handle the CORS headers and the Post
method will handle the ‘contact us’ form submission.
CORS Options Method:
ApiGatewayMethodOptions:
Type: AWS::ApiGateway::Method
Properties:
AuthorizationType: NONE
ResourceId:
Ref: ApiGatewayResource
RestApiId:
Ref: ApiGatewayContactUs
HttpMethod: OPTIONS
Integration:
IntegrationResponses:
- StatusCode: 200
ResponseParameters:
method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'"
method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'"
method.response.header.Access-Control-Allow-Origin: "'*'"
method.response.header.Access-Control-Allow-Credentials: "'false'"
ResponseTemplates:
application/json: ''
PassthroughBehavior: WHEN_NO_MATCH
RequestTemplates:
application/json: '{"statusCode": 200}'
Type: MOCK
MethodResponses:
- StatusCode: 200
ResponseModels:
application/json: 'Empty'
ResponseParameters:
method.response.header.Access-Control-Allow-Headers: false
method.response.header.Access-Control-Allow-Methods: false
method.response.header.Access-Control-Allow-Origin: false
method.response.header.Access-Control-Allow-Credentials: true
Post Method:
ApiGatewayMethodPost:
Type: AWS::ApiGateway::Method
Properties:
HttpMethod: POST
RequestParameters: {}
ResourceId:
Ref: ApiGatewayResource
RestApiId:
Ref: ApiGatewayContactUs
AuthorizationType: NONE
Integration:
IntegrationHttpMethod: POST
Type: AWS_PROXY
Uri:
Fn::Join:
- ''
- - 'arn:aws:apigateway:'
- Ref: AWS::Region
- ':lambda:path/2015-03-31/functions/'
- Fn::GetAtt:
- ContactUsFunction
- Arn
- '/invocations'
MethodResponses: []
This Method uses the Api Gateway AWS_PROXY
Lambda integration type.
The Lambda proxy integration, designated by AWS_PROXY
in the API Gateway REST API, is for integrating a method request with a Lambda function in the backend. With this integration type, API Gateway applies a default mapping template to send the entire request to the Lambda function and transforms the output from the Lambda function to HTTP responses.
The AWS::ApiGateway::Deployment
resource deploys an Amazon API Gateway (API Gateway) RestApi
resource to a stage so that clients can call the API over the Internet. The stage acts as an environment.
ApiGatewayDeployment:
Type: AWS::ApiGateway::Deployment
Properties:
RestApiId:
Ref: ApiGatewayContactUs
StageName: prod
DependsOn:
- ApiGatewayMethodPost
- ApiGatewayMethodOptions
Finally we need to grant our Api Gateway endpoint permission to invoke our Lambda.
ContactUsFunctionPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:invokeFunction
FunctionName:
Ref: ContactUsFunction
Principal: apigateway.amazonaws.com
SourceArn:
Fn::Join:
- ''
- - 'arn:aws:execute-api:'
- Ref: AWS::Region
- ':'
- Ref: AWS::AccountId
- ':'
- Ref: ApiGatewayContactUs
- '/*/*'
We are now at the end of our CloudFormation script. Lets output our new API Gateway endpoint so we can use it within our ‘contact us’ form.
Outputs:
ApiUrl:
Description: URL of your API endpoint
Value: !Join
- ''
- - https://
- !Ref ApiGatewayContactUs
- '.execute-api.'
- !Ref 'AWS::Region'
- '.amazonaws.com/prod/api'
We are using Bootstrap as the CSS framework. Use your API Gateway url in the forms action attribute.
I ran into a CORS issue when I posted a JSON object. It worked when I posted a stringified JSON object.
This didn’t work:
$.post(url, {}, function(data) {}, 'json');
This did work:
$.post(url, JSON.stringify({}), function(data) {}, 'json');
.errors {
color: red;
display: none;
}
.thanks, .sending {
display: none;
}
.grecaptcha-badge {
float: right;
}
I hope you enjoyed this post and I hope this CloudFormation template makes some developers life a little bit easier. Please let me know what you think.
Create your free account to unlock your custom reading experience.