From 41a5d6b560c736e39b1f6e33f21a7de70eaf7a7f Mon Sep 17 00:00:00 2001 From: Kyle Somers Date: Sun, 19 Jun 2016 12:13:56 -0400 Subject: [PATCH] v2.0.0 Release Major version release to address updates/issues identified in CHANGELOG. --- CHANGELOG.md | 15 + .../ZombieGetMessages.js | 24 +- .../ZombiePostMessage.js | 28 +- CloudFormation/CreateZombieWorkshop.json | 261 ++++++----- .../src/ApiGatewayCreate.js | 48 +- .../{ => src}/WK305_Gateway.zip | Bin 690008 -> 689846 bytes .../CopyS3Files/S3GetFilesFunction.js | 24 +- README.md | 413 +++++++++++------- 8 files changed, 476 insertions(+), 337 deletions(-) create mode 100644 CHANGELOG.md rename CloudFormation/CustomResources/APIGatewayCreateScript/{ => src}/WK305_Gateway.zip (91%) mode change 100755 => 100644 CloudFormation/CustomResources/CopyS3Files/S3GetFilesFunction.js diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..af37bc5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +========= +CHANGELOG +========= + +2.0.0 +===== +* Introduces multi-region stack support for workshop to run in any of the 5 existing regions that have API Gateway and Lambda. +* Solves hardcoded attribute dependencies, allowing for multiple stacks to run simultaneously in the same AWS account. Resources are created with stack name prepended to the resource names. +* Removes caching from API Gateway stage to reduce costs for long running stacks. + +1.0.0 +========== +* All commits prior to release 2.0.0 are considered a part of v1.0.0 release. +* Initial release on Github. +* Provides baseline zombie survivor chat application via CloudFormation stack, Lambda functions, DynamoDB tables, and API Gateway resources. diff --git a/ChatServiceLambdaFunctions/ZombieGetMessages.js b/ChatServiceLambdaFunctions/ZombieGetMessages.js index 364b34f..4a94d3e 100644 --- a/ChatServiceLambdaFunctions/ZombieGetMessages.js +++ b/ChatServiceLambdaFunctions/ZombieGetMessages.js @@ -1,24 +1,34 @@ console.log('Loading function'); var aws = require('aws-sdk'); -var ddb = new aws.DynamoDB( - {region: "us-west-2", - params: {TableName: "messages"}}); - +var ddb; + var theContext; function dynamoCallback(err, response) { - if (err) { + if (err) { console.log('error' + err, err.stack); // an error occurred theContext.fail(err); } - + else { - console.log('result: ' + JSON.stringify(response)) // successful response + console.log('result: ' + JSON.stringify(response)) // successful response theContext.succeed(response); } } +function init(context) { + if(!ddb) { + var stackName = context.functionName.split('-z0mb1es-')[0]; + var stackRegion = context.functionName.split('-GetMessagesFromDynamoDB-')[1]; + ddb = new aws.DynamoDB({ + region: stackRegion, + params: { TableName: stackName + "-messages" } + }); + } +} + exports.handler = function(event, context) { + init(context); theContext = context; var params = { "KeyConditions": { diff --git a/ChatServiceLambdaFunctions/ZombiePostMessage.js b/ChatServiceLambdaFunctions/ZombiePostMessage.js index 78f9ab3..6af8c4d 100644 --- a/ChatServiceLambdaFunctions/ZombiePostMessage.js +++ b/ChatServiceLambdaFunctions/ZombiePostMessage.js @@ -1,9 +1,6 @@ // Processes incoming messages for Zombie chat service var aws = require('aws-sdk'); -var ddb = new aws.DynamoDB( - {region: "us-west-2", - params: {TableName: "messages"}} -); +var ddb; var querystring = require('querystring'); var theContext; @@ -16,20 +13,21 @@ var channel = 'default'; exports.handler = function(event, context) { + init(context); theContext = context; - + if(event.message == null || event.message == 'null' || event.name == null || event.name == 'null') { - return context.fail("Message and Name cannot be null"); + return context.fail("Message and Name cannot be null"); } else { message = event.message; from = event.name; } - + if (event.timestamp == null || event.timestamp == 'null') { event.timestamp = "" + new Date().getTime(); timestamp = event.timestamp; } - + /** * For Debubugging input params to the lambda function console.log('Message: ' + message); @@ -45,10 +43,22 @@ exports.handler = function(event, context) { "name":{"S":from} } }; - + dynamoPut(DDBparams); }; +function init(context) { + if(!ddb) { + console.log("Initializing DynamoDB client."); + var stackName = context.functionName.split('-z0mb1es-')[0]; + var stackRegion = context.functionName.split('-WriteMessagesToDynamoDB-')[1]; + ddb = new aws.DynamoDB({ + region: stackRegion, + params: { TableName: stackName + "-messages" } + }); + } +} + function dynamoPut(params){ console.log("Putting item into DynamoDB"); ddb.putItem(params, dynamoCallback); diff --git a/CloudFormation/CreateZombieWorkshop.json b/CloudFormation/CreateZombieWorkshop.json index f241e33..91f577c 100644 --- a/CloudFormation/CreateZombieWorkshop.json +++ b/CloudFormation/CreateZombieWorkshop.json @@ -1,12 +1,6 @@ { "AWSTemplateFormatVersion": "2010-09-09", - "Description": "AWS CloudFormation template to launch resources for a serverless group chat. This was designed for ReInvent 2015 (WRK305: Zombie Apocalypse Survival: Building Serverless Microservices) ", - - "Mappings": { - "AllowedRegions": { - "us-west-2": {"Region": "https://s3-us-west-2"} - } - }, + "Description": "AWS CloudFormation template to launch resources for a serverless group chat. This was designed for the AWS Zombie Apocalypse Workshop: Building Serverless Microservices", "Parameters": { "NumberOfTeammates": { @@ -18,6 +12,31 @@ } }, + "Mappings": { + "AllowedRegions": { + "us-west-2": { + "S3Endpoint": "https://s3-us-west-2", + "S3ContentsBucket": "aws-zombie-workshop-us-west-2" + }, + "us-east-1": { + "S3Endpoint": "https://s3", + "S3ContentsBucket": "aws-zombie-workshop-us-east-1" + }, + "eu-west-1": { + "S3Endpoint": "https://s3-eu-west-1", + "S3ContentsBucket": "aws-zombie-workshop-eu-west-1" + }, + "eu-central-1": { + "S3Endpoint": "https://s3-eu-central-1", + "S3ContentsBucket": "aws-zombie-workshop-eu-central-1" + }, + "ap-northeast-1": { + "S3Endpoint": "https://s3-ap-northeast-1", + "S3ContentsBucket": "aws-zombie-workshop-ap-northeast-1" + } + } + }, + "Conditions": { "CreateIamResources": { "Fn::Not": [{ @@ -83,7 +102,7 @@ { "Effect": "Allow", "Action": ["apigateway:*"], - "Resource": ["*"] + "Resource": ["*", "arn:aws:apigateway:*::/*"] }, { "Effect": "Allow", @@ -137,7 +156,7 @@ "MessagesDynamoDBTable": { "Type": "AWS::DynamoDB::Table", "Properties": { - "TableName": "messages", + "TableName": { "Fn::Join": ["-", [{"Ref": "AWS::StackName"}, "messages"]]}, "AttributeDefinitions": [ { "AttributeName": "channel", @@ -162,7 +181,7 @@ "TalkersDynamoDBTable": { "Type": "AWS::DynamoDB::Table", "Properties": { - "TableName": "talkers", + "TableName": { "Fn::Join": ["-", [{"Ref": "AWS::StackName"}, "talkers"]]}, "AttributeDefinitions": [ { "AttributeName": "channel", @@ -201,7 +220,7 @@ "Properties": { "ServiceToken": { "Fn::GetAtt" : ["S3GetFilesFunction", "Arn"] }, "StackName": { "Ref": "AWS::StackName" }, - "BucketName": "reinvent-wrk305-2015", + "BucketName": { "Fn::FindInMap" : [ "AllowedRegions", { "Ref" : "AWS::Region" }, "S3ContentsBucket"]}, "WebsiteBucketCreatedEarlier": { "Ref" : "S3BucketForWebsiteContent" }, "S3Region": { "Ref" : "AWS::Region" } }, @@ -217,11 +236,11 @@ "Handler": "S3GetFilesFunction.handler", "Role": { "Fn::GetAtt": ["ZombieLabLambdaRole", "Arn"] }, "Code": { - "S3Bucket": "reinvent-wrk305-2015", + "S3Bucket": { "Fn::FindInMap" : [ "AllowedRegions", { "Ref" : "AWS::Region" }, "S3ContentsBucket"]}, "S3Key": "S3GetFilesFunction.zip" }, "Runtime": "nodejs", - "Timeout": "60" + "Timeout": "120" }, "DependsOn": [ "ZombieLabLambdaRole", @@ -238,7 +257,8 @@ "region": { "Ref": "AWS::Region" }, "iamrole": { "Fn::GetAtt": ["ZombieLabLambdaRole", "Arn"] }, "s3bucket": { "Ref": "S3BucketForWebsiteContent" }, - "s3key": "S3/assets/js/constants.js" + "s3key": "S3/assets/js/constants.js", + "apiname": { "Fn::Join": [ "", [ { "Ref": "AWS::StackName" }, "- Zombie Workshop API Gateway" ] ] } }, "DependsOn": [ "S3BucketForWebsiteContent", @@ -256,7 +276,7 @@ "Handler": "LambdaHandler.handleGatewayEvent", "Role": { "Fn::GetAtt" : ["ZombieLabLambdaRole", "Arn"] }, "Code": { - "S3Bucket": "reinvent-wrk305-2015", + "S3Bucket": { "Ref": "S3BucketForWebsiteContent" }, "S3Key": "WK305_Gateway.zip" }, "Runtime": "nodejs", @@ -265,7 +285,9 @@ }, "DependsOn": [ "MessagesDynamoDBTable", - "ZombieLabLambdaRole" + "ZombieLabLambdaRole", + "S3BucketForWebsiteContent", + "PutWebsiteFilesInS3" ] }, @@ -274,14 +296,19 @@ "Properties": { "Handler": "ZombiePostMessage.handler", "Role": { "Fn::GetAtt" : ["ZombieLabLambdaRole", "Arn"] }, + "FunctionName": { "Fn::Join": ["-", [{"Ref": "AWS::StackName"}, "z0mb1es", "WriteMessagesToDynamoDB", {"Ref": "AWS::Region"}]]}, "Code": { - "S3Bucket": "reinvent-wrk305-2015", + "S3Bucket": { "Ref" : "S3BucketForWebsiteContent" }, "S3Key": "ZombiePostMessage.zip" }, "Runtime": "nodejs", "Timeout": "60" }, - "DependsOn": "MessagesDynamoDBTable" + "DependsOn": [ + "MessagesDynamoDBTable", + "S3BucketForWebsiteContent", + "PutWebsiteFilesInS3" + ] }, "GetMessagesFromDynamoDB": { @@ -289,14 +316,19 @@ "Properties": { "Handler": "ZombieGetMessages.handler", "Role": { "Fn::GetAtt" : ["ZombieLabLambdaRole", "Arn"] }, + "FunctionName": { "Fn::Join": ["-", [{"Ref": "AWS::StackName"}, "z0mb1es", "GetMessagesFromDynamoDB", {"Ref": "AWS::Region"}]]}, "Code": { - "S3Bucket": "reinvent-wrk305-2015", + "S3Bucket": { "Ref" : "S3BucketForWebsiteContent" }, "S3Key": "ZombieGetMessages.zip" }, "Runtime": "nodejs", "Timeout": "60" }, - "DependsOn" : "MessagesDynamoDBTable" + "DependsOn" : [ + "MessagesDynamoDBTable", + "S3BucketForWebsiteContent", + "PutWebsiteFilesInS3" + ] }, "WriteTalkersToDynamoDB": { @@ -304,44 +336,45 @@ "Properties": { "Handler": "index.handler", "Role": { "Fn::GetAtt" : ["ZombieLabLambdaRole", "Arn"] }, + "FunctionName": { "Fn::Join": ["-", [{"Ref": "AWS::StackName"}, "z0mb1es", "WriteTalkersToDynamoDB", {"Ref": "AWS::Region"}]]}, "Code": { - "ZipFile": { "Fn::Join": ["\n", [ - "console.log('Loading function');", - "", - "var AWS = require('aws-sdk');", - "", - "var docClient = new AWS.DynamoDB.DocumentClient({", - " region: 'us-west-2'", - "});", - "", - "exports.handler = function(event, context) {", - " console.log('Received event:', JSON.stringify(event, null, 2));", - "", - " if (event.name == null) {", - " context.fail(new Error('name cannot be null: ' + JSON.stringify(event, null, 2)));", - " }", - "", - " var params = {", - " TableName: 'talkers',", - " Item: {", - " channel: 'default',", - " talktime: Date.now(),", - " name: event.name", - " }", - " };", - "", - " docClient.put(params, function(err, data) {", - " if (err) {", - " console.log('DDB Err:' + err);", - " context.fail(new Error('DynamoDB Error: ' + err));", - " } else {", - " console.log(data);", - " context.done(null, {Satus: 'Success'});", - " }", - "", - " });", - "", - "};" + "ZipFile": { "Fn::Join": ["", [ + "console.log('Loading function');\n", + "\n", + "var AWS = require('aws-sdk');\n", + "\n", + "var docClient = new AWS.DynamoDB.DocumentClient({\n", + " region: '", {"Ref": "AWS::Region"}, "',\n", + "});\n", + "\n", + "exports.handler = function(event, context) {\n", + " console.log('Received event:', JSON.stringify(event, null, 2));\n", + "\n", + " if (event.name == null) {\n", + " context.fail(new Error('name cannot be null: ' + JSON.stringify(event, null, 2)));\n", + " }\n", + "\n", + " var params = {\n", + " TableName: '", {"Ref": "AWS::StackName"}, "-talkers',\n", + " Item: {\n", + " channel: 'default',\n", + " talktime: Date.now(),\n", + " name: event.name\n", + " }\n", + " };\n", + "\n", + " docClient.put(params, function(err, data) {\n", + " if (err) {\n", + " console.log('DDB Err:' + err);\n", + " context.fail(new Error('DynamoDB Error: ' + err));\n", + " } else {\n", + " console.log(data);\n", + " context.done(null, {Satus: 'Success'});\n", + " }\n", + "\n", + " });\n", + "\n", + "};\n" ]]} }, "Runtime": "nodejs", @@ -355,52 +388,53 @@ "Properties": { "Handler": "index.handler", "Role": { "Fn::GetAtt" : ["ZombieLabLambdaRole", "Arn"] }, + "FunctionName": { "Fn::Join": ["-", [{"Ref": "AWS::StackName"}, "z0mb1es", "GetTalkersToDynamoDB", {"Ref": "AWS::Region"}]]}, "Code": { - "ZipFile": { "Fn::Join": ["\n", [ - "console.log('Loading function');", - "", - "var AWS = require('aws-sdk');", - "", - "var docClient = new AWS.DynamoDB.DocumentClient({", - " region: 'us-west-2'", - "});", - "", - "exports.handler = function(event, context) {", - " console.log('Received event:', JSON.stringify(event, null, 2));", - "", - " var params = {", - " TableName: 'talkers',", - " KeyConditionExpression: 'channel = :hkey and talktime > :rkey',", - " ExpressionAttributeValues: {", - " ':hkey': 'default',", - " ':rkey': (Date.now() - 2000)", - " },", - " ConsistentRead: true", - " };", - "", - " docClient.query(params, function(err, data) {", - " if (err) {", - " console.log('DDB Err:' + err);", - " context.fail(new Error('DynamoDB Error: ' + err));", - " } else {", - " console.log((Date.now() - 2000));", - " console.log(data);", - " Talkers = [];", - " Pushed = {};", - " data.Items.forEach(function(talker, index, array) {", - " if (Pushed.hasOwnProperty(talker.name) == false) {", - " Talkers.push(talker.name);", - " Pushed[talker.name] = true;", - " }", - "", - " });", - " context.done(null, {", - " Talkers: Talkers", - " });", - " }", - "", - " });", - "};" + "ZipFile": { "Fn::Join": ["", [ + "console.log('Loading function');\n", + "\n", + "var AWS = require('aws-sdk');\n", + "\n", + "var docClient = new AWS.DynamoDB.DocumentClient({\n", + " region: '", {"Ref": "AWS::Region"}, "',\n", + "});\n", + "\n", + "exports.handler = function(event, context) {\n", + " console.log('Received event:', JSON.stringify(event, null, 2));\n", + "\n", + " var params = {\n", + " TableName: '", {"Ref": "AWS::StackName"}, "-talkers',\n", + " KeyConditionExpression: 'channel = :hkey and talktime > :rkey',\n", + " ExpressionAttributeValues: {\n", + " ':hkey': 'default',\n", + " ':rkey': (Date.now() - 2000)\n", + " },\n", + " ConsistentRead: true\n", + " };\n", + "\n", + " docClient.query(params, function(err, data) {\n", + " if (err) {\n", + " console.log('DDB Err:' + err);\n", + " context.fail(new Error('DynamoDB Error: ' + err));\n", + " } else {\n", + " console.log((Date.now() - 2000));\n", + " console.log(data);\n", + " Talkers = [];\n", + " Pushed = {};\n", + " data.Items.forEach(function(talker, index, array) {\n", + " if (Pushed.hasOwnProperty(talker.name) == false) {\n", + " Talkers.push(talker.name);\n", + " Pushed[talker.name] = true;\n", + " }\n", + "\n", + " });\n", + " context.done(null, {\n", + " Talkers: Talkers\n", + " });\n", + " }\n", + "\n", + " });\n", + "};\n" ]]} }, "Runtime": "nodejs", @@ -430,14 +464,17 @@ "Handler": "IamLambdaHandler.handleIAM", "Role": { "Fn::GetAtt": ["ZombieLabLambdaRole", "Arn"] }, "Code": { - "S3Bucket": "reinvent-wrk305-2015", + "S3Bucket": { "Ref" : "S3BucketForWebsiteContent" }, "S3Key": "IamUsers.zip" }, "Runtime": "nodejs", - "Timeout": "30" + "Timeout": "60" }, "DependsOn": [ - "ZombieLabLambdaRole" + "ZombieLabLambdaRole", + "S3BucketForWebsiteContent", + "PutWebsiteFilesInS3", + "S3GetFilesFunction" ] } }, @@ -448,7 +485,7 @@ "Description": "The region where the stack was created." }, "MyChatRoomURL": { - "Value": { "Fn::Join": [ "", [ { "Fn::FindInMap" : [ "AllowedRegions", { "Ref" : "AWS::Region" }, "Region"]}, ".amazonaws.com/", { "Ref": "S3BucketForWebsiteContent" }, "/S3/index.html" ] ] }, + "Value": { "Fn::Join": [ "", [ { "Fn::FindInMap" : [ "AllowedRegions", { "Ref" : "AWS::Region" }, "S3Endpoint"]}, ".amazonaws.com/", { "Ref": "S3BucketForWebsiteContent" }, "/S3/index.html" ] ] }, "Description": "The URL to access your newly created chat." }, "DynamoDBMessagesTableName": { @@ -467,6 +504,14 @@ "Value": { "Ref": "GetMessagesFromDynamoDB" }, "Description": "This Lambda function is used for getting chat messages from the Messages table." }, + "WriteTalkersToDynamoDBLambdaFunction": { + "Value": { "Ref": "WriteTalkersToDynamoDB" }, + "Description": "This Lambda function is used for writing talkers to the Talkers table." + }, + "GetTalkersFromDynamoDBLambdaFunction": { + "Value": { "Ref": "GetTalkersFromDynamoDB" }, + "Description": "This Lambda function is used for getting talkers from the Talkers table." + }, "DynamoDBWriteMessagesARN": { "Value": { "Fn::GetAtt": ["WriteMessagesToDynamoDB", "Arn"] }, "Description": "The ARN for the Write Messages Lambda function" @@ -504,6 +549,10 @@ "Value": { "Fn::GetAtt": ["IamUsersFunction", "Arn"] }, "Description": "The ARN for the Lambda function that creates additional IAM users.", "Condition": "CreateIamResources" + }, + "BucketCopiedContentsFrom": { + "Value": { "Fn::FindInMap" : [ "AllowedRegions", { "Ref" : "AWS::Region" }, "S3ContentsBucket"]}, + "Description": "This is the local region AWS bucket where your files were copied from." } } } diff --git a/CloudFormation/CustomResources/APIGatewayCreateScript/src/ApiGatewayCreate.js b/CloudFormation/CustomResources/APIGatewayCreateScript/src/ApiGatewayCreate.js index 95ad25a..4115b33 100644 --- a/CloudFormation/CustomResources/APIGatewayCreateScript/src/ApiGatewayCreate.js +++ b/CloudFormation/CustomResources/APIGatewayCreateScript/src/ApiGatewayCreate.js @@ -13,6 +13,7 @@ var getmessagearn; var postmessagearn; var iamRole; var apigatewayuuid; +var apiName; // Variables for the callback... var theEvent; @@ -32,6 +33,7 @@ module.exports = { postmessagearn = event.ResourceProperties.postmessagelambdaapiuri; iamRole = event.ResourceProperties.iamrole; apigatewayuuid = event.StackId; + apiName = event.ResourceProperties.apiname; AWS.config.update({region: region}); apigateway = new AWS.APIGateway(); @@ -61,8 +63,7 @@ function createGatewayImplementation() { createTwilioIntegrationResponses, createIntegrations, createIntegrationResponses, - createDeployment, - updateStage + createDeployment ], done); } @@ -71,7 +72,7 @@ function createRestAPI(callback) { console.log('Creating REST API'); var params = { - name: 'Zombie Workshop API Gateway', + name: apiName, description: apigatewayuuid }; apigateway.createRestApi(params, function(err, data) { @@ -339,8 +340,7 @@ function createDeployment(callback) { var params = { restApiId: restAPIId, /* required */ stageName: 'ZombieWorkshopStage', /* required */ - cacheClusterEnabled: true, - cacheClusterSize: '0.5', + cacheClusterEnabled: false, description: 'ZombieWorkshopStage deployment', stageDescription: 'ZombieWorkshopStage deployment' }; @@ -354,44 +354,6 @@ function createDeployment(callback) { }); } -function updateStage(callback) { - console.log('Updating Stage for custom method settings'); - var params = { - restApiId: restAPIId, /* required */ - stageName: 'ZombieWorkshopStage', /* required */ - patchOperations: [ - { - op: 'replace', - path: '/~1zombie~1message/GET/caching/enabled', - value: 'true' - }, - { - op: 'replace', - path: '/~1zombie~1message/POST/caching/enabled', - value: 'false' - }, - { - op: 'replace', - path: '/~1zombie~1message/GET/caching/ttlInSeconds', - value: '2' - }, - { - op: 'replace', - path: '/~1zombie~1message/POST/caching/ttlInSeconds', - value: '0' - } - ] - }; - apigateway.updateStage(params, function(err, data) { - if(!err) { - callback(null); - } - else { - callback(err); - } - }); -} - function createTalkerResource(callback) { console.log('Creating Talker Resource'); var params = { diff --git a/CloudFormation/CustomResources/APIGatewayCreateScript/WK305_Gateway.zip b/CloudFormation/CustomResources/APIGatewayCreateScript/src/WK305_Gateway.zip similarity index 91% rename from CloudFormation/CustomResources/APIGatewayCreateScript/WK305_Gateway.zip rename to CloudFormation/CustomResources/APIGatewayCreateScript/src/WK305_Gateway.zip index 83206d882bbfb1aedb1ca719b7ca58953da61c13..757e55fca9ab7e67624723822bd161a8ba0c65ca 100644 GIT binary patch delta 18759 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 delta 19134 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 Un{NTU0G?ujf`Y38yuPRKKge%6Qvd(} diff --git a/CloudFormation/CustomResources/CopyS3Files/S3GetFilesFunction.js b/CloudFormation/CustomResources/CopyS3Files/S3GetFilesFunction.js old mode 100755 new mode 100644 index 842ded0..1e2bab8 --- a/CloudFormation/CustomResources/CopyS3Files/S3GetFilesFunction.js +++ b/CloudFormation/CustomResources/CopyS3Files/S3GetFilesFunction.js @@ -2,15 +2,17 @@ var aws = require("aws-sdk"); exports.handler = function(event, context) { console.log("REQUEST RECEIVED:\n", JSON.stringify(event)); - aws.config.update({ - region: event.ResourceProperties.S3Region - }) + + // Set region to the destination region where the user's bucket is hosted. + //aws.config.update({region: event.ResourceProperties.S3Region}) + aws.config.update({region: event.ResourceProperties.S3Region}) var responseData = {}; var responseStatus = "FAILED"; // Start out with response of FAILED until we confirm SUCCESS explicitly. var s3 = new aws.S3(); - var srcS3Bucket = "reinvent-wrk305-2015"; // S3 bucket where AWS has hosted the lab content - var dstS3Bucket = event.ResourceProperties.WebsiteBucketCreatedEarlier; // Bucket name that is passed in from CloudFormation. + var srcS3Bucket = event.ResourceProperties.BucketName; // S3 bucket where AWS has hosted the lab content + var dstS3Bucket = event.ResourceProperties.WebsiteBucketCreatedEarlier; + var keys = [ "S3/index.html", "S3/assets/css/zombie.css", @@ -23,7 +25,11 @@ exports.handler = function(event, context) { "S3/app/controllers/chatMessageController.js", "S3/app/controllers/chatPanelController.js", "S3/app/controllers/talkersPanelController.js", - "S3/app/controllers/loginController.js" + "S3/app/controllers/loginController.js", + "WK305_Gateway.zip", + "ZombieGetMessages.zip", + "ZombiePostMessage.zip", + "IamUsers.zip" ]; // Objects that are copied from AWS to user's bucket // CloudFormation cannot delete S3 bucket if there are objects in it. @@ -62,7 +68,7 @@ exports.handler = function(event, context) { } else { // if request type is CREATE or UPDATE, we get files into user's S3 bucket. - for (var i = 0; i < keys.length; ++i) { + for (var i in keys) { var srcS3Key = keys[i]; var dstS3Key = srcS3Key; @@ -80,10 +86,12 @@ exports.handler = function(event, context) { if (err) { responseData = {Error: 'Object ' + srcS3Key + ' failed to transfer to your bucket.'}; console.log(responseData.Error + ':\\n', err); + sendResponse(event, context, responseStatus, responseData); } else { + responseStatus = "SUCCESS"; console.log(data); - sendResponse(event, context, "SUCCESS"); + sendResponse(event, context, responseStatus, responseData); } }); } diff --git a/README.md b/README.md index 5f89e51..ff1909b 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,52 @@ # Zombie Microservices Workshop: Lab Guide -## All Labs must be performed in us-west-2 (Oregon) - ## Overview -The [Zombie Microservices Workshop](http://aws.amazon.com/events/zombie-microservices-roadshow/) introduces the basics of building serverless microservices using [AWS Lambda](http://aws.amazon.com/lambda/), Amazon API Gateway, Amazon DynamoDB, and other AWS services. This workshop has several lab exercises that you can complete to extend the functionality of the base chat app that is provided when you launch the CloudFormation template provided. Each of these labs is an independent section and you may choose to do some or all of them. +The [Zombie Microservices Workshop](http://aws.amazon.com/events/zombie-microservices-roadshow/) introduces the basics of building serverless applications using [AWS Lambda](http://aws.amazon.com/lambda/), Amazon API Gateway, Amazon DynamoDB, and other AWS services. This workshop has several lab exercises that you can complete to extend the functionality of the base chat app that is provided when you launch the CloudFormation template provided. + +Each of these labs is an independent section and you may choose to do some or all of them, or in any order that you prefer. +**What you will build...** + * **Typing Indicator** - This exercise already has the UI and backend implemented, and focuses on how to setup the API Gateway to provide a RESTful endpoint. + This exercise already has the UI and backend implemented, and focuses on how to setup the API Gateway to provide a RESTful endpoint. You will configure the survivor chat application to display which survivors are currently typing in the chat room. * **SMS Integration with Twilio** - This exercise wires together Twilio to an existing API Gateway stack. It shows how you can leverage templates in API Gateway to transform form posted data into JSON format for the backend lambda function. -* **Search over the chat messages** - This exercise adds an Elasticsearch cluster, which is used to index chat messages streamed from a DynamoDB table. + This exercise wires uses [Twilio](http://twilio.com) to integrate SMS text functionality with the survivor chat application. You will configure a free-trial Twilio phone number so that users can send text messages to the survivor chat application. You'll learn to leverage mapping templates in API Gateway to perform data transformations in an API. +* **Search Integration with Elasticsearch** + This exercise adds an Elasticsearch cluster to the application which is used to index chat messages streamed from the DynamoDB table containing chat messages. * **Slack Integration** - This exercise integrates Slack into the chat application to send messages from Slack. -* **Intel Edison Zombie Motion Sensor** - This exercise integrates motion sensor detection of zombies to the chat system using an Intel Edison board and a Grove PIR Motion Sensor. + This exercise integrates the popular messaging app, [Slack](http://slack.com), into the chat application so that survivors can send messages to the survivor chat from within the Slack app. +* **Intel Edison Zombie Motion Sensor** (IoT device required) + This exercise integrates motion sensor detection of zombies to the chat system using an Intel Edison board and a Grove PIR Motion Sensor. You will configure a Lambda function to consume motion detection events and push them into the survivor chat! * **Workshop Cleanup** - This section points out some instructions to tear down your environment when you're done working on the labs. + This section provides instructions to tear down your environment when you're done working on the labs. * * * ### Let's Begin! Launch the CloudFormation Stack -1\. To begin this workshop, click the 'Deploy to AWS' button below. +1\. To begin this workshop, **click one of the 'Deploy to AWS' buttons below for the region you'd like to use**. This is the AWS region where you will launch resources for the duration of this workshop. This will open the CloudFormation template in the AWS Management Console for the region you select. + +Region | Launch Template +------------ | ------------- +Oregon (us-west-2) | [![Launch Zombie Workshop Stack into Oregon with CloudFormation](/Images/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/new?stackName=zombiestack&templateURL=https://s3-us-west-2.amazonaws.com/aws-zombie-workshop-us-west-2/CreateZombieWorkshop.json) +Virginia (us-east-1) | [![Launch Zombie Workshop Stack into Virginia with CloudFormation](/Images/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/new?stackName=zombiestack&templateURL=https://s3.amazonaws.com/aws-zombie-workshop-us-east-1/CreateZombieWorkshop.json) +Ireland (eu-west-1) | [![Launch Zombie Workshop Stack into Ireland with CloudFormation](/Images/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=eu-west-1#/stacks/new?stackName=zombiestack&templateURL=https://s3-eu-west-1.amazonaws.com/aws-zombie-workshop-eu-west-1/CreateZombieWorkshop.json) +Frankfurt (eu-central-1) | [![Launch Zombie Workshop Stack into Frankfurt with CloudFormation](/Images/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=eu-central-1#/stacks/new?stackName=zombiestack&templateURL=https://s3-eu-central-1.amazonaws.com/aws-zombie-workshop-eu-central-1/CreateZombieWorkshop.json) +Tokyo (ap-northeast-1) | [![Launch Zombie Workshop Stack into Tokyo with CloudFormation](/Images/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=ap-northeast-1#/stacks/new?stackName=zombiestack&templateURL=https://s3-ap-northeast-1.amazonaws.com/aws-zombie-workshop-ap-northeast-1/CreateZombieWorkshop.json) -[![Launch Zombie Workshop Stack into Oregon with CloudFormation](/Images/deploy-to-aws.png)](https://console.aws.amazon.com/cloudformation/home?region=us-west-2#/stacks/new?stackName=ZombieWorkshopStack&templateURL=https://s3-us-west-2.amazonaws.com/reinvent-wrk305-2015/CreateZombieWorkshop.json) +*Lambda is currently available in the above 5 AWS Regions. As additional regions introduce support for both AWS Lambda and API Gateway, we'll add those as options for the workshop!* -2\. On the "Specify Details" page, your Stack is prepopulated with the name "ZombieWorkshopStack". +2\. Once you have chosen a region and are inside the AWS CloudFormation Console, you should be on a screen titled "Select Template". We are providing CloudFormation with a template on your behalf, so click the blue **Next** button to proceed. -3\. For the parameter section, if you want to develop with a team and would like to create IAM Users in your account to grant them access, then specify how many teammates/users you want to be created in the **NumberOfTeammates** box. Otherwise, leave it defaulted to 0 and no additional users will be created. Click **Next**. +3\. On the following screen, "Specify Details", your Stack is pre-populated with the name "zombiestack". You can customize that to a name of your choice **less than 15 characters in length** or leave as is. For the parameters section, if you want to develop with a team and would like to create IAM Users in your account to grant your teammates access, then specify how many teammates/users you want to be created in the **NumberOfTeammates** text box. Otherwise, leave it defaulted to 0 and no additional users will be created. The user launching the stack (you) already have the necessary permissions. Click **Next**. *If you create IAM users, an IAM group will also be created and those users will be added to that group. On deletion of the stack, those resources will be deleted for you.* 4\. On the "Options" page, leave the defaults and click **Next**. -5\. On the "Review" page, verify your selections, then scroll to the bottom and acknowledge that your Stack will launch IAM resources for you. Then click **Create** to launch your stack. +5\. On the "Review" page, verify your selections, then scroll to the bottom and acknowledge that your Stack will launch IAM resources for you. Then click **Create** to launch your stack. 6\. Your stack will take about 3 minutes to launch and you can track its progress in the "Events" tab. When it is done creating, the status will change to "CREATE_COMPLETE". -7\. Click the "Outputs" tab and click the link for "MyChatRoomURL". This should open your chat application in a new tab. +7\. Click the "Outputs" tab and click the link for "MyChatRoomURL". This should open your chat application in a new tab. 8\. In your chat application, type your name (or a fun username!) in the "User Name" field, then begin typing messages in the textbox at the bottom of the screen where it displays "Enter a message and save humanity". @@ -46,212 +56,261 @@ The [Zombie Microservices Workshop](http://aws.amazon.com/events/zombie-microser ## Lab 1 - Typing Indicator -The typing indicator shows up in the web chat client. It's a section above the post message input that shows when other survivors are typing. The UI and backend Lambda functions have been implemented, and this lab focuses on how to enable the feature in API Gateway. +**What you'll do in this lab...** + +In this section you will create functionality that shows which survivors are currently typing in the chat room. To enable this, you'll modify the newly created API with Lambda functions to push typing metadata to a DynamoDB table that contains details about which survivors are typing. The survivor chat app continuously polls this API endpoint to determine who is typing. The typing indicator shows up in the web chat client in a section below the chat message panel. The UI and backend Lambda functions have been implemented, and this lab focuses on how to enable the feature in API Gateway. The application uses [CORS](http://docs.aws.amazon.com/AmazonS3/latest/dev/cors.html). This lab will both wire up the backend Lambda function as well as perform the necessary steps to enable CORS. **Typing Indicator Architecture** ![Overview of Typing Indicator Architecture](/Images/TypingIndicatorOverview.png) -1\. Select the API Gateway Service from the main console page -![API Gateway in Management Console](/Images/Typing-Step1.png) +1\. Select the API Gateway Service from the main console page +![API Gateway in Management Console](/Images/Typing-Step1.png) + +2\. Select the Zombie Workshop API Gateway. + +3\. Go into the /zombie/talkers/GET method flow. Do this by clicking the "GET" method under the /zombie/talkers resource. This is highlighted in blue in the image below. +![GET Method](/Images/Typing-Step3.png) -2\. Select the Zombie Workshop API Gateway +*This GET HTTP method is used by the survivor chat app to perform continuous queries on the DynamoDB talkers table to determine which users are typing.* -3\. Go into the /zombie/talkers/GET method flow -![GET Method](/Images/Typing-Step3.png) +4\. Click the **Integration Request** box. -4\. Select the Integration Request component in the flow +5\. Under "Integration Type", Select **Lambda Function.** -5\. Under Integration Type, Select Lambda Function +* Currently, this API method is configured to a "MOCK" integration. MOCK integrations are dummy backends that are useful when you are testing and don't yet have the backend built out but need the API to return sample dummy data. You will remove the MOCK integration and configure this GET method to connect to a Lambda function that queries DynamoDB. -6\. Select the us-west-2 region +6\. For the **Lambda Region** field, select the region in which you launched the CloudFormation stack. (HINT: Select the region code that corresponds with the yellow CloudFormation button you clicked to launch the CloudFormation template). For example if you launched your stack in Virginia (us-east-1), then you will select us-east-1 as your Lambda Region. -7\. Select the **_[CloudformationTemplateName]_**-GetTalkersFromDynamoDB-**_[XXXXXXXXXX]_** Function +* When you launched the CloudFormation template, it also created several Lambda functions for you locally in the region you selected, including functions for retrieving data from and putting data into a DynamoDB "Talkers" table with details about which survivors are currently typing in the chat room. -8\. Select Save and Grant access for API Gateway to invoke the Lambda function. +7\. For the **Lambda Function** field, begin typing "gettalkers" in the text box. In the auto-fill dropdown, select the function that contains "GetTalkersFromDynamoDB" in the name. It should look something like this.... **_[CloudformationTemplateName]_**-GetTalkersFromDynamoDB-**_[XXXXXXXXXX]_**. -9\. Click the Method Response section of the Method Execution Flow +* This Lambda function is written in NodeJs. It performs GetItem DynamoDB requests on a Table called Talkers. This talkers table contains records that are continuously updated whenever users type in the chat room. By hooking up this Lambda function to your GET method, it will get invoked by API Gateway when the chat app polls the API with GET requests. + +8\. Select the blue **Save** button and click **OK** if a pop up asks you to confirm that you want to switch to Lambda integration. Then grant access for API Gateway to invoke the Lambda function by clicking "OK" again. This 2nd popup asks you to confirm that you want to allow API Gateway to be able to invoke your Lambda function. + +9\. Click the Method Response section of the Method Execution Flow. You'll now tell API Gateway how what types of HTTP response types you want your API to expose. 10\. Add a 200 HTTP Status response. Click "Add Response", type "200" in the status code text box and then click the little checkmark to save the method response, as shown below. -![Method Response](/Images/Typing-Step10.png) +![Method Response](/Images/Typing-Step10.png) + +* You've configured the GET method of the /talkers resource to allow responses with HTTP status of 200. We could add more response types but we'll skip that for simplicity in this workshop. 11\. Go to the /zombie/talkers/POST method by clicking the "POST" option in the resource tree on the left navigation pane. -![POST Method](/Images/Typing-Step11.png) +![POST Method](/Images/Typing-Step11.png) -12\. Perform Steps 4-10 again as you did for the GET method , but instead, select the **_[CloudformationTemplateName]_**-WriteTalkersToDynamoDB-**_[XXXXXXXXXX]_** Lambda Function for the Integration Request Lambda function. +12\. Perform Steps 4-10 again as you did for the GET method. However, this time when you are selecting the Lambda Function for the Integration Request, you'll type "writetalkers" in the auto-fill and select the function that looks something like this... **_[CloudformationTemplateName]_**-WriteTalkersToDynamoDB-**_[XXXXXXXXXX]_** -13\. Go to the /zombie/talkers/OPTIONS method +* In these steps you are configuring the POST method that is used by the chat app to insert data into DynamoDB Talkers table with details about which users are typing. You're performing the same exact method configuration for the POST method as you did for your GET method. However, since this POST method is used for sending data to the database, it triggers a different backend Lambda function. This function writes data to DynamoDB while the "GetTalkersToDynamoDB" function was used to retrieve data from DynamoDB. -14\. Select the Method Response +13\. Go to the /zombie/talkers/OPTIONS method + +14\. Select the Method Response. 15\. Add a 200 method response. Click "Add Response", type "200" in the status code text box and then click the little checkmark to save the method response. -16\. Go back to the OPTIONS method flow and select the Integration Response +16\. Go back to the OPTIONS method flow and select the Integration Response. (To go back, there should be a blue hyperlink titled "Method Execution" which will bring you back to the method execution overview screen). + +17\. Select the Integration Response. -17\. Select the Integration Response +18\. Add a new Integration response with a method response status of 200. Click the "Method response status" dropdown and select "200". (leaving the regex box blank). When done, click the blue **Save** button. -18\. Add a new Integration response with a method response status of 200. Click the "Method response status" dropdown and select "200". (leaving the regex box blank). Click "Save". +* In this section you configured the OPTIONS method simply to respond with HTTP 200 status code. The OPTIONS method type is simply used so that clients can retrieve details about the API resources that are available as well as the methods associated with them. Think of this as the mechanism that allows clients to query the API to learn what "Options" are available to them. 19\. Select the /zombie/talkers resource on the left navigation tree. -![talker resource](/Images/Typing-Step19.png) +![talker resource](/Images/Typing-Step19.png) 20\. Click the "Actions" box and select "Enable CORS" in the dropdown. 21\. Select Enable and Yes to replace the existing values. You should see all green checkmarks for the CORS options that were enabled, as shown below. -![talker resource](/Images/Typing-Step21.png) +![talker resource](/Images/Typing-Step21.png) + +* If you don't see all green checkmarks, this is probably because you forgot to add the HTTP Status 200 code for the Method Response Section. Go back to the method overview section for your POST, GET, and OPTIONS method and make sure that it shows "HTTP Status: 200" in the Method Response box. 22\. Click the "Actions" box and select Deploy API -![talker resource](/Images/Typing-Step22.png) +![talker resource](/Images/Typing-Step22.png) -23\. Select the ZombieWorkshopStage deployment and hit the Deploy button. The typing indicator should now show when survivors are typing. -![talker resource](/Images/Typing-Done.png) +23\. Select the ZombieWorkshopStage deployment and hit the Deploy button. + +* In this workshop we deploy the API to a stage called "ZombieWorkshopStage". In your real world scenario, you'll likely deploy to stages such as "production" or "development" which align to the actual stages of your API development process. + +**LAB 1 COMPLETE** As you type, POST requests are being made to the Talkers DynamoDB table to continuously update the table with timestamps for who is typing. Continuous polling (GET Requests) on that table also occurs to check which survivors are typing, which updates the "Users Typing" field in the web app. +![talker resource](/Images/Typing-Done.png) + * * * ## Lab 2 - SMS Integration with Twilio -In this section, you’ll wire together Twilio with an existing API Gateway endpoint created in the CloudFormation stack to bring SMS texting functionality into the Zombie Chat application. +**What you'll do in this lab...** + +In this section, you’ll create a free-trial Twilio SMS phone number. You will configure this Twilio phone number with a webhook to forward all incoming text messages sent to your Twilio number to the /zombie/twilio API resource in API Gateway. This will allow you to communicate with survivors in the chat room via text message. **SMS Twilio Integration Architecture** ![Overview of Twilio Integration](/Images/TwilioOverview.png) -1\. Sign up for a free trial Twilio account at https://www.twilio.com/try-twilio. +1\. Sign up for a free trial Twilio account at https://www.twilio.com/try-twilio. Or if you have an existing Twilio account, login. 2\. Once you have created your account, login to the Twilio console and navigate to the Home icon on the left navigation pane. On the Home screen/console dashboard, scroll down to the **Phone Numbers** section and click "Phone Numbers". -![Manage Twilio Phone Number](/Images/Twilio-Step2.png) +![Manage Twilio Phone Number](/Images/Twilio-Step2.png) -3\. On the Phone Numbers screen, click "Get Started" to assign a phone number to your account. Then click the red "Get your first Twilio phone number" button. We’re going to generate a 10-digit phone number in this lab, but a short-code would also work if preferred. This number should be enabled for voice and messaging by default. A popup will appear with your new phone number, click "Choose this number". **These are US phone numbers. You can provision an international phone number if doing this workshop outside the U.S. Twilio terms and conditions and pricing applies. Please see their website for those details. +3\. On the Phone Numbers screen, click "Get Started" to assign a phone number to your account. Then click the red "Get your first Twilio phone number" button. We’re going to generate a 10-digit phone number in this lab, but a short-code would also work if preferred. This number should be enabled for voice and messaging by default. A popup will appear with your new phone number, click "Choose this number". -4\. Once you’ve received a phone number, click the **Manage Numbers** button on the left navigation pane. Click on your phone number, which will take you to the properties page for that number. +*These are US phone numbers. You can provision an international phone number if doing this workshop outside the U.S. Twilio terms and conditions and pricing applies. Please see their website for those details.* -5\. Scroll to the bottom of the properties page, to the messaging section. In the **Configure With** dropdown, select the **Webhooks/TwiML** option. +4\. Once you’ve received a phone number, click the **Manage Numbers** button on the left navigation pane. Click on your phone number, which will take you to the properties page for that number. -6\. Now you’ll retrieve your **/twilio** API endpoint from API Gateway and provide it to Twilio to hook up to AWS. Open the AWS Management console in a new tab, and navigate to API Gateway, as illustrated below. Be sure to leave the Twilio tab open as you’ll need it again to finish setup. -![API Gateway in Management Console](/Images/Twilio-Step6.png) +5\. Scroll to the bottom of the properties page, to the messaging section. In the **Configure With** dropdown, select the **Webhooks/TwiML** option. Leave this page open for now and proceed to the next step. -7\. In the API Gateway console, select your API, **Zombie Workshop API Gateway**. On the left navigation tree, click "Stages". -![API Gateway Resources Page](/Images/Twilio-Step7.png) +* The Twilio webhooks section allows you to integrate your phone number with third party services. In this case, you're going to configure your Twilio phone number to forward any messages sent to that number to your API Gateway endpoint with POST HTTP requests. -8\. With "Stages" selected, expand the "Zombie Workshop Stage" by clicking the blue arrow, and select the **POST** method for the **/zombie/twilio** resource. The twilio resource is the endpoint that CloudFormation created for SMS messages from your Twilio phone number. You should see an **Invoke URL** displayed for your **/zombie/twilio** resource, as shown below. -![API Gateway Invoke URL](/Images/Twilio-Step8.png) +6\. Now you’ll retrieve your **/zombie/twilio** API endpoint from API Gateway and provide it to Twilio to hook up to AWS. Open the AWS Management console in a new tab, and navigate to API Gateway, as illustrated below. Be sure to leave the Twilio tab open as you’ll need it again to finish setup. +![API Gateway in Management Console](/Images/Twilio-Step6.png) -9\. Copy the Invoke URL and return to the Twilio website. On the Twilio page you left open, paste the Invoke URL you copied from API Gateway into the textbox next to label **A message comes in**. Ensure that the request type is set to **HTTP POST**. This is illustrated below. -![Twilio Request URL](/Images/Twilio-Step9.png) +7\. In the API Gateway console, select your API, **Zombie Workshop API Gateway**. On the left navigation tree, click **Stages**. +![API Gateway Resources Page](/Images/Twilio-Step7.png) -10\. Click **Save** to finalize the setup connecting Twilio to your /twilio URL. +8\. With "Stages" selected, expand the "Zombie Workshop Stage" by clicking the blue arrow, and select the **POST** method for the **/zombie/twilio** resource. The **/zombie/twilio** resource is the endpoint that CloudFormation created specifically for SMS integration with Twilio. You should see an **Invoke URL** displayed for your **/zombie/twilio** resource, as shown below. +![API Gateway Invoke URL](/Images/Twilio-Step8.png) + +9\. Copy the **Invoke URL** and return to the Twilio website. On the Twilio page that you left open, paste the Invoke URL from API Gateway into the text box next to the label **A message comes in**. Ensure that the request type is set to **HTTP POST**. This is illustrated below. +![Twilio Request URL](/Images/Twilio-Step9.png) + +10\. Click **Save** to finalize the setup connecting Twilio to your API. 11\. You will now create the Lambda Function that processes your incoming Twilio messages, parses them, and pushes them to the "/messages" Chat Service. To begin, navigate to the Lambda console. +* As you'll see throughout this workshop, we will leverage separate Lambda functions to pre-process data before sending standardized/formatted requests to the /zombie/message resource. This allows us to-reuse the existing DynamoDB logic behind the /message resource multiple times rather than writing multiple functions that all interact with DynamoDB individually. As messages come in to your Twilio number, the Twilio webhook forwards them as POSTs to your /zombie/twilio resource, which will be integrated with a backend pre-processing Lambda function. This function will strip apart the Twilio payload and format it before making an HTTP POST to your /zombie/message service. + 12\. Click **Create a Lambda function** and select **Skip** on the blueprint screen as we will be creating a brand new function. -13\. Create a name for the function, such as "TwilioProcessing". Leave the "Runtime" as **Node.js**. From the GitHub repo, open the **TwilioProcessing.js** file. Copy the entire contents from this file into the Lambda code entry section. Once you have copied the code into Lambda, scroll down to the section in the code where the "host" variable is declared. It should show a value of "INSERT YOUR API GATEWAY URL HERE EXCLUDING THE HTTPS://". Please replace this string with the URL of your **/message** POST method. Please be sure to remove the "https://" portion of the URL and the end of the URL "/ZombieWorkshopStage/zombie/message". Your final URL inputted into the code should look something like "xxxxxxxx.execute-api.us-west-2.amazonaws.com". +13\. Create a name for the function, such as **"[Your CloudFormation stack name]-TwilioProcessing"**. Leave the "Runtime" as **Node.js 4.3**. From the GitHub repo, open the **TwilioProcessing.js** file. Copy the entire contents from this file into the Lambda code entry section. Once you have copied the code into Lambda, scroll down to [line 47](/Twilio/TwilioProcessing.js#L47) in the code where the "host" variable is declared. It should show a value of "INSERT YOUR API GATEWAY URL HERE EXCLUDING THE HTTPS://". Please replace this string with the fully qualified domain name (FQDN) of the URL for your **/zombie/message** POST method found in API Gateway. For example, it should look something like "xxxxxxxx.execute-api.us-west-2.amazonaws.com". + +* The functions in this workshop are authored in Nodejs 0.10 but will work regardless of the version of Node you choose when creating your Function in the console. The workshop will soon be upgraded to use Nodejs 4.3. 14\. After you have copied the code into the Lambda inline code console and modifed the POST URL, scroll down to the **Lambda function handler and role** section. For the role, select **Basic execution role** from the dropdown and click "Allow" on the popup window to confirm the creation of the role. For this Lambda function we do not need any IAM permissions to other AWS services. -15\. Keep all the rest of the defaults set and click **Next** and then **Create function** on the Review page to create your Lambda function. You have just created a Lambda function that accepts the querystring params from the incoming API Gateway /twilio endpoint, converts the parameters to the correct format for our Chat Service including a conversion to JSON format, and finally makes an HTTPS POST request to the /messages Chat Service endpoint. +15\. Keep all the rest of the defaults set and click **Next** and then **Create function** on the Review page to create your Lambda function. + +* You have just created a Lambda function that accepts the querystring params from the incoming API Gateway /twilio endpoint, converts the parameters to the correct format for our Chat Service including a conversion to JSON format, and finally makes an HTTPS POST request to the /zombie/message Chat Service endpoint. That endpoint will take care of inserting the data into the DynamoDB messages table. -16\. Now that you have created the TwilioProcessing function, you need to connect it to the **POST** method for your /twilio endpoint. Navigate back to the API Gateway console and select **POST** under the **/twilio** endpoint. +16\. Now that you have created the TwilioProcessing function, you need to connect it to the **POST** method for your /zombie/twilio endpoint. Navigate back to the API Gateway console and select **POST** under the **/twilio** endpoint. 17\. On the **Method Execution** screen for the "POST" method, the "Integration Request" box should show a type of **MOCK** for your /twilio resource. -18\. You will now change the **Integration Request** so that instead of integrating with a Mock integration, it will integrate with your TwilioProcessing function. Click **Integration Request**. On the Integration Request screen, change the "Integration type" radio button to **Lambda Function**. In the "Lambda Region" dropdown, select **us-west-2** which is the Region where your Lambda function resides. For the **Lambda Function**, begin typing "TwilioProcessing" and the autofill should display your function. Select **TwilioProcessing** from the autofill. Click **Save**. +18\. You will now change the **Integration Request** so that instead of integrating with a Mock integration, it will integrate with your TwilioProcessing function. Click **Integration Request**. On the Integration Request screen, change the "Integration type" radio button to **Lambda Function**. In the "Lambda Region" dropdown, select the region in which you created your TwilioProcessing Lambda function, and where you launched your CloudFormation Stack. For the **Lambda Function**, begin typing "TwilioProcessing" and the autofill should display your function. Select your **TwilioProcessing** function from the autofill. Click **Save**. In the popup window, confirm that you want to switch to Lambda Integration by clicking **OK**. Then confirm that you want to give API Gateway permission to invoke your function by clicking **OK**. Wait a few seconds for the changes to save. 19\. After clicking **Save**, you will be brought back to the Method Execution page for your "POST" method. Return back to the **Integration Request** screen so that you can configure a Mapping Template. To do this, click **Integration Request** in the Method Execution screen. -20\. Twilio sends data from their API with a content-type of "application/x-www-form-urlencoded", but Lambda requires the content-type to be "application/json". You will configure a Mapping Template so that API Gateway converts the content type of incoming messages into JSON before executing your backend Lambda TwilioProcessing function. +20\. Twilio sends data from their API with a content-type of "application/x-www-form-urlencoded", but Lambda requires the content-type to be "application/json" for any payload parameters sent to it. You will configure a Mapping Template so that API Gateway converts the content type of incoming messages into JSON before executing your backend Lambda TwilioProcessing function with the parameters. 21\. On the Integration Request screen for your /twilio POST method, expand the **Body Mapping Templates** section and click **Add mapping template**. In the textbox for "Content-Type", input **application/x-www-form-urlencoded** and click the little checkmark button to continue. Once you have clicked the little checkbox, a new section will appear on the right side of the screen with a dropdown for **Generate Template**. Click that dropdown and select **Method Request Passthrough**. -22\. A "Template" text editor window will appear. In this section you will input a piece of VTL transformation logic to convert the incoming Twilio data to a JSON object. In this text editor, **delete all of the pre-filled content** and copy the following code into the editor. +22\. A "Template" text editor window will appear. In this section you will input a piece of VTL transformation logic to convert the incoming Twilio data to a JSON object. In this text editor, **delete all of the pre-filled content** and copy the following code into the editor. -```{"postBody" : "$input.path('$')"}``` +```{"postBody" : "$input.path('$')"}``` -After copying the code into the editor, click the "Save" button. You have now setup the POST method to convert the incoming data to JSON anytime a POST request is made to your /twilio endpoint with a Content-Type of "application/x-www-form-urlencoded". This should look like the screenshot below: -![Twilio Integration Request Mapping Template](/Images/Twilio-Step22.png) +After copying the code into the editor, click the **Save** button. You have now setup the POST method to convert the incoming data to JSON anytime a POST request is made to your /twilio endpoint with a Content-Type of "application/x-www-form-urlencoded". This should look like the screenshot below: +![Twilio Integration Request Mapping Template](/Images/Twilio-Step22.png) -23\. Now that you have configured the Integration Request to transform incoming messages into JSON, we need to configure the Integration Response to transform outgoing responses into XML since the Twilio API requires XML as a response Content-Type. This step is required so that when you send SMS messages to the Chat Service, it can respond back to your Twilio Phone Number with a confirmation message that you successfully sent SMS to the Survivor Chat Service. +23\. Now that you have configured the Integration Request to transform incoming messages into JSON, we need to configure the Integration Response to transform outgoing responses back to Twilio into XML format since the Twilio API requires XML as a response Content-Type. This step is required so that when you send SMS messages to the survivor Chat Service, it can respond back to your Twilio Phone Number with a confirmation message that your message was received successfully. -24\. Head back to the Method Execution screen for the twilio POST method. On the "Method Execution" screen for your /twilio POST method, click **Integration Response**. On the "Integration Response" screen, click the black arrow. Expand the **Body Mapping Templates** section. You should see a Content-Type of "application/json". We need a Content-Type of XML, not JSON, so **delete this Content-Type by clicking the little black minus icon** and click **Delete** on the pop-up window. +24\. Head back to the Method Execution screen for the twilio POST method. On the "Method Execution" screen for your /twilio POST method, click **Integration Response**. On the "Integration Response" screen, click the black arrow to expand the method response section. Expand the **Body Mapping Templates** section. You should see a Content-Type of "application/json". We need a Content-Type of XML, not JSON, so **delete this Content-Type by clicking the little black minus icon** and click **Delete** on the pop-up window. -25\. Click **Add mapping template** similar to the way you did this in the earlier steps for the Integration Request section. +25\. Click **Add mapping template** similar to the way you did this in the earlier steps for the Integration Request section. 26\. In the "Content-Type" text box, insert **application/xml** and click the little black checkmark to continue. Similar to the steps done earlier, we are going to copy VTL mapping logic to convert the response data to XML from JSON. This will result in your /twilio POST method responding to requests with XML format. After you have created the new content-type, a new section will appear on the right side of the screen with a dropdown for **Generate Template**. Click that dropdown and select **Method Request Passthrough**. -In the text editor, delete all the code already in there and copy the following into the editor: +In the text editor, delete all the code already in there and copy the following into the editor: -``` #set($inputRoot = $input.path('$'))$inputRoot ``` +``` #set($inputRoot = $input.path('$'))$inputRoot ``` Click the grey "Save" button to continue. The result should look like the screenshot below: -![Twilio Integration Response Mapping Template](/Images/Twilio-Step26.png) +![Twilio Integration Response Mapping Template](/Images/Twilio-Step26.png) 27\. Then scroll up and click the blue **Save** button on the screen. Finally click the **Actions** button on the left side of the API Gateway console and choose **Deploy API** to deploy your API. In the Deploy API window, select **ZombieWorkshopStage** from the dropdown and click **Deploy**. -28\. You are now ready to test out Twilio integration with your API. Send a text message to your Twilio phone number, you should receive a confirmation response text message and the message you sent should display in the web app chat room as coming from your Twilio Phone Number. You have successfully integrated Twilio text message functionality with API Gateway. +28\. You are now ready to test out Twilio integration with your API. Send a text message to your Twilio phone number. + +**LAB 2 COMPLETE** + +If the integration was successful, you should receive a confirmation response text message and your text message you sent should display in the web app chat room as coming from your Twilio Phone Number. You have successfully integrated Twilio text message functionality with API Gateway. * * * ## Lab 3 - Search over the chat messages with Elasticsearch Service -In this section you'll configure an Elasticsearch Service domain to index chat messages in real-time from DynamoDB. +**What you'll do in this lab...** + +In this lab you'll launch an Elasticsearch Service cluster and setup DynamoDB Streams to automatically index chat messages in Elasticsearch for future ad hoc analysis of messages. **Elasticsearch Service Architecture** ![Overview of Elasticsearch Service Integration](/Images/ElasticsearchServiceOverview.png) -1\. Select the Amazon Elasticsearch icon from the main console page. +1\. Select the Amazon Elasticsearch icon from the main console page. -2\. Create a new Amazon Elasticsearch domain. Provide it a name such as "zombiemessages". Click **Next**. +2\. Create a new Amazon Elasticsearch domain. Provide it a name such as "[Your CloudFormation stack name]-zombiemessages". Click **Next**. -3\. On the **Configure Cluster** page, leave the default cluster settings and click **Next**. +3\. On the **Configure Cluster** page, leave the default cluster settings and click **Next**. -4\. For the access policy, select the **Allow or deny access to one or more AWS accounts or IAM users** option in the dropdown and fill in your account ID. Make sure **Allow** is selected for the "Effect" dropdown option. Click **OK**. +4\. For the access policy, select the **Allow or deny access to one or more AWS accounts or IAM users** option in the dropdown and fill in your account ID. Your AWS Account ID is actually provided to you in the examples section so just copy and paste it into the text box. Make sure **Allow** is selected for the "Effect" dropdown option. Click **OK**. -5\. Select **Next** to go to the domain review page. +5\. Select **Next** to go to the domain review page. 6\. On the Review page, select **Confirm and create** to create your Elasticsearch cluster. -7\. The creation of the Elasticsearch cluster takes approximately 10 minutes. +7\. The creation of the Elasticsearch cluster takes approximately 10 minutes. + +* Since it takes roughly 10 minutes to launch an Elasticsearch cluster, you can either wait for this launch before proceeding, or you can move on to Lab 4 and come back to finish this lab when the cluster is ready. -8\. Take note of the Endpoint once the cluster starts, we'll need that for the Lambda function. -![API Gateway Invoke URL](/Images/Search-Step8.png) +8\. Take note of the Endpoint once the cluster starts, we'll need that for the Lambda function. +![API Gateway Invoke URL](/Images/Search-Step8.png) -9\. Go into the Lambda service page by clicking on Lambda in the Management Console. +9\. Go into the Lambda service page by clicking on Lambda in the Management Console. -10\. Select **Create a Lambda Function**. +10\. Select **Create a Lambda Function**. -11\. Skip the Blueprint section by selecting the Skip button in the bottom right. +11\. Skip the Blueprint section by selecting the Skip button in the bottom right. -12\. Fill in "ZombieWorkshopSearchIndexing" as the Name of the function. Keep the runtime as Node.js. You can set a description for the function if you'd like. +12\. Give your function a name, such as **"[Your CloudFormation stack name]-ESsearch"**. Keep the runtime as Node.js 4.3. You can set a description for the function if you'd like. -13\. Paste in the code from the ZombieWorkshopSearchIndexing.js file provided to you. +13\. Paste in the code from the ZombieWorkshopSearchIndexing.js file provided to you. This is found in the Github repo in the "ElasticsearchLambda" folder. -14\. On line 7 in the code provided, replace ENDPOINT_HERE with the Elasticsearch endpoint created in step 8\. Make sure it starts with https:// +14\. On [line 6](/ElasticsearchLambda/ZombieWorkshopSearchIndexing.js#L6) in the code provided, replace **region** with the region code you are working in (the region you launched your stack, created your Lambda function etc). -15\. Under the Role, create a new DynamoDB event stream role. When a new page opens confirming that you want to create a role, just click **Allow** to proceed. +Then on line 7, replace the **endpoint** variable that has a value of **ENDPOINT_HERE** with the Elasticsearch endpoint created in step 8\. **Make sure the endpoint you paste starts with https://**. -16\. In the "Timeout" field, change the function timeout to **1** minute. This ensure Lambda can process the batch of messages before Lambda times out. Keep all the other defaults on the page set as is. Select **Next** and then on the Review page, select **Create function** to create your Lambda function. +* This step requires that your cluster is finished creating and in "Active" state before you'll have access to see the endpoint of your cluster. -17\. Select the "Event Sources" tab for the new ZombieWorkshopSearchIndexing function. +15\. Now you'll add an IAM role to your Lambda function. For the Role (you may need to navigate to the Configuration tab in Lambda to see this), create a new DynamoDB event stream role by clicking **DynamoDB event stream role** in the role dropdown. This will open a new page confirming that you want to create a role, just click **Allow** to proceed. -18\. Select **Add event source** +16\. In the "Timeout" field for your Lambda function (you may need to visit the Configuration tab and Advanced Settings to see this), change the function timeout to **1** minute. This ensures Lambda can process the batch of messages before Lambda times out. Keep all the other defaults on the page set as is. Select **Next** and then on the Review page, select **Create function** to create your Lambda function. -19\. Select the DynamoDB Event source type and the **messages** DynamoDB table. You can leave the rest as the defaults. +17\. Select the "Event Sources" tab for the new **"[Your CloudFormation stack name]-ESsearch"** function that you created. + +18\. Select **Add event source**. + +19\. Select the DynamoDB Event source type and then select the **messages** DynamoDB table. It should appear as **"[Your CloudFormation stack name]-messages"** You can leave the rest as the defaults. 20\. After creation, you should see an event source that is similar to the screenshot below: -![API Gateway Invoke URL](/Images/Search-Step20.png) +![API Gateway Invoke URL](/Images/Search-Step20.png) 21\. In the above step, we configured [DynamoDB Streams](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.html) to capture incoming messages on the table and trigger a Lambda function to push them to our Elasticsearch cluster. 22\. The "lambda_dynamo_streams" role that you selected for your Lambda function earlier does not currently have permissions to write to your Elasticsearch cluster. We will configure that now. -23\. Navigate to **Identity and Access Management (IAM)** in the AWS Management Console. The icon for this service is green and is listed under the "Security & Identity" section. +23\. Navigate to [Identity and Access Management](https://console.aws.amazon.com/iam/) in the AWS Management Console. The icon for this service is green and is listed under the "Security & Identity" section. + +24\. In the Identity and Access Management console, select the link for **Roles**. -24\. In the Identity and Access Management console, select the link for **Roles**. [IAM Roles](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html), similar to users, have permissions that you associate with them, which allows you to define what access can be granted to various entities. Roles can be assumed by EC2 Instances, Lambda Functions, and other applications and services. +* [IAM Roles](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html), similar to users, have permissions that you associate with them, which allows you to define what access can be granted to various entities. Roles can be assumed by EC2 Instances, Lambda Functions, and other applications and services. -25\. In the "Filter" textbox on the Roles screen, type in **lambda_dynamo_streams** and click on the role. This is the role you assigned to your ZombieWorkshopSearchIndexing function earlier. +25\. In the "Filter" textbox on the Roles screen, type in **lambda_dynamo_streams** and click on the role. This is the role you assigned to your Lambda function earlier. 26\. Scroll to the "Inline Policies" section where you will find a policy similar to "oneClick_lambda_dynamo_streams_xxxxxxxxxx". Click **Edit Policy** to edit the policy. Delete all the contents of the Inline Policy, and replace it with the policy block below: @@ -285,16 +344,29 @@ In this section you'll configure an Elasticsearch Service domain to index chat m ] } ``` -27\. This new policy you have copied over includes a new Allow action, ```es:*``` which allows the role all actions on the Amazon Elasticsearch Service. Click the **Validate Policy** button and ensure that AWS returns a successful message "The Policy is valid". Then select **Apply Policy** to save it. + +27\. Click the **Validate Policy** button and ensure that AWS returns a successful message "The Policy is valid". Then select **Apply Policy** to save it. + +* This new policy you have copied over includes a new Allow action, ```es:*``` which allows the role all actions on the Amazon Elasticsearch Service. In production it is recommended that you specify the actual ARN of the cluster you created instead of just any ES cluster, so that Lambda can only interact with that specific ES domain. 28\. Now with the IAM permissions in place, your messages posted in the chat from this point forward will be indexed to Elasticsearch. Post a few messages in the chat. You should be able to see that messages are being indexed in the "Indices" section for your cluster in the Elasticsearch Service console. ![API Gateway Invoke URL](/Images/Search-Done.png) +**LAB 3 COMPLETE** + +If you would like to explore and search over the messages in the Kibana web UI that is provided with your cluster, you will need to navigate to the Elasticsearch domain you created and change the permissions. Currently you've configured the permissions so that only your AWS account has access. This allows your Lambda function to index messages into the cluster. + +To use the web UI to build charts and search over the index, you will need to implement an IP based policy to whitelist your computer/laptop/network or for simplicity, choose to allow everyone access. For instructions on how to modify the access policy of an ES cluster, visit [this documentation](http://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-gsg-configure-access.html). If you choose to open access to anyone be aware that anyone can see your messages, so please be sure to restrict access back to your AWS account when you're done exploring Kibana, or simply delete your ES cluster. + * * * ## Lab 4 - Slack Integration -In this section, you'll create a slack group and wire it up to the Chat Service. Survivors comminicating on Slack can send messages to survivors in the Zombie Chat App. +**What you'll do in this lab...** + +In this lab, you'll integrate a Slack channel with your survivor chat. There may be survivors who use different chat systems and you'll want to communicate with them! After completing this lab, survivors communicating on Slack can send messages to survivors in the Zombie Chat App by configuring a slash command prefix to be used on any messages in their Slack channel that they want to send to the survivors. When Slack users type messages with this Slash command, it will pass the message to your survivor chat API, similiar to the webhook functionality enabled in the Twilio lab! + +If you aren't familiar with Slack, they offer a free chat communications service that is popular, especially among the developer community. Slack uses a concept called "Channels" to distinguish different chat rooms. Visit their website to learn more! **Slack Integration Architecture** ![Overview of Slack Integration](/Images/SlackOverview.png) @@ -303,11 +375,11 @@ In this section, you'll create a slack group and wire it up to the Chat Service. 2\. Once logged into Slack, navigate to [https://slack.com/apps](https://slack.com/apps) and click **Build your own** near the top of the page. Then on the next screen, select **Make a Custom Integration**. -3\. On the "Custom Integration" page, select **Slash Commands** to create a Slack Command. Slash commands allow you to define a command that will inform Slack to forward your message to an external source with a webhook. In this case you'll configure your Slash Command to make a POST request to an external URL (the URL for your API Gateway endpoint). +3\. On the "Custom Integration" page, select **Slash Commands** to create a Slash Command. Slash commands allow you to define a command that will inform Slack to forward your message to an external source with a webhook. In this case you'll configure your Slash Command to make a POST request to an external URL (the URL for your API Gateway endpoint). 4\. On the Slash Commands page, define a command in the **Commands** text box. Insert **/survivors** as your Slash Command. Then select "Add Slash Command Integration" to save it. -5\. On the Integration Settings page, make sure the **Method** section has "POST" selected from the dropdown options. Then scroll to the **Token** section and copy the Token (or generate a new one) to a text file as you'll need it in the following steps. +5\. On the Integration Settings page, make sure the **Method** section has "POST" selected from the dropdown options. Then scroll to the **Token** section and copy the Token (or generate a new one) to a text file as you'll need it in the following steps. 6\. Keep the Slack browser tab open and in another tab navigate to the Lambda service in the AWS Management Console. @@ -315,15 +387,19 @@ In this section, you'll create a slack group and wire it up to the Chat Service. 8\. Skip past the blueprints page as we will not be using one. -9\. Name the function **SlackService**. Now navigate to the GitHub repo for this workshop, or the location where you downloaded the GitHub files to your local machine. +9\. Give your function a name such as **"[Your CloudFormation Stack name]-SlackService"**. For the Nodejs version, you can keep the default Nodejs 4.3 selected. Now navigate to the GitHub repo for this workshop, or the location where you downloaded the GitHub files to your local machine. 10\. Open the **SlackService.js** file from the GitHub repo, found in the slack folder. Copy the entire contents of this js file into the Lambda inline edit window. -11\. Input the Slack Token string that you copied earlier into the function. You should copy the Token string from Slack into the "token" variable in the Lambda function, replacing the string **INSERT YOUR TOKEN FROM SLACK HERE** with your own token. +11\. Input the Slack Token string that you copied earlier into the function. You should copy the Token string from Slack into the "token" variable on [line 4](/Slack/SlackService.js#L4) in the Lambda function, replacing the string **INSERT YOUR TOKEN FROM SLACK HERE** with your own token. + +* Slack provides a unique token associated with your integration. You are copying this token into your Lambda function as a form of validation. When incoming requests from Slack are sent to your API endpoint, and your Lambda function is invoked with the Slack payload, your Lambda function will check to verify that the incoming Token in the request matches the Token you provided in the code. If the token does not match, Lambda returns an error and doesn't process the request. + +12\. In the "post_options" host variable, you will insert the fully qualified domain name (FQDN) to your Chat Service (/zombie/message) API Gateway resource so that the HTTPS requests can be sent with the messages from Slack. It should show a value of "INSERT YOUR API GATEWAY FQDN HERE EXCLUDING THE HTTPS://" for the **host** variable on [line 42](/Slack/SlackService.js#L42). Replace this string with the FQDN of your **/message POST method**. Your final FQDN inserted into the code should look something like "xxxxxxxx.execute-api.us-west-2.amazonaws.com". -12\. In the "post_options" host variable, you will insert the URL to your Chat Service (/zombie/message) API Gateway resource so that the HTTPS requests can be sent with the messages from Slack. It should show a value of "INSERT YOUR API GATEWAY URL HERE EXCLUDING THE HTTPS://" for the **host** variable. Replace this string with the URL of your **/message POST method**. Please be sure to remove the "https://" portion of the URL and the end of the URL "/ZombieWorkshopStage/zombie/message". Your final URL inputted into the code should look something like "xxxxxxxx.execute-api.us-west-2.amazonaws.com". Your code is now configured to check if the token sent with the request matches the token for your Slack integration. If so, it parses the data and makes an HTTPS request to your **/message** endpoint with the message from Slack. +* Your code is now configured to check if the token sent with the request matches the token for your Slack integration. If so, it parses the data and makes an HTTPS request to your **/message** endpoint with the message from Slack. -13\. Scroll down to the Role dropdown and select **Basic execution role**. On the verification window that appears to confirm the role, click **Allow**. This Lambda function will not interact with any AWS services. It does emit event data to CloudWatch Logs but that permission is provided by default with the basic execution role. +13\. Scroll down to the Role dropdown and select **Basic execution role**, or select the existing "lambda_basic_execution" role if it exists. If creating a new Basic Exeuction Role, make sure to click **Allow** on the verification window that appears to confirm the role. This Lambda function will not interact with any AWS services. It does emit event data to CloudWatch Logs but that permission is provided by default with the basic execution role. 14\. Click **Next**. @@ -331,43 +407,52 @@ In this section, you'll create a slack group and wire it up to the Chat Service. 16\. Click **Create function**. Your Lambda function will be created. -17\. When the function is created, navigate to the API Gateway service in the AWS Management Console. Click into your "Zombie Workshop API Gateway" API. On the left Resources pane, click/highlight the "/zombie" resource so that it is selected. Then select the **Actions** button and choose "Create Resource". For Resource Name, insert **slack** and for Resource Path, insert **slack**. The final resource for your Slack API should be as shown below. +17\. When the function is created, navigate to the API Gateway service in the AWS Management Console. Click into your "Zombie Workshop API Gateway" API. On the left Resources pane, click/highlight the "/zombie" resource so that it is selected. Then select the **Actions** button and choose "Create Resource". For Resource Name, insert **slack** and for Resource Path, insert **slack**. Click "Create Resource" to create your slack API resource. The final resource for your Slack API should be as shown below. ![Create Slack API Resource](/Images/Slack-Step17.png) -18\. Click "Create Resource" to create your slack API resource. For your newly created "/slack" resource, highlight it, then click **Actions** and select **Create Method** to create the POST method for the slack resource. In the dropdown, select POST. Click the checkmark to create the POST method. On the Setup page, choose an Integration Type of **Lambda Function**, select "us-west-2" for the region dropdown, and type "SlackService" for the name of the Lambda Function. It should autofill your function name. Click **Save** and then **OK** to confirm. +* In this step, you are creating a new API resource that the Slack slash command webhook can forward requests to. In the next steps, you'll create a POST method associated with this resource that triggers your Lambda function. When you type messages in Slack with the correct slash command, Slack will send requests to this resource, which will invoke your SlackService Lambda function to pre-process the payload and make a call to your /zombie/message endpoint to insert the data into DynamoDB. + +18\. For your newly created "/slack" resource, highlight it, then click **Actions** and select **Create Method** to create the **POST** method for the /zombie/slack resource. In the dropdown, select **POST**. Click the checkmark to create the POST method. On the Setup page, choose an Integration Type of **Lambda Function**, and select the region that you are working in for the region dropdown. For the Lambda Function field, type "SlackService" for the name of the Lambda Function. It should autofill your function name. Click **Save** and then **OK** to confirm. 19\. Click **Integration Request** for the /slack POST method. We'll create a Mapping Template to convert the incoming query string parameters from Slack into JSON which is the format Lambda requires for parameters. This mapping template is required so that the incoming Slack message can be converted to the right format. -20\. Expand the **Body Mapping Templates** arrow and click **Add mapping template**. In the Content-Type box, enter **application/x-www-form-urlencoded** and click the little checkmark to continue. As you did in the Twilio lab, we're going to copy VTL mapping logic to convert the request to JSON. A new section will appear on the right side of the screen with a dropdown for **Generate Template**. Click that dropdown and select **Method Request Passthrough**. +20\. Expand the **Body Mapping Templates** arrow and click **Add mapping template**. In the Content-Type box, enter **application/x-www-form-urlencoded** and click the little checkmark to continue. If a popup appears asking if you would like to secure the integration, click **Yes, secure this integration**. This ensures that only requests with the defined content-types will be allowed. + +As you did in the Twilio lab, we're going to copy VTL mapping logic to convert the request to JSON. A new section will appear on the right side of the screen with a dropdown for **Generate Template**. Click that dropdown and select **Method Request Passthrough**. -In the text editor, delete all of the exiting VTL code and copy the following into the editor: +In the text editor, delete all of the exiting VTL code and copy the following into the editor: ``` {"body": $input.json("$")} ``` Click the grey **Save** button to continue. The result should look like the screenshot below: -![Slack Integration Response Mapping Template](/Images/Slack-Step20.png) +![Slack Integration Response Mapping Template](/Images/Slack-Step20.png) 21\. Click the **Actions** button on the left side of the API Gateway console and select **Deploy API** to deploy your API. In the Deploy API window, select **ZombieWorkshopStage** from the dropdown and click **Deploy**. 22\. On the left pane navigation tree, expand the ZombieWorkshopStage tree. Click the **POST** method for the **/zombie/slack** resource. You should see an Invoke URL appear for that resource as shown below. -![Slack Resource Invoke URL](/Images/Slack-Step22.png) +![Slack Resource Invoke URL](/Images/Slack-Step22.png) 23\. Copy the entire Invoke URL. Navigate back to the Slack.com website to the Slash Command setup page and insert the Slack API Gateway Invoke URL you just copied into the "URL" textbox. Make sure to copy the entire url including "HTTPS://". Scroll to the bottom of the Slash Command screen and click **Save Integration**. -24\. You're ready to test out the Slash Command integration. In the team chat for your Slack account, type the Slash Command "/survivors" followed by a message. For example, type "/survivors Please help me I am stuck and zombies are trying to get me!". After sending it, you should get a confirmation response message from Slack Bot like the one below: -![Slack Command Success](/Images/Slack-Step24.png) +24\. You're ready to test out the Slash Command integration. In the team chat channel for your Slack account, type the Slash Command "/survivors" followed by a message. For example, type "/survivors Please help me I am stuck and zombies are trying to get me!". After sending it, you should get a confirmation response message from Slack Bot like the one below: +![Slack Command Success](/Images/Slack-Step24.png) + +**LAB 4 COMPLETE** Navigate to your zombie survivor chat app and you should see the message from Slack appear. You have configured Slack to send messages to your chat app! ![Slack Command in Chat App](/Images/Slack-Step25.png) -**Bonus:** You've configured Slack to forward messages to your zombie survivor chat app. But can you get messages sent in the chat app to appear in your Slack chat (i.e.: the reverse)? Give it a try or come back and attempt it later when you've finished the rest of the labs! HINT: You'll want to configure Slack's "Incoming Webhooks" integration feature along with a Lambda code configuration change to make POST requests to the Slack Webhook whenever users send messages in the chat app! +**Bonus Step:** + +You've configured Slack to forward messages to your zombie survivor chat app. But can you get messages sent in the chat app to appear in your Slack chat (i.e.: the reverse)? Give it a try or come back and attempt it later when you've finished the rest of the labs! HINT: You'll want to configure Slack's "Incoming Webhooks" integration feature along with a Lambda code configuration change to make POST requests to the Slack Webhook whenever users send messages in the chat app! + * * * ## Lab 5 - Motion Sensor Integration with Intel Edison and Grove -In this section, you'll consume motion sensor events from an IoT device and push them into your chat application. +In this section, you'll help protect suvivors from zombies. Zombie motion sensor devices allow communities to determine if zombies (or intruders) are nearby. You'll setup a Lambda function to consume motion sensor events from an IoT device and push the messages into your chat application. **IoT Integration Architecture** ![Zombie Sensor IoT Integration](/Images/EdisonOverview.png) @@ -378,9 +463,9 @@ If you wish to utilize the Zombie Sensor as a part of the workshop, this guide w * How to create the AWS backend (Simple Notification Service Topic) for the Zombie Sensor * How to install the Node.js device code provided in this workshop onto the device -**Please note that this section requires an IoT device that can emit messages to SNS. If you are setting this up on your own device outside of the workshop, please proceed through the sections below to do that, otherwise skip the device setup instructions as the device has been setup by AWS for you by the workshop instructor.** +**Please note that this section requires an IoT device that can emit messages to SNS. If you are setting this up on your own device outside of the workshop, please proceed through the sections below to do that, otherwise skip the device setup instructions as the device has been setup by AWS for you by the workshop instructor.** -**Items Required** +**Items Required** 1\. One Intel® Edison and Grove IoT Starter Kit Powered by AWS. This can be purchased [here](http://www.amazon.com/gp/product/B0168KU5FK?*Version*=1&*entries*=0). 2\. Within this starter kit you will be using the following components for this exercise: @@ -389,81 +474,81 @@ If you wish to utilize the Zombie Sensor as a part of the workshop, this guide w * Base Shield * USB Cable; 480mm-Black x1 * USB Wall Power Supply x1 -* Grove - PIR Motion Sensor: The application code is a very simple app that publishes a message to an Amazon Simple Notification Service (SNS) topic when motion is detected on the Grove PIR Motion Sensor. For the purpose of a workshop, this should be done only once in a central account by the workshop organizer - the SNS topic will be made public so that participants can subscribe to this topic and make use of it during the workshop. +* Grove - PIR Motion Sensor: The application code is a very simple app that publishes a message to an Amazon Simple Notification Service (SNS) topic when motion is detected on the Grove PIR Motion Sensor. For the purpose of a workshop, this should be done only once in a central account by the workshop organizer - the SNS topic will be made public so that participants can subscribe to this topic and make use of it during the workshop. An example output message from the Intel Edison: -``` {"message":"A Zombie has been detected in London!", "value":"1", "city":"London", "longtitude":"-0.127758", "lattitude":"51.507351"} ``` +``` {"message":"A Zombie has been detected in London!", "value":"1", "city":"London", "longtitude":"-0.127758", "lattitude":"51.507351"} ``` -A simple workflow of this architecture is: +A simple workflow of this architecture is: -Intel Edison -> SNS topic -> Your AWS Lambda functions subscribed to the topic. +Intel Edison -> SNS topic -> Your AWS Lambda functions subscribed to the topic. -####Creating the AWS Backend +####Creating the AWS Backend -**If you are following this guide during a workshop presented by AWS, please ignore the steps below, 1-3\. An SNS topic should already be configured for the workshop participants to consume messages from. That SNS topic ARN will be provided to you.** +**If you are following this guide during a workshop presented by AWS, please ignore the steps below, 1-3\. An SNS topic should already be configured for the workshop participants to consume messages from. That SNS topic ARN will be provided to you.** -1\. Create the SNS Topic. Navigate to the SNS product page within the AWS Management Console and click **Topics** in the left hand menu. Then click on 'Create New Topic'. You will be presented with the following window. Fill in the fields with your desired values and click create topic. -![Create Topic Screenshot](/Images/MotionSensor-createTopic.png) +1\. Create the SNS Topic. Navigate to the SNS product page within the AWS Management Console and click **Topics** in the left hand menu. Then click on 'Create New Topic'. You will be presented with the following window. Fill in the fields with your desired values and click create topic. +![Create Topic Screenshot](/Images/MotionSensor-createTopic.png) -2\. You will now need to edit the topic policy to permit any AWS account to subscribe lambda functions to your SNS topic. Select the check box next to your new topic, and then click **Actions -> Edit topic policy**. You need to configure these settings presented as shown the below screenshot. Then click **Update Policy**. This part is what allows others (perhaps teammates working on this lab with you, to consume notifications from your SNS topic. -![Edit Topic Policy Screenshot](/Images/MotionSensor-createTopicPolicy.png) +2\. You will now need to edit the topic policy to permit any AWS account to subscribe lambda functions to your SNS topic. Select the check box next to your new topic, and then click **Actions -> Edit topic policy**. You need to configure these settings presented as shown the below screenshot. Then click **Update Policy**. This part is what allows others (perhaps teammates working on this lab with you, to consume notifications from your SNS topic. +![Edit Topic Policy Screenshot](/Images/MotionSensor-createTopicPolicy.png) -3\. You now have your central SNS topic configured and ready to use. Ensure that you make a note of the Topic ARN and region where you have created the topic, you will need it in some of the following steps. +3\. You now have your central SNS topic configured and ready to use. Ensure that you make a note of the Topic ARN and region where you have created the topic, you will need it in some of the following steps. -####Installing the application on the Intel Edison -**If you are following this guide during a workshop presented by AWS, please ignore this section. An Intel Edison board should already be configured for the workshop particants to consume messages from.** +####Installing the application on the Intel Edison +**If you are following this guide during a workshop presented by AWS, please ignore this section. An Intel Edison board should already be configured for the workshop particants to consume messages from.** -1\. First, you will need to get your Edison board set up. You can find a getting started guide for this on the Intel site [here](https://software.intel.com/en-us/articles/assemble-intel-edison-on-the-arduino-board). Note that for the purpose of this tutorial, we will be writing our client code for the Edison in Node.js and will therefore be using the Intel® XDK for IoT (referred to as 'XDK' from here on, and which you will need to install) as our IDE. +1\. First, you will need to get your Edison board set up. You can find a getting started guide for this on the Intel site [here](https://software.intel.com/en-us/articles/assemble-intel-edison-on-the-arduino-board). Note that for the purpose of this tutorial, we will be writing our client code for the Edison in Node.js and will therefore be using the Intel® XDK for IoT (referred to as 'XDK' from here on, and which you will need to install) as our IDE. -2\. You will need to physically connect the Grove PIR Motion Sensor to pin D6 on the breakout board. +2\. You will need to physically connect the Grove PIR Motion Sensor to pin D6 on the breakout board. -3\. Download all of the code from the 'zombieIntelEdisonCode' folder in the GitHub repository and store it in a folder locally on your machine. This simply consists of a main.js file (our application) and our package.json (our app dependencies). +3\. Download all of the code from the 'zombieIntelEdisonCode' folder in the GitHub repository and store it in a folder locally on your machine. This simply consists of a main.js file (our application) and our package.json (our app dependencies). -4\. Navigate to the homepage in the XDK and start a new project. +4\. Navigate to the homepage in the XDK and start a new project. -5\. Choose to import an existing Node.js project and select the folder where you stored the code from this repository in the previous step. +5\. Choose to import an existing Node.js project and select the folder where you stored the code from this repository in the previous step. -6\. Give your project a name. We called ours **zombieSensor**. +6\. Give your project a name. We called ours **zombieSensor**. -7\. You now need to edit the code in main.js to include your AWS credentials and the SNS topic that you have created. Firstly, we'll need some AWS credentials. +7\. You now need to edit the code in main.js to include your AWS credentials and the SNS topic that you have created. Firstly, we'll need some AWS credentials. -8\. You will need to create an IAM User with Access and Secret Access Keys for your Edison to publish messages to your SNS topic. There is a guide on how to create IAM Users [here](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html). Your IAM policy for the user should look like the following: +8\. You will need to create an IAM User with Access and Secret Access Keys for your Edison to publish messages to your SNS topic. There is a guide on how to create IAM Users [here](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html). Your IAM policy for the user should look like the following: -``` +``` { - "Version": "2012-10-17", - "Statement": [{ - "Action": [ "sns:Publish" ], - "Effect": "Allow", - "Resource": "ENTER YOUR SNS TOPIC ARN HERE" + "Version": "2012-10-17", + "Statement": [{ + "Action": [ "sns:Publish" ], + "Effect": "Allow", + "Resource": "ENTER YOUR SNS TOPIC ARN HERE" }] -} -``` +} +``` -9\. Now let's add your credentials to the client side code. Edit the following line in main.js to include your user access keys and the region where you have set up your SNS topic. +9\. Now let's add your credentials to the client side code. Edit the following line in main.js to include your user access keys and the region where you have set up your SNS topic. -``` AWS.config.update({accessKeyId: 'ENTER ACCESSKEY HERE', secretAccessKey: 'ENTER SECRET ACCESS KEY HERE', region: 'ENTER REGION HERE'}); ``` +``` AWS.config.update({accessKeyId: 'ENTER ACCESSKEY HERE', secretAccessKey: 'ENTER SECRET ACCESS KEY HERE', region: 'ENTER REGION HERE'}); ``` -10\. Edit the following line in main.js to reflect the region in which you created the SNS topic. +10\. Edit the following line in main.js to reflect the region in which you created the SNS topic. -``` var sns = new AWS.SNS({region: 'ENTER REGION HERE'}); ``` +``` var sns = new AWS.SNS({region: 'ENTER REGION HERE'}); ``` -11\. Edit the following line in main.js to reflect the Amazon Resource Name (ARN) of the SNS topic that you created earlier. +11\. Edit the following line in main.js to reflect the Amazon Resource Name (ARN) of the SNS topic that you created earlier. -``` TopicArn: "ENTER YOUR SNS TOPIC ARN HERE" ``` +``` TopicArn: "ENTER YOUR SNS TOPIC ARN HERE" ``` -12\. You now need to connect the XDK to your Intel Edison device. There is a guide on the Intel site on how to do this [here](https://software.intel.com/en-us/getting-started-with-the-intel-xdk-iot-edition) under the 'Connect to your Intel® IoT Platform' section. +12\. You now need to connect the XDK to your Intel Edison device. There is a guide on the Intel site on how to do this [here](https://software.intel.com/en-us/getting-started-with-the-intel-xdk-iot-edition) under the 'Connect to your Intel® IoT Platform' section. -13\. You now need to build the app and push it to your device. First, hit the build/install icon, this looks like a hammer in the XDK. It may take a couple of minutes to install the required packages etc. +13\. You now need to build the app and push it to your device. First, hit the build/install icon, this looks like a hammer in the XDK. It may take a couple of minutes to install the required packages etc. -14\. Once the app has been built succesfully, you can run the app by pressing the run icon, this looks like a circuit board with a green 'play' sign. +14\. Once the app has been built succesfully, you can run the app by pressing the run icon, this looks like a circuit board with a green 'play' sign. -15\. Your app should now be running on the Edison device and your messages being published to the SNS topic. You can consume these messages using AWS Lambda. There is some documentation to get you started [here](http://docs.aws.amazon.com/sns/latest/dg/sns-lambda.html). Continue below to learn how to integrate the SNS notifications into the chat application. +15\. Your app should now be running on the Edison device and your messages being published to the SNS topic. You can consume these messages using AWS Lambda. There is some documentation to get you started [here](http://docs.aws.amazon.com/sns/latest/dg/sns-lambda.html). Continue below to learn how to integrate the SNS notifications into the chat application. -####Consuming the SNS Topic Messages with AWS Lambda +####Consuming the SNS Topic Messages with AWS Lambda -Using the things learned in this workshop, can you develop a Lambda function that alerts survivors in the chat application when zombies are detected from the zombie sensor? In this section you will configure a Lambda function that triggers when messages are sent from the Edison device to the zombie sensor SNS topic. This function will push the messages to the chat application to notify survivors of zombies! +Using the things learned in this workshop, can you develop a Lambda function that alerts survivors in the chat application when zombies are detected from the zombie sensor? In this section you will configure a Lambda function that triggers when messages are sent from the Edison device to the zombie sensor SNS topic. This function will push the messages to the chat application to notify survivors of zombies! 1\. Open up the Lambda console and create a new Lambda function. @@ -471,9 +556,9 @@ Using the things learned in this workshop, can you develop a Lambda function tha 3\. On the next page, leave the event source type as "SNS". For the SNS topic selection, either select the SNS topic you created earlier (if you're working on this outside of a workshop) or if you are working in an AWS workshop, insert the shared SNS topic ARN provided to you by the AWS organizer. Click **Next**. -4\. On the "Configure Function" screen, name your function "ZombieSensorData". +4\. On the "Configure Function" screen, name your function "[Your CloudFormation Stack Name]-sensor". -5\. For the **Role**, select the "basic execution role" option. On the pop-up page asking you to confirm the creation of that role, click **Allow** through that. This role simply allows you to push events to CloudWatch Logs from Lambda. +5\. For the **Role**, select the "basic execution role" option or choose an exisitng basic_execution_role if one exists. On the pop-up page asking you to confirm the creation of that role, click **Allow** through that. This role simply allows you to push events to CloudWatch Logs from Lambda. 6\. Leave all other options as default on the Lambda creation page and click **Next**. @@ -481,7 +566,7 @@ Using the things learned in this workshop, can you develop a Lambda function tha 8\. Once the function is created, on the overview page for your Lambda function, select the **Monitoring** tab and then on the right side select **View logs in CloudWatch**. -9\. You should now be on the CloudWatch Logs console page looking at the log streams for your Lambda function. +9\. You should now be on the CloudWatch Logs console page looking at the log streams for your Lambda function. 10\. As data is sent to the SNS topic, it will trigger your function to consume the messages. The blueprint you used simply logs the message data to CloudWatch Logs. Verify that events are showing up in your CloudWatch Logs stream with Zombie Sensor messages from the Intel Edison. On the **Monitoring** tab for the function (as you did in Step 8), click the link **View logs in CloudWatch**. When you have confirmed that messages from Intel are showing up, now you need to get those alerts into the Chat application for survivors to see! @@ -499,11 +584,11 @@ Using the things learned in this workshop, can you develop a Lambda function tha ## Workshop Cleanup -1\. To cleanup your environment, you can click "Delete Stack" to delete all the components that were launched as a part of the lab. However, the components that you manually launched in the above labs after the stack was created need to be deleted manually. +1\. To cleanup your environment, it is recommended to first delete these manual resources you created in the labs before deleting your CloudFormation stack, as there may be resource dependencies that stop the Stack from deleting. Follow steps 2-6 before deleting your Stack. 2\. Be sure to delete the TwilioProcessing Lambda Function. Also if you no longer plan to use Twilio, please delete your Twilio free trial account and/or phone numbers that you provisioned. -3\. Be sure to delete the Elasticsearch cluster and the associated Lambda function that you created for the Elasticsearch lab. +3\. Be sure to delete the Elasticsearch cluster and the associated Lambda function that you created for the Elasticsearch lab. 4\. Be sure to delete the Lambda function created as a part of the Slack lab and the Slack API resource you created. Also delete Slack if you no longer want an account. @@ -511,6 +596,6 @@ Using the things learned in this workshop, can you develop a Lambda function tha 6\. Navigate to CloudWatch Logs and make sure to delete unnecessary Log Groups if they exist. -7\. Once those resources have been deleted, go to the CloudFormation console and find the Stack that you launched in the beginning of the workshop, select it, and click **Delete Stack**. When the stack has been successfully deleted, it should no longer display in the list of Active stacks. If you run into any issues deleting stacks, please notify a workshop instructor or contact [AWS Support](https://console.aws.amazon.com/support/home) for additional assistance. +7\. Once those resources have been deleted, go to the CloudFormation console and find the Stack that you launched in the beginning of the workshop, select it, and click **Delete Stack**. When the stack has been successfully deleted, it should no longer display in the list of Active stacks. If you run into any issues deleting stacks, please notify a workshop instructor or contact [AWS Support](https://console.aws.amazon.com/support/home) for additional assistance. * * *