diff --git a/builder/amazon/common/run_config.go b/builder/amazon/common/run_config.go index db8a76488..94b508e60 100644 --- a/builder/amazon/common/run_config.go +++ b/builder/amazon/common/run_config.go @@ -73,6 +73,7 @@ type RunConfig struct { SecurityGroupIds []string `mapstructure:"security_group_ids"` SourceAmi string `mapstructure:"source_ami"` SourceAmiFilter AmiFilterOptions `mapstructure:"source_ami_filter"` + SpotInstanceTypes []string `mapstructure:"spot_instance_types"` SpotPrice string `mapstructure:"spot_price"` SpotPriceAutoProduct string `mapstructure:"spot_price_auto_product"` SpotTags map[string]string `mapstructure:"spot_tags"` @@ -137,8 +138,14 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error { errs = append(errs, fmt.Errorf("For security reasons, your source AMI filter must declare an owner.")) } - if c.InstanceType == "" { - errs = append(errs, fmt.Errorf("An instance_type must be specified")) + if c.InstanceType == "" && len(c.SpotInstanceTypes) == 0 { + errs = append(errs, fmt.Errorf("either instance_type or "+ + "spot_instance_types must be specified")) + } + + if c.InstanceType != "" && len(c.SpotInstanceTypes) > 0 { + errs = append(errs, fmt.Errorf("either instance_type or "+ + "spot_instance_types must be specified, not both")) } if c.BlockDurationMinutes%60 != 0 { diff --git a/builder/amazon/common/step_ami_region_copy_test.go b/builder/amazon/common/step_ami_region_copy_test.go index ee45ec708..7428d63b5 100644 --- a/builder/amazon/common/step_ami_region_copy_test.go +++ b/builder/amazon/common/step_ami_region_copy_test.go @@ -14,10 +14,6 @@ import ( "github.com/hashicorp/packer/packer" ) -func boolPointer(tf bool) *bool { - return &tf -} - // Define a mock struct to be used in unit tests for common aws steps. type mockEC2Conn struct { ec2iface.EC2API @@ -120,7 +116,7 @@ func TestStepAmiRegionCopy_false_encryption(t *testing.T) { Regions: make([]string, 0), AMIKmsKeyId: "", RegionKeyIds: make(map[string]string), - EncryptBootVolume: boolPointer(false), + EncryptBootVolume: aws.Bool(false), Name: "fake-ami-name", OriginalRegion: "us-east-1", } @@ -145,7 +141,7 @@ func TestStepAmiRegionCopy_true_encryption(t *testing.T) { Regions: make([]string, 0), AMIKmsKeyId: "", RegionKeyIds: make(map[string]string), - EncryptBootVolume: boolPointer(true), + EncryptBootVolume: aws.Bool(true), Name: "fake-ami-name", OriginalRegion: "us-east-1", } diff --git a/builder/amazon/common/step_run_spot_instance.go b/builder/amazon/common/step_run_spot_instance.go index 3bec69747..2ec57114c 100644 --- a/builder/amazon/common/step_run_spot_instance.go +++ b/builder/amazon/common/step_run_spot_instance.go @@ -13,6 +13,7 @@ import ( "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2/ec2iface" "github.com/hashicorp/packer/common/retry" "github.com/hashicorp/packer/helper/communicator" "github.com/hashicorp/packer/helper/multistep" @@ -35,6 +36,7 @@ type StepRunSpotInstance struct { SpotPrice string SpotPriceProduct string SpotTags TagMap + SpotInstanceTypes []string Tags TagMap VolumeTags TagMap UserData string @@ -45,56 +47,11 @@ type StepRunSpotInstance struct { spotRequest *ec2.SpotInstanceRequest } -func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { - ec2conn := state.Get("ec2").(*ec2.EC2) - securityGroupIds := aws.StringSlice(state.Get("securityGroupIds").([]string)) - ui := state.Get("ui").(packer.Ui) - - userData := s.UserData - if s.UserDataFile != "" { - contents, err := ioutil.ReadFile(s.UserDataFile) - if err != nil { - state.Put("error", fmt.Errorf("Problem reading user data file: %s", err)) - return multistep.ActionHalt - } - - userData = string(contents) - } - - // Test if it is encoded already, and if not, encode it - if _, err := base64.StdEncoding.DecodeString(userData); err != nil { - log.Printf("[DEBUG] base64 encoding user data...") - userData = base64.StdEncoding.EncodeToString([]byte(userData)) - } - - ui.Say("Launching a source AWS instance...") - image, ok := state.Get("source_image").(*ec2.Image) - if !ok { - state.Put("error", fmt.Errorf("source_image type assertion failed")) - return multistep.ActionHalt - } - s.SourceAMI = *image.ImageId - - if s.ExpectedRootDevice != "" && *image.RootDeviceType != s.ExpectedRootDevice { - state.Put("error", fmt.Errorf( - "The provided source AMI has an invalid root device type.\n"+ - "Expected '%s', got '%s'.", - s.ExpectedRootDevice, *image.RootDeviceType)) - return multistep.ActionHalt - } - +func (s *StepRunSpotInstance) CalculateSpotPrice(az string, ec2conn ec2iface.EC2API) (string, error) { + // Calculate the spot price for a given availability zone spotPrice := s.SpotPrice - azConfig := "" - if azRaw, ok := state.GetOk("availability_zone"); ok { - azConfig = azRaw.(string) - } - az := azConfig if spotPrice == "auto" { - ui.Message(fmt.Sprintf( - "Finding spot price for %s %s...", - s.SpotPriceProduct, s.InstanceType)) - // Detect the spot price startTime := time.Now().Add(-1 * time.Hour) resp, err := ec2conn.DescribeSpotPriceHistory(&ec2.DescribeSpotPriceHistoryInput{ @@ -104,10 +61,7 @@ func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag) StartTime: &startTime, }) if err != nil { - err := fmt.Errorf("Error finding spot price: %s", err) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt + return "", fmt.Errorf("Error finding spot price: %s", err) } var price float64 @@ -120,16 +74,13 @@ func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag) } if price == 0 || current < price { price = current - if azConfig == "" { + if az == "" { az = *history.AvailabilityZone } } } if price == 0 { - err := fmt.Errorf("No candidate spot prices found!") - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt + return "", fmt.Errorf("No candidate spot prices found!") } else { // Add 0.5 cents to minimum spot bid to ensure capacity will be available // Avoids price-too-low error in active markets which can fluctuate @@ -139,101 +90,232 @@ func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag) spotPrice = strconv.FormatFloat(price, 'f', -1, 64) } - var instanceId string + s.SpotPrice = spotPrice - ui.Say("Adding tags to source instance") - if _, exists := s.Tags["Name"]; !exists { - s.Tags["Name"] = "Packer Builder" - } + return spotPrice, nil - ec2Tags, err := s.Tags.EC2Tags(s.Ctx, *ec2conn.Config.Region, state) - if err != nil { - err := fmt.Errorf("Error tagging source instance: %s", err) - state.Put("error", err) - ui.Error(err.Error()) - return multistep.ActionHalt - } - ec2Tags.Report(ui) +} - ui.Message(fmt.Sprintf( - "Requesting spot instance '%s' for: %s", - s.InstanceType, spotPrice)) +func (s *StepRunSpotInstance) CreateTemplateData(userData *string, az string, + state multistep.StateBag, marketOptions *ec2.LaunchTemplateInstanceMarketOptionsRequest) *ec2.RequestLaunchTemplateData { + // Convert the BlockDeviceMapping into a + // LaunchTemplateBlockDeviceMappingRequest. These structs are identical, + // except for the EBS field -- on one, that field contains a + // LaunchTemplateEbsBlockDeviceRequest, and on the other, it contains an + // EbsBlockDevice. The EbsBlockDevice and + // LaunchTemplateEbsBlockDeviceRequest structs are themselves + // identical except for the struct's name, so you can cast one directly + // into the other. + blockDeviceMappings := s.BlockDevices.BuildLaunchDevices() + var launchMappingRequests []*ec2.LaunchTemplateBlockDeviceMappingRequest + for _, mapping := range blockDeviceMappings { + launchRequest := &ec2.LaunchTemplateBlockDeviceMappingRequest{ + DeviceName: mapping.DeviceName, + Ebs: (*ec2.LaunchTemplateEbsBlockDeviceRequest)(mapping.Ebs), + NoDevice: mapping.NoDevice, + VirtualName: mapping.VirtualName, + } + launchMappingRequests = append(launchMappingRequests, launchRequest) + } - runOpts := &ec2.RequestSpotLaunchSpecification{ - ImageId: &s.SourceAMI, - InstanceType: &s.InstanceType, - UserData: &userData, - IamInstanceProfile: &ec2.IamInstanceProfileSpecification{Name: &s.IamInstanceProfile}, - Placement: &ec2.SpotPlacement{ + // Create a launch template. + templateData := ec2.RequestLaunchTemplateData{ + BlockDeviceMappings: launchMappingRequests, + DisableApiTermination: aws.Bool(false), + EbsOptimized: &s.EbsOptimized, + IamInstanceProfile: &ec2.LaunchTemplateIamInstanceProfileSpecificationRequest{Name: &s.IamInstanceProfile}, + ImageId: &s.SourceAMI, + InstanceMarketOptions: marketOptions, + Placement: &ec2.LaunchTemplatePlacementRequest{ AvailabilityZone: &az, }, - BlockDeviceMappings: s.BlockDevices.BuildLaunchDevices(), - EbsOptimized: &s.EbsOptimized, + UserData: userData, } - + // Create a network interface + securityGroupIds := aws.StringSlice(state.Get("securityGroupIds").([]string)) subnetId := state.Get("subnet_id").(string) - if subnetId != "" && s.AssociatePublicIpAddress { - runOpts.NetworkInterfaces = []*ec2.InstanceNetworkInterfaceSpecification{ - { - DeviceIndex: aws.Int64(0), - AssociatePublicIpAddress: &s.AssociatePublicIpAddress, - SubnetId: &subnetId, - Groups: securityGroupIds, - DeleteOnTermination: aws.Bool(true), - }, + if subnetId != "" { + // Set up a full network interface + networkInterface := ec2.LaunchTemplateInstanceNetworkInterfaceSpecificationRequest{ + Groups: securityGroupIds, + DeleteOnTermination: aws.Bool(true), + DeviceIndex: aws.Int64(0), + SubnetId: aws.String(subnetId), } + if s.AssociatePublicIpAddress { + networkInterface.SetAssociatePublicIpAddress(s.AssociatePublicIpAddress) + } + templateData.SetNetworkInterfaces([]*ec2.LaunchTemplateInstanceNetworkInterfaceSpecificationRequest{&networkInterface}) } else { - runOpts.SubnetId = &subnetId - runOpts.SecurityGroupIds = securityGroupIds + templateData.SetSecurityGroupIds(securityGroupIds) + + } + + // If instance type is not set, we'll just pick the lowest priced instance + // available. + if s.InstanceType != "" { + templateData.SetInstanceType(s.InstanceType) } if s.Comm.SSHKeyPairName != "" { - runOpts.KeyName = &s.Comm.SSHKeyPairName + templateData.SetKeyName(s.Comm.SSHKeyPairName) } - spotInstanceInput := &ec2.RequestSpotInstancesInput{ - LaunchSpecification: runOpts, - SpotPrice: &spotPrice, + + return &templateData +} + +func (s *StepRunSpotInstance) LoadUserData() (string, error) { + userData := s.UserData + if s.UserDataFile != "" { + contents, err := ioutil.ReadFile(s.UserDataFile) + if err != nil { + return "", fmt.Errorf("Problem reading user data file: %s", err) + } + + userData = string(contents) } - if s.BlockDurationMinutes != 0 { - spotInstanceInput.BlockDurationMinutes = &s.BlockDurationMinutes + + // Test if it is encoded already, and if not, encode it + if _, err := base64.StdEncoding.DecodeString(userData); err != nil { + log.Printf("[DEBUG] base64 encoding user data...") + userData = base64.StdEncoding.EncodeToString([]byte(userData)) } + return userData, nil +} + +func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + ec2conn := state.Get("ec2").(*ec2.EC2) + ui := state.Get("ui").(packer.Ui) + + ui.Say("Launching a spot AWS instance...") - runSpotResp, err := ec2conn.RequestSpotInstances(spotInstanceInput) + // Get and validate the source AMI + image, ok := state.Get("source_image").(*ec2.Image) + if !ok { + state.Put("error", fmt.Errorf("source_image type assertion failed")) + return multistep.ActionHalt + } + s.SourceAMI = *image.ImageId + + if s.ExpectedRootDevice != "" && *image.RootDeviceType != s.ExpectedRootDevice { + state.Put("error", fmt.Errorf( + "The provided source AMI has an invalid root device type.\n"+ + "Expected '%s', got '%s'.", + s.ExpectedRootDevice, *image.RootDeviceType)) + return multistep.ActionHalt + } + + azConfig := "" + if azRaw, ok := state.GetOk("availability_zone"); ok { + azConfig = azRaw.(string) + } + az := azConfig + + ui.Message(fmt.Sprintf("Finding spot price for %s %s...", + s.SpotPriceProduct, s.InstanceType)) + spotPrice, err := s.CalculateSpotPrice(az, ec2conn) if err != nil { - err := fmt.Errorf("Error launching source spot instance: %s", err) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt } - s.spotRequest = runSpotResp.SpotInstanceRequests[0] + ui.Message(fmt.Sprintf("Determined spot instance price of: %s.", spotPrice)) + + var instanceId string + + ui.Say("Interpolating tags for spot instance...") + // s.Tags will tag the eventually launched instance + // s.SpotTags apply to the spot request itself, and do not automatically + // get applied to the spot instance that is launched once the request is + // fulfilled + if _, exists := s.Tags["Name"]; !exists { + s.Tags["Name"] = "Packer Builder" + } - spotRequestId := s.spotRequest.SpotInstanceRequestId - ui.Message(fmt.Sprintf("Waiting for spot request (%s) to become active...", *spotRequestId)) - err = WaitUntilSpotRequestFulfilled(ctx, ec2conn, *spotRequestId) + // Convert tags from the tag map provided by the user into *ec2.Tag s + ec2Tags, err := s.Tags.EC2Tags(s.Ctx, *ec2conn.Config.Region, state) if err != nil { - err := fmt.Errorf("Error waiting for spot request (%s) to become ready: %s", *spotRequestId, err) + err := fmt.Errorf("Error generating tags for source instance: %s", err) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt } + // This prints the tags to the ui; it doesn't actually add them to the + // instance yet + ec2Tags.Report(ui) - spotResp, err := ec2conn.DescribeSpotInstanceRequests(&ec2.DescribeSpotInstanceRequestsInput{ - SpotInstanceRequestIds: []*string{spotRequestId}, - }) + spotOptions := ec2.LaunchTemplateSpotMarketOptionsRequest{ + MaxPrice: &s.SpotPrice, + } + if s.BlockDurationMinutes != 0 { + spotOptions.BlockDurationMinutes = &s.BlockDurationMinutes + } + marketOptions := &ec2.LaunchTemplateInstanceMarketOptionsRequest{ + SpotOptions: &spotOptions, + } + marketOptions.SetMarketType(ec2.MarketTypeSpot) + + // Create a launch template for the instance + ui.Message("Loading User Data File...") + userData, err := s.LoadUserData() + if err != nil { + state.Put("error", err) + return multistep.ActionHalt + } + ui.Message("Creating Spot Fleet launch template...") + templateData := s.CreateTemplateData(&userData, az, state, marketOptions) + launchTemplate := &ec2.CreateLaunchTemplateInput{ + LaunchTemplateData: templateData, + LaunchTemplateName: aws.String("packer-fleet-launch-template"), + VersionDescription: aws.String("template generated by packer for launching spot instances"), + } + + // Tell EC2 to create the template + _, err = ec2conn.CreateLaunchTemplate(launchTemplate) if err != nil { - err := fmt.Errorf("Error finding spot request (%s): %s", *spotRequestId, err) + err := fmt.Errorf("Error creating launch template for spot instance: %s", err) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt } - instanceId = *spotResp.SpotInstanceRequests[0].InstanceId - // Tag spot instance request + // Add overrides for each user-provided instance type + var overrides []*ec2.FleetLaunchTemplateOverridesRequest + for _, instanceType := range s.SpotInstanceTypes { + override := ec2.FleetLaunchTemplateOverridesRequest{ + InstanceType: aws.String(instanceType), + } + overrides = append(overrides, &override) + } + + createFleetInput := &ec2.CreateFleetInput{ + LaunchTemplateConfigs: []*ec2.FleetLaunchTemplateConfigRequest{ + { + LaunchTemplateSpecification: &ec2.FleetLaunchTemplateSpecificationRequest{ + LaunchTemplateName: aws.String("packer-fleet-launch-template"), + Version: aws.String("1"), + }, + Overrides: overrides, + }, + }, + ReplaceUnhealthyInstances: aws.Bool(false), + TargetCapacitySpecification: &ec2.TargetCapacitySpecificationRequest{ + TotalTargetCapacity: aws.Int64(1), + DefaultTargetCapacityType: aws.String("spot"), + }, + Type: aws.String("instant"), + } + + // Create the request for the spot instance. + req, createOutput := ec2conn.CreateFleetRequest(createFleetInput) + ui.Message(fmt.Sprintf("Sending spot request (%s)...", req.RequestID)) + + // Tag the spot instance request (not the eventual spot instance) spotTags, err := s.SpotTags.EC2Tags(s.Ctx, *ec2conn.Config.Region, state) if err != nil { - err := fmt.Errorf("Error tagging spot request: %s", err) + err := fmt.Errorf("Error generating tags for spot request: %s", err) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt @@ -248,7 +330,7 @@ func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag) }.Run(ctx, func(ctx context.Context) error { _, err := ec2conn.CreateTags(&ec2.CreateTagsInput{ Tags: spotTags, - Resources: []*string{spotRequestId}, + Resources: []*string{aws.String(req.RequestID)}, }) return err }) @@ -260,21 +342,29 @@ func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag) } } - // Set the instance ID so that the cleanup works properly - s.instanceId = instanceId - - ui.Message(fmt.Sprintf("Instance ID: %s", instanceId)) - ui.Say(fmt.Sprintf("Waiting for instance (%v) to become ready...", instanceId)) - describeInstance := &ec2.DescribeInstancesInput{ - InstanceIds: []*string{aws.String(instanceId)}, + // Actually send the spot connection request. + err = req.Send() + if err != nil { + err := fmt.Errorf("Error waiting for spot request (%s) to become ready: %s", req.RequestID, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt } - if err := ec2conn.WaitUntilInstanceRunningWithContext(ctx, describeInstance); err != nil { - err := fmt.Errorf("Error waiting for instance (%s) to become ready: %s", instanceId, err) + + if len(createOutput.Errors) > 0 { + err := fmt.Errorf("error sending spot request: %s", *createOutput.Errors[0]) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt } + instanceId = *createOutput.Instances[0].InstanceIds[0] + + // Set the instance ID so that the cleanup works properly + s.instanceId = instanceId + + ui.Message(fmt.Sprintf("Instance ID: %s", instanceId)) + r, err := ec2conn.DescribeInstances(&ec2.DescribeInstancesInput{ InstanceIds: []*string{aws.String(instanceId)}, }) @@ -401,4 +491,12 @@ func (s *StepRunSpotInstance) Cleanup(state multistep.StateBag) { ui.Error(err.Error()) } } + + // Delete the launch template used to create the spot fleet + deleteInput := &ec2.DeleteLaunchTemplateInput{ + LaunchTemplateName: aws.String("packer-fleet-launch-template"), + } + if _, err := ec2conn.DeleteLaunchTemplate(deleteInput); err != nil { + ui.Error(err.Error()) + } } diff --git a/builder/amazon/common/step_run_spot_instance_test.go b/builder/amazon/common/step_run_spot_instance_test.go new file mode 100644 index 000000000..0139e0b5a --- /dev/null +++ b/builder/amazon/common/step_run_spot_instance_test.go @@ -0,0 +1,131 @@ +package common + +import ( + "bytes" + "strconv" + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2/ec2iface" + "github.com/hashicorp/packer/helper/communicator" + "github.com/hashicorp/packer/helper/multistep" + "github.com/hashicorp/packer/packer" +) + +// Define a mock struct to be used in unit tests for common aws steps. +type mockEC2ConnSpot struct { + ec2iface.EC2API + Config *aws.Config + + // Counters to figure out what code path was taken + describeSpotPriceHistoryCount int +} + +// Generates fake SpotPriceHistory data and returns it in the expected output +// format. Also increments a +func (m *mockEC2ConnSpot) DescribeSpotPriceHistory(copyInput *ec2.DescribeSpotPriceHistoryInput) (*ec2.DescribeSpotPriceHistoryOutput, error) { + m.describeSpotPriceHistoryCount++ + testTime := time.Now().Add(-1 * time.Hour) + sp := []*ec2.SpotPrice{ + { + AvailabilityZone: aws.String("us-east-1c"), + InstanceType: aws.String("t2.micro"), + ProductDescription: aws.String("Linux/UNIX"), + SpotPrice: aws.String("0.003500"), + Timestamp: &testTime, + }, + { + AvailabilityZone: aws.String("us-east-1f"), + InstanceType: aws.String("t2.micro"), + ProductDescription: aws.String("Linux/UNIX"), + SpotPrice: aws.String("0.003500"), + Timestamp: &testTime, + }, + { + AvailabilityZone: aws.String("us-east-1b"), + InstanceType: aws.String("t2.micro"), + ProductDescription: aws.String("Linux/UNIX"), + SpotPrice: aws.String("0.003500"), + Timestamp: &testTime, + }, + } + output := &ec2.DescribeSpotPriceHistoryOutput{SpotPriceHistory: sp} + + return output, nil + +} + +func getMockConnSpot() ec2iface.EC2API { + mockConn := &mockEC2ConnSpot{ + Config: aws.NewConfig(), + } + + return mockConn +} + +// Create statebag for running test +func tStateSpot() multistep.StateBag { + state := new(multistep.BasicStateBag) + state.Put("ui", &packer.BasicUi{ + Reader: new(bytes.Buffer), + Writer: new(bytes.Buffer), + }) + state.Put("availability_zone", "us-east-1c") + state.Put("securityGroupIds", []string{"sg-0b8984db72f213dc3"}) + state.Put("subnet_id", "subnet-077fde4e") + state.Put("source_image", "") + return state +} + +func getBasicStep() *StepRunSpotInstance { + stepRunSpotInstance := StepRunSpotInstance{ + AssociatePublicIpAddress: false, + BlockDevices: BlockDevices{ + AMIBlockDevices: AMIBlockDevices{ + AMIMappings: []BlockDevice(nil), + }, + LaunchBlockDevices: LaunchBlockDevices{ + LaunchMappings: []BlockDevice(nil), + }, + }, + BlockDurationMinutes: 0, + Debug: false, + Comm: &communicator.Config{ + SSHKeyPairName: "foo", + }, + EbsOptimized: false, + ExpectedRootDevice: "ebs", + IamInstanceProfile: "", + InstanceInitiatedShutdownBehavior: "stop", + InstanceType: "t2.micro", + SourceAMI: "", + SpotPrice: "auto", + SpotPriceProduct: "Linux/UNIX", + SpotTags: TagMap(nil), + Tags: TagMap{}, + VolumeTags: TagMap(nil), + UserData: "", + UserDataFile: "", + } + + return &stepRunSpotInstance +} +func TestCalculateSpotPrice(t *testing.T) { + stepRunSpotInstance := getBasicStep() + // Set spot price and spot price product + stepRunSpotInstance.SpotPrice = "auto" + stepRunSpotInstance.SpotPriceProduct = "Linux/UNIX" + ec2conn := getMockConnSpot() + // state := tStateSpot() + spotPrice, err := stepRunSpotInstance.CalculateSpotPrice("", ec2conn) + if err != nil { + t.Fatalf("Should not have had an error calculating spot price") + } + sp, _ := strconv.ParseFloat(spotPrice, 64) + expected := 0.008500 + if sp != expected { // 0.003500 (from spot history) + .005 + t.Fatalf("Expected spot price of \"0.008500\", not %s", spotPrice) + } +} diff --git a/builder/amazon/ebs/builder.go b/builder/amazon/ebs/builder.go index 6e045d86a..d847f9d57 100644 --- a/builder/amazon/ebs/builder.go +++ b/builder/amazon/ebs/builder.go @@ -124,6 +124,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack SpotPriceProduct: b.config.SpotPriceAutoProduct, SpotTags: b.config.SpotTags, Tags: b.config.RunTags, + SpotInstanceTypes: b.config.SpotInstanceTypes, UserData: b.config.UserData, UserDataFile: b.config.UserDataFile, VolumeTags: b.config.VolumeRunTags, diff --git a/builder/amazon/ebssurrogate/builder.go b/builder/amazon/ebssurrogate/builder.go index 6c6c6a130..31fec0bb1 100644 --- a/builder/amazon/ebssurrogate/builder.go +++ b/builder/amazon/ebssurrogate/builder.go @@ -152,6 +152,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack SourceAMI: b.config.SourceAmi, SpotPrice: b.config.SpotPrice, SpotPriceProduct: b.config.SpotPriceAutoProduct, + SpotInstanceTypes: b.config.SpotInstanceTypes, SpotTags: b.config.SpotTags, Tags: b.config.RunTags, UserData: b.config.UserData, diff --git a/builder/amazon/ebsvolume/builder.go b/builder/amazon/ebsvolume/builder.go index 93c425377..c7d7b539c 100644 --- a/builder/amazon/ebsvolume/builder.go +++ b/builder/amazon/ebsvolume/builder.go @@ -122,6 +122,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack SourceAMI: b.config.SourceAmi, SpotPrice: b.config.SpotPrice, SpotPriceProduct: b.config.SpotPriceAutoProduct, + SpotInstanceTypes: b.config.SpotInstanceTypes, SpotTags: b.config.SpotTags, Tags: b.config.RunTags, UserData: b.config.UserData, diff --git a/builder/amazon/instance/builder.go b/builder/amazon/instance/builder.go index 41bad000b..d15498694 100644 --- a/builder/amazon/instance/builder.go +++ b/builder/amazon/instance/builder.go @@ -202,6 +202,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack InstanceType: b.config.InstanceType, SourceAMI: b.config.SourceAmi, SpotPrice: b.config.SpotPrice, + SpotInstanceTypes: b.config.SpotInstanceTypes, SpotPriceProduct: b.config.SpotPriceAutoProduct, Tags: b.config.RunTags, SpotTags: b.config.SpotTags, diff --git a/website/source/docs/builders/amazon-ebs.html.md.erb b/website/source/docs/builders/amazon-ebs.html.md.erb index a096a9998..27b40f803 100644 --- a/website/source/docs/builders/amazon-ebs.html.md.erb +++ b/website/source/docs/builders/amazon-ebs.html.md.erb @@ -112,8 +112,8 @@ builder. of `source_ami`. Can be `paravirtual` or `hvm`. - `associate_public_ip_address` (boolean) - If using a non-default VPC, - public IP addresses are not provided by default. If this is toggled, your - new instance will get a Public IP. + public IP addresses are not provided by default. If this is `true`, your + new instance will get a Public IP. default: `false` - `availability_zone` (string) - Destination availability zone to launch instance in. Leave this empty to allow Amazon to auto-assign. @@ -352,22 +352,7 @@ builder. criteria provided in `source_ami_filter`; this pins the AMI returned by the filter, but will cause Packer to fail if the `source_ami` does not exist. -- `spot_price` (string) - The maximum hourly price to pay for a spot instance - to create the AMI. Spot instances are a type of instance that EC2 starts - when the current spot price is less than the maximum price you specify. - Spot price will be updated based on available spot instance capacity and - current spot instance requests. It may save you some costs. You can set - this to `auto` for Packer to automatically discover the best spot price or - to "0" to use an on demand instance (default). - -- `spot_price_auto_product` (string) - Required if `spot_price` is set to - `auto`. This tells Packer what sort of AMI you're launching to find the - best spot price. This must be one of: `Linux/UNIX`, `SUSE Linux`, - `Windows`, `Linux/UNIX (Amazon VPC)`, `SUSE Linux (Amazon VPC)`, - `Windows (Amazon VPC)` - -- `spot_tags` (object of key/value strings) - Requires `spot_price` to be - set. This tells Packer to apply tags to the spot request that is issued. +<%= partial "partials/builders/aws-spot-docs" %> - `sriov_support` (boolean) - Enable enhanced networking (SriovNetSupport but not ENA) on HVM-compatible AMIs. If true, add `ec2:ModifyInstanceAttribute` diff --git a/website/source/docs/builders/amazon-ebssurrogate.html.md.erb b/website/source/docs/builders/amazon-ebssurrogate.html.md.erb index 448da2686..cc1f7fcdb 100644 --- a/website/source/docs/builders/amazon-ebssurrogate.html.md.erb +++ b/website/source/docs/builders/amazon-ebssurrogate.html.md.erb @@ -104,8 +104,8 @@ builder. of `source_ami`. Can be `paravirtual` or `hvm`. - `associate_public_ip_address` (boolean) - If using a non-default VPC, - public IP addresses are not provided by default. If this is toggled, your - new instance will get a Public IP. + public IP addresses are not provided by default. If this is `true`, your + new instance will get a Public IP. default: `false` - `availability_zone` (string) - Destination availability zone to launch instance in. Leave this empty to allow Amazon to auto-assign. @@ -353,22 +353,7 @@ builder. criteria provided in `source_ami_filter`; this pins the AMI returned by the filter, but will cause Packer to fail if the `source_ami` does not exist. -- `spot_price` (string) - The maximum hourly price to pay for a spot instance - to create the AMI. Spot instances are a type of instance that EC2 starts - when the current spot price is less than the maximum price you specify. - Spot price will be updated based on available spot instance capacity and - current spot instance requests. It may save you some costs. You can set - this to `auto` for Packer to automatically discover the best spot price or - to "0" to use an on demand instance (default). - -- `spot_price_auto_product` (string) - Required if `spot_price` is set to - `auto`. This tells Packer what sort of AMI you're launching to find the - best spot price. This must be one of: `Linux/UNIX`, `SUSE Linux`, - `Windows`, `Linux/UNIX (Amazon VPC)`, `SUSE Linux (Amazon VPC)`, - `Windows (Amazon VPC)` - -- `spot_tags` (object of key/value strings) - Requires `spot_price` to be - set. This tells Packer to apply tags to the spot request that is issued. +<%= partial "partials/builders/aws-spot-docs" %> - `sriov_support` (boolean) - Enable enhanced networking (SriovNetSupport but not ENA) on HVM-compatible AMIs. If true, add `ec2:ModifyInstanceAttribute` diff --git a/website/source/docs/builders/amazon-ebsvolume.html.md.erb b/website/source/docs/builders/amazon-ebsvolume.html.md.erb index 2c643c9ba..c9e5d885c 100644 --- a/website/source/docs/builders/amazon-ebsvolume.html.md.erb +++ b/website/source/docs/builders/amazon-ebsvolume.html.md.erb @@ -107,8 +107,8 @@ builder. data](#build-template-data) for more information. - `associate_public_ip_address` (boolean) - If using a non-default VPC, - public IP addresses are not provided by default. If this is toggled, your - new instance will get a Public IP. + public IP addresses are not provided by default. If this is `true`, your + new instance will get a Public IP. default: `false` - `availability_zone` (string) - Destination availability zone to launch instance in. Leave this empty to allow Amazon to auto-assign. @@ -302,22 +302,7 @@ builder. criteria provided in `source_ami_filter`; this pins the AMI returned by the filter, but will cause Packer to fail if the `source_ami` does not exist. -- `spot_price` (string) - The maximum hourly price to pay for a spot instance - to create the AMI. Spot instances are a type of instance that EC2 starts - when the current spot price is less than the maximum price you specify. - Spot price will be updated based on available spot instance capacity and - current spot instance requests. It may save you some costs. You can set - this to `auto` for Packer to automatically discover the best spot price or - to `0` to use an on-demand instance (default). - -- `spot_price_auto_product` (string) - Required if `spot_price` is set to - `auto`. This tells Packer what sort of AMI you're launching to find the - best spot price. This must be one of: `Linux/UNIX`, `SUSE Linux`, - `Windows`, `Linux/UNIX (Amazon VPC)`, `SUSE Linux (Amazon VPC)` or - `Windows (Amazon VPC)` - -- `spot_tags` (object of key/value strings) - Requires `spot_price` to be - set. This tells Packer to apply tags to the spot request that is issued. +<%= partial "partials/builders/aws-spot-docs" %> - `sriov_support` (boolean) - Enable enhanced networking (SriovNetSupport but not ENA) on HVM-compatible AMIs. If true, add `ec2:ModifyInstanceAttribute` diff --git a/website/source/docs/builders/amazon-instance.html.md.erb b/website/source/docs/builders/amazon-instance.html.md.erb index e6f28d99b..7c8048a32 100644 --- a/website/source/docs/builders/amazon-instance.html.md.erb +++ b/website/source/docs/builders/amazon-instance.html.md.erb @@ -131,8 +131,8 @@ builder. `paravirtual` (default) or `hvm`. - `associate_public_ip_address` (boolean) - If using a non-default VPC, - public IP addresses are not provided by default. If this is toggled, your - new instance will get a Public IP. + public IP addresses are not provided by default. If this is `true`, your + new instance will get a Public IP. default: `false` - `availability_zone` (string) - Destination availability zone to launch instance in. Leave this empty to allow Amazon to auto-assign. @@ -343,22 +343,7 @@ builder. - `snapshot_tags` (object of key/value strings) - Tags to apply to snapshot. They will override AMI tags if already applied to snapshot. -- `spot_price` (string) - The maximum hourly price to launch a spot instance - to create the AMI. It is a type of instances that EC2 starts when the - maximum price that you specify exceeds the current spot price. Spot price - will be updated based on available spot instance capacity and current spot - Instance requests. It may save you some costs. You can set this to `auto` - for Packer to automatically discover the best spot price or to `0` to use - an on-demand instance (default). - -- `spot_price_auto_product` (string) - Required if `spot_price` is set to - `auto`. This tells Packer what sort of AMI you're launching to find the - best spot price. This must be one of: `Linux/UNIX`, `SUSE Linux`, - `Windows`, `Linux/UNIX (Amazon VPC)`, `SUSE Linux (Amazon VPC)`, - `Windows (Amazon VPC)` - -- `spot_tags` (object of key/value strings) - Requires `spot_price` to be - set. This tells Packer to apply tags to the spot request that is issued. +<%= partial "partials/builders/aws-spot-docs" %> - `sriov_support` (boolean) - Enable enhanced networking (SriovNetSupport but not ENA) on HVM-compatible AMIs. If true, add `ec2:ModifyInstanceAttribute` diff --git a/website/source/partials/builders/_aws-spot-docs.html.md b/website/source/partials/builders/_aws-spot-docs.html.md new file mode 100644 index 000000000..c68731230 --- /dev/null +++ b/website/source/partials/builders/_aws-spot-docs.html.md @@ -0,0 +1,26 @@ +- `spot_instance_types` (array of strings) - a list of acceptable instance + types to run your build on. We will request a spot instance using the max + price of `spot_price` and the allocation strategy of "lowest price". + Your instance will be launched on an instance type of the lowest available + price that you have in your list. This is used in place of instance_type. + You may only set either spot_instance_types or instance_type, not both. + This feature exists to help prevent situations where a Packer build fails + because a particular availability zone does not have capacity for the + specific instance_type requested in instance_type. + +- `spot_price` (string) - The maximum hourly price to pay for a spot instance + to create the AMI. Spot instances are a type of instance that EC2 starts + when the current spot price is less than the maximum price you specify. + Spot price will be updated based on available spot instance capacity and + current spot instance requests. It may save you some costs. You can set + this to `auto` for Packer to automatically discover the best spot price or + to "0" to use an on demand instance (default). + +- `spot_price_auto_product` (string) - Required if `spot_price` is set to + `auto`. This tells Packer what sort of AMI you're launching to find the + best spot price. This must be one of: `Linux/UNIX`, `SUSE Linux`, + `Windows`, `Linux/UNIX (Amazon VPC)`, `SUSE Linux (Amazon VPC)`, + `Windows (Amazon VPC)` + +- `spot_tags` (object of key/value strings) - Requires `spot_price` to be + set. This tells Packer to apply tags to the spot request that is issued. \ No newline at end of file