Serverless ‘Contact Us’ Stack
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.
click here to launch the stack
You can see the entire stack here.
There are 2 input parameters:
Metadata:AWS::CloudFormation::Interface:ParameterGroups: -Label:default: "Configuration"Parameters: - ToEmailAddress- ReCaptchaSecretParameters:ToEmailAddress:Type: StringDescription: Email address you want contact form submittions to go toReCaptchaSecret:Type: StringDescription: 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::TopicProperties:DisplayName:Fn::Join: - ''- - Ref: AWS::StackName- ' Topic'Subscription: - Endpoint: !Ref ToEmailAddressProtocol: emailTopicName: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::RoleProperties:AssumeRolePolicyDocument:Version: '2012-10-17'Statement: - Effect: AllowPrincipal:Service: - lambda.amazonaws.comAction: - sts:AssumeRolePath: '/'
_## Create a Policy and attach it to our Lambda Role.
_IamPolicyLambdaExecution:Type: AWS::IAM::PolicyProperties:PolicyName: IamPolicyLambdaExecutionPolicyDocument:Version: '2012-10-17'Statement: - Effect: AllowAction: - logs:CreateLogGroup- logs:CreateLogStreamResource: arn:aws:logs:us-east-1:*:*- Effect: AllowAction: - logs:PutLogEventsResource: arn:aws:logs:us-east-1:*:*Resource: '*'- Effect: AllowAction: - sns:PublishResource: !Ref ContactUsSNSTopicRoles: - 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::FunctionProperties:Handler: index.handlerTimeout: 5Role:Fn::GetAtt: - IamRoleLambdaExecution- ArnCode: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::RestApiProperties:Name: ApiGatewayContactUs
The AWS::ApiGateway::Resource
resource creates a resource in an Amazon API Gateway (API Gateway) API.
ApiGatewayResource:Type: AWS::ApiGateway::ResourceProperties:ParentId:Fn::GetAtt: - ApiGatewayContactUs- RootResourceIdPathPart: apiRestApiId: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::MethodProperties:AuthorizationType: NONEResourceId:Ref: ApiGatewayResourceRestApiId:Ref: ApiGatewayContactUsHttpMethod: OPTIONSIntegration:IntegrationResponses: - StatusCode: 200ResponseParameters: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_MATCHRequestTemplates:application/json: '{"statusCode": 200}'Type: MOCKMethodResponses: - StatusCode: 200ResponseModels:application/json: 'Empty'ResponseParameters:method.response.header.Access-Control-Allow-Headers: falsemethod.response.header.Access-Control-Allow-Methods: falsemethod.response.header.Access-Control-Allow-Origin: falsemethod.response.header.Access-Control-Allow-Credentials: true
Post Method:
ApiGatewayMethodPost:Type: AWS::ApiGateway::MethodProperties:HttpMethod: POSTRequestParameters: {}ResourceId:Ref: ApiGatewayResourceRestApiId:Ref: ApiGatewayContactUsAuthorizationType: NONEIntegration:IntegrationHttpMethod: POSTType: AWS_PROXYUri: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](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-restapi.html)
resource to a stage so that clients can call the API over the Internet. The stage acts as an environment.
ApiGatewayDeployment:Type: AWS::ApiGateway::DeploymentProperties:RestApiId:Ref: ApiGatewayContactUsStageName: prodDependsOn: - ApiGatewayMethodPost
Finally we need to grant our Api Gateway endpoint permission to invoke our Lambda.
ContactUsFunctionPermission:Type: AWS::Lambda::PermissionProperties:Action: lambda:invokeFunctionFunctionName:Ref: ContactUsFunctionPrincipal: apigateway.amazonaws.comSourceArn: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 endpointValue: !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.