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