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