From c33e7cc8674e4208853ef7ee40b40251b4897cf1 Mon Sep 17 00:00:00 2001 From: Henry Huang Date: Thu, 8 May 2014 01:13:27 +0800 Subject: [PATCH] Add the support of launching spot instances in "amazon-ebs" AMI --- builder/amazon/common/run_config.go | 1 + builder/amazon/common/state.go | 26 +++++ .../amazon/common/step_run_source_instance.go | 94 +++++++++++++++---- builder/amazon/ebs/builder.go | 3 +- builder/amazon/ebs/step_stop_instance.go | 10 +- 5 files changed, 113 insertions(+), 21 deletions(-) diff --git a/builder/amazon/common/run_config.go b/builder/amazon/common/run_config.go index c50c22f7e..95fc7b3be 100644 --- a/builder/amazon/common/run_config.go +++ b/builder/amazon/common/run_config.go @@ -17,6 +17,7 @@ type RunConfig struct { InstanceType string `mapstructure:"instance_type"` RunTags map[string]string `mapstructure:"run_tags"` SourceAmi string `mapstructure:"source_ami"` + SpotPrice string `mapstructure:"spot_price"` RawSSHTimeout string `mapstructure:"ssh_timeout"` SSHUsername string `mapstructure:"ssh_username"` SSHPrivateKeyFile string `mapstructure:"ssh_private_key_file"` diff --git a/builder/amazon/common/state.go b/builder/amazon/common/state.go index 688a918a5..bab45d5db 100644 --- a/builder/amazon/common/state.go +++ b/builder/amazon/common/state.go @@ -81,6 +81,32 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) StateRefreshFunc { } } +// SpotRequestStateRefreshFunc returns a StateRefreshFunc that is used to watch +// a spot request for state changes. +func SpotRequestStateRefreshFunc(conn *ec2.EC2, spotRequestId string) StateRefreshFunc { + return func() (interface{}, string, error) { + resp, err := conn.DescribeSpotRequests([]string{spotRequestId}, ec2.NewFilter()) + if err != nil { + if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidSpotInstanceRequestID.NotFound" { + // Set this to nil as if we didn't find anything. + resp = nil + } else { + log.Printf("Error on SpotRequestStateRefresh: %s", err) + return nil, "", err + } + } + + if resp == nil || len(resp.SpotRequestResults) == 0 { + // Sometimes AWS has consistency issues and doesn't see the + // SpotRequest. Return an empty state. + return nil, "", nil + } + + i := resp.SpotRequestResults[0] + return i, i.State, nil + } +} + // WaitForState watches an object and waits for it to achieve a certain // state. func WaitForState(conf *StateChangeConf) (i interface{}, err error) { diff --git a/builder/amazon/common/step_run_source_instance.go b/builder/amazon/common/step_run_source_instance.go index c9827e589..60289af4c 100644 --- a/builder/amazon/common/step_run_source_instance.go +++ b/builder/amazon/common/step_run_source_instance.go @@ -10,6 +10,7 @@ import ( type StepRunSourceInstance struct { AssociatePublicIpAddress bool + SpotPrice string AvailabilityZone string BlockDevices BlockDevices Debug bool @@ -47,21 +48,6 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi securityGroups[n] = ec2.SecurityGroup{Id: securityGroupId} } - runOpts := &ec2.RunInstances{ - KeyName: keyName, - ImageId: s.SourceAMI, - InstanceType: s.InstanceType, - UserData: []byte(userData), - MinCount: 0, - MaxCount: 0, - SecurityGroups: securityGroups, - IamInstanceProfile: s.IamInstanceProfile, - SubnetId: s.SubnetId, - AssociatePublicIpAddress: s.AssociatePublicIpAddress, - BlockDevices: s.BlockDevices.BuildLaunchDevices(), - AvailZone: s.AvailabilityZone, - } - ui.Say("Launching a source AWS instance...") imageResp, err := ec2conn.Images([]string{s.SourceAMI}, ec2.NewFilter()) if err != nil { @@ -82,15 +68,85 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi return multistep.ActionHalt } - runResp, err := ec2conn.RunInstances(runOpts) + var instanceId []string + if s.SpotPrice == "" { + runOpts := &ec2.RunInstances{ + KeyName: keyName, + ImageId: s.SourceAMI, + InstanceType: s.InstanceType, + UserData: []byte(userData), + MinCount: 0, + MaxCount: 0, + SecurityGroups: securityGroups, + IamInstanceProfile: s.IamInstanceProfile, + SubnetId: s.SubnetId, + AssociatePublicIpAddress: s.AssociatePublicIpAddress, + BlockDevices: s.BlockDevices.BuildLaunchDevices(), + AvailZone: s.AvailabilityZone, + } + runResp, err := ec2conn.RunInstances(runOpts) + if err != nil { + err := fmt.Errorf("Error launching source instance: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + instanceId = []string{runResp.Instances[0].InstanceId} + + } else { + runOpts := &ec2.RequestSpotInstances{ + SpotPrice: s.SpotPrice, + KeyName: keyName, + ImageId: s.SourceAMI, + InstanceType: s.InstanceType, + UserData: []byte(userData), + SecurityGroups: securityGroups, + IamInstanceProfile: s.IamInstanceProfile, + SubnetId: s.SubnetId, + AssociatePublicIpAddress: s.AssociatePublicIpAddress, + BlockDevices: s.BlockDevices.BuildLaunchDevices(), + AvailZone: s.AvailabilityZone, + } + runSpotResp, err := ec2conn.RequestSpotInstances(runOpts) + if err != nil { + err := fmt.Errorf("Error launching source spot instance: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + spotRequestId := runSpotResp.SpotRequestResults[0].SpotRequestId + ui.Say(fmt.Sprintf("Waiting for spot request (%s) to become ready...", spotRequestId)) + stateChange := StateChangeConf{ + Pending: []string{"open"}, + Target: "active", + Refresh: SpotRequestStateRefreshFunc(ec2conn, spotRequestId), + StepState: state, + } + _, err = WaitForState(&stateChange) + if err != nil { + err := fmt.Errorf("Error waiting for spot request (%s) to become ready: %s", spotRequestId, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + spotResp, err := ec2conn.DescribeSpotRequests([]string{spotRequestId}, nil) + if err != nil { + err := fmt.Errorf("Error finding spot request (%s): %s", spotRequestId, err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + instanceId = []string{spotResp.SpotRequestResults[0].InstanceId} + } + + instanceResp, err := ec2conn.Instances(instanceId, nil) if err != nil { - err := fmt.Errorf("Error launching source instance: %s", err) + err := fmt.Errorf("Error finding source instance (%s): %s", instanceId, err) state.Put("error", err) ui.Error(err.Error()) return multistep.ActionHalt } - - s.instance = &runResp.Instances[0] + s.instance = &instanceResp.Reservations[0].Instances[0] ui.Message(fmt.Sprintf("Instance ID: %s", s.instance.InstanceId)) ec2Tags := make([]ec2.Tag, 1, len(s.Tags)+1) diff --git a/builder/amazon/ebs/builder.go b/builder/amazon/ebs/builder.go index 2e898671e..a5240a61b 100644 --- a/builder/amazon/ebs/builder.go +++ b/builder/amazon/ebs/builder.go @@ -96,6 +96,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe &awscommon.StepRunSourceInstance{ Debug: b.config.PackerDebug, ExpectedRootDevice: "ebs", + SpotPrice: b.config.SpotPrice, InstanceType: b.config.InstanceType, UserData: b.config.UserData, UserDataFile: b.config.UserDataFile, @@ -113,7 +114,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe SSHWaitTimeout: b.config.SSHTimeout(), }, &common.StepProvision{}, - &stepStopInstance{}, + &stepStopInstance{SpotPrice: b.config.SpotPrice}, &stepCreateAMI{}, &awscommon.StepAMIRegionCopy{ Regions: b.config.AMIRegions, diff --git a/builder/amazon/ebs/step_stop_instance.go b/builder/amazon/ebs/step_stop_instance.go index 8603fb807..c64e23c7d 100644 --- a/builder/amazon/ebs/step_stop_instance.go +++ b/builder/amazon/ebs/step_stop_instance.go @@ -8,13 +8,21 @@ import ( "github.com/mitchellh/packer/packer" ) -type stepStopInstance struct{} +type stepStopInstance struct{ + SpotPrice string +} func (s *stepStopInstance) Run(state multistep.StateBag) multistep.StepAction { ec2conn := state.Get("ec2").(*ec2.EC2) instance := state.Get("instance").(*ec2.Instance) ui := state.Get("ui").(packer.Ui) + // Skip when it is a spot instance + if s.SpotPrice != "" { + ui.Say(fmt.Sprintf("This is a spot instance, no need to stop for the AMI")) + return multistep.ActionContinue + } + // Stop the instance so we can create an AMI from it ui.Say("Stopping the source instance...") _, err := ec2conn.StopInstances(instance.InstanceId)