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 }