Permalink
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
423 lines (413 sloc) 15.3 KB
---
AWSTemplateFormatVersion: "2010-09-09"
Description: >
"AutoSpotting: automated EC2 Spot market bidder integrated with AutoScaling"
Parameters:
AllowedInstanceTypes:
Default: ""
Description: >
"Comma separated list of allowed instance types for spot, in case you
may want to limit it to a smaller set of instance types (also support
globs). If unset, instances will be chosen by autospotting's algorithm.
Example: 'm4.xlarge,r4.xlarge,m5.*'"
Type: "String"
BiddingPolicy:
AllowedValues:
- "normal"
- "aggressive"
Default: "normal"
Description: >
"Policy choice for spot bid. If set to 'normal', we bid at the on-demand
price. If set to 'aggressive', we bid at a multiple of the spot price."
Type: "String"
DisallowedInstanceTypes:
Default: ""
Description: >
"Comma separated list of disallowed instance types for spot, in case you
want to exclude specific types (also support globs). Example:
't2.*,m4.large'"
Type: "String"
ExecutionFrequency:
Default: "rate(5 minutes)"
Description: >
"Frequency of executing the Lambda function, influences the speed of
replacing your instances. Can accept any value documented at
http://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html"
Type: "String"
InstanceTerminationMethod:
Default: "autoscaling"
Description: >
"Instance termination method. Must be one of 'autoscaling' (default) or
'detach' (compatibility mode, not recommended)"
Type: "String"
FilterByTags:
Default: ""
Description: >
"Comma separated list of tag=value on which to filter the ASGs that
autospotting considers. By default (if no filters are specific) then
'spot-enabled=true' is used. Example:
'spot-enabled=true,environment=dev'"
Type: "String"
LambdaFunctionTagKey:
Description: "Name of the to be applied to the Lambda function"
Default: "Name"
Type: "String"
LambdaFunctionTagValue:
Description: "Value of the tag to be applied to the Lambda function"
Default: "autospotting"
Type: "String"
LambdaHandlerFunction:
Default: "autospotting"
Description: "Handler function for Lambda"
Type: "String"
LambdaMemorySize:
Default: "1024"
Description: "Memory allocated to the Lambda function"
Type: "Number"
LambdaS3Bucket:
Default: "cloudprowess"
Description: "S3 bucket that contains the function"
Type: "String"
LambdaZipPath:
Default: "nightly/lambda.zip"
Description: >
"Path to the Lambda function zip file inside the S3 bucket. Can be used
to update to a more recent version, such as
'nightly/lambda_build_57.zip'. Build numbers can be taken from TravisCI:
https://travis-ci.org/AutoSpotting/AutoSpotting/builds"
Type: "String"
LogRetentionPeriod:
Default: "7"
Description: "Number of days to keep the Lambda function logs in CloudWatch."
Type: "Number"
MinOnDemandNumber:
Default: "0"
Description: >
"Minimum on-demand instances (absolute number) to be kept in each of
your groups. It is a global default value that can be overridden on a
per-group basis using the autospotting_min_on_demand_number tag. It takes
precedence over MinOnDemandPercentage, so it doesn't make sense to pass
both of them."
Type: "Number"
MinOnDemandPercentage:
Default: "0.0"
Description: >
"Minimum on-demand instances (percentage of the instances currently
running in each group) that will be kept when replacing with spot
instances. It is also a global default value that can be overridden on a
per-group basis using the autospotting_min_on_demand_percentage tag.
MinOnDemandNumber takes precedence if both these parameters are passed"
Type: "Number"
OnDemandPriceMultiplier:
Default: "1.0"
Description: >
"Multiplier for the on-demand price. This is useful for volume discounts
or if you want to set your bid price to be higher than the on demand
price to reduce the chances that your spot instances will be
terminated."
Type: "Number"
Regions:
Default: ""
Description: >
"Space separated list of regions where it should run (supports globs),
in case you may want to limit it to a smaller set of regions. If unset
it will run against all available regions. Example: 'us-east-1 eu-*'"
Type: "String"
SpotPricePercentageBuffer:
Default: "10.0"
Description: >
"Percentage Value of the bid above the current spot price. A spot bid
would be placed at a value = current_spot_price * [1 +
(spot_price_buffer_percentage/100.0)]. The main benefit is that it
protects the group from running spot instances that got significantly
more expensive than when they were initially launched, but still
somewhat less than the on-demand price."
Type: "Number"
SpotProductDescription:
AllowedValues:
- "Linux/UNIX"
- "SUSE Linux"
- "Windows"
- "Linux/UNIX (Amazon VPC)"
- "SUSE Linux (Amazon VPC)"
- "Windows (Amazon VPC)"
Default: "Linux/UNIX (Amazon VPC)"
Description: >
"The Spot Product or operating system to use when looking up spot price
history in the market. Valid choices: Linux/UNIX | SUSE Linux | Windows
| Linux/UNIX (Amazon VPC) | SUSE Linux (Amazon VPC) | Windows (Amazon
VPC)"
Type: "String"
TagFilteringMode:
AllowedValues:
- "opt-in"
- "opt-out"
Default: "opt-in"
Description: >
"Controls the behavior against the AutoScaling groups tagged as per the
FilterByTags option. Defaults to 'opt-in', only processing the tagged
groups. The opposite behavior can be configured using the opt-out mode"
Type: "String"
Resources:
LambdaExecutionRole:
Properties:
AssumeRolePolicyDocument:
Statement:
-
Action: "sts:AssumeRole"
Effect: "Allow"
Principal:
Service:
- "lambda.amazonaws.com"
Path: "/lambda/"
Type: "AWS::IAM::Role"
LambdaFunction:
Properties:
Code:
S3Bucket:
Ref: "LambdaS3Bucket"
S3Key:
Ref: "LambdaZipPath"
Description: "Implements SPOT instance automation"
Environment:
Variables:
ALLOWED_INSTANCE_TYPES:
Ref: "AllowedInstanceTypes"
BIDDING_POLICY:
Ref: "BiddingPolicy"
DISALLOWED_INSTANCE_TYPES:
Ref: "DisallowedInstanceTypes"
INSTANCE_TERMINATION_METHOD:
Ref: "InstanceTerminationMethod"
MIN_ON_DEMAND_NUMBER:
Ref: "MinOnDemandNumber"
MIN_ON_DEMAND_PERCENTAGE:
Ref: "MinOnDemandPercentage"
ON_DEMAND_PRICE_MULTIPLIER:
Ref: "OnDemandPriceMultiplier"
REGIONS:
Ref: "Regions"
SPOT_PRICE_BUFFER_PERCENTAGE:
Ref: "SpotPricePercentageBuffer"
SPOT_PRODUCT_DESCRIPTION:
Ref: "SpotProductDescription"
TAG_FILTERING_MODE:
Ref: "TagFilteringMode"
TAG_FILTERS:
Ref: "FilterByTags"
Handler:
Ref: "LambdaHandlerFunction"
MemorySize:
Ref: "LambdaMemorySize"
Role:
Fn::GetAtt:
- "LambdaExecutionRole"
- "Arn"
Runtime: "go1.x"
Tags:
-
Key:
Ref: "LambdaFunctionTagKey"
Value:
Ref: "LambdaFunctionTagValue"
Timeout: "300"
Type: "AWS::Lambda::Function"
LambdaPolicy:
Properties:
PolicyDocument:
Statement:
-
Action:
- "autoscaling:AttachInstances"
- "autoscaling:DescribeAutoScalingGroups"
- "autoscaling:DescribeAutoScalingInstances"
- "autoscaling:DescribeLaunchConfigurations"
- "autoscaling:DescribeTags"
- "autoscaling:DetachInstances"
- "autoscaling:TerminateInstanceInAutoScalingGroup"
- "autoscaling:UpdateAutoScalingGroup"
- "cloudformation:CreateStack"
- "cloudformation:DeleteStack"
- "cloudformation:Describe*"
- "ec2:CreateTags"
- "ec2:DescribeInstanceAttribute"
- "ec2:DescribeInstances"
- "ec2:DescribeRegions"
- "ec2:DescribeSpotPriceHistory"
- "ec2:RunInstances"
- "ec2:TerminateInstances"
- "events:PutRule"
- "events:PutTargets"
- "events:DeleteRule"
- "events:DescribeRule"
- "events:RemoveTargets"
- "iam:CreateServiceLinkedRole"
- "iam:PassRole"
- "logs:CreateLogGroup"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
- "sns:CreateTopic"
- "sns:DeleteTopic"
- "sns:GetTopicAttributes"
- "sns:SetTopicAttributes"
- "sns:Subscribe"
Effect: "Allow"
Resource: "*"
PolicyName: "LambdaPolicy"
Roles:
-
Ref: "LambdaExecutionRole"
Type: "AWS::IAM::Policy"
LogGroup:
Properties:
LogGroupName:
Fn::Join:
- ""
-
- "/aws/lambda/"
-
Ref: "LambdaFunction"
RetentionInDays:
Ref: "LogRetentionPeriod"
Type: "AWS::Logs::LogGroup"
PermissionForEventsToInvokeLambda:
Properties:
Action: "lambda:InvokeFunction"
FunctionName:
Ref: "LambdaFunction"
Principal: "events.amazonaws.com"
SourceArn:
Fn::GetAtt:
- "ScheduledRule"
- "Arn"
Type: "AWS::Lambda::Permission"
ScheduledRule:
Properties:
Description: "ScheduledRule for launching the AutoSpotting Lambda function"
ScheduleExpression:
Ref: "ExecutionFrequency"
State: "ENABLED"
Targets:
-
Arn:
Fn::GetAtt:
- "LambdaFunction"
- "Arn"
Id: "AutoSpottingEventGenerator"
Type: "AWS::Events::Rule"
TerminationEventRuleFunction:
Type: AWS::Lambda::Function
Properties:
Description: "Creates, deletes EventRule for launching AutoSpotting Lambda function on spot instance termination notification"
Handler: "index.handler"
Runtime: "python3.6"
Timeout: "300"
Role:
Fn::GetAtt:
- "LambdaExecutionRole"
- "Arn"
Code:
ZipFile: |
import boto3
import botocore
import cfnresponse
import json
import sys
import traceback
stack_name = 'AutoSpotTerminationEvent'
def get_cf_template(lambda_arn):
return {
"AWSTemplateFormatVersion" : "2010-09-09",
"Description" : "Notification stack for spot termination event",
"Resources": {
"AutoSpotTerminationNotificationTopic": {
"Type" : "AWS::SNS::Topic",
"Properties" : {
"DisplayName" : "autospotting-termination-notification-topic",
"Subscription": [
{
"Endpoint": lambda_arn,
"Protocol": "lambda"
}
]
}
},
"AutoSpotTeminationEventRule": {
"Type" : "AWS::Events::Rule",
"Properties" : {
"Description" : "This rule is triggered 2 minutes before AWS terminates a spot instance",
"EventPattern" : {
"detail-type": ["EC2 Spot Instance Interruption Warning"],
"source": ["aws.ec2"]
},
"State" : "ENABLED",
"Targets" : [
{
"Id": "AutoSpottingTerminationEventGenerator",
"Arn": { "Ref": "AutoSpotTerminationNotificationTopic" }
}
]
}
}
}
}
def handle_delete(event):
ec2 = boto3.client('ec2')
wait_for_deletion = False
for region in ec2.describe_regions()['Regions']:
client = boto3.client('cloudformation', region['RegionName'])
#try except to make sure stack exists before deletion
try:
client.describe_stacks(
StackName=stack_name
)
client.delete_stack(
StackName=stack_name
)
wait_for_deletion = True
except botocore.exceptions.ClientError:
pass
#Wait only for last otherwise stack deletion will take forever
if wait_for_deletion == True:
waiter = client.get_waiter('stack_delete_complete')
waiter.wait(
StackName=stack_name
)
def handle_create(event):
ec2 = boto3.client('ec2')
lambda_arn = event['ResourceProperties']['LambdaARN']
for region in ec2.describe_regions()['Regions']:
client = boto3.client('cloudformation', region['RegionName'])
#try except to make sure stack does not exist before creation
try:
client.describe_stacks(
StackName=stack_name
)
except botocore.exceptions.ClientError:
client.create_stack(
StackName=stack_name,
TemplateBody=json.dumps(get_cf_template(lambda_arn))
)
def handler(event, context):
try:
if event['RequestType'] == 'Delete':
handle_delete(event)
if event['RequestType'] == 'Create':
handle_create(event)
cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
except:
traceback.print_exc()
print("Unexpected error:", sys.exc_info()[0])
cfnresponse.send(event, context, cfnresponse.FAILED, {})
TerminationEventRuleCallout:
Type: "Custom::LambdaCallout"
Properties:
ServiceToken:
Fn::GetAtt:
- "TerminationEventRuleFunction"
- "Arn"
LambdaARN:
Fn::GetAtt:
- "LambdaFunction"
- "Arn"
DependsOn: LambdaPolicy