Commit db90c161 authored by Mitchell Hashimoto's avatar Mitchell Hashimoto

builder/amazon: support auto spot price discovery [GH-1465]

parent a587bd47
...@@ -20,6 +20,7 @@ type RunConfig struct { ...@@ -20,6 +20,7 @@ type RunConfig struct {
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"` SpotPrice string `mapstructure:"spot_price"`
SpotPriceAutoProduct string `mapstructure:"spot_price_auto_product"`
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"`
...@@ -50,6 +51,7 @@ func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error { ...@@ -50,6 +51,7 @@ func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error {
"iam_instance_profile": &c.IamInstanceProfile, "iam_instance_profile": &c.IamInstanceProfile,
"instance_type": &c.InstanceType, "instance_type": &c.InstanceType,
"spot_price": &c.SpotPrice, "spot_price": &c.SpotPrice,
"spot_price_auto_product": &c.SpotPriceAutoProduct,
"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,
...@@ -97,6 +99,13 @@ func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error { ...@@ -97,6 +99,13 @@ func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error {
errs = append(errs, errors.New("An instance_type must be specified")) errs = append(errs, errors.New("An instance_type must be specified"))
} }
if c.SpotPrice == "auto" {
if c.SpotPriceAutoProduct == "" {
errs = append(errs, errors.New(
"spot_price_auto_product must be specified when spot_price is auto"))
}
}
if c.SSHUsername == "" { if c.SSHUsername == "" {
errs = append(errs, errors.New("An ssh_username must be specified")) errs = append(errs, errors.New("An ssh_username must be specified"))
} }
......
...@@ -47,6 +47,19 @@ func TestRunConfigPrepare_SourceAmi(t *testing.T) { ...@@ -47,6 +47,19 @@ func TestRunConfigPrepare_SourceAmi(t *testing.T) {
} }
} }
func TestRunConfigPrepare_SpotAuto(t *testing.T) {
c := testConfig()
c.SpotPrice = "auto"
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
}
c.SpotPriceAutoProduct = "foo"
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
}
func TestRunConfigPrepare_SSHPort(t *testing.T) { func TestRunConfigPrepare_SSHPort(t *testing.T) {
c := testConfig() c := testConfig()
c.SSHPort = 0 c.SSHPort = 0
......
...@@ -2,15 +2,18 @@ package common ...@@ -2,15 +2,18 @@ package common
import ( import (
"fmt" "fmt"
"io/ioutil"
"log"
"strconv"
"time"
"github.com/mitchellh/goamz/ec2" "github.com/mitchellh/goamz/ec2"
"github.com/mitchellh/multistep" "github.com/mitchellh/multistep"
"github.com/mitchellh/packer/packer" "github.com/mitchellh/packer/packer"
"io/ioutil"
) )
type StepRunSourceInstance struct { type StepRunSourceInstance struct {
AssociatePublicIpAddress bool AssociatePublicIpAddress bool
SpotPrice string
AvailabilityZone string AvailabilityZone string
BlockDevices BlockDevices BlockDevices BlockDevices
Debug bool Debug bool
...@@ -18,12 +21,15 @@ type StepRunSourceInstance struct { ...@@ -18,12 +21,15 @@ type StepRunSourceInstance struct {
InstanceType string InstanceType string
IamInstanceProfile string IamInstanceProfile string
SourceAMI string SourceAMI string
SpotPrice string
SpotPriceProduct string
SubnetId string SubnetId string
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
spotRequest *ec2.SpotRequestResult
} }
func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepAction { func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepAction {
...@@ -68,8 +74,52 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi ...@@ -68,8 +74,52 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi
return multistep.ActionHalt return multistep.ActionHalt
} }
var instanceId []string spotPrice := s.SpotPrice
if s.SpotPrice == "" { 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.DescribeSpotPriceHistory{
InstanceType: []string{s.InstanceType},
ProductDescription: []string{s.SpotPriceProduct},
AvailabilityZone: s.AvailabilityZone,
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
}
var price float64
for _, history := range resp.History {
log.Printf("[INFO] Candidate spot price: %s", history.SpotPrice)
current, err := strconv.ParseFloat(history.SpotPrice, 64)
if err != nil {
log.Printf("[ERR] Error parsing spot price: %s", err)
continue
}
if price == 0 || current < price {
price = current
}
}
if price == 0 {
err := fmt.Errorf("No candidate spot prices found!")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
spotPrice = strconv.FormatFloat(price, 'f', -1, 64)
}
var instanceId string
if spotPrice == "" {
runOpts := &ec2.RunInstances{ runOpts := &ec2.RunInstances{
KeyName: keyName, KeyName: keyName,
ImageId: s.SourceAMI, ImageId: s.SourceAMI,
...@@ -91,14 +141,14 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi ...@@ -91,14 +141,14 @@ 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} instanceId = runResp.Instances[0].InstanceId
} else { } else {
ui.Message(fmt.Sprintf( ui.Message(fmt.Sprintf(
"Requesting spot instance '%s' for: %s", "Requesting spot instance '%s' for: %s",
s.InstanceType, s.SpotPrice)) s.InstanceType, spotPrice))
runOpts := &ec2.RequestSpotInstances{ runOpts := &ec2.RequestSpotInstances{
SpotPrice: s.SpotPrice, SpotPrice: spotPrice,
KeyName: keyName, KeyName: keyName,
ImageId: s.SourceAMI, ImageId: s.SourceAMI,
InstanceType: s.InstanceType, InstanceType: s.InstanceType,
...@@ -142,10 +192,10 @@ func (s *StepRunSourceInstance) Run(state multistep.StateBag) multistep.StepActi ...@@ -142,10 +192,10 @@ 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{spotResp.SpotRequestResults[0].InstanceId} instanceId = spotResp.SpotRequestResults[0].InstanceId
} }
instanceResp, err := ec2conn.Instances(instanceId, nil) instanceResp, err := ec2conn.Instances([]string{instanceId}, nil)
if err != nil { if err != nil {
err := fmt.Errorf("Error finding source instance (%s): %s", instanceId, err) err := fmt.Errorf("Error finding source instance (%s): %s", instanceId, err)
state.Put("error", err) state.Put("error", err)
......
...@@ -103,6 +103,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -103,6 +103,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Debug: b.config.PackerDebug, Debug: b.config.PackerDebug,
ExpectedRootDevice: "ebs", ExpectedRootDevice: "ebs",
SpotPrice: b.config.SpotPrice, SpotPrice: b.config.SpotPrice,
SpotPriceProduct: b.config.SpotPriceAutoProduct,
InstanceType: b.config.InstanceType, InstanceType: b.config.InstanceType,
UserData: b.config.UserData, UserData: b.config.UserData,
UserDataFile: b.config.UserDataFile, UserDataFile: b.config.UserDataFile,
......
...@@ -207,6 +207,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe ...@@ -207,6 +207,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, SpotPrice: b.config.SpotPrice,
SpotPriceProduct: b.config.SpotPriceAutoProduct,
InstanceType: b.config.InstanceType, InstanceType: b.config.InstanceType,
IamInstanceProfile: b.config.IamInstanceProfile, IamInstanceProfile: b.config.IamInstanceProfile,
UserData: b.config.UserData, UserData: b.config.UserData,
......
...@@ -124,8 +124,13 @@ each category, the available configuration keys are alphabetized. ...@@ -124,8 +124,13 @@ each category, the available configuration keys are alphabetized.
to create the AMI. It is a type of instances that EC2 starts when the maximum 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 price that you specify exceeds the current spot price. Spot price will be
updated based on available spot instance capacity and current spot Instance 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 requests. It may save you some costs. You can set this to "auto" for
launch a spot "m3.medium" instance while "0.07" needed for on-demand. Packer to automatically discover the best spot price.
* `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)`
* `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.
......
...@@ -162,8 +162,13 @@ each category, the available configuration keys are alphabetized. ...@@ -162,8 +162,13 @@ each category, the available configuration keys are alphabetized.
to create the AMI. It is a type of instances that EC2 starts when the maximum 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 price that you specify exceeds the current spot price. Spot price will be
updated based on available spot instance capacity and current spot Instance 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 requests. It may save you some costs. You can set this to "auto" for
launch a spot "m3.medium" instance while "0.07" needed for on-demand. Packer to automatically discover the best spot price.
* `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)`
* `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.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment