github.com/jrperritt/terraform@v0.1.1-0.20170525065507-96f391dafc38/builtin/providers/aws/resource_aws_spot_instance_request.go (about) 1 package aws 2 3 import ( 4 "fmt" 5 "log" 6 "time" 7 8 "github.com/aws/aws-sdk-go/aws" 9 "github.com/aws/aws-sdk-go/aws/awserr" 10 "github.com/aws/aws-sdk-go/service/ec2" 11 "github.com/hashicorp/terraform/helper/resource" 12 "github.com/hashicorp/terraform/helper/schema" 13 ) 14 15 func resourceAwsSpotInstanceRequest() *schema.Resource { 16 return &schema.Resource{ 17 Create: resourceAwsSpotInstanceRequestCreate, 18 Read: resourceAwsSpotInstanceRequestRead, 19 Delete: resourceAwsSpotInstanceRequestDelete, 20 Update: resourceAwsSpotInstanceRequestUpdate, 21 22 Timeouts: &schema.ResourceTimeout{ 23 Create: schema.DefaultTimeout(10 * time.Minute), 24 Delete: schema.DefaultTimeout(10 * time.Minute), 25 }, 26 27 Schema: func() map[string]*schema.Schema { 28 // The Spot Instance Request Schema is based on the AWS Instance schema. 29 s := resourceAwsInstance().Schema 30 31 // Everything on a spot instance is ForceNew except tags 32 for k, v := range s { 33 if k == "tags" { 34 continue 35 } 36 v.ForceNew = true 37 } 38 39 s["volume_tags"] = &schema.Schema{ 40 Type: schema.TypeMap, 41 Optional: true, 42 } 43 44 s["spot_price"] = &schema.Schema{ 45 Type: schema.TypeString, 46 Required: true, 47 ForceNew: true, 48 } 49 s["spot_type"] = &schema.Schema{ 50 Type: schema.TypeString, 51 Optional: true, 52 Default: "persistent", 53 } 54 s["wait_for_fulfillment"] = &schema.Schema{ 55 Type: schema.TypeBool, 56 Optional: true, 57 Default: false, 58 } 59 s["spot_bid_status"] = &schema.Schema{ 60 Type: schema.TypeString, 61 Computed: true, 62 } 63 s["spot_request_state"] = &schema.Schema{ 64 Type: schema.TypeString, 65 Computed: true, 66 } 67 s["spot_instance_id"] = &schema.Schema{ 68 Type: schema.TypeString, 69 Computed: true, 70 } 71 s["block_duration_minutes"] = &schema.Schema{ 72 Type: schema.TypeInt, 73 Optional: true, 74 ForceNew: true, 75 } 76 77 return s 78 }(), 79 } 80 } 81 82 func resourceAwsSpotInstanceRequestCreate(d *schema.ResourceData, meta interface{}) error { 83 conn := meta.(*AWSClient).ec2conn 84 85 instanceOpts, err := buildAwsInstanceOpts(d, meta) 86 if err != nil { 87 return err 88 } 89 90 spotOpts := &ec2.RequestSpotInstancesInput{ 91 SpotPrice: aws.String(d.Get("spot_price").(string)), 92 Type: aws.String(d.Get("spot_type").(string)), 93 94 // Though the AWS API supports creating spot instance requests for multiple 95 // instances, for TF purposes we fix this to one instance per request. 96 // Users can get equivalent behavior out of TF's "count" meta-parameter. 97 InstanceCount: aws.Int64(1), 98 99 LaunchSpecification: &ec2.RequestSpotLaunchSpecification{ 100 BlockDeviceMappings: instanceOpts.BlockDeviceMappings, 101 EbsOptimized: instanceOpts.EBSOptimized, 102 Monitoring: instanceOpts.Monitoring, 103 IamInstanceProfile: instanceOpts.IAMInstanceProfile, 104 ImageId: instanceOpts.ImageID, 105 InstanceType: instanceOpts.InstanceType, 106 KeyName: instanceOpts.KeyName, 107 Placement: instanceOpts.SpotPlacement, 108 SecurityGroupIds: instanceOpts.SecurityGroupIDs, 109 SecurityGroups: instanceOpts.SecurityGroups, 110 SubnetId: instanceOpts.SubnetID, 111 UserData: instanceOpts.UserData64, 112 }, 113 } 114 115 if v, ok := d.GetOk("block_duration_minutes"); ok { 116 spotOpts.BlockDurationMinutes = aws.Int64(int64(v.(int))) 117 } 118 119 // If the instance is configured with a Network Interface (a subnet, has 120 // public IP, etc), then the instanceOpts.SecurityGroupIds and SubnetId will 121 // be nil 122 if len(instanceOpts.NetworkInterfaces) > 0 { 123 spotOpts.LaunchSpecification.SecurityGroupIds = instanceOpts.NetworkInterfaces[0].Groups 124 spotOpts.LaunchSpecification.SubnetId = instanceOpts.NetworkInterfaces[0].SubnetId 125 } 126 127 // Make the spot instance request 128 log.Printf("[DEBUG] Requesting spot bid opts: %s", spotOpts) 129 130 var resp *ec2.RequestSpotInstancesOutput 131 err = resource.Retry(15*time.Second, func() *resource.RetryError { 132 var err error 133 resp, err = conn.RequestSpotInstances(spotOpts) 134 // IAM instance profiles can take ~10 seconds to propagate in AWS: 135 // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#launch-instance-with-role-console 136 if isAWSErr(err, "InvalidParameterValue", "Invalid IAM Instance Profile") { 137 log.Printf("[DEBUG] Invalid IAM Instance Profile referenced, retrying...") 138 return resource.RetryableError(err) 139 } 140 // IAM roles can also take time to propagate in AWS: 141 if isAWSErr(err, "InvalidParameterValue", " has no associated IAM Roles") { 142 log.Printf("[DEBUG] IAM Instance Profile appears to have no IAM roles, retrying...") 143 return resource.RetryableError(err) 144 } 145 return resource.NonRetryableError(err) 146 }) 147 148 if err != nil { 149 return fmt.Errorf("Error requesting spot instances: %s", err) 150 } 151 if len(resp.SpotInstanceRequests) != 1 { 152 return fmt.Errorf( 153 "Expected response with length 1, got: %s", resp) 154 } 155 156 sir := *resp.SpotInstanceRequests[0] 157 d.SetId(*sir.SpotInstanceRequestId) 158 159 if d.Get("wait_for_fulfillment").(bool) { 160 spotStateConf := &resource.StateChangeConf{ 161 // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-bid-status.html 162 Pending: []string{"start", "pending-evaluation", "pending-fulfillment"}, 163 Target: []string{"fulfilled"}, 164 Refresh: SpotInstanceStateRefreshFunc(conn, sir), 165 Timeout: d.Timeout(schema.TimeoutCreate), 166 Delay: 10 * time.Second, 167 MinTimeout: 3 * time.Second, 168 } 169 170 log.Printf("[DEBUG] waiting for spot bid to resolve... this may take several minutes.") 171 _, err = spotStateConf.WaitForState() 172 173 if err != nil { 174 return fmt.Errorf("Error while waiting for spot request (%s) to resolve: %s", sir, err) 175 } 176 } 177 178 return resourceAwsSpotInstanceRequestUpdate(d, meta) 179 } 180 181 // Update spot state, etc 182 func resourceAwsSpotInstanceRequestRead(d *schema.ResourceData, meta interface{}) error { 183 conn := meta.(*AWSClient).ec2conn 184 185 req := &ec2.DescribeSpotInstanceRequestsInput{ 186 SpotInstanceRequestIds: []*string{aws.String(d.Id())}, 187 } 188 resp, err := conn.DescribeSpotInstanceRequests(req) 189 190 if err != nil { 191 // If the spot request was not found, return nil so that we can show 192 // that it is gone. 193 if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidSpotInstanceRequestID.NotFound" { 194 d.SetId("") 195 return nil 196 } 197 198 // Some other error, report it 199 return err 200 } 201 202 // If nothing was found, then return no state 203 if len(resp.SpotInstanceRequests) == 0 { 204 d.SetId("") 205 return nil 206 } 207 208 request := resp.SpotInstanceRequests[0] 209 210 // if the request is cancelled, then it is gone 211 if *request.State == "cancelled" { 212 d.SetId("") 213 return nil 214 } 215 216 d.Set("spot_bid_status", *request.Status.Code) 217 // Instance ID is not set if the request is still pending 218 if request.InstanceId != nil { 219 d.Set("spot_instance_id", *request.InstanceId) 220 // Read the instance data, setting up connection information 221 if err := readInstance(d, meta); err != nil { 222 return fmt.Errorf("[ERR] Error reading Spot Instance Data: %s", err) 223 } 224 } 225 226 d.Set("spot_request_state", request.State) 227 d.Set("block_duration_minutes", request.BlockDurationMinutes) 228 d.Set("tags", tagsToMap(request.Tags)) 229 230 return nil 231 } 232 233 func readInstance(d *schema.ResourceData, meta interface{}) error { 234 conn := meta.(*AWSClient).ec2conn 235 236 resp, err := conn.DescribeInstances(&ec2.DescribeInstancesInput{ 237 InstanceIds: []*string{aws.String(d.Get("spot_instance_id").(string))}, 238 }) 239 if err != nil { 240 // If the instance was not found, return nil so that we can show 241 // that the instance is gone. 242 if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidInstanceID.NotFound" { 243 return fmt.Errorf("no instance found") 244 } 245 246 // Some other error, report it 247 return err 248 } 249 250 // If nothing was found, then return no state 251 if len(resp.Reservations) == 0 { 252 return fmt.Errorf("no instances found") 253 } 254 255 instance := resp.Reservations[0].Instances[0] 256 257 // Set these fields for connection information 258 if instance != nil { 259 d.Set("public_dns", instance.PublicDnsName) 260 d.Set("public_ip", instance.PublicIpAddress) 261 d.Set("private_dns", instance.PrivateDnsName) 262 d.Set("private_ip", instance.PrivateIpAddress) 263 264 // set connection information 265 if instance.PublicIpAddress != nil { 266 d.SetConnInfo(map[string]string{ 267 "type": "ssh", 268 "host": *instance.PublicIpAddress, 269 }) 270 } else if instance.PrivateIpAddress != nil { 271 d.SetConnInfo(map[string]string{ 272 "type": "ssh", 273 "host": *instance.PrivateIpAddress, 274 }) 275 } 276 if err := readBlockDevices(d, instance, conn); err != nil { 277 return err 278 } 279 280 var ipv6Addresses []string 281 if len(instance.NetworkInterfaces) > 0 { 282 for _, ni := range instance.NetworkInterfaces { 283 if *ni.Attachment.DeviceIndex == 0 { 284 d.Set("subnet_id", ni.SubnetId) 285 d.Set("network_interface_id", ni.NetworkInterfaceId) 286 d.Set("associate_public_ip_address", ni.Association != nil) 287 d.Set("ipv6_address_count", len(ni.Ipv6Addresses)) 288 289 for _, address := range ni.Ipv6Addresses { 290 ipv6Addresses = append(ipv6Addresses, *address.Ipv6Address) 291 } 292 } 293 } 294 } else { 295 d.Set("subnet_id", instance.SubnetId) 296 d.Set("network_interface_id", "") 297 } 298 299 if err := d.Set("ipv6_addresses", ipv6Addresses); err != nil { 300 log.Printf("[WARN] Error setting ipv6_addresses for AWS Spot Instance (%s): %s", d.Id(), err) 301 } 302 } 303 304 return nil 305 } 306 307 func resourceAwsSpotInstanceRequestUpdate(d *schema.ResourceData, meta interface{}) error { 308 conn := meta.(*AWSClient).ec2conn 309 310 d.Partial(true) 311 if err := setTags(conn, d); err != nil { 312 return err 313 } else { 314 d.SetPartial("tags") 315 } 316 317 d.Partial(false) 318 319 return resourceAwsSpotInstanceRequestRead(d, meta) 320 } 321 322 func resourceAwsSpotInstanceRequestDelete(d *schema.ResourceData, meta interface{}) error { 323 conn := meta.(*AWSClient).ec2conn 324 325 log.Printf("[INFO] Cancelling spot request: %s", d.Id()) 326 _, err := conn.CancelSpotInstanceRequests(&ec2.CancelSpotInstanceRequestsInput{ 327 SpotInstanceRequestIds: []*string{aws.String(d.Id())}, 328 }) 329 330 if err != nil { 331 return fmt.Errorf("Error cancelling spot request (%s): %s", d.Id(), err) 332 } 333 334 if instanceId := d.Get("spot_instance_id").(string); instanceId != "" { 335 log.Printf("[INFO] Terminating instance: %s", instanceId) 336 if err := awsTerminateInstance(conn, instanceId, d); err != nil { 337 return fmt.Errorf("Error terminating spot instance: %s", err) 338 } 339 } 340 341 return nil 342 } 343 344 // SpotInstanceStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch 345 // an EC2 spot instance request 346 func SpotInstanceStateRefreshFunc( 347 conn *ec2.EC2, sir ec2.SpotInstanceRequest) resource.StateRefreshFunc { 348 349 return func() (interface{}, string, error) { 350 resp, err := conn.DescribeSpotInstanceRequests(&ec2.DescribeSpotInstanceRequestsInput{ 351 SpotInstanceRequestIds: []*string{sir.SpotInstanceRequestId}, 352 }) 353 354 if err != nil { 355 if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidSpotInstanceRequestID.NotFound" { 356 // Set this to nil as if we didn't find anything. 357 resp = nil 358 } else { 359 log.Printf("Error on StateRefresh: %s", err) 360 return nil, "", err 361 } 362 } 363 364 if resp == nil || len(resp.SpotInstanceRequests) == 0 { 365 // Sometimes AWS just has consistency issues and doesn't see 366 // our request yet. Return an empty state. 367 return nil, "", nil 368 } 369 370 req := resp.SpotInstanceRequests[0] 371 return req, *req.Status.Code, nil 372 } 373 }