core/autoscaling_configuration.go
// Copyright (c) 2016-2022 Cristian Măgherușan-Stanciu
// Licensed under the Open Software License version 3.0
package autospotting
import (
"log"
"math"
"strconv"
)
const (
// The tag names below allow overriding global setting on a per-group level.
// They should follow the format below:
// "autospotting_${overridden_command_line_parameter_name}"
// For example the tag named "autospotting_min_on_demand_number" will override
// the command-line option named "min_on_demand_number", and so on.
// OnDemandPercentageTag is the name of a tag that can be defined on a
// per-group level for overriding maintained on-demand capacity given as a
// percentage of the group's running instances.
OnDemandPercentageTag = "autospotting_min_on_demand_percentage"
// OnDemandNumberLong is the name of a tag that can be defined on a
// per-group level for overriding maintained on-demand capacity given as an
// absolute number.
OnDemandNumberLong = "autospotting_min_on_demand_number"
// OnDemandPriceMultiplierTag is the name of a tag that can be defined on a
// per-group level for overriding multiplier for the on-demand price.
OnDemandPriceMultiplierTag = "autospotting_on_demand_price_multiplier"
// BiddingPolicyTag stores the bidding policy for the spot instance
BiddingPolicyTag = "autospotting_bidding_policy"
// SpotPriceBufferPercentageTag stores percentage value above the
// current spot price to place the bid
SpotPriceBufferPercentageTag = "autospotting_spot_price_buffer_percentage"
// AllowedInstanceTypesTag is the name of a tag that can indicate which
// instance types are allowed in the current group
AllowedInstanceTypesTag = "autospotting_allowed_instance_types"
// DisallowedInstanceTypesTag is the name of a tag that can indicate which
// instance types are not allowed in the current group
DisallowedInstanceTypesTag = "autospotting_disallowed_instance_types"
// Default constant values should be defined below:
// DefaultSpotProductDescription stores the default operating system
// to use when looking up spot price history in the market.
DefaultSpotProductDescription = "Linux/UNIX (Amazon VPC)"
// DefaultSpotProductPremium stores the default value to add to the
// on demand price for premium instance types.
DefaultSpotProductPremium = 0.0
// DefaultMinOnDemandValue stores the default on-demand capacity to be kept
// running in a group managed by autospotting.
DefaultMinOnDemandValue = 0
// DefaultSpotPriceBufferPercentage stores the default percentage value
// above the current spot price to place a bid
DefaultSpotPriceBufferPercentage = 10.0
// DefaultBiddingPolicy stores the default bidding policy for
// the spot bid on a per-group level
DefaultBiddingPolicy = "normal"
// DefaultOnDemandPriceMultiplier stores the default OnDemand price multiplier
// on a per-group level
DefaultOnDemandPriceMultiplier = 1.0
// DefaultInstanceTerminationMethod is the default value for the instance termination
// method configuration option
DefaultInstanceTerminationMethod = AutoScalingTerminationMethod
// ScheduleTag is the name of the tag set on the AutoScaling Group that
// can override the global value of the Schedule parameter
ScheduleTag = "autospotting_cron_schedule"
// TimezoneTag is the name of the tag set on the AutoScaling Group that
// can override the global value of the Timezone parameter
TimezoneTag = "autospotting_cron_timezone"
// CronScheduleStateOn controls whether to run or not to run during the time interval
// specified in the Schedule variable or its per-group tag overrides. It
// accepts "on|off" as valid values
CronScheduleStateOn = "on"
// CronScheduleStateTag is the name of the tag set on the AutoScaling Group that
// can override the global value of the CronScheduleState parameter
CronScheduleStateTag = "autospotting_cron_schedule_state"
// EnableInstanceLaunchEventHandlingTag is the name of the tag set on the
// AutoScaling Group that enables the event-based instance replacement logic
// for this group. It is set automatically once the legacy cron-based
// replacement logic is done replacing instances in any given group.
EnableInstanceLaunchEventHandlingTag = "autospotting_enable_instance_launch_event_handling"
// PatchBeanstalkUserdataTag is the name of the tag set on the AutoScaling Group that
// can override the global value of the PatchBeanstalkUserdata parameter
PatchBeanstalkUserdataTag = "autospotting_patch_beanstalk_userdata"
// GP2ConversionThresholdTag is the name of the tag set on the AutoScaling Group that
// can override the global value of the GP2ConversionThreshold parameter
GP2ConversionThresholdTag = "autospotting_gp2_conversion_threshold"
// SpotAllocationStrategyTag is the name of the tag set on the AutoScaling Group that
// can override the global value of the SpotAllocationStrategy parameter
SpotAllocationStrategyTag = "autospotting_spot_allocation_strategy"
// PrioritizedInstanceTypesBiasTag is the name of the tag set on the AutoScaling Group that
// can override the global value of the PrioritizedInstanceTypesBias parameter
PrioritizedInstanceTypesBiasTag = "autospotting_prioritized_instance_types_bias"
)
// AutoScalingConfig stores some group-specific configurations that can override
// their corresponding global values
type AutoScalingConfig struct {
MinOnDemand int64
MinOnDemandNumber int64
MinOnDemandPercentage float64
AllowedInstanceTypes string
DisallowedInstanceTypes string
OnDemandPriceMultiplier float64
SpotPriceBufferPercentage float64
SpotProductDescription string
SpotProductPremium float64
BiddingPolicy string
TerminationMethod string
// Instance termination method
InstanceTerminationMethod string
// Termination Notification action
TerminationNotificationAction string
CronSchedule string
CronTimezone string
CronScheduleState string // "on" or "off", dictate whether to run inside the CronSchedule or not
PatchBeanstalkUserdata bool
// Threshold for converting EBS volumes from GP2 to GP3, since after a certain
// size GP2 may be more performant than GP3.
GP2ConversionThreshold int64
// Controls the instance type selection when launching new Spot instances.
// Further information about this is available at
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-fleet-allocation-strategy.html
SpotAllocationStrategy string
// PrioritizedInstanceTypesBias can be used to tweak the ordering of the instance types when using the
//"capacity-optimized-prioritized" allocation strategy, biasing towards newer instance types.
PrioritizedInstanceTypesBias string
}
func (a *autoScalingGroup) loadPercentageOnDemand(tagValue *string) (int64, bool) {
percentage, err := strconv.ParseFloat(*tagValue, 64)
if err != nil {
log.Printf("Error with ParseFloat: %s\n", err.Error())
} else if percentage == 0 {
log.Printf("Loaded MinOnDemand value to %f from tag %s\n", percentage, OnDemandPercentageTag)
return int64(percentage), true
} else if percentage > 0 && percentage <= 100 {
instanceNumber := float64(a.instances.count())
onDemand := int64(math.Floor((instanceNumber * percentage / 100.0) + .5))
log.Printf("Loaded MinOnDemand value to %d from tag %s\n", onDemand, OnDemandPercentageTag)
return onDemand, true
}
log.Printf("Ignoring value out of range %f\n", percentage)
return DefaultMinOnDemandValue, false
}
func (a *autoScalingGroup) loadSpotPriceBufferPercentage(tagValue *string) (float64, bool) {
spotPriceBufferPercentage, err := strconv.ParseFloat(*tagValue, 64)
if err != nil {
log.Printf("Error with ParseFloat: %s\n", err.Error())
return DefaultSpotPriceBufferPercentage, false
} else if spotPriceBufferPercentage < 0 {
log.Printf("Ignoring out of range value : %f\n", spotPriceBufferPercentage)
return DefaultSpotPriceBufferPercentage, false
}
log.Printf("Loaded SpotPriceBufferPercentage value to %f from tag %s\n", spotPriceBufferPercentage, SpotPriceBufferPercentageTag)
return spotPriceBufferPercentage, true
}
func (a *autoScalingGroup) loadNumberOnDemand(tagValue *string) (int64, bool) {
onDemand, err := strconv.Atoi(*tagValue)
if err != nil {
log.Printf("Error with Atoi: %s\n", err.Error())
} else if onDemand >= 0 && int64(onDemand) <= *a.MaxSize {
log.Printf("Loaded MinOnDemand value to %d from tag %s\n", onDemand, OnDemandNumberLong)
return int64(onDemand), true
} else {
log.Printf("Ignoring value out of range %d\n", onDemand)
}
return DefaultMinOnDemandValue, false
}
func (a *autoScalingGroup) loadOnDemandPriceMultiplier(tagValue *string) (float64, bool) {
onDemandPriceMultiplier, err := strconv.ParseFloat(*tagValue, 64)
if err != nil {
log.Printf("Error with ParseFloat: %s\n", err.Error())
return DefaultOnDemandPriceMultiplier, false
} else if onDemandPriceMultiplier <= 0 {
log.Printf("Ignoring out of range value : %f\n", onDemandPriceMultiplier)
return DefaultOnDemandPriceMultiplier, false
}
log.Printf("Loaded OnDemandPriceMultiplier value to %f from tag %s\n", onDemandPriceMultiplier, OnDemandPriceMultiplierTag)
return onDemandPriceMultiplier, true
}
func (a *autoScalingGroup) getTagValue(keyMatch string) *string {
for _, asgTag := range a.Tags {
if *asgTag.Key == keyMatch {
return asgTag.Value
}
}
return nil
}
func (a *autoScalingGroup) setMinOnDemandIfLarger(newValue int64, hasMinOnDemand bool) bool {
if !hasMinOnDemand || newValue > a.config.MinOnDemand {
a.config.MinOnDemand = newValue
}
return true
}
func (a *autoScalingGroup) loadConfOnDemand() bool {
tagList := [2]string{OnDemandNumberLong, OnDemandPercentageTag}
loadDyn := map[string]func(*string) (int64, bool){
OnDemandPercentageTag: a.loadPercentageOnDemand,
OnDemandNumberLong: a.loadNumberOnDemand,
}
foundLimit := false
for _, tagKey := range tagList {
if tagValue := a.getTagValue(tagKey); tagValue != nil {
if _, ok := loadDyn[tagKey]; ok {
if newValue, done := loadDyn[tagKey](tagValue); done {
foundLimit = a.setMinOnDemandIfLarger(newValue, foundLimit)
}
}
}
debug.Println("Couldn't find tag", tagKey)
}
return foundLimit
}
func (a *autoScalingGroup) loadPatchBeanstalkUserdata() bool {
tagValue := a.getTagValue(PatchBeanstalkUserdataTag)
if tagValue != nil {
log.Printf("Loaded PatchBeanstalkUserdata value %v from tag %v\n", *tagValue, PatchBeanstalkUserdataTag)
val, err := strconv.ParseBool(*tagValue)
if err != nil {
log.Printf("Failed to parse PatchBeanstalkUserdata value %v as a boolean", *tagValue)
return false
}
a.config.PatchBeanstalkUserdata = val
return true
}
debug.Println("Couldn't find tag", PatchBeanstalkUserdataTag, "on the group", a.name, "using the default configuration")
a.config.PatchBeanstalkUserdata = a.region.conf.PatchBeanstalkUserdata
return false
}
func (a *autoScalingGroup) loadSpotAllocationStrategy() bool {
a.config.SpotAllocationStrategy = a.region.conf.SpotAllocationStrategy
tagValue := a.getTagValue(SpotAllocationStrategyTag)
if tagValue != nil {
log.Printf("Loaded AllocationStrategy value %v from tag %v\n", *tagValue, SpotAllocationStrategyTag)
a.config.SpotAllocationStrategy = *tagValue
return true
}
debug.Println("Couldn't find tag", SpotAllocationStrategyTag, "on the group", a.name, "using the default configuration")
return false
}
func (a *autoScalingGroup) loadPrioritizedInstanceTypesBiasTag() bool {
a.config.PrioritizedInstanceTypesBias = a.region.conf.PrioritizedInstanceTypesBias
tagValue := a.getTagValue(PrioritizedInstanceTypesBiasTag)
if tagValue != nil {
log.Printf("Loaded PrioritizedInstanceTypesBiasTag value %v from tag %v\n", *tagValue, PrioritizedInstanceTypesBiasTag)
a.config.PrioritizedInstanceTypesBias = *tagValue
return true
}
debug.Println("Couldn't find tag", PrioritizedInstanceTypesBiasTag, "on the group", a.name, "using the default configuration")
return false
}
func (a *autoScalingGroup) loadGP2ConversionThreshold() bool {
// setting the default value
a.config.GP2ConversionThreshold = a.region.conf.GP2ConversionThreshold
tagValue := a.getTagValue(GP2ConversionThresholdTag)
if tagValue == nil {
log.Printf("Couldn't load the GP2ConversionThreshold from tag %v, using the globally configured value of %v\n", GP2ConversionThresholdTag, a.config.GP2ConversionThreshold)
return false
}
log.Printf("Loaded GP2ConversionThreshold value %v from tag %v\n", *tagValue, GP2ConversionThresholdTag)
threshold, err := strconv.Atoi(*tagValue)
if err != nil {
log.Printf("Error parsing %v qs integer: %s\n", *tagValue, err.Error())
return false
}
debug.Println("Successfully parsed", GP2ConversionThresholdTag, "on the group", a.name, "overriding the default configuration")
a.config.GP2ConversionThreshold = int64(threshold)
return true
}
func (a *autoScalingGroup) loadBiddingPolicy(tagValue *string) (string, bool) {
biddingPolicy := *tagValue
if biddingPolicy != "aggressive" {
return DefaultBiddingPolicy, false
}
log.Printf("Loaded BiddingPolicy value with %s from tag %s\n", biddingPolicy, BiddingPolicyTag)
return biddingPolicy, true
}
func (a *autoScalingGroup) LoadCronSchedule() bool {
tagValue := a.getTagValue(ScheduleTag)
if tagValue != nil {
log.Printf("Loaded CronSchedule value %v from tag %v\n", *tagValue, ScheduleTag)
a.config.CronSchedule = *tagValue
return true
}
debug.Println("Couldn't find tag", ScheduleTag, "on the group", a.name, "using the default configuration")
a.config.CronSchedule = a.region.conf.CronSchedule
return false
}
func (a *autoScalingGroup) LoadCronTimezone() bool {
tagValue := a.getTagValue(TimezoneTag)
if tagValue != nil {
log.Printf("Loaded CronTimezone value %v from tag %v\n", *tagValue, TimezoneTag)
a.config.CronTimezone = *tagValue
return true
}
debug.Println("Couldn't find tag", TimezoneTag, "on the group", a.name, "using the default configuration")
a.config.CronTimezone = a.region.conf.CronTimezone
return false
}
func (a *autoScalingGroup) LoadCronScheduleState() bool {
tagValue := a.getTagValue(CronScheduleStateTag)
if tagValue != nil {
log.Printf("Loaded CronScheduleState value %v from tag %v\n", *tagValue, CronScheduleStateTag)
a.config.CronScheduleState = *tagValue
return true
}
debug.Println("Couldn't find tag", CronScheduleStateTag, "on the group", a.name, "using the default configuration")
a.config.CronScheduleState = a.region.conf.CronScheduleState
return false
}
func (a *autoScalingGroup) loadConfSpot() bool {
tagValue := a.getTagValue(BiddingPolicyTag)
if tagValue == nil {
debug.Println("Couldn't find tag", BiddingPolicyTag)
return false
}
if newValue, done := a.loadBiddingPolicy(tagValue); done {
a.region.conf.BiddingPolicy = newValue
debug.Println("BiddingPolicy =", a.region.conf.BiddingPolicy)
return done
}
return false
}
func (a *autoScalingGroup) loadConfSpotPrice() bool {
tagValue := a.getTagValue(SpotPriceBufferPercentageTag)
if tagValue == nil {
return false
}
newValue, done := a.loadSpotPriceBufferPercentage(tagValue)
if !done {
debug.Println("Couldn't find tag", SpotPriceBufferPercentageTag)
return false
}
a.region.conf.SpotPriceBufferPercentage = newValue
return done
}
func (a *autoScalingGroup) loadConfOnDemandPriceMultiplier() bool {
a.config.OnDemandPriceMultiplier = a.region.conf.OnDemandPriceMultiplier
tagValue := a.getTagValue(OnDemandPriceMultiplierTag)
if tagValue == nil {
return false
}
newValue, done := a.loadOnDemandPriceMultiplier(tagValue)
if !done {
debug.Println("Couldn't find tag", OnDemandPriceMultiplierTag)
return false
}
a.config.OnDemandPriceMultiplier = newValue
return done
}
// Add configuration of other elements here: prices, whitelisting, etc
func (a *autoScalingGroup) loadConfigFromTags() bool {
ret := false
if a.loadConfOnDemand() {
log.Println("Found and applied configuration for OnDemand value")
ret = true
}
if a.loadConfOnDemandPriceMultiplier() {
log.Println("Found and applied configuration for OnDemand Price Multiplier")
ret = true
}
if a.loadConfSpot() {
log.Println("Found and applied configuration for Spot Bid")
ret = true
}
if a.loadConfSpotPrice() {
log.Println("Found and applied configuration for Spot Price")
ret = true
}
if a.LoadCronSchedule() {
log.Println("Found and applied configuration for Cron Schedule")
ret = true
}
if a.LoadCronTimezone() {
log.Println("Found and applied configuration for Cron Timezone")
ret = true
}
if a.LoadCronScheduleState() {
log.Println("Found and applied configuration for Cron Schedule State")
ret = true
}
if a.loadPatchBeanstalkUserdata() {
log.Println("Found and applied configuration for Beanstalk Userdata")
ret = true
}
if a.loadGP2ConversionThreshold() {
log.Println("Found and applied configuration for GP2 Conversion Threshold")
ret = true
}
if a.loadSpotAllocationStrategy() {
log.Println("Found and applied configuration for Spot Allocation Strategy")
ret = true
}
if a.loadPrioritizedInstanceTypesBiasTag() {
log.Println("Found and applied configuration for Prioritized Instance Types Bias")
ret = true
}
return ret
}
func (a *autoScalingGroup) loadDefaultConfigNumber() (int64, bool) {
onDemand := a.region.conf.MinOnDemandNumber
if onDemand >= 0 && onDemand <= int64(a.instances.count()) {
log.Printf("Loaded default value %d from conf number.", onDemand)
return onDemand, true
}
log.Println("Ignoring default value out of range:", onDemand)
return DefaultMinOnDemandValue, false
}
func (a *autoScalingGroup) loadDefaultConfigPercentage() (int64, bool) {
percentage := a.region.conf.MinOnDemandPercentage
if percentage < 0 || percentage > 100 {
log.Printf("Ignoring default value out of range: %f", percentage)
return DefaultMinOnDemandValue, false
}
instanceNumber := a.instances.count()
onDemand := int64(math.Floor((float64(instanceNumber) * percentage / 100.0) + .5))
log.Printf("Loaded default value %d from conf percentage.", onDemand)
return onDemand, true
}
func (a *autoScalingGroup) loadDefaultConfig() bool {
done := false
a.config.MinOnDemand = DefaultMinOnDemandValue
if a.region.conf.SpotPriceBufferPercentage <= 0 {
a.region.conf.SpotPriceBufferPercentage = DefaultSpotPriceBufferPercentage
}
if a.region.conf.MinOnDemandNumber != 0 {
a.config.MinOnDemand, done = a.loadDefaultConfigNumber()
}
if !done && a.region.conf.MinOnDemandPercentage != 0 {
a.config.MinOnDemand, done = a.loadDefaultConfigPercentage()
} else {
log.Println("No default value for on-demand instances specified, skipping.")
}
return done
}