github.com/jefferai/terraform@v0.3.7-0.20150310153852-f7512ca29fcf/builtin/providers/aws/resource_aws_instance.go (about) 1 package aws 2 3 import ( 4 "bytes" 5 "crypto/sha1" 6 "encoding/hex" 7 "fmt" 8 "log" 9 "strconv" 10 "strings" 11 "time" 12 13 "github.com/hashicorp/terraform/helper/hashcode" 14 "github.com/hashicorp/terraform/helper/resource" 15 "github.com/hashicorp/terraform/helper/schema" 16 "github.com/mitchellh/goamz/ec2" 17 ) 18 19 func resourceAwsInstance() *schema.Resource { 20 return &schema.Resource{ 21 Create: resourceAwsInstanceCreate, 22 Read: resourceAwsInstanceRead, 23 Update: resourceAwsInstanceUpdate, 24 Delete: resourceAwsInstanceDelete, 25 26 Schema: map[string]*schema.Schema{ 27 "ami": &schema.Schema{ 28 Type: schema.TypeString, 29 Required: true, 30 ForceNew: true, 31 }, 32 33 "associate_public_ip_address": &schema.Schema{ 34 Type: schema.TypeBool, 35 Optional: true, 36 ForceNew: true, 37 }, 38 39 "availability_zone": &schema.Schema{ 40 Type: schema.TypeString, 41 Optional: true, 42 Computed: true, 43 ForceNew: true, 44 }, 45 46 "instance_type": &schema.Schema{ 47 Type: schema.TypeString, 48 Required: true, 49 ForceNew: true, 50 }, 51 52 "key_name": &schema.Schema{ 53 Type: schema.TypeString, 54 Optional: true, 55 ForceNew: true, 56 Computed: true, 57 }, 58 59 "subnet_id": &schema.Schema{ 60 Type: schema.TypeString, 61 Optional: true, 62 Computed: true, 63 ForceNew: true, 64 }, 65 66 "private_ip": &schema.Schema{ 67 Type: schema.TypeString, 68 Optional: true, 69 ForceNew: true, 70 Computed: true, 71 }, 72 73 "source_dest_check": &schema.Schema{ 74 Type: schema.TypeBool, 75 Optional: true, 76 }, 77 78 "user_data": &schema.Schema{ 79 Type: schema.TypeString, 80 Optional: true, 81 ForceNew: true, 82 StateFunc: func(v interface{}) string { 83 switch v.(type) { 84 case string: 85 hash := sha1.Sum([]byte(v.(string))) 86 return hex.EncodeToString(hash[:]) 87 default: 88 return "" 89 } 90 }, 91 }, 92 93 "security_groups": &schema.Schema{ 94 Type: schema.TypeSet, 95 Optional: true, 96 Computed: true, 97 ForceNew: true, 98 Elem: &schema.Schema{Type: schema.TypeString}, 99 Set: func(v interface{}) int { 100 return hashcode.String(v.(string)) 101 }, 102 }, 103 104 "public_dns": &schema.Schema{ 105 Type: schema.TypeString, 106 Computed: true, 107 }, 108 109 "public_ip": &schema.Schema{ 110 Type: schema.TypeString, 111 Computed: true, 112 }, 113 114 "private_dns": &schema.Schema{ 115 Type: schema.TypeString, 116 Computed: true, 117 }, 118 119 "ebs_optimized": &schema.Schema{ 120 Type: schema.TypeBool, 121 Optional: true, 122 }, 123 124 "iam_instance_profile": &schema.Schema{ 125 Type: schema.TypeString, 126 ForceNew: true, 127 Optional: true, 128 }, 129 "tenancy": &schema.Schema{ 130 Type: schema.TypeString, 131 Optional: true, 132 Computed: true, 133 ForceNew: true, 134 }, 135 "tags": tagsSchema(), 136 137 "block_device": &schema.Schema{ 138 Type: schema.TypeSet, 139 Optional: true, 140 Computed: true, 141 Elem: &schema.Resource{ 142 Schema: map[string]*schema.Schema{ 143 "device_name": &schema.Schema{ 144 Type: schema.TypeString, 145 Required: true, 146 ForceNew: true, 147 }, 148 149 "virtual_name": &schema.Schema{ 150 Type: schema.TypeString, 151 Optional: true, 152 ForceNew: true, 153 }, 154 155 "snapshot_id": &schema.Schema{ 156 Type: schema.TypeString, 157 Optional: true, 158 Computed: true, 159 ForceNew: true, 160 }, 161 162 "volume_type": &schema.Schema{ 163 Type: schema.TypeString, 164 Optional: true, 165 Computed: true, 166 ForceNew: true, 167 }, 168 169 "volume_size": &schema.Schema{ 170 Type: schema.TypeInt, 171 Optional: true, 172 Computed: true, 173 ForceNew: true, 174 }, 175 176 "delete_on_termination": &schema.Schema{ 177 Type: schema.TypeBool, 178 Optional: true, 179 Default: true, 180 ForceNew: true, 181 }, 182 183 "encrypted": &schema.Schema{ 184 Type: schema.TypeBool, 185 Optional: true, 186 Computed: true, 187 ForceNew: true, 188 }, 189 190 "iops": &schema.Schema{ 191 Type: schema.TypeInt, 192 Optional: true, 193 Computed: true, 194 ForceNew: true, 195 }, 196 }, 197 }, 198 Set: resourceAwsInstanceBlockDevicesHash, 199 }, 200 201 "root_block_device": &schema.Schema{ 202 // TODO: This is a list because we don't support singleton 203 // sub-resources today. We'll enforce that the list only ever has 204 // length zero or one below. When TF gains support for 205 // sub-resources this can be converted. 206 Type: schema.TypeList, 207 Optional: true, 208 Computed: true, 209 Elem: &schema.Resource{ 210 // "You can only modify the volume size, volume type, and Delete on 211 // Termination flag on the block device mapping entry for the root 212 // device volume." - bit.ly/ec2bdmap 213 Schema: map[string]*schema.Schema{ 214 "delete_on_termination": &schema.Schema{ 215 Type: schema.TypeBool, 216 Optional: true, 217 Default: true, 218 ForceNew: true, 219 }, 220 221 "device_name": &schema.Schema{ 222 Type: schema.TypeString, 223 Optional: true, 224 ForceNew: true, 225 Default: "/dev/sda1", 226 }, 227 228 "volume_size": &schema.Schema{ 229 Type: schema.TypeInt, 230 Optional: true, 231 Computed: true, 232 ForceNew: true, 233 }, 234 235 "volume_type": &schema.Schema{ 236 Type: schema.TypeString, 237 Optional: true, 238 Computed: true, 239 ForceNew: true, 240 }, 241 242 "iops": &schema.Schema{ 243 Type: schema.TypeInt, 244 Optional: true, 245 Computed: true, 246 ForceNew: true, 247 }, 248 }, 249 }, 250 }, 251 }, 252 } 253 } 254 255 func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error { 256 ec2conn := meta.(*AWSClient).ec2conn 257 258 // Figure out user data 259 userData := "" 260 if v := d.Get("user_data"); v != nil { 261 userData = v.(string) 262 } 263 264 associatePublicIPAddress := false 265 if v := d.Get("associate_public_ip_address"); v != nil { 266 associatePublicIPAddress = v.(bool) 267 } 268 269 // Build the creation struct 270 runOpts := &ec2.RunInstances{ 271 ImageId: d.Get("ami").(string), 272 AvailZone: d.Get("availability_zone").(string), 273 InstanceType: d.Get("instance_type").(string), 274 KeyName: d.Get("key_name").(string), 275 SubnetId: d.Get("subnet_id").(string), 276 PrivateIPAddress: d.Get("private_ip").(string), 277 AssociatePublicIpAddress: associatePublicIPAddress, 278 UserData: []byte(userData), 279 EbsOptimized: d.Get("ebs_optimized").(bool), 280 IamInstanceProfile: d.Get("iam_instance_profile").(string), 281 Tenancy: d.Get("tenancy").(string), 282 } 283 284 if v := d.Get("security_groups"); v != nil { 285 for _, v := range v.(*schema.Set).List() { 286 str := v.(string) 287 288 var g ec2.SecurityGroup 289 if runOpts.SubnetId != "" { 290 g.Id = str 291 } else { 292 g.Name = str 293 } 294 295 runOpts.SecurityGroups = append(runOpts.SecurityGroups, g) 296 } 297 } 298 299 blockDevices := make([]interface{}, 0) 300 301 if v := d.Get("block_device"); v != nil { 302 blockDevices = append(blockDevices, v.(*schema.Set).List()...) 303 } 304 305 if v := d.Get("root_block_device"); v != nil { 306 rootBlockDevices := v.([]interface{}) 307 if len(rootBlockDevices) > 1 { 308 return fmt.Errorf("Cannot specify more than one root_block_device.") 309 } 310 blockDevices = append(blockDevices, rootBlockDevices...) 311 } 312 313 if len(blockDevices) > 0 { 314 runOpts.BlockDevices = make([]ec2.BlockDeviceMapping, len(blockDevices)) 315 for i, v := range blockDevices { 316 bd := v.(map[string]interface{}) 317 runOpts.BlockDevices[i].DeviceName = bd["device_name"].(string) 318 runOpts.BlockDevices[i].VolumeType = bd["volume_type"].(string) 319 runOpts.BlockDevices[i].VolumeSize = int64(bd["volume_size"].(int)) 320 runOpts.BlockDevices[i].DeleteOnTermination = bd["delete_on_termination"].(bool) 321 if v, ok := bd["virtual_name"].(string); ok { 322 runOpts.BlockDevices[i].VirtualName = v 323 } 324 if v, ok := bd["snapshot_id"].(string); ok { 325 runOpts.BlockDevices[i].SnapshotId = v 326 } 327 if v, ok := bd["encrypted"].(bool); ok { 328 runOpts.BlockDevices[i].Encrypted = v 329 } 330 if v, ok := bd["iops"].(int); ok { 331 runOpts.BlockDevices[i].IOPS = int64(v) 332 } 333 } 334 } 335 336 // Create the instance 337 log.Printf("[DEBUG] Run configuration: %#v", runOpts) 338 runResp, err := ec2conn.RunInstances(runOpts) 339 if err != nil { 340 return fmt.Errorf("Error launching source instance: %s", err) 341 } 342 343 instance := &runResp.Instances[0] 344 log.Printf("[INFO] Instance ID: %s", instance.InstanceId) 345 346 // Store the resulting ID so we can look this up later 347 d.SetId(instance.InstanceId) 348 349 // Wait for the instance to become running so we can get some attributes 350 // that aren't available until later. 351 log.Printf( 352 "[DEBUG] Waiting for instance (%s) to become running", 353 instance.InstanceId) 354 355 stateConf := &resource.StateChangeConf{ 356 Pending: []string{"pending"}, 357 Target: "running", 358 Refresh: InstanceStateRefreshFunc(ec2conn, instance.InstanceId), 359 Timeout: 10 * time.Minute, 360 Delay: 10 * time.Second, 361 MinTimeout: 3 * time.Second, 362 } 363 364 instanceRaw, err := stateConf.WaitForState() 365 if err != nil { 366 return fmt.Errorf( 367 "Error waiting for instance (%s) to become ready: %s", 368 instance.InstanceId, err) 369 } 370 371 instance = instanceRaw.(*ec2.Instance) 372 373 // Initialize the connection info 374 d.SetConnInfo(map[string]string{ 375 "type": "ssh", 376 "host": instance.PublicIpAddress, 377 }) 378 379 // Set our attributes 380 if err := resourceAwsInstanceRead(d, meta); err != nil { 381 return err 382 } 383 384 // Update if we need to 385 return resourceAwsInstanceUpdate(d, meta) 386 } 387 388 func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error { 389 ec2conn := meta.(*AWSClient).ec2conn 390 391 resp, err := ec2conn.Instances([]string{d.Id()}, ec2.NewFilter()) 392 if err != nil { 393 // If the instance was not found, return nil so that we can show 394 // that the instance is gone. 395 if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidInstanceID.NotFound" { 396 d.SetId("") 397 return nil 398 } 399 400 // Some other error, report it 401 return err 402 } 403 404 // If nothing was found, then return no state 405 if len(resp.Reservations) == 0 { 406 d.SetId("") 407 return nil 408 } 409 410 instance := &resp.Reservations[0].Instances[0] 411 412 // If the instance is terminated, then it is gone 413 if instance.State.Name == "terminated" { 414 d.SetId("") 415 return nil 416 } 417 418 d.Set("availability_zone", instance.AvailZone) 419 d.Set("key_name", instance.KeyName) 420 d.Set("public_dns", instance.DNSName) 421 d.Set("public_ip", instance.PublicIpAddress) 422 d.Set("private_dns", instance.PrivateDNSName) 423 d.Set("private_ip", instance.PrivateIpAddress) 424 d.Set("subnet_id", instance.SubnetId) 425 d.Set("ebs_optimized", instance.EbsOptimized) 426 d.Set("tags", tagsToMap(instance.Tags)) 427 d.Set("tenancy", instance.Tenancy) 428 429 // Determine whether we're referring to security groups with 430 // IDs or names. We use a heuristic to figure this out. By default, 431 // we use IDs if we're in a VPC. However, if we previously had an 432 // all-name list of security groups, we use names. Or, if we had any 433 // IDs, we use IDs. 434 useID := instance.SubnetId != "" 435 if v := d.Get("security_groups"); v != nil { 436 match := false 437 for _, v := range v.(*schema.Set).List() { 438 if strings.HasPrefix(v.(string), "sg-") { 439 match = true 440 break 441 } 442 } 443 444 useID = match 445 } 446 447 // Build up the security groups 448 sgs := make([]string, len(instance.SecurityGroups)) 449 for i, sg := range instance.SecurityGroups { 450 if useID { 451 sgs[i] = sg.Id 452 } else { 453 sgs[i] = sg.Name 454 } 455 } 456 d.Set("security_groups", sgs) 457 458 blockDevices := make(map[string]ec2.BlockDevice) 459 for _, bd := range instance.BlockDevices { 460 blockDevices[bd.VolumeId] = bd 461 } 462 463 volIDs := make([]string, 0, len(blockDevices)) 464 for volID := range blockDevices { 465 volIDs = append(volIDs, volID) 466 } 467 468 volResp, err := ec2conn.Volumes(volIDs, ec2.NewFilter()) 469 if err != nil { 470 return err 471 } 472 473 nonRootBlockDevices := make([]map[string]interface{}, 0) 474 rootBlockDevice := make([]interface{}, 0, 1) 475 for _, vol := range volResp.Volumes { 476 volSize, err := strconv.Atoi(vol.Size) 477 if err != nil { 478 return err 479 } 480 481 blockDevice := make(map[string]interface{}) 482 blockDevice["device_name"] = blockDevices[vol.VolumeId].DeviceName 483 blockDevice["volume_type"] = vol.VolumeType 484 blockDevice["volume_size"] = volSize 485 blockDevice["delete_on_termination"] = 486 blockDevices[vol.VolumeId].DeleteOnTermination 487 488 // If this is the root device, save it. We stop here since we 489 // can't put invalid keys into this map. 490 if blockDevice["device_name"] == instance.RootDeviceName { 491 rootBlockDevice = []interface{}{blockDevice} 492 continue 493 } 494 495 blockDevice["snapshot_id"] = vol.SnapshotId 496 blockDevice["encrypted"] = vol.Encrypted 497 blockDevice["iops"] = vol.IOPS 498 nonRootBlockDevices = append(nonRootBlockDevices, blockDevice) 499 } 500 d.Set("block_device", nonRootBlockDevices) 501 d.Set("root_block_device", rootBlockDevice) 502 503 return nil 504 } 505 506 func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error { 507 ec2conn := meta.(*AWSClient).ec2conn 508 opts := new(ec2.ModifyInstance) 509 510 opts.SetSourceDestCheck = true 511 opts.SourceDestCheck = d.Get("source_dest_check").(bool) 512 513 log.Printf("[INFO] Modifying instance %s: %#v", d.Id(), opts) 514 if _, err := ec2conn.ModifyInstance(d.Id(), opts); err != nil { 515 return err 516 } 517 518 // TODO(mitchellh): wait for the attributes we modified to 519 // persist the change... 520 521 if err := setTags(ec2conn, d); err != nil { 522 return err 523 } else { 524 d.SetPartial("tags") 525 } 526 527 return nil 528 } 529 530 func resourceAwsInstanceDelete(d *schema.ResourceData, meta interface{}) error { 531 ec2conn := meta.(*AWSClient).ec2conn 532 533 log.Printf("[INFO] Terminating instance: %s", d.Id()) 534 if _, err := ec2conn.TerminateInstances([]string{d.Id()}); err != nil { 535 return fmt.Errorf("Error terminating instance: %s", err) 536 } 537 538 log.Printf( 539 "[DEBUG] Waiting for instance (%s) to become terminated", 540 d.Id()) 541 542 stateConf := &resource.StateChangeConf{ 543 Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"}, 544 Target: "terminated", 545 Refresh: InstanceStateRefreshFunc(ec2conn, d.Id()), 546 Timeout: 10 * time.Minute, 547 Delay: 10 * time.Second, 548 MinTimeout: 3 * time.Second, 549 } 550 551 _, err := stateConf.WaitForState() 552 if err != nil { 553 return fmt.Errorf( 554 "Error waiting for instance (%s) to terminate: %s", 555 d.Id(), err) 556 } 557 558 d.SetId("") 559 return nil 560 } 561 562 // InstanceStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch 563 // an EC2 instance. 564 func InstanceStateRefreshFunc(conn *ec2.EC2, instanceID string) resource.StateRefreshFunc { 565 return func() (interface{}, string, error) { 566 resp, err := conn.Instances([]string{instanceID}, ec2.NewFilter()) 567 if err != nil { 568 if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidInstanceID.NotFound" { 569 // Set this to nil as if we didn't find anything. 570 resp = nil 571 } else { 572 log.Printf("Error on InstanceStateRefresh: %s", err) 573 return nil, "", err 574 } 575 } 576 577 if resp == nil || len(resp.Reservations) == 0 || len(resp.Reservations[0].Instances) == 0 { 578 // Sometimes AWS just has consistency issues and doesn't see 579 // our instance yet. Return an empty state. 580 return nil, "", nil 581 } 582 583 i := &resp.Reservations[0].Instances[0] 584 return i, i.State.Name, nil 585 } 586 } 587 588 func resourceAwsInstanceBlockDevicesHash(v interface{}) int { 589 var buf bytes.Buffer 590 m := v.(map[string]interface{}) 591 buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) 592 buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string))) 593 buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool))) 594 return hashcode.String(buf.String()) 595 }