From 1c4a71867bc0647f9fc035332603f3ec85b7ece1 Mon Sep 17 00:00:00 2001 From: Phillip Leslie Date: Sun, 28 May 2017 23:30:50 -0500 Subject: [PATCH] Build API Gateway resources into CF template --- README.md | 10 +- lambda_webapp.template | 221 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 222 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4abe8b2..4bd1627 100644 --- a/README.md +++ b/README.md @@ -34,15 +34,11 @@ This example demonstrates receiving votes via text message from users via a phon Step 1 – Create an AWS CloudFormation stack with the [template](https://s3.amazonaws.com/awslambda-reference-architectures/web-app/lambda_webapp.template) using a lowercase name for the stack. -Step 2 – Visit the [API Gateway dashboard](https://console.aws.amazon.com/apigateway/home) in your AWS account and create a new resource with a `/vote` endpoint. Assign a POST method that has the `Integration Request` type of "Lambda Function," and point to the Lambda function created by the AWS CloudFormation script that receives votes from your third-party voting service (in this example, Twilio). +Step 2 – Visit the [Amazon Cognito dashboard](https://console.aws.amazon.com/cognito/create) and create a new identity pool that allows access to unauthenticated identities. Modify the policy document to allow `GetItem` and `Scan` access to the aggregates DynamoDB table created by the AWS CloudFormation script above. This allows unauthenticated users to retrieve data from the vote aggregation table in DynamoDB. Amazon Cognito will provide sample code for the JavaScript platform. Note the value for identity pool ID; you'll need it in step 4. -Under `Body Mapping Templates`, set the "Content-Type" to `application/x-www-form-urlencoded`, and add [this mapping template](apigateway-mappingtemplate.txt). +Step 3 – In the __VoteApp__ table in DynamoDB, create a new [Trigger](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.Lambda.html), and point it to the existing Lambda function to [aggregate votes](lambda-functions/aggregate-votes/app.js). This function will monitor any changes to your __VoteApp__ table, writing new, aggregate values into __VoteAppAggregates__. -Step 3 – Visit the [Amazon Cognito dashboard](https://console.aws.amazon.com/cognito/home) and create a new identity pool that allows access to unauthenticated identities. Modify the policy document to allow `GetItem` and `Scan` access to the aggregates DynamoDB table created by the AWS CloudFormation script above. This allows unauthenticated users to retrieve data from the vote aggregation table in DynamoDB. Amazon Cognito will provide sample code for the JavaScript platform. Note the value for identity pool ID; you'll need it in step 5. - -Step 4 – In the __VoteApp__ table in DynamoDB, create a new [Trigger](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.Lambda.html), and point it to the existing Lambda function to [aggregate votes](lambda-functions/aggregate-votes/app.js). This function will monitor any changes to your __VoteApp__ table, writing new, aggregate values into __VoteAppAggregates__. - -Step 5 – Copy the HTML, CSS, and JS files from this repo and into the static S3 bucket that was created to hold your dashboard. You'll need to open `refresh.js` and replace default values of `region` and `identity-pool-id` with your own values. +Step 4 – Copy the HTML, CSS, and JS files from this repo and into the static S3 bucket that was created to hold your dashboard. You'll need to open `refresh.js` and replace default values of `region` and `identity-pool-id` with your own values, and your SMS phone number in index.html. Congratulations! You now should have a working example of the reference architecture. You are able to receive votes in real time, tune your DynamoDB table to handle various levels of incoming traffic, and watch your results change on your dashboard in real time! diff --git a/lambda_webapp.template b/lambda_webapp.template index 2175ca1..3ebc67a 100644 --- a/lambda_webapp.template +++ b/lambda_webapp.template @@ -69,7 +69,224 @@ }, "Resources": { + "ApiGatewayResource": { + "Type" : "AWS::ApiGateway::Resource", + "Properties": { + "RestApiId": { "Ref": "ApiGatewayRestApi" }, + "ParentId": { "Fn::GetAtt": ["ApiGatewayRestApi", "RootResourceId"] }, + "PathPart": "vote" + } + }, + "CloudWatchRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Principal": { "Service": [ "apigateway.amazonaws.com" ] }, + "Action": "sts:AssumeRole" + }] + }, + "Path": "/", + "ManagedPolicyArns": ["arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"] + } + }, + "ApiGatewayAccount": { + "Type" : "AWS::ApiGateway::Account", + "Properties" : { + "CloudWatchRoleArn": { "Fn::GetAtt": ["CloudWatchRole", "Arn"] } + } + }, + "ApiGatewayDeployment": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { "Ref": "ApiGatewayRestApi" }, + "Description": "Production deployment", + "StageName": "production" + }, + "DependsOn": [ + "ApiGatewayRestApi", + "ApiGatewayResource", + "ApiGatewayMethod", + "ApiGatewayLambdaPermission" + ] + }, + "ApiLoggableStage": { + "DependsOn" : ["ApiGatewayAccount"], + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": {"Ref": "ApiGatewayDeployment"}, + "MethodSettings": [{ + "DataTraceEnabled": true, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*" + }], + "RestApiId": {"Ref": "ApiGatewayRestApi"}, + "StageName": "prod_loggable" + } + }, + + "ApiGatewayRestApi": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Description": "RESTful API endpoint", + "Name": { + "Fn::Join": [ + "", + [ + "ApiGatewayRestApi-", + { "Ref": "AWS::StackName" } + ] + ] + } + } + }, + "ApiGatewayLambdaPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "FunctionName" : { "Fn::GetAtt" : [ "LambdaVoteFunction", "Arn" ] }, + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:aws:execute-api:", + { "Ref" : "AWS::Region" }, + ":", + { "Ref" : "AWS::AccountId" }, + ":", + { "Ref" : "ApiGatewayRestApi" }, + "/*/POST/vote" + ] + ] + } + } + }, + "ApiGatewayMethod": { + "DependsOn": ["LambdaVoteFunction","ApiGatewayRestApi"], + "Type": "AWS::ApiGateway::Method", + "Properties": { + "RestApiId": { "Ref": "ApiGatewayRestApi" }, + "ResourceId": { "Ref": "ApiGatewayResource" }, + "HttpMethod": "POST", + "AuthorizationType": "NONE", + "Integration": { + "Type": "AWS", + "IntegrationHttpMethod": "POST", + "Uri": { + "Fn::Join": [ + "", + [ + "arn:aws:apigateway:", + { "Ref" : "AWS::Region" }, + ":lambda:path/2015-03-31/functions/", + { "Fn::GetAtt" : [ "LambdaVoteFunction", "Arn" ] }, + "/invocations" + ] + ] + }, + "RequestTemplates": { + "application/x-www-form-urlencoded": {"Fn::Join": [ + "\n", + [ + "## convert x-www-form-urlencoded to JSON", + "##", + "{", + " #foreach( $token in $input.path('$').split('&') )", + " #set( $keyVal = $token.split('=') )", + " #set( $keyValSize = $keyVal.size() )", + " #if( $keyValSize >= 1 )", + " #set( $key = $util.urlDecode($keyVal[0]) )", + " #if( $keyValSize >= 2 )", + " #set( $val = $util.urlDecode($keyVal[1]) )", + " #else", + " #set( $val = '' )", + " #end", + " \"$key\": \"$val\"#if($foreach.hasNext),#end", + " #end", + " #end", + "}" + ]] + } + } + } + } + }, + "OptionsApiMethod": { + "Type": "AWS::ApiGateway::Method", + "Properties": { + "RestApiId": { "Ref": "ApiGatewayRestApi" }, + "ResourceId": { "Ref": "ApiGatewayResource" }, + "HttpMethod": "OPTIONS", + "AuthorizationType": "NONE", + "Integration": { + "Type": "MOCK", + "RequestTemplates": { + "application/json": "{ \"statusCode\": 200 }" + }, + "IntegrationResponses": [ + { + "StatusCode": "200", + "ResponseParameters" : { + "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'", + "method.response.header.Access-Control-Allow-Methods": "'POST,OPTIONS'", + "method.response.header.Access-Control-Allow-Origin": "'*'" + } + } + ] + }, + "MethodResponses": [ + { + "StatusCode": "200", + "ResponseParameters" : { + "method.response.header.Access-Control-Allow-Headers": true, + "method.response.header.Access-Control-Allow-Methods": true, + "method.response.header.Access-Control-Allow-Origin": true + } + } + ] + } + }, + "LambdaCallableFromApiGatewayPolicy": { + "Type": "AWS::IAM::ManagedPolicy", + "Properties": { + "Description": "Managed Policy for API Gateway Lambda function", + "Path": "/lambda/apigateway/", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Resource": "arn:aws:logs:*:*:*" + }, + { + "Effect": "Allow", + "Action": [ + "apigateway:*" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "iam:PassRole", + "iam:GetServerCertificate" + ], + "Resource": "*" + } + ] + } + } + }, "DynamoDBTable": { "Type": "AWS::DynamoDB::Table", "Properties": { @@ -173,7 +390,7 @@ "Ref": "LambdaReceiveS3Key" } }, - "Runtime": "nodejs", + "Runtime": "nodejs4.3", "Description": "Receives votes from Twilio and adds to DynamoDB", "Handler": "app.handler", "Role": { @@ -197,7 +414,7 @@ "Ref": "LambdaAggregateS3Key" } }, - "Runtime": "nodejs", + "Runtime": "nodejs4.3", "Description": "Receives updated items from DynamoDB streams for aggregation", "Handler": "app.handler", "Role": {