AutoSpotting/AutoSpotting

View on GitHub
core/config.go

Summary

Maintainability
B
4 hrs
Test Coverage
// Copyright (c) 2016-2022 Cristian Măgherușan-Stanciu
// Licensed under the Open Software License version 3.0

package autospotting

import (
    "fmt"
    "io"
    "log"
    "os"
    "time"

    "github.com/aws/aws-sdk-go/aws/endpoints"
    ec2instancesinfo "github.com/cristim/ec2-instances-info"
    "github.com/namsral/flag"
)

const (
    // AutoScalingTerminationMethod uses the TerminateInstanceInAutoScalingGroup
    // API method to terminate instances.  This method is recommended because it
    // will require termination Lifecycle Hooks that have been configured on the
    // Auto Scaling Group to be invoked before terminating the instance.  It's
    // also safe even if there are no such hooks configured.
    AutoScalingTerminationMethod = "autoscaling"

    // DetachTerminationMethod detaches the instance from the Auto Scaling Group
    // and then terminates it.  This method exists for historical reasons and is
    // no longer recommended.
    DetachTerminationMethod = "detach"

    // TerminateTerminationNotificationAction terminate the spot instance, which will be terminated
    // by AWS in 2 minutes, without reducing the ASG capacity, so that a new instance will
    // be launched. LifeCycle Hooks are triggered.
    TerminateTerminationNotificationAction = "terminate"

    // DetachTerminationNotificationAction detach the spot instance, which will be terminated
    // by AWS in 2 minutes, without reducing the ASG capacity, so that a new instance will
    // be launched. LifeCycle Hooks are not triggered.
    DetachTerminationNotificationAction = "detach"

    // AutoTerminationNotificationAction if ASG has a LifeCycleHook with LifecycleTransition = EC2_INSTANCE_TERMINATING
    // terminate the spot instance (as TerminateTerminationNotificationAction), if not detach it.
    AutoTerminationNotificationAction = "auto"

    // DefaultCronSchedule is the default value for the execution schedule in
    // simplified Cron-style definition the cron format only accepts the hour and
    // day of week fields, for example "9-18 1-5" would define the working week
    // hours. AutoSpotting will only run inside this time interval. The action can
    // also be reverted using the CronScheduleState parameter, so in order to run
    // outside this interval set the CronScheduleState to "off" either globally or
    // on a per-group override.
    DefaultCronSchedule = "* *"

    // Spot stores the string "spot"  to avoid typos as it's used in various places
    Spot = "spot"
    // OnDemand  stores the string "on-demand" to avoid typos as it's used in various places
    OnDemand = "on-demand"
    // DefaultGP2ConversionThreshold is the size under which GP3 is more performant than GP2 for both throughput and IOPS
    DefaultGP2ConversionThreshold = 170
)

// Config extends the AutoScalingConfig struct and in addition contains a
// number of global flags.
type Config struct {
    AutoScalingConfig

    // Static data fetched from ec2instances.info
    InstanceData *ec2instancesinfo.InstanceData

    // Logging
    LogFile io.Writer
    LogFlag int

    // The regions where it should be running, given as a single CSV-string
    Regions string

    // The region where the Lambda function is deployed
    MainRegion string

    // This is only here for tests, where we want to be able to somehow mock
    // time.Sleep without actually sleeping. While testing it defaults to 0 (which won't sleep at all), in
    // real-world usage it's expected to be set to 1
    SleepMultiplier time.Duration

    // Filter on ASG tags
    // for example: spot-enabled=true,environment=dev,team=interactive
    FilterByTags string
    // Controls how are the tags used to filter the groups.
    // Available options: 'opt-in' and 'opt-out', default: 'opt-in'
    TagFilteringMode string

    // The AutoSpotting version
    Version string

    // The percentage of the savings
    SavingsCut float64

    // The license of this AutoSpotting build - obsolete
    LicenseType string

    // Controls whether AutoSpotting patches Elastic Beanstalk UserData scripts to use
    // the instance role when calling CloudFormation helpers instead of the standard CloudFormation
    // authentication method
    PatchBeanstalkUserdata bool

    // JSON file containing event data used for locally simulating execution from Lambda.
    EventFile string

    // Final Recap String Array to show actions taken by ScheduleRun on ASGs
    FinalRecap map[string][]string

    // SQS Queue URl
    SQSQueueURL string

    // SQS MessageID
    sqsReceiptHandle string

    // DisableEventBasedInstanceReplacement forces execution in cron mode only
    DisableEventBasedInstanceReplacement bool

    // DisableInstanceRebalanceRecommendation disable the handling of Instance Rebalance Recommendation events.
    DisableInstanceRebalanceRecommendation bool

    // BillingOnly - only billing related actions will be taken, no instance replacement will be performed.
    BillingOnly bool
}

