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 }