├── LICENSE ├── LIST_OF_REGIONS.txt ├── README.md ├── example-events ├── get-instance-count.json ├── get-region.json ├── launch-session.json └── set-region.json ├── index.js ├── intents.json └── sample_utterances.txt /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 1Strategy, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LIST_OF_REGIONS.txt: -------------------------------------------------------------------------------- 1 | Virginia 2 | Oregon 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # alexa-aws-administration / VoiceOps 2 | Do various administration tasks in your AWS account using your Amazon Echo. 3 | 4 | This project started as a demo at the [Utah AWS Meetup](http://www.meetup.com/utah-aws/) in February 2016. 5 | 6 | See the blog post on the 1Strategy Blog: [http://www.1strategy.com/blog/utah-aws-lambda-alexa/]( http://www.1strategy.com/blog/utah-aws-lambda-alexa/) 7 | 8 | See a video of [this project in action on YouTube](https://www.youtube.com/watch?v=YH1P8ckFoLU). 9 | 10 | ## Lambda Function 11 | The Lambda function is organized into "intent" handlers (with associated helper methods) with some boilerplate code for selecting the correct handler to run based on what the Alexa service determines the user is trying to do. 12 | 13 | The onIntent function maps the intent that Alexa believes the user is trying to invoke, and executes the corresponding function to handle that intent. Here's a brief summary of the intents that are currently supported: 14 |

Welcome Response (getWelcomeResponse)

15 | This function provides an introduction when a user launches the service on their Echo. It provides instructions on available commands and suggests an initial command to use, in this case "Select a region to use by saying, set the region to Virginia." 16 |

Set Region (setRegion)

17 | When this intent is triggered, the user will have specified a desired region to work in and that region will be set in session (to be used in later requests). 18 |

Get Region (getRegionFromSession)

19 | Will echo back which region is currently in session. 20 |

Get Instance Count (getInstanceCount)

21 | Returns the number of running instances in the region that is currently in session. 22 |

Terminate Untagged Instances (terminateUntaggedInstances)

23 | Will filter your instances and find those that are untagged (including instances with a blank name tag), and then terminate those instances. For more information on the code that does the filtering and terminating, please see http://www.1strategy.com/blog/use-aws-lambda-terminate-untagged-ec2-instances/. 24 |

Preparing the Lambda Function to Be an Alexa Skill

25 | Make sure to create the Lambda function in the "N. Virginia (us-east-1)" region, as that it is the only region that supports receiving events from Alexa Skills Kit. After saving your Lambda function, make note of the Lambda's ARN, we'll need that when we setup the Alexa Skill. 26 | 27 | In addition, we'll need the following items: 28 |

intents.json

29 | The Alexa Skills Kit needs to know what intents are available in your new skill. This is done by providing a JSON document that describes each of the intents. You may notice that the SetRegionIntent has an additional "slots" property. You can think of a "slot" as a variable that the user provides. In this case, we want the user to specify a region variable, so we know which AWS region to operate against. You'll also notice that it references a "LIST_OF_REGIONS" type. We're going to define that type now. 30 |

LIST_OF_REGIONS

31 | We want to limit the number of choices that can be used for the region slot (variable). We don't want the user to be able to specify a location that doesn't exist, so we limit what is accepted by supplying a list of valid words for this slot. Currently, we're limiting the options for regions to "Virginia" and "Oregon". To support more regions, we would simply add additional lines to this file and then ensure that the getRegionIdentifier function in our Lambda knows how to map the additional choices. 32 |

Sample Utterances

33 | The Alexa Skills Kit uses machine learning to determine what the user is intending to do. We feed example phrases into the Alexa Skills Kit as input, including the desired intent to be triggered. We supply these examples by defining "sample utterances." We're supplying just a few examples of things users could say, and what intent should triggered. Note the use of the "{Region}" placeholder in some of the SetRegionIntent phrases. This is a link to the slot discussed earlier. 34 |

Testing your Alexa Skill

35 | Now that we have our Lambda and the resources that the Alexa Skills Kit needs, let's put it all together and test it out. Start by heading to http://developer.amazon.com. If you haven't registered for an Amazon Developer Account, do so now, it's free! In order to test on your own Echo, make sure the email address you're using for a developer account matches that email address to which your Echo is registered. 36 | 37 | Once logged in to the Amazon Developer Console, navigate to "Apps and Services" -> "Alexa". Click the "Add a New Skill" button. 38 |

Skill Information

39 | Fill in the required fields. The "invocation name" will be the identifier you use to open up this skill. For example, I used "admin tools". In order to use the Alexa Skill, I would say, "Alexa, open admin tools." Make sure to use the ARN captured above after you created your Lambda function. 40 |
Tip: I wasn't able to find any information about this in the Alexa Skills Kit documentation but the skill never seemed to work when I included "AWS" or "Amazon" in the invocation name.
41 |

Interaction Model

42 | This is where we tell the Alexa Skills Kit what our skill can do. In the "Intent Schema" box, paste the contents of the intents.json file. Next, add a new slot type, giving the new slot a type name of "LIST_OF_REGIONS" and paste the contents of LIST_OF_REGIONS.txt into the value field. Paste the contents of sample_utterances.txt into the "Sample Utterances" field and click next. 43 |

Test

44 | You'll be presented with a screen that you can use to test example inputs and see what the skill returns. It can take a few seconds for the Alexa Skills Kit to build the model for use on your Echo. As soon as you see three green checkmarks on the left side, you should be good to test it out on your Echo! 45 | 46 | # LICENSE 47 | 48 | Code released under [the MIT license](https://github.com/1Strategy/alexa-aws-administration/blob/master/LICENSE). 49 | -------------------------------------------------------------------------------- /example-events/get-instance-count.json: -------------------------------------------------------------------------------- 1 | { 2 | "session": { 3 | "new": false, 4 | "sessionId": "session1234", 5 | "attributes": { 6 | "region": "Virginia" 7 | }, 8 | "user": { 9 | "userId": null 10 | }, 11 | "application": { 12 | "applicationId": "amzn1.echo-sdk-ams.app.[unique-value-here]" 13 | } 14 | }, 15 | "version": "1.0", 16 | "request": { 17 | "intent": { 18 | "name": "InstanceCountIntent" 19 | }, 20 | "type": "IntentRequest", 21 | "requestId": "request5678" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example-events/get-region.json: -------------------------------------------------------------------------------- 1 | { 2 | "session": { 3 | "new": false, 4 | "sessionId": "session1234", 5 | "attributes": { 6 | "region": "Virginia" 7 | }, 8 | "user": { 9 | "userId": null 10 | }, 11 | "application": { 12 | "applicationId": "amzn1.echo-sdk-ams.app.[unique-value-here]" 13 | } 14 | }, 15 | "version": "1.0", 16 | "request": { 17 | "intent": { 18 | "name": "GetRegionIntent" 19 | }, 20 | "type": "IntentRequest", 21 | "requestId": "request5678" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example-events/launch-session.json: -------------------------------------------------------------------------------- 1 | { 2 | "session": { 3 | "new": true, 4 | "sessionId": "session1234", 5 | "attributes": {}, 6 | "user": { 7 | "userId": null 8 | }, 9 | "application": { 10 | "applicationId": "amzn1.echo-sdk-ams.app.[unique-value-here]" 11 | } 12 | }, 13 | "version": "1.0", 14 | "request": { 15 | "type": "LaunchRequest", 16 | "requestId": "request5678" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example-events/set-region.json: -------------------------------------------------------------------------------- 1 | { 2 | "session": { 3 | "new": false, 4 | "sessionId": "session1234", 5 | "attributes": {}, 6 | "user": { 7 | "userId": null 8 | }, 9 | "application": { 10 | "applicationId": "amzn1.echo-sdk-ams.app.[unique-value-here]" 11 | } 12 | }, 13 | "version": "1.0", 14 | "request": { 15 | "intent": { 16 | "slots": { 17 | "Region": { 18 | "name": "Region", 19 | "value": "Virginia" 20 | } 21 | }, 22 | "name": "SetRegionIntent" 23 | }, 24 | "type": "IntentRequest", 25 | "requestId": "request5678" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var aws = require("aws-sdk"); 2 | 3 | // Route the incoming request based on type (LaunchRequest, IntentRequest, 4 | // etc.) The JSON body of the request is provided in the event parameter. 5 | exports.handler = function(event, context) { 6 | try { 7 | console.log("event.session.application.applicationId=" + event.session.application.applicationId); 8 | 9 | if (event.session.new) { 10 | onSessionStarted({ 11 | requestId: event.request.requestId 12 | }, event.session); 13 | } 14 | 15 | if (event.request.type === "LaunchRequest") { 16 | onLaunch(event.request, 17 | event.session, 18 | function callback(sessionAttributes, speechletResponse) { 19 | context.succeed(buildResponse(sessionAttributes, speechletResponse)); 20 | }); 21 | } else if (event.request.type === "IntentRequest") { 22 | onIntent(event.request, 23 | event.session, 24 | function callback(sessionAttributes, speechletResponse) { 25 | context.succeed(buildResponse(sessionAttributes, speechletResponse)); 26 | }); 27 | } else if (event.request.type === "SessionEndedRequest") { 28 | onSessionEnded(event.request, event.session); 29 | context.succeed(); 30 | } 31 | } catch (e) { 32 | context.fail("Exception: " + e); 33 | } 34 | }; 35 | 36 | /** 37 | * Called when the session starts. 38 | */ 39 | function onSessionStarted(sessionStartedRequest, session) { 40 | console.log("onSessionStarted requestId=" + sessionStartedRequest.requestId + 41 | ", sessionId=" + session.sessionId); 42 | } 43 | 44 | /** 45 | * Called when the user launches the skill without specifying what they want. 46 | */ 47 | function onLaunch(launchRequest, session, callback) { 48 | console.log("onLaunch requestId=" + launchRequest.requestId + 49 | ", sessionId=" + session.sessionId); 50 | 51 | // Dispatch to your skill's launch. 52 | getWelcomeResponse(callback); 53 | } 54 | 55 | /** 56 | * Called when the user specifies an intent for this skill. 57 | */ 58 | function onIntent(intentRequest, session, callback) { 59 | console.log("onIntent requestId=" + intentRequest.requestId + 60 | ", sessionId=" + session.sessionId); 61 | 62 | var intent = intentRequest.intent, 63 | intentName = intentRequest.intent.name; 64 | 65 | if ("InstanceCountIntent" === intentName) { 66 | getInstanceCount(intent, session, callback); 67 | } else if ("SetRegionIntent" === intentName) { 68 | setRegion(intent, session, callback); 69 | } else if ("GetRegionIntent" === intentName) { 70 | getRegion(intent, session, callback); 71 | } else if ("TerminateUntaggedInstancesIntent" === intentName) { 72 | terminateUntaggedInstances(intent, session, callback); 73 | } else if ("AMAZON.HelpIntent" === intentName) { 74 | getWelcomeResponse(callback); 75 | } else { 76 | throw "Invalid intent"; 77 | } 78 | } 79 | 80 | /** 81 | * Called when the user ends the session. 82 | * Is not called when the skill returns shouldEndSession=true. 83 | */ 84 | function onSessionEnded(sessionEndedRequest, session) { 85 | console.log("onSessionEnded requestId=" + sessionEndedRequest.requestId + 86 | ", sessionId=" + session.sessionId); 87 | } 88 | 89 | // --------------- Functions that control the skill's behavior ----------------------- 90 | 91 | function getWelcomeResponse(callback) { 92 | // If we wanted to initialize the session to have some attributes we could add those here. 93 | var sessionAttributes = {}; 94 | var cardTitle = "Alexa AWS Admin"; 95 | var speechOutput = "Welcome to the Alexa AWS Administration Sample. " + 96 | "Select a region to use by saying, set the region to Virginia."; 97 | // If the user either does not reply to the welcome message or says something that is not 98 | // understood, they will be prompted again with this text. 99 | var repromptText = "Select a region by saying, set the region to Virginia."; 100 | var shouldEndSession = false; 101 | 102 | callback(sessionAttributes, 103 | buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession)); 104 | } 105 | 106 | /** 107 | * Sets the region in the session and prepares the speech to reply to the user. 108 | */ 109 | function setRegion(intent, session, callback) { 110 | var cardTitle = intent.name; 111 | var selectedRegionSlot = intent.slots.Region; 112 | var repromptText = ""; 113 | var sessionAttributes = {}; 114 | var shouldEndSession = false; 115 | var speechOutput = ""; 116 | 117 | if (selectedRegionSlot) { 118 | var selectedRegion = selectedRegionSlot.value; 119 | sessionAttributes = createRegionAttributes(selectedRegion); 120 | speechOutput = "You set the region to " + selectedRegion + ". You can now find out " + 121 | "how many instances are running in this region by saying, how many instances are running?"; 122 | repromptText = "You can find out how many instances are running this region by saying, how many instances are running?"; 123 | } else { 124 | speechOutput = "I'm not sure what region you selected. Please try again."; 125 | repromptText = "I'm not sure what region you selected. You can set the selected region by saying, " + 126 | "Set the region to Virginia."; 127 | } 128 | 129 | callback(sessionAttributes, 130 | buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession)); 131 | } 132 | 133 | function createRegionAttributes(selectedRegion) { 134 | return { 135 | region: selectedRegion 136 | }; 137 | } 138 | 139 | function getRegionIdentifier(selectedRegion){ 140 | var regionIdentifier = "us-east-1"; 141 | 142 | if (selectedRegion === "Oregon") { 143 | regionIdentifier = "us-west-2"; 144 | } else if (selectedRegion === "Virginia") { 145 | regionIdentifier = "us-east-1"; 146 | } else if (selectedRegion === "California") { 147 | regionIdentifier = "us-west-1"; 148 | } 149 | 150 | return regionIdentifier; 151 | } 152 | 153 | function getInstanceCount(intent, session, callback) { 154 | var selectedRegion; 155 | var repromptText = null; 156 | var sessionAttributes = {}; 157 | var shouldEndSession = false; 158 | var speechOutput = ""; 159 | 160 | if (session.attributes) { 161 | selectedRegion = session.attributes.region; 162 | } 163 | 164 | if (selectedRegion) { 165 | var regionIdentifier = getRegionIdentifier(selectedRegion); 166 | 167 | var ec2 = new aws.EC2({ 168 | region: regionIdentifier 169 | }); 170 | 171 | console.log('Using ' + selectedRegion + ' (' + regionIdentifier + ') for this intent.'); 172 | var params = { 173 | Filters: [{ 174 | Name: "instance-state-name", 175 | Values: ["running"] 176 | }] 177 | }; 178 | 179 | ec2.describeInstanceStatus(params, function(err, data) { 180 | if (err) { 181 | console.log(err); 182 | } 183 | 184 | var instanceCount = 0; 185 | 186 | console.log(data); 187 | instanceCount = (data.InstanceStatuses && data.InstanceStatuses.length > 0) ? data.InstanceStatuses.length : 0; 188 | 189 | if (instanceCount == 1) { 190 | speechOutput = "There is currently " + instanceCount + " instance running."; 191 | } else if (instanceCount > 1) { 192 | speechOutput = "There are currently " + instanceCount + " instances running."; 193 | } else { 194 | speechOutput = "There are currently no instances running."; 195 | } 196 | 197 | shouldEndSession = false; 198 | 199 | callback(sessionAttributes, 200 | buildSpeechletResponse(intent.name, speechOutput, repromptText, shouldEndSession)); 201 | }); 202 | } else { 203 | speechOutput = "I'm not sure what region you would like to use. Please select a region first by saying, " + 204 | "Set the region to Virginia."; 205 | 206 | callback(sessionAttributes, 207 | buildSpeechletResponse(intent.name, speechOutput, repromptText, shouldEndSession)); 208 | } 209 | } 210 | 211 | function getRegionFromSession(intent, session, callback) { 212 | var selectedRegion; 213 | var repromptText = null; 214 | var sessionAttributes = {}; 215 | var shouldEndSession = false; 216 | var speechOutput = ""; 217 | 218 | if (session.attributes) { 219 | selectedRegion = session.attributes.region; 220 | } 221 | 222 | if (selectedRegion) { 223 | speechOutput = "Your region is currently set to " + selectedRegion + "."; 224 | shouldEndSession = true; 225 | } else { 226 | speechOutput = "I'm not sure what region you would like to select. You can select a region by saying, " + 227 | "Set the region to Virginia."; 228 | } 229 | 230 | // Setting repromptText to null signifies that we do not want to reprompt the user. 231 | // If the user does not respond or says something that is not understood, the session 232 | // will end. 233 | callback(sessionAttributes, 234 | buildSpeechletResponse(intent.name, speechOutput, repromptText, shouldEndSession)); 235 | } 236 | 237 | function terminateUntaggedInstances(intent, session, callback) { 238 | var selectedRegion = null; 239 | var repromptText = null; 240 | var sessionAttributes = {}; 241 | var shouldEndSession = false; 242 | var speechOutput = ""; 243 | 244 | if (session.attributes) { 245 | console.log(session.attributes); 246 | selectedRegion = session.attributes.region; 247 | } 248 | 249 | if (selectedRegion){ 250 | var regionIdentifier = getRegionIdentifier(selectedRegion); 251 | 252 | var ec2 = new aws.EC2({ 253 | region: regionIdentifier 254 | }); 255 | 256 | var params = { 257 | Filters: [{ 258 | Name: "instance-state-name", 259 | Values: ["running"] 260 | }] 261 | }; 262 | 263 | ec2.describeInstances(params, function(err, data) { 264 | if (err) { 265 | console.log(err); 266 | 267 | speechOutput = "Something went wrong while trying to find untagged instances. Please try again."; 268 | 269 | callback(sessionAttributes, 270 | buildSpeechletResponse(intent.name, speechOutput, repromptText, shouldEndSession)); 271 | 272 | return; 273 | } 274 | 275 | var ids = []; 276 | 277 | for (var i = 0; i < data.Reservations.length; i++) { 278 | var Instances = data.Reservations[i].Instances; 279 | for (var j = 0; j < Instances.length; j++) { 280 | var instance = Instances[j]; 281 | if (isTagless(instance)) ids.push(instance.InstanceId); 282 | } 283 | } 284 | 285 | if (ids.length === 0) { 286 | speechOutput = "I could not find any untagged instances."; 287 | 288 | callback(sessionAttributes, 289 | buildSpeechletResponse(intent.name, speechOutput, repromptText, shouldEndSession)); 290 | 291 | return; 292 | } 293 | 294 | console.log("To Terminate (IDs): " + ids); 295 | 296 | var terminateParams = { 297 | InstanceIds: ids 298 | }; 299 | 300 | ec2.terminateInstances(terminateParams, function(err,data){ 301 | if (err){ 302 | console.log(err); 303 | 304 | speechOutput = "Something went wrong while trying to terminate the untagged instances. Please try again."; 305 | 306 | callback(sessionAttributes, 307 | buildSpeechletResponse(intent.name, speechOutput, repromptText, shouldEndSession)); 308 | 309 | return; 310 | } 311 | 312 | speechOutput = ids.length + " untagged instances were found and terminated."; 313 | 314 | callback(sessionAttributes, 315 | buildSpeechletResponse(intent.name, speechOutput, repromptText, shouldEndSession)); 316 | }); 317 | }); 318 | } else { 319 | speechOutput = "I'm not sure what region you would like to use. Please select a region first by saying, " + 320 | "Set the region to Virginia."; 321 | 322 | callback(sessionAttributes, 323 | buildSpeechletResponse(intent.name, speechOutput, repromptText, shouldEndSession)); 324 | } 325 | } 326 | 327 | function isTagless(instance) { 328 | try { 329 | if (!instance.Tags || instance.Tags.length === 0) { 330 | return true; 331 | } 332 | if (instance.Tags.length == 1) if (instance.Tags[0].Key == "Name" && instance.Tags[0].Value === "") return true; 333 | } catch (e) {} 334 | return false; 335 | } 336 | 337 | // --------------- Helpers that build all of the responses ----------------------- 338 | 339 | function buildSpeechletResponse(title, output, repromptText, shouldEndSession) { 340 | return { 341 | outputSpeech: { 342 | type: "PlainText", 343 | text: output 344 | }, 345 | card: { 346 | type: "Simple", 347 | title: "SessionSpeechlet - " + title, 348 | content: "SessionSpeechlet - " + output 349 | }, 350 | reprompt: { 351 | outputSpeech: { 352 | type: "PlainText", 353 | text: repromptText 354 | } 355 | }, 356 | shouldEndSession: shouldEndSession 357 | }; 358 | } 359 | 360 | function buildResponse(sessionAttributes, speechletResponse) { 361 | return { 362 | version: "1.0", 363 | sessionAttributes: sessionAttributes, 364 | response: speechletResponse 365 | }; 366 | } 367 | -------------------------------------------------------------------------------- /intents.json: -------------------------------------------------------------------------------- 1 | { 2 | "intents": [ 3 | { 4 | "intent": "SetRegionIntent", 5 | "slots": [ 6 | { 7 | "name": "Region", 8 | "type": "LIST_OF_REGIONS" 9 | } 10 | ] 11 | }, 12 | { 13 | "intent": "GetRegionIntent" 14 | }, 15 | { 16 | "intent": "InstanceCountIntent" 17 | }, 18 | { 19 | "intent": "TerminateUntaggedInstancesIntent" 20 | }, 21 | { 22 | "intent": "AMAZON.HelpIntent" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /sample_utterances.txt: -------------------------------------------------------------------------------- 1 | SetRegionIntent set my region to {Region} 2 | SetRegionIntent set the region to {Region} 3 | GetRegionIntent what region is set 4 | GetRegionIntent what's my region 5 | GetRegionIntent what's the selected region 6 | GetRegionIntent what region is selected 7 | InstanceCountIntent how many instances are running 8 | InstanceCountIntent what's the instance count 9 | InstanceCountIntent what's the current instance count 10 | TerminateUntaggedInstancesIntent clean up untagged instances 11 | TerminateUntaggedInstancesIntent delete untagged instances 12 | TerminateUntaggedInstancesIntent clean up instances 13 | --------------------------------------------------------------------------------