// ParseConfig loads configuration from command line flags, environments variables, and config files.
func ParseConfig(conf *Config) {

    // The use of FlagSet allows us to parse config multiple times, which is useful for unit tests.
    flagSet := flag.NewFlagSet("AutoSpotting", flag.ExitOnError)

    var region string

    if r := os.Getenv("AWS_REGION"); r != "" {
        region = r
    } else {
        region = endpoints.UsEast1RegionID
    }

    conf.LogFile = os.Stdout
    conf.LogFlag = log.Ldate | log.Ltime | log.Lshortfile

    log.SetOutput(conf.LogFile)
    log.SetFlags(conf.LogFlag)

    conf.MainRegion = region
    conf.SleepMultiplier = 1
    conf.sqsReceiptHandle = ""

    flagSet.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")

    flagSet.StringVar(&conf.BiddingPolicy, "bidding_policy", 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")

    flagSet.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")

    flagSet.StringVar(&conf.InstanceTerminationMethod, "instance_termination_method", DefaultInstanceTerminationMethod,
        "\n\tInstance termination method.  Must be one of '"+DefaultInstanceTerminationMethod+"' (default),\n"+
            "\t or 'detach' (compatibility mode, not recommended)\n")

    flagSet.StringVar(&conf.TerminationNotificationAction, "termination_notification_action", DefaultTerminationNotificationAction,
        "\n\tTermination Notification Action.\n"+
            "\tValid choices:\n"+
            "\t'"+DefaultTerminationNotificationAction+
            "' (terminate if lifecyclehook else detach) | 'terminate' (lifecyclehook triggered)"+
            " | 'detach' (lifecyclehook not triggered)\n")

    flagSet.Int64Var(&conf.MinOnDemandNumber, "min_on_demand_number", 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 "+OnDemandNumberLong+".\n")

    flagSet.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 "+OnDemandPercentageTag+
            "\n\tIt is ignored if min_on_demand_number is also set.\n")

    flagSet.Float64Var(&conf.OnDemandPriceMultiplier, "on_demand_price_multiplier", DefaultOnDemandPriceMultiplier,
        "\n\tMultiplier for the on-demand price. Numbers less than 1.0 are useful for volume discounts.\n"+
            "The tag "+OnDemandPriceMultiplierTag+" can be used to override this on a group level.\n"+
            "\tExample: ./AutoSpotting -on_demand_price_multiplier 0.6 will have the on-demand price "+
            "considered at 60% of the actual value.\n")

    flagSet.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")

    flagSet.Float64Var(&conf.SpotPriceBufferPercentage, "spot_price_buffer_percentage", 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 "+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")

    flagSet.StringVar(&conf.SpotProductDescription, "spot_product_description", 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) | Red Hat Enterprise Linux\n\tDefault value: "+DefaultSpotProductDescription+"\n")

    flagSet.Float64Var(&conf.SpotProductPremium, "spot_product_premium", DefaultSpotProductPremium,
        "\n\tThe Product Premium to apply to the on demand price to improve spot selection and savings calculations\n"+
            "\twhen using a premium instance type such as RHEL.")

    flagSet.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")

    flagSet.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")

    flagSet.StringVar(&conf.CronSchedule, "cron_schedule", DefaultCronSchedule, "\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")

    flagSet.StringVar(&conf.CronTimezone, "cron_timezone", "UTC", "\n\tTimezone to"+
        "\tperform(or not) spot replacement actions. Format: timezone\n"+
        "\tExample: ./AutoSpotting --cron_timezone 'Europe/London' \n")

    flagSet.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")

    flagSet.StringVar(&conf.LicenseType, "license", "evaluation", "\n\t - obsoleted, kept for compatibility only\n"+
        "\tExample: ./AutoSpotting --license evaluation\n")

    flagSet.StringVar(&conf.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")

    flagSet.StringVar(&conf.SQSQueueURL, "sqs_queue_url", "", "\n\tThe Url of the SQS fifo queue used to manage spot replacement actions. "+
        "This needs to exist in the same region as the main AutoSpotting Lambda function"+
        "\tExample: ./AutoSpotting --sqs_queue_url https://sqs.{AwsRegion}.amazonaws.com/{AccountId}/AutoSpotting.fifo\n")

    flagSet.BoolVar(&conf.PatchBeanstalkUserdata, "patch_beanstalk_userdata", false,
        "\n\tControls whether AutoSpotting patches Elastic Beanstalk UserData scripts to use the "+
            "instance role when calling CloudFormation helpers instead of the standard CloudFormation "+
            "authentication method\n"+
            "\tExample: ./AutoSpotting --patch_beanstalk_userdata true\n")

    flagSet.Int64Var(&conf.GP2ConversionThreshold, "ebs_gp2_conversion_threshold", DefaultGP2ConversionThreshold,
        "\n\tThe EBS volume size below which to automatically replace GP2 EBS volumes to the newer GP3 "+
            "volume type, that's 20% cheaper and more performant than GP2 for smaller sizes, but it's not "+
            "getting more performant wth size as GP2 does. Over 170 GB GP2 gets better throughput, and at "+
            "1TB GP2 also has better IOPS than a baseline GP3 volume.\n"+
            "\tExample: ./AutoSpotting --ebs_gp2_conversion_threshold 170\n")

    flagSet.BoolVar(&conf.DisableEventBasedInstanceReplacement, "disable_event_based_instance_replacement", false,
        "\n\tDisables the event based instance replacement, forcing the legacy cron mode.\n"+
            "\tExample: ./AutoSpotting --disable_event_based_instance_replacement=true\n")

    flagSet.BoolVar(&conf.DisableInstanceRebalanceRecommendation, "disable_instance_rebalance_recommendation", false,
        "\n\tDisables handling of instance rebalance recommendation events.\n"+
            "\tExample: ./AutoSpotting --disable_instance_rebalance_recommendation=true\n")

    flagSet.StringVar(&conf.SpotAllocationStrategy, "spot_allocation_strategy", "capacity-optimized-prioritized",
        "\n\tControls the Spot allocation strategy for launching Spot instances. Allowed options: \n"+
            "\t'capacity-optimized-prioritized' (default), 'capacity-optimized', 'lowest-price'.\n"+
            "\tFurther information on this is available at "+
            "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-fleet-allocation-strategy.html\n"+
            "\tExample: ./AutoSpotting --spot_allocation_strategy capacity-optimized-prioritized\n")

    flagSet.BoolVar(&conf.BillingOnly, "billing_only", false,
        "\n\tControls whether AutoSpotting only does the Marketplace billing without taking any further\n"+
            "replacement actions when executed in cron mode\n"+
            "\tExample: ./AutoSpotting --billing_only true\n")

    flagSet.StringVar(&conf.PrioritizedInstanceTypesBias, "prioritized_instance_types_bias", "lower_cost",
        "\n\tControls the ordering of instance types when using the capacity-optimized-prioritized\n"+
            "\tSpot allocation strategy. By default, using the 'lower_cost' bias it sorts instances by Spot price\n"+
            "\tAlternatively, you can bias towards newer instance types by using the 'prefer_newer_generations' bias\n"+
            "\tExample: ./AutoSpotting --prioritized_instance_types_bias lower_cost\n")

    printVersion := flagSet.Bool("version", false, "Print version number and exit.\n")

    if err := flagSet.Parse(os.Args[1:]); err != nil {
        fmt.Printf("Error parsing config: %s\n", err.Error())
    }

    if *printVersion {
        fmt.Println("AutoSpotting build:", conf.Version)
        os.Exit(0)
    }

    data, err := ec2instancesinfo.Data()
    if err != nil {
        log.Fatal(err.Error())
    }
    conf.InstanceData = data

    conf.FinalRecap = make(map[string][]string)
}