github.com/StackPointCloud/packer@v0.10.2-0.20180716202532-b28098e0f79b/builder/amazon/common/step_run_spot_instance.go (about)

     1  package common
     2  
     3  import (
     4  	"context"
     5  	"encoding/base64"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"log"
     9  	"strconv"
    10  	"time"
    11  
    12  	"github.com/aws/aws-sdk-go/aws"
    13  	"github.com/aws/aws-sdk-go/aws/awserr"
    14  	"github.com/aws/aws-sdk-go/service/ec2"
    15  
    16  	retry "github.com/hashicorp/packer/common"
    17  	"github.com/hashicorp/packer/helper/multistep"
    18  	"github.com/hashicorp/packer/packer"
    19  	"github.com/hashicorp/packer/template/interpolate"
    20  )
    21  
    22  type StepRunSpotInstance struct {
    23  	AssociatePublicIpAddress          bool
    24  	AvailabilityZone                  string
    25  	BlockDevices                      BlockDevices
    26  	Debug                             bool
    27  	EbsOptimized                      bool
    28  	ExpectedRootDevice                string
    29  	IamInstanceProfile                string
    30  	InstanceInitiatedShutdownBehavior string
    31  	InstanceType                      string
    32  	SourceAMI                         string
    33  	SpotPrice                         string
    34  	SpotPriceProduct                  string
    35  	SpotTags                          TagMap
    36  	SubnetId                          string
    37  	Tags                              TagMap
    38  	VolumeTags                        TagMap
    39  	UserData                          string
    40  	UserDataFile                      string
    41  	Ctx                               interpolate.Context
    42  
    43  	instanceId  string
    44  	spotRequest *ec2.SpotInstanceRequest
    45  }
    46  
    47  func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
    48  	ec2conn := state.Get("ec2").(*ec2.EC2)
    49  	var keyName string
    50  	if name, ok := state.GetOk("keyPair"); ok {
    51  		keyName = name.(string)
    52  	}
    53  	securityGroupIds := aws.StringSlice(state.Get("securityGroupIds").([]string))
    54  	ui := state.Get("ui").(packer.Ui)
    55  
    56  	userData := s.UserData
    57  	if s.UserDataFile != "" {
    58  		contents, err := ioutil.ReadFile(s.UserDataFile)
    59  		if err != nil {
    60  			state.Put("error", fmt.Errorf("Problem reading user data file: %s", err))
    61  			return multistep.ActionHalt
    62  		}
    63  
    64  		userData = string(contents)
    65  	}
    66  
    67  	// Test if it is encoded already, and if not, encode it
    68  	if _, err := base64.StdEncoding.DecodeString(userData); err != nil {
    69  		log.Printf("[DEBUG] base64 encoding user data...")
    70  		userData = base64.StdEncoding.EncodeToString([]byte(userData))
    71  	}
    72  
    73  	ui.Say("Launching a source AWS instance...")
    74  	image, ok := state.Get("source_image").(*ec2.Image)
    75  	if !ok {
    76  		state.Put("error", fmt.Errorf("source_image type assertion failed"))
    77  		return multistep.ActionHalt
    78  	}
    79  	s.SourceAMI = *image.ImageId
    80  
    81  	if s.ExpectedRootDevice != "" && *image.RootDeviceType != s.ExpectedRootDevice {
    82  		state.Put("error", fmt.Errorf(
    83  			"The provided source AMI has an invalid root device type.\n"+
    84  				"Expected '%s', got '%s'.",
    85  			s.ExpectedRootDevice, *image.RootDeviceType))
    86  		return multistep.ActionHalt
    87  	}
    88  
    89  	spotPrice := s.SpotPrice
    90  	availabilityZone := s.AvailabilityZone
    91  	if spotPrice == "auto" {
    92  		ui.Message(fmt.Sprintf(
    93  			"Finding spot price for %s %s...",
    94  			s.SpotPriceProduct, s.InstanceType))
    95  
    96  		// Detect the spot price
    97  		startTime := time.Now().Add(-1 * time.Hour)
    98  		resp, err := ec2conn.DescribeSpotPriceHistory(&ec2.DescribeSpotPriceHistoryInput{
    99  			InstanceTypes:       []*string{&s.InstanceType},
   100  			ProductDescriptions: []*string{&s.SpotPriceProduct},
   101  			AvailabilityZone:    &s.AvailabilityZone,
   102  			StartTime:           &startTime,
   103  		})
   104  		if err != nil {
   105  			err := fmt.Errorf("Error finding spot price: %s", err)
   106  			state.Put("error", err)
   107  			ui.Error(err.Error())
   108  			return multistep.ActionHalt
   109  		}
   110  
   111  		var price float64
   112  		for _, history := range resp.SpotPriceHistory {
   113  			log.Printf("[INFO] Candidate spot price: %s", *history.SpotPrice)
   114  			current, err := strconv.ParseFloat(*history.SpotPrice, 64)
   115  			if err != nil {
   116  				log.Printf("[ERR] Error parsing spot price: %s", err)
   117  				continue
   118  			}
   119  			if price == 0 || current < price {
   120  				price = current
   121  				if s.AvailabilityZone == "" {
   122  					availabilityZone = *history.AvailabilityZone
   123  				}
   124  			}
   125  		}
   126  		if price == 0 {
   127  			err := fmt.Errorf("No candidate spot prices found!")
   128  			state.Put("error", err)
   129  			ui.Error(err.Error())
   130  			return multistep.ActionHalt
   131  		} else {
   132  			// Add 0.5 cents to minimum spot bid to ensure capacity will be available
   133  			// Avoids price-too-low error in active markets which can fluctuate
   134  			price = price + 0.005
   135  		}
   136  
   137  		spotPrice = strconv.FormatFloat(price, 'f', -1, 64)
   138  	}
   139  
   140  	var instanceId string
   141  
   142  	ui.Say("Adding tags to source instance")
   143  	if _, exists := s.Tags["Name"]; !exists {
   144  		s.Tags["Name"] = "Packer Builder"
   145  	}
   146  
   147  	ec2Tags, err := s.Tags.EC2Tags(s.Ctx, *ec2conn.Config.Region, state)
   148  	if err != nil {
   149  		err := fmt.Errorf("Error tagging source instance: %s", err)
   150  		state.Put("error", err)
   151  		ui.Error(err.Error())
   152  		return multistep.ActionHalt
   153  	}
   154  	ec2Tags.Report(ui)
   155  
   156  	ui.Message(fmt.Sprintf(
   157  		"Requesting spot instance '%s' for: %s",
   158  		s.InstanceType, spotPrice))
   159  
   160  	runOpts := &ec2.RequestSpotLaunchSpecification{
   161  		ImageId:            &s.SourceAMI,
   162  		InstanceType:       &s.InstanceType,
   163  		UserData:           &userData,
   164  		IamInstanceProfile: &ec2.IamInstanceProfileSpecification{Name: &s.IamInstanceProfile},
   165  		Placement: &ec2.SpotPlacement{
   166  			AvailabilityZone: &availabilityZone,
   167  		},
   168  		BlockDeviceMappings: s.BlockDevices.BuildLaunchDevices(),
   169  		EbsOptimized:        &s.EbsOptimized,
   170  	}
   171  
   172  	if s.SubnetId != "" && s.AssociatePublicIpAddress {
   173  		runOpts.NetworkInterfaces = []*ec2.InstanceNetworkInterfaceSpecification{
   174  			{
   175  				DeviceIndex:              aws.Int64(0),
   176  				AssociatePublicIpAddress: &s.AssociatePublicIpAddress,
   177  				SubnetId:                 &s.SubnetId,
   178  				Groups:                   securityGroupIds,
   179  				DeleteOnTermination:      aws.Bool(true),
   180  			},
   181  		}
   182  	} else {
   183  		runOpts.SubnetId = &s.SubnetId
   184  		runOpts.SecurityGroupIds = securityGroupIds
   185  	}
   186  
   187  	if keyName != "" {
   188  		runOpts.KeyName = &keyName
   189  	}
   190  
   191  	runSpotResp, err := ec2conn.RequestSpotInstances(&ec2.RequestSpotInstancesInput{
   192  		SpotPrice:           &spotPrice,
   193  		LaunchSpecification: runOpts,
   194  	})
   195  	if err != nil {
   196  		err := fmt.Errorf("Error launching source spot instance: %s", err)
   197  		state.Put("error", err)
   198  		ui.Error(err.Error())
   199  		return multistep.ActionHalt
   200  	}
   201  
   202  	s.spotRequest = runSpotResp.SpotInstanceRequests[0]
   203  
   204  	spotRequestId := s.spotRequest.SpotInstanceRequestId
   205  	ui.Message(fmt.Sprintf("Waiting for spot request (%s) to become active...", *spotRequestId))
   206  	err = WaitUntilSpotRequestFulfilled(ctx, ec2conn, *spotRequestId)
   207  	if err != nil {
   208  		err := fmt.Errorf("Error waiting for spot request (%s) to become ready: %s", *spotRequestId, err)
   209  		state.Put("error", err)
   210  		ui.Error(err.Error())
   211  		return multistep.ActionHalt
   212  	}
   213  
   214  	spotResp, err := ec2conn.DescribeSpotInstanceRequests(&ec2.DescribeSpotInstanceRequestsInput{
   215  		SpotInstanceRequestIds: []*string{spotRequestId},
   216  	})
   217  	if err != nil {
   218  		err := fmt.Errorf("Error finding spot request (%s): %s", *spotRequestId, err)
   219  		state.Put("error", err)
   220  		ui.Error(err.Error())
   221  		return multistep.ActionHalt
   222  	}
   223  	instanceId = *spotResp.SpotInstanceRequests[0].InstanceId
   224  
   225  	// Tag spot instance request
   226  	spotTags, err := s.SpotTags.EC2Tags(s.Ctx, *ec2conn.Config.Region, state)
   227  	if err != nil {
   228  		err := fmt.Errorf("Error tagging spot request: %s", err)
   229  		state.Put("error", err)
   230  		ui.Error(err.Error())
   231  		return multistep.ActionHalt
   232  	}
   233  	spotTags.Report(ui)
   234  
   235  	if len(spotTags) > 0 && s.SpotTags.IsSet() {
   236  		// Retry creating tags for about 2.5 minutes
   237  		err = retry.Retry(0.2, 30, 11, func(_ uint) (bool, error) {
   238  			_, err := ec2conn.CreateTags(&ec2.CreateTagsInput{
   239  				Tags:      spotTags,
   240  				Resources: []*string{spotRequestId},
   241  			})
   242  			return true, err
   243  		})
   244  		if err != nil {
   245  			err := fmt.Errorf("Error tagging spot request: %s", err)
   246  			state.Put("error", err)
   247  			ui.Error(err.Error())
   248  			return multistep.ActionHalt
   249  		}
   250  	}
   251  
   252  	// Set the instance ID so that the cleanup works properly
   253  	s.instanceId = instanceId
   254  
   255  	ui.Message(fmt.Sprintf("Instance ID: %s", instanceId))
   256  	ui.Say(fmt.Sprintf("Waiting for instance (%v) to become ready...", instanceId))
   257  	describeInstance := &ec2.DescribeInstancesInput{
   258  		InstanceIds: []*string{aws.String(instanceId)},
   259  	}
   260  	if err := ec2conn.WaitUntilInstanceRunningWithContext(ctx, describeInstance); err != nil {
   261  		err := fmt.Errorf("Error waiting for instance (%s) to become ready: %s", instanceId, err)
   262  		state.Put("error", err)
   263  		ui.Error(err.Error())
   264  		return multistep.ActionHalt
   265  	}
   266  
   267  	r, err := ec2conn.DescribeInstances(&ec2.DescribeInstancesInput{
   268  		InstanceIds: []*string{aws.String(instanceId)},
   269  	})
   270  	if err != nil || len(r.Reservations) == 0 || len(r.Reservations[0].Instances) == 0 {
   271  		err := fmt.Errorf("Error finding source instance.")
   272  		state.Put("error", err)
   273  		ui.Error(err.Error())
   274  		return multistep.ActionHalt
   275  	}
   276  	instance := r.Reservations[0].Instances[0]
   277  
   278  	// Retry creating tags for about 2.5 minutes
   279  	err = retry.Retry(0.2, 30, 11, func(_ uint) (bool, error) {
   280  		_, err := ec2conn.CreateTags(&ec2.CreateTagsInput{
   281  			Tags:      ec2Tags,
   282  			Resources: []*string{instance.InstanceId},
   283  		})
   284  		if err == nil {
   285  			return true, nil
   286  		}
   287  		if awsErr, ok := err.(awserr.Error); ok {
   288  			if awsErr.Code() == "InvalidInstanceID.NotFound" {
   289  				return false, nil
   290  			}
   291  		}
   292  		return true, err
   293  	})
   294  
   295  	if err != nil {
   296  		err := fmt.Errorf("Error tagging source instance: %s", err)
   297  		state.Put("error", err)
   298  		ui.Error(err.Error())
   299  		return multistep.ActionHalt
   300  	}
   301  
   302  	volumeIds := make([]*string, 0)
   303  	for _, v := range instance.BlockDeviceMappings {
   304  		if ebs := v.Ebs; ebs != nil {
   305  			volumeIds = append(volumeIds, ebs.VolumeId)
   306  		}
   307  	}
   308  
   309  	if len(volumeIds) > 0 && s.VolumeTags.IsSet() {
   310  		ui.Say("Adding tags to source EBS Volumes")
   311  
   312  		volumeTags, err := s.VolumeTags.EC2Tags(s.Ctx, *ec2conn.Config.Region, state)
   313  		if err != nil {
   314  			err := fmt.Errorf("Error tagging source EBS Volumes on %s: %s", *instance.InstanceId, err)
   315  			state.Put("error", err)
   316  			ui.Error(err.Error())
   317  			return multistep.ActionHalt
   318  		}
   319  		volumeTags.Report(ui)
   320  
   321  		_, err = ec2conn.CreateTags(&ec2.CreateTagsInput{
   322  			Resources: volumeIds,
   323  			Tags:      volumeTags,
   324  		})
   325  
   326  		if err != nil {
   327  			err := fmt.Errorf("Error tagging source EBS Volumes on %s: %s", *instance.InstanceId, err)
   328  			state.Put("error", err)
   329  			ui.Error(err.Error())
   330  			return multistep.ActionHalt
   331  		}
   332  
   333  	}
   334  
   335  	if s.Debug {
   336  		if instance.PublicDnsName != nil && *instance.PublicDnsName != "" {
   337  			ui.Message(fmt.Sprintf("Public DNS: %s", *instance.PublicDnsName))
   338  		}
   339  
   340  		if instance.PublicIpAddress != nil && *instance.PublicIpAddress != "" {
   341  			ui.Message(fmt.Sprintf("Public IP: %s", *instance.PublicIpAddress))
   342  		}
   343  
   344  		if instance.PrivateIpAddress != nil && *instance.PrivateIpAddress != "" {
   345  			ui.Message(fmt.Sprintf("Private IP: %s", *instance.PrivateIpAddress))
   346  		}
   347  	}
   348  
   349  	state.Put("instance", instance)
   350  
   351  	return multistep.ActionContinue
   352  }
   353  
   354  func (s *StepRunSpotInstance) Cleanup(state multistep.StateBag) {
   355  
   356  	ec2conn := state.Get("ec2").(*ec2.EC2)
   357  	ui := state.Get("ui").(packer.Ui)
   358  
   359  	// Cancel the spot request if it exists
   360  	if s.spotRequest != nil {
   361  		ui.Say("Cancelling the spot request...")
   362  		input := &ec2.CancelSpotInstanceRequestsInput{
   363  			SpotInstanceRequestIds: []*string{s.spotRequest.SpotInstanceRequestId},
   364  		}
   365  		if _, err := ec2conn.CancelSpotInstanceRequests(input); err != nil {
   366  			ui.Error(fmt.Sprintf("Error cancelling the spot request, may still be around: %s", err))
   367  			return
   368  		}
   369  
   370  		err := WaitUntilSpotRequestFulfilled(aws.BackgroundContext(), ec2conn, *s.spotRequest.SpotInstanceRequestId)
   371  		if err != nil {
   372  			ui.Error(err.Error())
   373  		}
   374  
   375  	}
   376  
   377  	// Terminate the source instance if it exists
   378  	if s.instanceId != "" {
   379  		ui.Say("Terminating the source AWS instance...")
   380  		if _, err := ec2conn.TerminateInstances(&ec2.TerminateInstancesInput{InstanceIds: []*string{&s.instanceId}}); err != nil {
   381  			ui.Error(fmt.Sprintf("Error terminating instance, may still be around: %s", err))
   382  			return
   383  		}
   384  
   385  		if err := WaitUntilInstanceTerminated(aws.BackgroundContext(), ec2conn, s.instanceId); err != nil {
   386  			ui.Error(err.Error())
   387  		}
   388  	}
   389  }