DevOps related tasks are a large part of life for engineers in many orgs. Wouldn't being able to offload some of this to bots be awesome? Well, let's try and see what we can do.
We'll build a simple chat bot that helps figure out what bids you need to place for Spot instances based on historical prices. This normally requires logging into the console and looking at various historical graphs. It would be great if we could just ask our bot what our bid price should be.
So the idea then, is to get something similar to a conversation like this going...
Hey Jared
Hi there.
I want to find latest spot prices
Sure. In which region?
us-west-2
Got it. What instance type?
c4.4xlarge
OK, max price for c4.4xlarge in us-west-2 over the past 2 days is $0.96340.
Can I help with other instance types and regions?
How about r3.8xlarge in us-east-1
OK, max price for r3.8xlarge in us-east-1 over the past 2 days is $1.23340.
Can I help with other instance types and regions?
No, but can you check if changes were made to my environment
Sure. What service?
RDS
I don't see any changes to RDS in your environment over the past 30 days
Can I help check other services?
No thanks, I have everything
Ok. Ping me if you need anything else.
Looks neat. Let's build it!
We'll use the following services on AWS to develop our bot.
Familiarity with AWS Lambda and Amazon CloudWatch and how to use & leverage them can greatly help.
Please ensure that the region chosen in your console is Virginia
(us-east-1). You can see this in the region dropdown in the top right-hand corner.
Because we'll be using AWS Lambda for the backend logic, we will need to grant this Lambda appropriate execution permissions and the appropriate trust relationships.
To do this, we will need to create the following IAM policies and attach it to an IAM role to associate with our Lambda.
Go to the AWS IAM console.
Create a Lambda execution policy called DevopsBotLambdaPolicy
that, when attached (to a role, user, or group), can give permissions required to execute Lambda functions.
Policieslink.
Create policybutton.
Create Your Own Policyoption and click
Select.
DevopsBotLambdaPolicy
Policy Documentsection, add the following
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Stmt1475958598000",
"Effect": "Allow",
"Action": [
"lambda:InvokeFunction",
"lambda:UpdateFunctionConfiguration"
],
"Resource": [ "*" ]
}
]
}
Repeat the same steps above and create a Lex and Polly invokation policy called DevopsBotLexPollyPollicy
that allows access to Lex and Polly APIs.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Stmt1480120057000",
"Effect": "Allow",
"Action": [
"lex:*",
"polly:*"
],
"Resource": [
"*"
]
}
]
}
Now follow the same steps to create a final policy called DevopsBotLambdaLogWriter
that grants permissions to write to CloudWatch logs. This is useful to see errors and debug your bot.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:*"
}
]
}
Now we need to create an IAM Role and attach policies to it -- the 3 policies we created above and an default EC2 policy to this role to make it useful.
Create new rolebutton
AWS Service Rolesoption should be chosen. Select
AWS Lambdafrom under this list and click
Select
AmazonEC2ReadOnlyAccesspolicy by selecting it (start typing in the Filter box to narrow the list down) and click
Next step
DevopsBotRole
Filterto narrow down the options)
Permissions(if not, click it)
Attach policy
DevopsBotto narrow it down and attach the 3 policies we created above.
Trust relationshipstab (the second one)
Edit trust relationshipbutton and add the following:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
},
{
"Effect": "Allow",
"Principal": {
"Service": "lex.amazonaws.com"
},
"Action": "sts:AssumeRole"
},
{
"Effect": "Allow",
"Principal": {
"Service": "channels.lex.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
Here's a screenshot to helps show how that screen should look. Ignore the policy names and such, since they may not match verbatim.
We assume you already know (and have used) Lambda, so not diving too deep into this here. But adding some quick instructions anyway on how to set it up.
Blank Function
Next
Code Entry Typeupload the devops_lambda.zip
Roleselect
Choose an existing role
Existing Roleselect
DevopsBotRole(this is the same role we created and attached various policies to under the initial IAM section)
Advanced Settingsto expand it
Timeoutenter 30s
Next
Create function
If you want to test your lambda, you can. Click on Test
button and enter this event.json to test it
{
"messageVersion": "1.0",
"invocationSource": "FulfillmentCodeHook",
"userId": "johnnycash",
"sessionAttributes":
{
"dummyKey1": "dummyValue1",
"dummyKey2": "dummyValue2"
},
"bot":
{
"name": "SpotPriceBot",
"alias": null,
"version": "$LATEST"
},
"outputDialogMode": "Text",
"currentIntent":
{
"name": "findMaxPrice",
"slots":
{
"region": "us-west-2",
"instanceType": "c4.8xlarge"
},
"confirmationStatus": "None"
}
}
Now we have everything we need to start building our Lex bot.
Go to the Amazon Lex console. If you've never been to the Lex console before, you'll be presented with an introductory screen with a Get Started
button. Click on the Get Started
button.
And then click on the Custom Bot
button.
Give your bot a memorable name. I call mine JARED B. (Just A Really Excellent Devops Bot :)
Choose Joanna
for Output voice
Set Session timeout
to 3 mins (this defines how long you want your bot to maintain context)
We're building a Devops bot, so choose No
for Child-Directed?
.
sayHiintent
Now let's create our very first intent. This is the simplest of intents used just for greetings / salutations.
sayHiintent
Let's now update the sayHi
intent and configure it with a few appropriate Utterances
(a few different ways of how one person can greet another) and a Confirmation Prompt
(the way the bot greets you back)
After providing each utterance, remember click the + button next to it.
Here are some sample greetings. For convenience, just copy-paste.
In the Cancel
text box, enter (for when the user cancels, or doesn't want to continue chatting)
Once you've created these utterances, click on Save Intent.
Let's create our second intent. Let's call this findSpotPrices
and click on Add.
Finally, click on Save Intent
at the bottom (you may need to scroll down if it's off your screen).
We're going to leave this intent as-is and move forward with creating slot types (the next few steps) and then come back and update this intent to use those newly created slot types.
In the left hand pane of the Lex console, look for a + button to the right of Slot Types
. Click on the + button.
Call the slot AWSRegion
and enter the region values below (copy-paste is fine). We just need a few, no need to enter the entire AWS region list.
And finally click on Add slot to intent
button. What we're doing here is to add this SlotType to the findSpotPrices intent.
Once added, look under the Slots
section. You will find the AWSRegion slot type that you just created.
Let's edit the name
of this slot (the text area that contains text with colored background) and call this region
.
Ensure that the Required
checkbox is checked
Edit the Prompt
field and enter: Sure. In which region?
Scroll down and click on Save Intent
What we've just done here, is configured Lex so that whenever it detects a findSpotPrices
intent (via sample utterances that you will soon train it with) to always ask for an AWSRegion (because Required is checked) by prompting the user with
Sure. In which region?".
Let's create another slot type for AWSInstanceType. In the left hand pane, click on the + button next to Slot types.
Now follow similar steps as above and create the AWSInstanceType slot.
Here are some sample AWS instance types to configure this slot with (fine to copy paste). Just a few instance types should do.
Here's a screenshot (ignore the exact values shown)
Click on Add slot to intent
and you should now see an additional AWSInstanceType
slot type.
Ensure that the Required
checkbox is checked
Edit the name
field (colored text) and let's call this instanceType
Edit the Prompt
field and let's enter: Got it. What instance type?
Once you're done, click on Save Intent
. You may have to scroll down if you can't see it.
We have now configured Lex to additionally ask for AWSInstanceType (since Required
is checked) when Lex detects the findSpotPrices intent.
Utterances:
You can think of utterances as - 'In how many ways can my user say something to express this intent?'
So let's go ahead and enter some sample utterances for the findSpotPrices intent.
Click on the + button and start adding utterances. Here are some below that you can copy-paste. Feel free to change it to what you think sounds most natural.
This last utterannce (the one above) is a bit special and shows you how you can use slot types in utterances to enable natural conversations.
SlotTypes
You should have already configured the prompt, but just give it a look and ensure that the prompts are configured for each SlotType.
The slots, slot types, and associated prompts should look something like this:
Required | Name | Slot Type | Prompt |
---|---|---|---|
Yes | region | AWSRegion | Sure. In which region? |
Yes | instanceType | AWSInstanceType | Got it. What instance type? |
Be sure that you have clicked the + button next to the last utterance as well as the last slot promopt.
Fulfillment
Choose AWS Lambda function and select the lambda function that you just uploaded from the dropdown.
Follow-up message
You can copy-paste these message into the Follow-up message
and Cancel
text input boxes.
Choose the Follow-up message
radio button and enter this:
[closeMessage]. Can I help with other instance families and regions?
In the Cancel
text box below, enter:
Ok. Happy bidding.
And finally click on Save Intent
You should now have everything you need to start testing. Let's build the bot that we just created. To do this, click Build
on the top right hand corner.
If everything's good and the build finishes successfully, you should see the Chat Bot
in the lower right corner open up automatically.
Fire it up and start talking with Jared! You can start of by saying Hey Jared
to see how your bot responds. Scroll up to see sample interaction.
The bot's Lambda backend is comprised of 4 files -- mostly to keep things small and separate.
This is Lambda's starting point (or Lambda Hook).
/*jshint esversion: 6 */
'use strict';
let SpotBot = require('./spot_bot');
let LexMixin = require('./lex_mixin');
let Colloquialism = require('./colloquialisms');
// Instantiate a new SpotBot with Lex features mixed in to it
let spotBot = SpotBot.newInstance(LexMixin);
/*
* Lambda...
*/
exports.handler = (event, context, callback) =>
{
console.info("Received event: ");
console.dir(event); // Log a human readable event hash
//
// Part I. Get all the params and build the request
//
let regionSlot = spotBot.getSlotValue(event, "region");
let region = Colloquialism.getRegion(regionSlot);
let instanceTypeSlot = spotBot.getSlotValue(event, "instanceType");
let instanceType = Colloquialism.getInstanceType(instanceTypeSlot);
// Build the instance types parameter for the spot API call from Lex intent (event)
let spotRequest = SpotBot.buildSpotRequest(instanceType);
// If different from default region, change it
if (SpotBot.DEFAULT_REGION !== region)
{
SpotBot.changeRegion(region);
}
//
// Part II. Find spot prices
//
SpotBot.findMaxPrice(spotRequest).then((price) =>
{
let response = `OK. Max price for ${instanceTypeSlot} in ${regionSlot} over past ${SpotBot.DURATION} days is $${price}`;
callback(null, spotBot.respond(event, response));
}).catch((exception) =>
{
// promise failed, log exception to console (for convenience) and let it bubble up
console.log(exception);
callback(exception);
});
};
This class contains all code related to getting / calculating spot prices.
/*jshint esversion: 6 */
'use strict';
/**
* The meat of the bot.
*/
class SpotBot
{
/**
* Method to create a new SpotBot with Lex mixed in
*/
static newInstance(mixin)
{
// Mix Lex into the Bot. Nice to keep Lex and Bot code separate.
Object.assign(this.prototype, mixin);
return new SpotBot();
}
static changeRegion(region)
{
console.log(`Changing region to "${region}"`);
SpotBot.AWS.config.region = region;
SpotBot.ec2 = new SpotBot.AWS.EC2();
}
/**
* Build & return a request for the describeSpotPriceHistory API call
*/
static buildSpotRequest(instanceType)
{
let now = new Date();
return {
MaxResults: 10,
StartTime: new Date(now.setDate(now.getDate() - 1)),
EndTime: now,
InstanceTypes: [instanceType]
};
}
/**
* Returns a promise that resolves to a JSON array of spot prices
*/
static findSpotPriceHistory(instances)
{
return SpotBot.ec2.describeSpotPriceHistory(instances).promise();
}
/**
* Returns a promise that resolves to a single maximum price
*/
static findMaxPrice(instances)
{
// Nested promise (since findSpotPriceHistory also returns a promise)
return Promise.resolve(SpotBot.findSpotPriceHistory(instances))
.then(function(data)
{
let maxPrice = 0.0;
data.SpotPriceHistory.forEach((pricing) =>
{
if (pricing.SpotPrice > maxPrice)
{
maxPrice = pricing.SpotPrice;
}
});
return maxPrice;
});
}
}
SpotBot.DURATION = 2;
// This should be Lambda environment var, but doing it to simplify instructions
SpotBot.DEFAULT_REGION = 'us-west-2';
SpotBot.AWS = require('aws-sdk');
SpotBot.AWS.config.region = SpotBot.DEFAULT_REGION;
SpotBot.ec2 = new SpotBot.AWS.EC2();
module.exports = SpotBot;
This class contains code specific to Lex (constants and such) -- this is just to keep each source file small and focused.
/*jshint esversion: 6 */
'use strict';
/**
* An adapter (mixin) that can be added to any bot class to make it easier to interface
* with Lex (so the bot code stays clean, focused, and separate from Lex code)
*/
let LexMixin =
{
/**
* Returns a slot's value
*/
getSlotValue: function(event, slotName)
{
return event.currentIntent.slots[slotName];
},
/**
* Return conversational message as a Lex JSON response
*/
respond: function(event, message)
{
return this.close(message, LexMixin.fulfillmentState.FULFILLED, event);
},
/**
* Each convo started with Lex is a session. Close the session when we're done.
*/
close: function(message, fulfillmentState, event)
{
// Set the response message as part of the sessionAttributes, so Lex can respond with
// "Follow Up" configuration
if (event.sessionAttributes === null)
{
event.sessionAttributes = {};
}
event.sessionAttributes["closeMessage"] = message;
let responseJson = {
sessionAttributes: event.sessionAttributes,
dialogAction:
{
type: LexMixin.dialogActionType.CLOSE,
fulfillmentState: fulfillmentState
}
};
console.log(responseJson);
return responseJson;
},
// Some Lex constants (for convenience)
fulfillmentState:
{
FULFILLED: "Fulfilled",
FAILED: "Failed"
},
dialogActionType:
{
ELICIT_INTENT: "ElicitIntent",
ELICIT_SLOT: "ElicitSlot",
CONFIRM_INTENT: "ConfirmIntent",
DELEGATE: "Delegate",
CLOSE: "Close",
},
contentType:
{
PLAIN_TEXT: "PlainText",
SSML: "SSML"
}
};
module.exports = LexMixin;
Enables conversational references to things that are jargon-y
/*jshint esversion: 6 */
'use strict';
/**
* Colloquialisms - to help define conversational references to complicated things
*/
let Colloquialisms =
{
getRegion: function(region)
{
let _region = Colloquialisms.AWS_REGIONS[region.toLowerCase()];
return _region === undefined ? region : _region;
},
getInstanceType: function(instanceType)
{
let _instanceType = Colloquialisms.AWS_INSTANCE_TYPES[instanceType.toLowerCase()];
return _instanceType === undefined ? instanceType : _instanceType;
},
// Allow user to type Virginia instead of us-east-1, for example
AWS_REGIONS:
{
virginia: 'us-east-1',
ohio: 'us-east-2',
california: 'us-west-1',
oregon: 'us-west-2',
ireland: 'eu-west-1',
frankfurt: 'eu-central-1',
tokyo: 'ap-northeast-1',
seoul: 'ap-northeast-2',
singapore: 'ap-southeast-1',
sydney: 'ap-southeast-2',
mumbai: 'ap-south-1',
"sao paolo": 'sa-east-1'
},
// Instance type colloquialisms (feel free to tweak to suit your users)
AWS_INSTANCE_TYPES:
{
"general purpose": 't2.nano', // 't2.micro', 't2.small', 't2.medium', 't2.large'],
"compute": 'c4.large', // 'c4.xlarge', 'c4.2xlarge', 'c4.4xlarge', 'c4.8xlarge'],
"compute optimized": 'c4.large', // 'c4.xlarge', 'c4.2xlarge', 'c4.4xlarge', 'c4.8xlarge'],
"memory": 'r3.large', // 'r3.xlarge', 'r3.2xlarge', 'r3.4xlarge', 'r3.8xlarge'],
"memory optimized": 'r3.large', // 'r3.xlarge', 'r3.2xlarge', 'r3.4xlarge', 'r3.8xlarge'],
"storage": 'i2.xlarge', // 'i2.2xlarge', 'i2.4xlarge', 'i2.8xlarge'],
"storage opimized": 'i2.xlarge', // 'i2.2xlarge', 'i2.4xlarge', 'i2.8xlarge'],
"gpu": 'p2.xlarge', // 'p2.8xlarge', 'p2.16xlarge', 'g2.2xlarge', 'g2.8xlarge']
"accelerated": 'p2.xlarge' // 'p2.8xlarge', 'p2.16xlarge', 'g2.2xlarge', 'g2.8xlarge']
}
};
module.exports = Colloquialisms;
This here really just scratches the surface of what's possible. We could take this forward in many different ways, from offloading a vast array of mundane tasks all the way to sophisticated things like incorporating a Machine Learning backend that can predictively respond to user interactions!
The world is our Oyster. :)