-
-
Notifications
You must be signed in to change notification settings - Fork 311
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement event-based instance replacement #354
Merged
Merged
Changes from 8 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
9719e17
Implement event-based instance replacement
cristim cceb686
Further improvements
cristim a0288f7
More improvements
cristim 0783910
Fix CloudWatch event rule
cristim 74938a4
Allow flavor to be customised (#359)
f11b2dc
Fix typo (#361)
28944e7
Increase automated testing coverage
cristim 1b4a838
Suspend termination processes for 5 minutes
cristim 5e035ee
Feat/event based instance replacement (#371)
mello7tre 565b644
Feat/event based instance replacement - Fix to terminateRandomSpotIns…
mello7tre 5677c61
Feat/event based instance replacement - Merged all "new" commit from …
mello7tre da91697
Feat/event based instance replacement (#434)
mello7tre 6c17fb9
terminateUnneededSpotInstance - fix to total is unused (#435)
mello7tre 7b4cabd
Merge branch 'master' into feat/event-based-instance-replacement
cristim 100ad17
Add missing import for ioutil
cristim e0e4831
Merge branch 'feat/event-based-instance-replacement' of github.com:Au…
cristim b03bef6
Fix build and tests
cristim d4f9fd1
Fix linter errors reported by `make ci-docker`
cristim 7c92d9d
Address a few codeclimate issues
cristim File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,218 +4,163 @@ import ( | |
"context" | ||
"encoding/json" | ||
"fmt" | ||
"io/ioutil" | ||
"log" | ||
"os" | ||
|
||
autospotting "github.com/AutoSpotting/AutoSpotting/core" | ||
"github.com/aws/aws-lambda-go/events" | ||
"github.com/aws/aws-lambda-go/lambda" | ||
"github.com/aws/aws-sdk-go/aws/endpoints" | ||
ec2instancesinfo "github.com/cristim/ec2-instances-info" | ||
"github.com/namsral/flag" | ||
) | ||
|
||
type cfgData struct { | ||
*autospotting.Config | ||
} | ||
|
||
var conf *cfgData | ||
var as *autospotting.AutoSpotting | ||
var conf autospotting.Config | ||
|
||
// Version represents the build version being used | ||
var Version = "number missing" | ||
|
||
var eventFile string | ||
|
||
func main() { | ||
|
||
if os.Getenv("AWS_LAMBDA_FUNCTION_NAME") != "" { | ||
lambda.Start(Handler) | ||
} else if eventFile != "" { | ||
parseEvent, err := ioutil.ReadFile(eventFile) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
Handler(context.TODO(), parseEvent) | ||
} else { | ||
run() | ||
eventHandler(nil) | ||
} | ||
} | ||
|
||
func run() { | ||
func eventHandler(event *json.RawMessage) { | ||
|
||
log.Println("Starting autospotting agent, build", Version) | ||
log.Printf("Configuration flags: %#v", conf) | ||
|
||
log.Printf("Parsed command line flags: "+ | ||
"regions='%s' "+ | ||
"min_on_demand_number=%d "+ | ||
"min_on_demand_percentage=%.1f "+ | ||
"allowed_instance_types=%v "+ | ||
"disallowed_instance_types=%v "+ | ||
"on_demand_price_multiplier=%.2f "+ | ||
"spot_price_buffer_percentage=%.3f "+ | ||
"bidding_policy=%s "+ | ||
"tag_filters=%s "+ | ||
"tag_filter_mode=%s "+ | ||
"spot_product_description=%v "+ | ||
"instance_termination_method=%s "+ | ||
"termination_notification_action=%s "+ | ||
"cron_schedule=%s "+ | ||
"cron_schedule_state=%s "+ | ||
"license=%s \n", | ||
conf.Regions, | ||
conf.MinOnDemandNumber, | ||
conf.MinOnDemandPercentage, | ||
conf.AllowedInstanceTypes, | ||
conf.DisallowedInstanceTypes, | ||
conf.OnDemandPriceMultiplier, | ||
conf.SpotPriceBufferPercentage, | ||
conf.BiddingPolicy, | ||
conf.FilterByTags, | ||
conf.TagFilteringMode, | ||
conf.SpotProductDescription, | ||
conf.InstanceTerminationMethod, | ||
conf.TerminationNotificationAction, | ||
conf.CronSchedule, | ||
conf.CronScheduleState, | ||
conf.LicenseType, | ||
) | ||
|
||
autospotting.Run(conf.Config) | ||
as.EventHandler(event) | ||
log.Println("Execution completed, nothing left to do") | ||
} | ||
|
||
// this is the equivalent of a main for when running from Lambda, but on Lambda | ||
// the run() is executed within the handler function every time we have an event | ||
// the runFromCronEvent() is executed within the handler function every time we have an event | ||
func init() { | ||
|
||
var region string | ||
|
||
as = &autospotting.AutoSpotting{} | ||
|
||
if r := os.Getenv("AWS_REGION"); r != "" { | ||
region = r | ||
} else { | ||
region = endpoints.UsEast1RegionID | ||
} | ||
|
||
conf = &cfgData{ | ||
&autospotting.Config{ | ||
LogFile: os.Stdout, | ||
LogFlag: log.Ldate | log.Ltime | log.Lshortfile, | ||
MainRegion: region, | ||
SleepMultiplier: 1, | ||
Version: Version, | ||
LicenseType: os.Getenv("LICENSE"), | ||
}, | ||
conf = autospotting.Config{ | ||
LogFile: os.Stdout, | ||
LogFlag: log.Ldate | log.Ltime | log.Lshortfile, | ||
MainRegion: region, | ||
SleepMultiplier: 1, | ||
Version: Version, | ||
LicenseType: os.Getenv("LICENSE"), | ||
LambdaManageASG: os.Getenv("LAMBDA_MANAGE_ASG"), | ||
} | ||
parseCommandLineFlags() | ||
|
||
conf.initialize() | ||
|
||
as.Init(&conf) | ||
} | ||
|
||
// Handler implements the AWS Lambda handler | ||
// Handler implements the AWS Lambda handler interface | ||
func Handler(ctx context.Context, rawEvent json.RawMessage) { | ||
|
||
var snsEvent events.SNSEvent | ||
var cloudwatchEvent events.CloudWatchEvent | ||
parseEvent := rawEvent | ||
|
||
// Try to parse event as an Sns Message | ||
if err := json.Unmarshal(parseEvent, &snsEvent); err != nil { | ||
log.Println(err.Error()) | ||
return | ||
} | ||
|
||
// If event is from Sns - extract Cloudwatch's one | ||
if snsEvent.Records != nil { | ||
snsRecord := snsEvent.Records[0] | ||
parseEvent = []byte(snsRecord.SNS.Message) | ||
} | ||
|
||
// Try to parse event as Cloudwatch Event Rule | ||
if err := json.Unmarshal(parseEvent, &cloudwatchEvent); err != nil { | ||
log.Println(err.Error()) | ||
return | ||
} | ||
|
||
// If event is Instance Spot Interruption | ||
if cloudwatchEvent.DetailType == "EC2 Spot Instance Interruption Warning" { | ||
if instanceID, err := autospotting.GetInstanceIDDueForTermination(cloudwatchEvent); err != nil { | ||
return | ||
} else if instanceID != nil { | ||
spotTermination := autospotting.NewSpotTermination(cloudwatchEvent.Region) | ||
spotTermination.ExecuteAction(instanceID, conf.TerminationNotificationAction) | ||
} | ||
} else { | ||
// Event is Autospotting Cron Scheduling | ||
run() | ||
} | ||
eventHandler(&rawEvent) | ||
} | ||
|
||
// Configuration handling | ||
func (c *cfgData) initialize() { | ||
|
||
c.parseCommandLineFlags() | ||
|
||
data, err := ec2instancesinfo.Data() | ||
if err != nil { | ||
log.Fatal(err.Error()) | ||
} | ||
c.InstanceData = data | ||
} | ||
|
||
func (c *cfgData) parseCommandLineFlags() { | ||
flag.StringVar(&c.AllowedInstanceTypes, "allowed_instance_types", "", | ||
func parseCommandLineFlags() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Function |
||
flag.StringVar(&conf.AllowedInstanceTypes, "allowed_instance_types", "", | ||
"\n\tIf specified, the spot instances will be searched only among these types.\n\tIf missing, any instance type is allowed.\n"+ | ||
"\tAccepts a list of comma or whitespace separated instance types (supports globs).\n"+ | ||
"\tExample: ./AutoSpotting -allowed_instance_types 'c5.*,c4.xlarge'\n") | ||
flag.StringVar(&c.BiddingPolicy, "bidding_policy", autospotting.DefaultBiddingPolicy, | ||
|
||
flag.StringVar(&conf.BiddingPolicy, "bidding_policy", autospotting.DefaultBiddingPolicy, | ||
"\n\tPolicy choice for spot bid. If set to 'normal', we bid at the on-demand price(times the multiplier).\n"+ | ||
"\tIf set to 'aggressive', we bid at a percentage value above the spot price \n"+ | ||
"\tconfigurable using the spot_price_buffer_percentage.\n") | ||
flag.StringVar(&c.DisallowedInstanceTypes, "disallowed_instance_types", "", | ||
|
||
flag.StringVar(&conf.DisallowedInstanceTypes, "disallowed_instance_types", "", | ||
"\n\tIf specified, the spot instances will _never_ be of these types.\n"+ | ||
"\tAccepts a list of comma or whitespace separated instance types (supports globs).\n"+ | ||
"\tExample: ./AutoSpotting -disallowed_instance_types 't2.*,c4.xlarge'\n") | ||
flag.StringVar(&c.InstanceTerminationMethod, "instance_termination_method", autospotting.DefaultInstanceTerminationMethod, | ||
|
||
flag.StringVar(&conf.InstanceTerminationMethod, "instance_termination_method", autospotting.DefaultInstanceTerminationMethod, | ||
"\n\tInstance termination method. Must be one of '"+autospotting.DefaultInstanceTerminationMethod+"' (default),\n"+ | ||
"\t or 'detach' (compatibility mode, not recommended)\n") | ||
flag.StringVar(&c.TerminationNotificationAction, "termination_notification_action", autospotting.DefaultTerminationNotificationAction, | ||
|
||
flag.StringVar(&conf.TerminationNotificationAction, "termination_notification_action", autospotting.DefaultTerminationNotificationAction, | ||
"\n\tTermination Notification Action.\n"+ | ||
"\tValid choices:\n"+ | ||
"\t'"+autospotting.DefaultTerminationNotificationAction+ | ||
"' (terminate if lifecyclehook else detach) | 'terminate' (lifecyclehook triggered)"+ | ||
" | 'detach' (lifecyclehook not triggered)\n") | ||
flag.Int64Var(&c.MinOnDemandNumber, "min_on_demand_number", autospotting.DefaultMinOnDemandValue, | ||
|
||
flag.Int64Var(&conf.MinOnDemandNumber, "min_on_demand_number", autospotting.DefaultMinOnDemandValue, | ||
"\n\tNumber of on-demand nodes to be kept running in each of the groups.\n\t"+ | ||
"Can be overridden on a per-group basis using the tag "+autospotting.OnDemandNumberLong+".\n") | ||
flag.Float64Var(&c.MinOnDemandPercentage, "min_on_demand_percentage", 0.0, | ||
|
||
flag.Float64Var(&conf.MinOnDemandPercentage, "min_on_demand_percentage", 0.0, | ||
"\n\tPercentage of the total number of instances in each group to be kept on-demand\n\t"+ | ||
"Can be overridden on a per-group basis using the tag "+autospotting.OnDemandPercentageTag+ | ||
"\n\tIt is ignored if min_on_demand_number is also set.\n") | ||
flag.Float64Var(&c.OnDemandPriceMultiplier, "on_demand_price_multiplier", 1.0, | ||
|
||
flag.Float64Var(&conf.OnDemandPriceMultiplier, "on_demand_price_multiplier", 1.0, | ||
"\n\tMultiplier for the on-demand price. Numbers less than 1.0 are useful for volume discounts.\n"+ | ||
"\tExample: ./AutoSpotting -on_demand_price_multiplier 0.6 will have the on-demand price "+ | ||
"considered at 60% of the actual value.\n") | ||
flag.StringVar(&c.Regions, "regions", "", | ||
|
||
flag.StringVar(&conf.Regions, "regions", "", | ||
"\n\tRegions where it should be activated (separated by comma or whitespace, also supports globs).\n"+ | ||
"\tBy default it runs on all regions.\n"+ | ||
"\tExample: ./AutoSpotting -regions 'eu-*,us-east-1'\n") | ||
flag.Float64Var(&c.SpotPriceBufferPercentage, "spot_price_buffer_percentage", autospotting.DefaultSpotPriceBufferPercentage, | ||
|
||
flag.Float64Var(&conf.SpotPriceBufferPercentage, "spot_price_buffer_percentage", autospotting.DefaultSpotPriceBufferPercentage, | ||
"\n\tBid a given percentage above the current spot price.\n\tProtects the group from running spot"+ | ||
"instances that got significantly more expensive than when they were initially launched\n"+ | ||
"\tThe tag "+autospotting.SpotPriceBufferPercentageTag+" can be used to override this on a group level.\n"+ | ||
"\tIf the bid exceeds the on-demand price, we place a bid at on-demand price itself.\n") | ||
flag.StringVar(&c.SpotProductDescription, "spot_product_description", autospotting.DefaultSpotProductDescription, | ||
|
||
flag.StringVar(&conf.SpotProductDescription, "spot_product_description", autospotting.DefaultSpotProductDescription, | ||
"\n\tThe Spot Product to use when looking up spot price history in the market.\n"+ | ||
"\tValid choices: Linux/UNIX | SUSE Linux | Windows | Linux/UNIX (Amazon VPC) | \n"+ | ||
"\tSUSE Linux (Amazon VPC) | Windows (Amazon VPC)\n\tDefault value: "+autospotting.DefaultSpotProductDescription+"\n") | ||
flag.StringVar(&c.TagFilteringMode, "tag_filtering_mode", "opt-in", "\n\tControls the behavior of the tag_filters option.\n"+ | ||
|
||
flag.StringVar(&conf.TagFilteringMode, "tag_filtering_mode", "opt-in", "\n\tControls the behavior of the tag_filters option.\n"+ | ||
"\tValid choices: opt-in | opt-out\n\tDefault value: 'opt-in'\n\tExample: ./AutoSpotting --tag_filtering_mode opt-out\n") | ||
flag.StringVar(&c.FilterByTags, "tag_filters", "", "\n\tSet of tags to filter the ASGs on.\n"+ | ||
|
||
flag.StringVar(&conf.FilterByTags, "tag_filters", "", "\n\tSet of tags to filter the ASGs on.\n"+ | ||
"\tDefault if no value is set will be the equivalent of -tag_filters 'spot-enabled=true'\n"+ | ||
"\tIn case the tag_filtering_mode is set to opt-out, it defaults to 'spot-enabled=false'\n"+ | ||
"\tExample: ./AutoSpotting --tag_filters 'spot-enabled=true,Environment=dev,Team=vision'\n") | ||
|
||
flag.StringVar(&c.CronSchedule, "cron_schedule", "* *", "\n\tCron-like schedule in which to"+ | ||
flag.StringVar(&conf.CronSchedule, "cron_schedule", "* *", "\n\tCron-like schedule in which to"+ | ||
"\tperform(or not) spot replacement actions. Format: hour day-of-week\n"+ | ||
"\tExample: ./AutoSpotting --cron_schedule '9-18 1-5' # workdays during the office hours \n") | ||
|
||
flag.StringVar(&c.CronScheduleState, "cron_schedule_state", "on", "\n\tControls whether to take actions "+ | ||
flag.StringVar(&conf.CronScheduleState, "cron_schedule_state", "on", "\n\tControls whether to take actions "+ | ||
"inside or outside the schedule defined by cron_schedule. Allowed values: on|off\n"+ | ||
"\tExample: ./AutoSpotting --cron_schedule_state='off' --cron_schedule '9-18 1-5' # would only take action outside the defined schedule\n") | ||
flag.StringVar(&c.LicenseType, "license", "evaluation", "\n\tControls the terms under which you use AutoSpotting"+ | ||
|
||
flag.StringVar(&conf.LicenseType, "license", "evaluation", "\n\tControls the terms under which you use AutoSpotting"+ | ||
"Allowed values: evaluation|I_am_supporting_it_on_Patreon|I_contributed_to_development_within_the_last_year|I_built_it_from_source_code\n"+ | ||
"\tExample: ./AutoSpotting --license evaluation\n") | ||
|
||
flag.StringVar(&eventFile, "event_file", "", "\n\tJSON file containing event data, "+ | ||
"used for locally simulating execution from Lambda. AutoSpotting now expects to be "+ | ||
"triggered by events and won't do anything if no event is passed either as result of "+ | ||
"AWS instance state notifications or simulated manually using this flag.\n") | ||
|
||
v := flag.Bool("version", false, "Print version number and exit.\n") | ||
flag.Parse() | ||
printVersion(v) | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cristim I don't see where this is populated anymore. Is it no longer necessary with these changes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like this logic was moved to core/main.go around line 40.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see. My mistake. Ctrl-F turned up nothing but that's because GitHub collapsed that file 🤦♂