github.com/rmenn/terraform@v0.3.8-0.20150225065417-fc84b3a78802/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 }, 191 Set: resourceAwsInstanceBlockDevicesHash, 192 }, 193 194 "root_block_device": &schema.Schema{ 195 // TODO: This is a list because we don't support singleton 196 // sub-resources today. We'll enforce that the list only ever has 197 // length zero or one below. When TF gains support for 198 // sub-resources this can be converted. 199 Type: schema.TypeList, 200 Optional: true, 201 Computed: true, 202 Elem: &schema.Resource{ 203 // "You can only modify the volume size, volume type, and Delete on 204 // Termination flag on the block device mapping entry for the root 205 // device volume." - bit.ly/ec2bdmap 206 Schema: map[string]*schema.Schema{ 207 "delete_on_termination": &schema.Schema{ 208 Type: schema.TypeBool, 209 Optional: true, 210 Default: true, 211 ForceNew: true, 212 }, 213 214 "device_name": &schema.Schema{ 215 Type: schema.TypeString, 216 Optional: true, 217 ForceNew: true, 218 Default: "/dev/sda1", 219 }, 220 221 "volume_size": &schema.Schema{ 222 Type: schema.TypeInt, 223 Optional: true, 224 Computed: true, 225 ForceNew: true, 226 }, 227 228 "volume_type": &schema.Schema{ 229 Type: schema.TypeString, 230 Optional: true, 231 Computed: true, 232 ForceNew: true, 233 }, 234 }, 235 }, 236 }, 237 }, 238 } 239 } 240 241 func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error { 242 ec2conn := meta.(*AWSClient).ec2conn 243 244 // Figure out user data 245 userData := "" 246 if v := d.Get("user_data"); v != nil { 247 userData = v.(string) 248 } 249 250 associatePublicIPAddress := false 251 if v := d.Get("associate_public_ip_address"); v != nil { 252 associatePublicIPAddress = v.(bool) 253 } 254 255 // Build the creation struct 256 runOpts := &ec2.RunInstances{ 257 ImageId: d.Get("ami").(string), 258 AvailZone: d.Get("availability_zone").(string), 259 InstanceType: d.Get("instance_type").(string), 260 KeyName: d.Get("key_name").(string), 261 SubnetId: d.Get("subnet_id").(string), 262 PrivateIPAddress: d.Get("private_ip").(string), 263 AssociatePublicIpAddress: associatePublicIPAddress, 264 UserData: []byte(userData), 265 EbsOptimized: d.Get("ebs_optimized").(bool), 266 IamInstanceProfile: d.Get("iam_instance_profile").(string), 267 Tenancy: d.Get("tenancy").(string), 268 } 269 270 if v := d.Get("security_groups"); v != nil { 271 for _, v := range v.(*schema.Set).List() { 272 str := v.(string) 273 274 var g ec2.SecurityGroup 275 if runOpts.SubnetId != "" { 276 g.Id = str 277 } else { 278 g.Name = str 279 } 280 281 runOpts.SecurityGroups = append(runOpts.SecurityGroups, g) 282 } 283 } 284 285 blockDevices := make([]interface{}, 0) 286 287 if v := d.Get("block_device"); v != nil { 288 blockDevices = append(blockDevices, v.(*schema.Set).List()...) 289 } 290 291 if v := d.Get("root_block_device"); v != nil { 292 rootBlockDevices := v.([]interface{}) 293 if len(rootBlockDevices) > 1 { 294 return fmt.Errorf("Cannot specify more than one root_block_device.") 295 } 296 blockDevices = append(blockDevices, rootBlockDevices...) 297 } 298 299 if len(blockDevices) > 0 { 300 runOpts.BlockDevices = make([]ec2.BlockDeviceMapping, len(blockDevices)) 301 for i, v := range blockDevices { 302 bd := v.(map[string]interface{}) 303 runOpts.BlockDevices[i].DeviceName = bd["device_name"].(string) 304 runOpts.BlockDevices[i].VolumeType = bd["volume_type"].(string) 305 runOpts.BlockDevices[i].VolumeSize = int64(bd["volume_size"].(int)) 306 runOpts.BlockDevices[i].DeleteOnTermination = bd["delete_on_termination"].(bool) 307 if v, ok := bd["virtual_name"].(string); ok { 308 runOpts.BlockDevices[i].VirtualName = v 309 } 310 if v, ok := bd["snapshot_id"].(string); ok { 311 runOpts.BlockDevices[i].SnapshotId = v 312 } 313 if v, ok := bd["encrypted"].(bool); ok { 314 runOpts.BlockDevices[i].Encrypted = v 315 } 316 } 317 } 318 319 // Create the instance 320 log.Printf("[DEBUG] Run configuration: %#v", runOpts) 321 runResp, err := ec2conn.RunInstances(runOpts) 322 if err != nil { 323 return fmt.Errorf("Error launching source instance: %s", err) 324 } 325 326 instance := &runResp.Instances[0] 327 log.Printf("[INFO] Instance ID: %s", instance.InstanceId) 328 329 // Store the resulting ID so we can look this up later 330 d.SetId(instance.InstanceId) 331 332 // Wait for the instance to become running so we can get some attributes 333 // that aren't available until later. 334 log.Printf( 335 "[DEBUG] Waiting for instance (%s) to become running", 336 instance.InstanceId) 337 338 stateConf := &resource.StateChangeConf{ 339 Pending: []string{"pending"}, 340 Target: "running", 341 Refresh: InstanceStateRefreshFunc(ec2conn, instance.InstanceId), 342 Timeout: 10 * time.Minute, 343 Delay: 10 * time.Second, 344 MinTimeout: 3 * time.Second, 345 } 346 347 instanceRaw, err := stateConf.WaitForState() 348 if err != nil { 349 return fmt.Errorf( 350 "Error waiting for instance (%s) to become ready: %s", 351 instance.InstanceId, err) 352 } 353 354 instance = instanceRaw.(*ec2.Instance) 355 356 // Initialize the connection info 357 d.SetConnInfo(map[string]string{ 358 "type": "ssh", 359 "host": instance.PublicIpAddress, 360 }) 361 362 // Set our attributes 363 if err := resourceAwsInstanceRead(d, meta); err != nil { 364 return err 365 } 366 367 // Update if we need to 368 return resourceAwsInstanceUpdate(d, meta) 369 } 370 371 func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error { 372 ec2conn := meta.(*AWSClient).ec2conn 373 374 resp, err := ec2conn.Instances([]string{d.Id()}, ec2.NewFilter()) 375 if err != nil { 376 // If the instance was not found, return nil so that we can show 377 // that the instance is gone. 378 if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidInstanceID.NotFound" { 379 d.SetId("") 380 return nil 381 } 382 383 // Some other error, report it 384 return err 385 } 386 387 // If nothing was found, then return no state 388 if len(resp.Reservations) == 0 { 389 d.SetId("") 390 return nil 391 } 392 393 instance := &resp.Reservations[0].Instances[0] 394 395 // If the instance is terminated, then it is gone 396 if instance.State.Name == "terminated" { 397 d.SetId("") 398 return nil 399 } 400 401 d.Set("availability_zone", instance.AvailZone) 402 d.Set("key_name", instance.KeyName) 403 d.Set("public_dns", instance.DNSName) 404 d.Set("public_ip", instance.PublicIpAddress) 405 d.Set("private_dns", instance.PrivateDNSName) 406 d.Set("private_ip", instance.PrivateIpAddress) 407 d.Set("subnet_id", instance.SubnetId) 408 d.Set("ebs_optimized", instance.EbsOptimized) 409 d.Set("tags", tagsToMap(instance.Tags)) 410 d.Set("tenancy", instance.Tenancy) 411 412 // Determine whether we're referring to security groups with 413 // IDs or names. We use a heuristic to figure this out. By default, 414 // we use IDs if we're in a VPC. However, if we previously had an 415 // all-name list of security groups, we use names. Or, if we had any 416 // IDs, we use IDs. 417 useID := instance.SubnetId != "" 418 if v := d.Get("security_groups"); v != nil { 419 match := false 420 for _, v := range v.(*schema.Set).List() { 421 if strings.HasPrefix(v.(string), "sg-") { 422 match = true 423 break 424 } 425 } 426 427 useID = match 428 } 429 430 // Build up the security groups 431 sgs := make([]string, len(instance.SecurityGroups)) 432 for i, sg := range instance.SecurityGroups { 433 if useID { 434 sgs[i] = sg.Id 435 } else { 436 sgs[i] = sg.Name 437 } 438 } 439 d.Set("security_groups", sgs) 440 441 blockDevices := make(map[string]ec2.BlockDevice) 442 for _, bd := range instance.BlockDevices { 443 blockDevices[bd.VolumeId] = bd 444 } 445 446 volIDs := make([]string, 0, len(blockDevices)) 447 for volID := range blockDevices { 448 volIDs = append(volIDs, volID) 449 } 450 451 volResp, err := ec2conn.Volumes(volIDs, ec2.NewFilter()) 452 if err != nil { 453 return err 454 } 455 456 nonRootBlockDevices := make([]map[string]interface{}, 0) 457 rootBlockDevice := make([]interface{}, 0, 1) 458 for _, vol := range volResp.Volumes { 459 volSize, err := strconv.Atoi(vol.Size) 460 if err != nil { 461 return err 462 } 463 464 blockDevice := make(map[string]interface{}) 465 blockDevice["device_name"] = blockDevices[vol.VolumeId].DeviceName 466 blockDevice["volume_type"] = vol.VolumeType 467 blockDevice["volume_size"] = volSize 468 blockDevice["delete_on_termination"] = 469 blockDevices[vol.VolumeId].DeleteOnTermination 470 471 // If this is the root device, save it. We stop here since we 472 // can't put invalid keys into this map. 473 if blockDevice["device_name"] == instance.RootDeviceName { 474 rootBlockDevice = []interface{}{blockDevice} 475 continue 476 } 477 478 blockDevice["snapshot_id"] = vol.SnapshotId 479 blockDevice["encrypted"] = vol.Encrypted 480 nonRootBlockDevices = append(nonRootBlockDevices, blockDevice) 481 } 482 d.Set("block_device", nonRootBlockDevices) 483 d.Set("root_block_device", rootBlockDevice) 484 485 return nil 486 } 487 488 func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error { 489 ec2conn := meta.(*AWSClient).ec2conn 490 opts := new(ec2.ModifyInstance) 491 492 opts.SetSourceDestCheck = true 493 opts.SourceDestCheck = d.Get("source_dest_check").(bool) 494 495 log.Printf("[INFO] Modifying instance %s: %#v", d.Id(), opts) 496 if _, err := ec2conn.ModifyInstance(d.Id(), opts); err != nil { 497 return err 498 } 499 500 // TODO(mitchellh): wait for the attributes we modified to 501 // persist the change... 502 503 if err := setTags(ec2conn, d); err != nil { 504 return err 505 } else { 506 d.SetPartial("tags") 507 } 508 509 return nil 510 } 511 512 func resourceAwsInstanceDelete(d *schema.ResourceData, meta interface{}) error { 513 ec2conn := meta.(*AWSClient).ec2conn 514 515 log.Printf("[INFO] Terminating instance: %s", d.Id()) 516 if _, err := ec2conn.TerminateInstances([]string{d.Id()}); err != nil { 517 return fmt.Errorf("Error terminating instance: %s", err) 518 } 519 520 log.Printf( 521 "[DEBUG] Waiting for instance (%s) to become terminated", 522 d.Id()) 523 524 stateConf := &resource.StateChangeConf{ 525 Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"}, 526 Target: "terminated", 527 Refresh: InstanceStateRefreshFunc(ec2conn, d.Id()), 528 Timeout: 10 * time.Minute, 529 Delay: 10 * time.Second, 530 MinTimeout: 3 * time.Second, 531 } 532 533 _, err := stateConf.WaitForState() 534 if err != nil { 535 return fmt.Errorf( 536 "Error waiting for instance (%s) to terminate: %s", 537 d.Id(), err) 538 } 539 540 d.SetId("") 541 return nil 542 } 543 544 // InstanceStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch 545 // an EC2 instance. 546 func InstanceStateRefreshFunc(conn *ec2.EC2, instanceID string) resource.StateRefreshFunc { 547 return func() (interface{}, string, error) { 548 resp, err := conn.Instances([]string{instanceID}, ec2.NewFilter()) 549 if err != nil { 550 if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidInstanceID.NotFound" { 551 // Set this to nil as if we didn't find anything. 552 resp = nil 553 } else { 554 log.Printf("Error on InstanceStateRefresh: %s", err) 555 return nil, "", err 556 } 557 } 558 559 if resp == nil || len(resp.Reservations) == 0 || len(resp.Reservations[0].Instances) == 0 { 560 // Sometimes AWS just has consistency issues and doesn't see 561 // our instance yet. Return an empty state. 562 return nil, "", nil 563 } 564 565 i := &resp.Reservations[0].Instances[0] 566 return i, i.State.Name, nil 567 } 568 } 569 570 func resourceAwsInstanceBlockDevicesHash(v interface{}) int { 571 var buf bytes.Buffer 572 m := v.(map[string]interface{}) 573 buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) 574 buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string))) 575 buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool))) 576 return hashcode.String(buf.String()) 577 }