Merge branch 'add-spot-instance-support' of github.com:henrysher/packer into henrysher-add-spot-instance-support
Conflicts: builder/amazon/common/run_config.go builder/amazon/ebs/builder.go builder/amazon/instance/builder.go
This commit is contained in:
commit
3a69b8c1b8
|
@ -17,6 +17,7 @@ type RunConfig struct {
|
||||||
InstanceType string `mapstructure:"instance_type"`
|
InstanceType string `mapstructure:"instance_type"`
|
||||||
RunTags map[string]string `mapstructure:"run_tags"`
|
RunTags map[string]string `mapstructure:"run_tags"`
|
||||||
SourceAmi string `mapstructure:"source_ami"`
|
SourceAmi string `mapstructure:"source_ami"`
|
||||||
|
SpotPrice string `mapstructure:"spot_price"`
|
||||||
RawSSHTimeout string `mapstructure:"ssh_timeout"`
|
RawSSHTimeout string `mapstructure:"ssh_timeout"`
|
||||||
SSHUsername string `mapstructure:"ssh_username"`
|
SSHUsername string `mapstructure:"ssh_username"`
|
||||||
SSHPrivateKeyFile string `mapstructure:"ssh_private_key_file"`
|
SSHPrivateKeyFile string `mapstructure:"ssh_private_key_file"`
|
||||||
|
@ -46,6 +47,7 @@ func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error {
|
||||||
templates := map[string]*string{
|
templates := map[string]*string{
|
||||||
"iam_instance_profile": &c.IamInstanceProfile,
|
"iam_instance_profile": &c.IamInstanceProfile,
|
||||||
"instance_type": &c.InstanceType,
|
"instance_type": &c.InstanceType,
|
||||||
|
"spot_price": &c.SpotPrice,
|
||||||
"ssh_timeout": &c.RawSSHTimeout,
|
"ssh_timeout": &c.RawSSHTimeout,
|
||||||
"ssh_username": &c.SSHUsername,
|
"ssh_username": &c.SSHUsername,
|
||||||
"ssh_private_key_file": &c.SSHPrivateKeyFile,
|
"ssh_private_key_file": &c.SSHPrivateKeyFile,
|
||||||
|
|
|
@ -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
|
// WaitForState watches an object and waits for it to achieve a certain
|
||||||
// state.
|
// state.
|
||||||
func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
|
func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
|
||||||
|
@ -125,8 +151,8 @@ func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
fmt.Errorf("unexpected state '%s', wanted target '%s'", currentState, conf.Target)
|
err := fmt.Errorf("unexpected state '%s', wanted target '%s'", currentState, conf.Target)
|
||||||
return
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
|
|
||||||
type StepRunSourceInstance struct {
|
type StepRunSourceInstance struct {
|
||||||
AssociatePublicIpAddress bool
|
AssociatePublicIpAddress bool
|
||||||
|
SpotPrice string
|
||||||
AvailabilityZone string
|
AvailabilityZone string
|
||||||
BlockDevices BlockDevices
|
BlockDevices BlockDevices
|
||||||
Debug bool
|
Debug bool
|
||||||
|
@ -21,7 +22,7 @@ type StepRunSourceInstance struct {
|
||||||
Tags map[string]string
|
Tags map[string]string
|
||||||
UserData string
|
UserData string
|
||||||
UserDataFile string
|
UserDataFile string
|
||||||
|
spotRequest *ec2.SpotRequestResult
|
||||||
instance *ec2.Instance
|
instance *ec2.Instance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,21 +48,6 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
|
||||||
securityGroups[n] = ec2.SecurityGroup{Id: securityGroupId}
|
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...")
|
ui.Say("Launching a source AWS instance...")
|
||||||
imageResp, err := ec2conn.Images([]string{s.SourceAMI}, ec2.NewFilter())
|
imageResp, err := ec2conn.Images([]string{s.SourceAMI}, ec2.NewFilter())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -82,6 +68,22 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
|
||||||
return multistep.ActionHalt
|
return multistep.ActionHalt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
runResp, err := ec2conn.RunInstances(runOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := fmt.Errorf("Error launching source instance: %s", err)
|
err := fmt.Errorf("Error launching source instance: %s", err)
|
||||||
|
@ -89,8 +91,62 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
|
||||||
ui.Error(err.Error())
|
ui.Error(err.Error())
|
||||||
return multistep.ActionHalt
|
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
|
||||||
|
}
|
||||||
|
s.spotRequest = &runSpotResp.SpotRequestResults[0]
|
||||||
|
spotRequestId := s.spotRequest.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}
|
||||||
|
}
|
||||||
|
|
||||||
s.instance = &runResp.Instances[0]
|
instanceResp, err := ec2conn.Instances(instanceId, nil)
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("Error finding source instance (%s): %s", instanceId, err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
s.instance = &instanceResp.Reservations[0].Instances[0]
|
||||||
ui.Message(fmt.Sprintf("Instance ID: %s", s.instance.InstanceId))
|
ui.Message(fmt.Sprintf("Instance ID: %s", s.instance.InstanceId))
|
||||||
|
|
||||||
ui.Say(fmt.Sprintf("Waiting for instance (%s) to become ready...", s.instance.InstanceId))
|
ui.Say(fmt.Sprintf("Waiting for instance (%s) to become ready...", s.instance.InstanceId))
|
||||||
|
@ -142,19 +198,35 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StepRunSourceInstance) Cleanup(state multistep.StateBag) {
|
func (s *StepRunSourceInstance) Cleanup(state multistep.StateBag) {
|
||||||
if s.instance == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||||
ui := state.Get("ui").(packer.Ui)
|
ui := state.Get("ui").(packer.Ui)
|
||||||
|
|
||||||
|
// Cancel the spot request if it exists
|
||||||
|
if s.spotRequest != nil {
|
||||||
|
ui.Say("Cancelling the spot request...")
|
||||||
|
if _, err := ec2conn.CancelSpotRequests([]string{s.spotRequest.SpotRequestId}); err != nil {
|
||||||
|
ui.Error(fmt.Sprintf("Error cancelling the spot request, may still be around: %s", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stateChange := StateChangeConf{
|
||||||
|
Pending: []string{"active", "open"},
|
||||||
|
Refresh: SpotRequestStateRefreshFunc(ec2conn, s.spotRequest.SpotRequestId),
|
||||||
|
Target: "cancelled",
|
||||||
|
}
|
||||||
|
|
||||||
|
WaitForState(&stateChange)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terminate the source instance if it exists
|
||||||
|
if s.instance != nil {
|
||||||
|
|
||||||
ui.Say("Terminating the source AWS instance...")
|
ui.Say("Terminating the source AWS instance...")
|
||||||
if _, err := ec2conn.TerminateInstances([]string{s.instance.InstanceId}); err != nil {
|
if _, err := ec2conn.TerminateInstances([]string{s.instance.InstanceId}); err != nil {
|
||||||
ui.Error(fmt.Sprintf("Error terminating instance, may still be around: %s", err))
|
ui.Error(fmt.Sprintf("Error terminating instance, may still be around: %s", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stateChange := StateChangeConf{
|
stateChange := StateChangeConf{
|
||||||
Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"},
|
Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"},
|
||||||
Refresh: InstanceStateRefreshFunc(ec2conn, s.instance),
|
Refresh: InstanceStateRefreshFunc(ec2conn, s.instance),
|
||||||
|
@ -162,4 +234,5 @@ func (s *StepRunSourceInstance) Cleanup(state multistep.StateBag) {
|
||||||
}
|
}
|
||||||
|
|
||||||
WaitForState(&stateChange)
|
WaitForState(&stateChange)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,6 +102,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||||
&awscommon.StepRunSourceInstance{
|
&awscommon.StepRunSourceInstance{
|
||||||
Debug: b.config.PackerDebug,
|
Debug: b.config.PackerDebug,
|
||||||
ExpectedRootDevice: "ebs",
|
ExpectedRootDevice: "ebs",
|
||||||
|
SpotPrice: b.config.SpotPrice,
|
||||||
InstanceType: b.config.InstanceType,
|
InstanceType: b.config.InstanceType,
|
||||||
UserData: b.config.UserData,
|
UserData: b.config.UserData,
|
||||||
UserDataFile: b.config.UserDataFile,
|
UserDataFile: b.config.UserDataFile,
|
||||||
|
@ -120,7 +121,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||||
SSHWaitTimeout: b.config.SSHTimeout(),
|
SSHWaitTimeout: b.config.SSHTimeout(),
|
||||||
},
|
},
|
||||||
&common.StepProvision{},
|
&common.StepProvision{},
|
||||||
&stepStopInstance{},
|
&stepStopInstance{SpotPrice: b.config.SpotPrice},
|
||||||
|
// TODO(mitchellh): verify works with spots
|
||||||
&stepModifyInstance{},
|
&stepModifyInstance{},
|
||||||
&stepCreateAMI{},
|
&stepCreateAMI{},
|
||||||
&awscommon.StepAMIRegionCopy{
|
&awscommon.StepAMIRegionCopy{
|
||||||
|
|
|
@ -8,13 +8,21 @@ import (
|
||||||
"github.com/mitchellh/packer/packer"
|
"github.com/mitchellh/packer/packer"
|
||||||
)
|
)
|
||||||
|
|
||||||
type stepStopInstance struct{}
|
type stepStopInstance struct {
|
||||||
|
SpotPrice string
|
||||||
|
}
|
||||||
|
|
||||||
func (s *stepStopInstance) Run(state multistep.StateBag) multistep.StepAction {
|
func (s *stepStopInstance) Run(state multistep.StateBag) multistep.StepAction {
|
||||||
ec2conn := state.Get("ec2").(*ec2.EC2)
|
ec2conn := state.Get("ec2").(*ec2.EC2)
|
||||||
instance := state.Get("instance").(*ec2.Instance)
|
instance := state.Get("instance").(*ec2.Instance)
|
||||||
ui := state.Get("ui").(packer.Ui)
|
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
|
// Stop the instance so we can create an AMI from it
|
||||||
ui.Say("Stopping the source instance...")
|
ui.Say("Stopping the source instance...")
|
||||||
_, err := ec2conn.StopInstances(instance.InstanceId)
|
_, err := ec2conn.StopInstances(instance.InstanceId)
|
||||||
|
|
|
@ -206,6 +206,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||||
},
|
},
|
||||||
&awscommon.StepRunSourceInstance{
|
&awscommon.StepRunSourceInstance{
|
||||||
Debug: b.config.PackerDebug,
|
Debug: b.config.PackerDebug,
|
||||||
|
SpotPrice: b.config.SpotPrice,
|
||||||
InstanceType: b.config.InstanceType,
|
InstanceType: b.config.InstanceType,
|
||||||
IamInstanceProfile: b.config.IamInstanceProfile,
|
IamInstanceProfile: b.config.IamInstanceProfile,
|
||||||
UserData: b.config.UserData,
|
UserData: b.config.UserData,
|
||||||
|
|
|
@ -120,6 +120,13 @@ each category, the available configuration keys are alphabetized.
|
||||||
described above. Note that if this is specified, you must omit the
|
described above. Note that if this is specified, you must omit the
|
||||||
`security_group_id`.
|
`security_group_id`.
|
||||||
|
|
||||||
|
* `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. For example, it takes only "0.001" to
|
||||||
|
launch a spot "m3.medium" instance while "0.07" needed for on-demand.
|
||||||
|
|
||||||
* `ssh_port` (integer) - The port that SSH will be available on. This defaults
|
* `ssh_port` (integer) - The port that SSH will be available on. This defaults
|
||||||
to port 22.
|
to port 22.
|
||||||
|
|
||||||
|
|
|
@ -158,6 +158,13 @@ each category, the available configuration keys are alphabetized.
|
||||||
described above. Note that if this is specified, you must omit the
|
described above. Note that if this is specified, you must omit the
|
||||||
`security_group_id`.
|
`security_group_id`.
|
||||||
|
|
||||||
|
* `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. For example, it takes only "0.001" to
|
||||||
|
launch a spot "m3.medium" instance while "0.07" needed for on-demand.
|
||||||
|
|
||||||
* `ssh_port` (integer) - The port that SSH will be available on. This defaults
|
* `ssh_port` (integer) - The port that SSH will be available on. This defaults
|
||||||
to port 22.
|
to port 22.
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue