github.com/vtorhonen/terraform@v0.9.0-beta2.0.20170307220345-5d894e4ffda7/builtin/providers/aws/resource_aws_launch_configuration.go (about) 1 package aws 2 3 import ( 4 "bytes" 5 "crypto/sha1" 6 "encoding/hex" 7 "fmt" 8 "log" 9 "strings" 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/autoscaling" 15 "github.com/aws/aws-sdk-go/service/ec2" 16 "github.com/hashicorp/terraform/helper/hashcode" 17 "github.com/hashicorp/terraform/helper/resource" 18 "github.com/hashicorp/terraform/helper/schema" 19 ) 20 21 func resourceAwsLaunchConfiguration() *schema.Resource { 22 return &schema.Resource{ 23 Create: resourceAwsLaunchConfigurationCreate, 24 Read: resourceAwsLaunchConfigurationRead, 25 Delete: resourceAwsLaunchConfigurationDelete, 26 Importer: &schema.ResourceImporter{ 27 State: schema.ImportStatePassthrough, 28 }, 29 30 Schema: map[string]*schema.Schema{ 31 "name": &schema.Schema{ 32 Type: schema.TypeString, 33 Optional: true, 34 Computed: true, 35 ForceNew: true, 36 ConflictsWith: []string{"name_prefix"}, 37 ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { 38 // https://github.com/boto/botocore/blob/9f322b1/botocore/data/autoscaling/2011-01-01/service-2.json#L1932-L1939 39 value := v.(string) 40 if len(value) > 255 { 41 errors = append(errors, fmt.Errorf( 42 "%q cannot be longer than 255 characters", k)) 43 } 44 return 45 }, 46 }, 47 48 "name_prefix": &schema.Schema{ 49 Type: schema.TypeString, 50 Optional: true, 51 ForceNew: true, 52 ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { 53 // https://github.com/boto/botocore/blob/9f322b1/botocore/data/autoscaling/2011-01-01/service-2.json#L1932-L1939 54 // uuid is 26 characters, limit the prefix to 229. 55 value := v.(string) 56 if len(value) > 229 { 57 errors = append(errors, fmt.Errorf( 58 "%q cannot be longer than 229 characters, name is limited to 255", k)) 59 } 60 return 61 }, 62 }, 63 64 "image_id": &schema.Schema{ 65 Type: schema.TypeString, 66 Required: true, 67 ForceNew: true, 68 }, 69 70 "instance_type": &schema.Schema{ 71 Type: schema.TypeString, 72 Required: true, 73 ForceNew: true, 74 }, 75 76 "iam_instance_profile": &schema.Schema{ 77 Type: schema.TypeString, 78 Optional: true, 79 ForceNew: true, 80 }, 81 82 "key_name": &schema.Schema{ 83 Type: schema.TypeString, 84 Optional: true, 85 Computed: true, 86 ForceNew: true, 87 }, 88 89 "user_data": &schema.Schema{ 90 Type: schema.TypeString, 91 Optional: true, 92 ForceNew: true, 93 StateFunc: func(v interface{}) string { 94 switch v.(type) { 95 case string: 96 hash := sha1.Sum([]byte(v.(string))) 97 return hex.EncodeToString(hash[:]) 98 default: 99 return "" 100 } 101 }, 102 }, 103 104 "security_groups": &schema.Schema{ 105 Type: schema.TypeSet, 106 Optional: true, 107 ForceNew: true, 108 Elem: &schema.Schema{Type: schema.TypeString}, 109 Set: schema.HashString, 110 }, 111 112 "vpc_classic_link_id": &schema.Schema{ 113 Type: schema.TypeString, 114 Optional: true, 115 ForceNew: true, 116 }, 117 118 "vpc_classic_link_security_groups": &schema.Schema{ 119 Type: schema.TypeSet, 120 Optional: true, 121 ForceNew: true, 122 Elem: &schema.Schema{Type: schema.TypeString}, 123 Set: schema.HashString, 124 }, 125 126 "associate_public_ip_address": &schema.Schema{ 127 Type: schema.TypeBool, 128 Optional: true, 129 ForceNew: true, 130 Default: false, 131 }, 132 133 "spot_price": &schema.Schema{ 134 Type: schema.TypeString, 135 Optional: true, 136 ForceNew: true, 137 }, 138 139 "ebs_optimized": &schema.Schema{ 140 Type: schema.TypeBool, 141 Optional: true, 142 ForceNew: true, 143 Computed: true, 144 }, 145 146 "placement_tenancy": &schema.Schema{ 147 Type: schema.TypeString, 148 Optional: true, 149 ForceNew: true, 150 }, 151 152 "enable_monitoring": &schema.Schema{ 153 Type: schema.TypeBool, 154 Optional: true, 155 ForceNew: true, 156 Default: true, 157 }, 158 159 "ebs_block_device": &schema.Schema{ 160 Type: schema.TypeSet, 161 Optional: true, 162 Computed: true, 163 Elem: &schema.Resource{ 164 Schema: map[string]*schema.Schema{ 165 "delete_on_termination": &schema.Schema{ 166 Type: schema.TypeBool, 167 Optional: true, 168 Default: true, 169 ForceNew: true, 170 }, 171 172 "device_name": &schema.Schema{ 173 Type: schema.TypeString, 174 Required: true, 175 ForceNew: true, 176 }, 177 178 "iops": &schema.Schema{ 179 Type: schema.TypeInt, 180 Optional: true, 181 Computed: true, 182 ForceNew: true, 183 }, 184 185 "snapshot_id": &schema.Schema{ 186 Type: schema.TypeString, 187 Optional: true, 188 Computed: true, 189 ForceNew: true, 190 }, 191 192 "volume_size": &schema.Schema{ 193 Type: schema.TypeInt, 194 Optional: true, 195 Computed: true, 196 ForceNew: true, 197 }, 198 199 "volume_type": &schema.Schema{ 200 Type: schema.TypeString, 201 Optional: true, 202 Computed: true, 203 ForceNew: true, 204 }, 205 206 "encrypted": &schema.Schema{ 207 Type: schema.TypeBool, 208 Optional: true, 209 Computed: true, 210 ForceNew: true, 211 }, 212 }, 213 }, 214 Set: func(v interface{}) int { 215 var buf bytes.Buffer 216 m := v.(map[string]interface{}) 217 buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) 218 buf.WriteString(fmt.Sprintf("%s-", m["snapshot_id"].(string))) 219 return hashcode.String(buf.String()) 220 }, 221 }, 222 223 "ephemeral_block_device": &schema.Schema{ 224 Type: schema.TypeSet, 225 Optional: true, 226 ForceNew: true, 227 Elem: &schema.Resource{ 228 Schema: map[string]*schema.Schema{ 229 "device_name": &schema.Schema{ 230 Type: schema.TypeString, 231 Required: true, 232 }, 233 234 "virtual_name": &schema.Schema{ 235 Type: schema.TypeString, 236 Required: true, 237 }, 238 }, 239 }, 240 Set: func(v interface{}) int { 241 var buf bytes.Buffer 242 m := v.(map[string]interface{}) 243 buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) 244 buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string))) 245 return hashcode.String(buf.String()) 246 }, 247 }, 248 249 "root_block_device": &schema.Schema{ 250 // TODO: This is a set because we don't support singleton 251 // sub-resources today. We'll enforce that the set only ever has 252 // length zero or one below. When TF gains support for 253 // sub-resources this can be converted. 254 Type: schema.TypeSet, 255 Optional: true, 256 Computed: true, 257 Elem: &schema.Resource{ 258 // "You can only modify the volume size, volume type, and Delete on 259 // Termination flag on the block device mapping entry for the root 260 // device volume." - bit.ly/ec2bdmap 261 Schema: map[string]*schema.Schema{ 262 "delete_on_termination": &schema.Schema{ 263 Type: schema.TypeBool, 264 Optional: true, 265 Default: true, 266 ForceNew: true, 267 }, 268 269 "iops": &schema.Schema{ 270 Type: schema.TypeInt, 271 Optional: true, 272 Computed: true, 273 ForceNew: true, 274 }, 275 276 "volume_size": &schema.Schema{ 277 Type: schema.TypeInt, 278 Optional: true, 279 Computed: true, 280 ForceNew: true, 281 }, 282 283 "volume_type": &schema.Schema{ 284 Type: schema.TypeString, 285 Optional: true, 286 Computed: true, 287 ForceNew: true, 288 }, 289 }, 290 }, 291 Set: func(v interface{}) int { 292 // there can be only one root device; no need to hash anything 293 return 0 294 }, 295 }, 296 }, 297 } 298 } 299 300 func resourceAwsLaunchConfigurationCreate(d *schema.ResourceData, meta interface{}) error { 301 autoscalingconn := meta.(*AWSClient).autoscalingconn 302 ec2conn := meta.(*AWSClient).ec2conn 303 304 createLaunchConfigurationOpts := autoscaling.CreateLaunchConfigurationInput{ 305 LaunchConfigurationName: aws.String(d.Get("name").(string)), 306 ImageId: aws.String(d.Get("image_id").(string)), 307 InstanceType: aws.String(d.Get("instance_type").(string)), 308 EbsOptimized: aws.Bool(d.Get("ebs_optimized").(bool)), 309 } 310 311 if v, ok := d.GetOk("user_data"); ok { 312 userData := base64Encode([]byte(v.(string))) 313 createLaunchConfigurationOpts.UserData = aws.String(userData) 314 } 315 316 createLaunchConfigurationOpts.InstanceMonitoring = &autoscaling.InstanceMonitoring{ 317 Enabled: aws.Bool(d.Get("enable_monitoring").(bool)), 318 } 319 320 if v, ok := d.GetOk("iam_instance_profile"); ok { 321 createLaunchConfigurationOpts.IamInstanceProfile = aws.String(v.(string)) 322 } 323 324 if v, ok := d.GetOk("placement_tenancy"); ok { 325 createLaunchConfigurationOpts.PlacementTenancy = aws.String(v.(string)) 326 } 327 328 if v, ok := d.GetOk("associate_public_ip_address"); ok { 329 createLaunchConfigurationOpts.AssociatePublicIpAddress = aws.Bool(v.(bool)) 330 } 331 332 if v, ok := d.GetOk("key_name"); ok { 333 createLaunchConfigurationOpts.KeyName = aws.String(v.(string)) 334 } 335 if v, ok := d.GetOk("spot_price"); ok { 336 createLaunchConfigurationOpts.SpotPrice = aws.String(v.(string)) 337 } 338 339 if v, ok := d.GetOk("security_groups"); ok { 340 createLaunchConfigurationOpts.SecurityGroups = expandStringList( 341 v.(*schema.Set).List(), 342 ) 343 } 344 345 if v, ok := d.GetOk("vpc_classic_link_id"); ok { 346 createLaunchConfigurationOpts.ClassicLinkVPCId = aws.String(v.(string)) 347 } 348 349 if v, ok := d.GetOk("vpc_classic_link_security_groups"); ok { 350 createLaunchConfigurationOpts.ClassicLinkVPCSecurityGroups = expandStringList( 351 v.(*schema.Set).List(), 352 ) 353 } 354 355 var blockDevices []*autoscaling.BlockDeviceMapping 356 357 // We'll use this to detect if we're declaring it incorrectly as an ebs_block_device. 358 rootDeviceName, err := fetchRootDeviceName(d.Get("image_id").(string), ec2conn) 359 if err != nil { 360 return err 361 } 362 if rootDeviceName == nil { 363 // We do this so the value is empty so we don't have to do nil checks later 364 var blank string 365 rootDeviceName = &blank 366 } 367 368 if v, ok := d.GetOk("ebs_block_device"); ok { 369 vL := v.(*schema.Set).List() 370 for _, v := range vL { 371 bd := v.(map[string]interface{}) 372 ebs := &autoscaling.Ebs{ 373 DeleteOnTermination: aws.Bool(bd["delete_on_termination"].(bool)), 374 } 375 376 if v, ok := bd["snapshot_id"].(string); ok && v != "" { 377 ebs.SnapshotId = aws.String(v) 378 } 379 380 if v, ok := bd["encrypted"].(bool); ok && v { 381 ebs.Encrypted = aws.Bool(v) 382 } 383 384 if v, ok := bd["volume_size"].(int); ok && v != 0 { 385 ebs.VolumeSize = aws.Int64(int64(v)) 386 } 387 388 if v, ok := bd["volume_type"].(string); ok && v != "" { 389 ebs.VolumeType = aws.String(v) 390 } 391 392 if v, ok := bd["iops"].(int); ok && v > 0 { 393 ebs.Iops = aws.Int64(int64(v)) 394 } 395 396 if *aws.String(bd["device_name"].(string)) == *rootDeviceName { 397 return fmt.Errorf("Root device (%s) declared as an 'ebs_block_device'. Use 'root_block_device' keyword.", *rootDeviceName) 398 } 399 400 blockDevices = append(blockDevices, &autoscaling.BlockDeviceMapping{ 401 DeviceName: aws.String(bd["device_name"].(string)), 402 Ebs: ebs, 403 }) 404 } 405 } 406 407 if v, ok := d.GetOk("ephemeral_block_device"); ok { 408 vL := v.(*schema.Set).List() 409 for _, v := range vL { 410 bd := v.(map[string]interface{}) 411 blockDevices = append(blockDevices, &autoscaling.BlockDeviceMapping{ 412 DeviceName: aws.String(bd["device_name"].(string)), 413 VirtualName: aws.String(bd["virtual_name"].(string)), 414 }) 415 } 416 } 417 418 if v, ok := d.GetOk("root_block_device"); ok { 419 vL := v.(*schema.Set).List() 420 if len(vL) > 1 { 421 return fmt.Errorf("Cannot specify more than one root_block_device.") 422 } 423 for _, v := range vL { 424 bd := v.(map[string]interface{}) 425 ebs := &autoscaling.Ebs{ 426 DeleteOnTermination: aws.Bool(bd["delete_on_termination"].(bool)), 427 } 428 429 if v, ok := bd["volume_size"].(int); ok && v != 0 { 430 ebs.VolumeSize = aws.Int64(int64(v)) 431 } 432 433 if v, ok := bd["volume_type"].(string); ok && v != "" { 434 ebs.VolumeType = aws.String(v) 435 } 436 437 if v, ok := bd["iops"].(int); ok && v > 0 { 438 ebs.Iops = aws.Int64(int64(v)) 439 } 440 441 if dn, err := fetchRootDeviceName(d.Get("image_id").(string), ec2conn); err == nil { 442 if dn == nil { 443 return fmt.Errorf( 444 "Expected to find a Root Device name for AMI (%s), but got none", 445 d.Get("image_id").(string)) 446 } 447 blockDevices = append(blockDevices, &autoscaling.BlockDeviceMapping{ 448 DeviceName: dn, 449 Ebs: ebs, 450 }) 451 } else { 452 return err 453 } 454 } 455 } 456 457 if len(blockDevices) > 0 { 458 createLaunchConfigurationOpts.BlockDeviceMappings = blockDevices 459 } 460 461 var lcName string 462 if v, ok := d.GetOk("name"); ok { 463 lcName = v.(string) 464 } else if v, ok := d.GetOk("name_prefix"); ok { 465 lcName = resource.PrefixedUniqueId(v.(string)) 466 } else { 467 lcName = resource.UniqueId() 468 } 469 createLaunchConfigurationOpts.LaunchConfigurationName = aws.String(lcName) 470 471 log.Printf( 472 "[DEBUG] autoscaling create launch configuration: %s", createLaunchConfigurationOpts) 473 474 // IAM profiles can take ~10 seconds to propagate in AWS: 475 // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#launch-instance-with-role-console 476 err = resource.Retry(30*time.Second, func() *resource.RetryError { 477 _, err := autoscalingconn.CreateLaunchConfiguration(&createLaunchConfigurationOpts) 478 if err != nil { 479 if awsErr, ok := err.(awserr.Error); ok { 480 if strings.Contains(awsErr.Message(), "Invalid IamInstanceProfile") { 481 return resource.RetryableError(err) 482 } 483 if strings.Contains(awsErr.Message(), "You are not authorized to perform this operation") { 484 return resource.RetryableError(err) 485 } 486 } 487 return resource.NonRetryableError(err) 488 } 489 return nil 490 }) 491 492 if err != nil { 493 return fmt.Errorf("Error creating launch configuration: %s", err) 494 } 495 496 d.SetId(lcName) 497 log.Printf("[INFO] launch configuration ID: %s", d.Id()) 498 499 // We put a Retry here since sometimes eventual consistency bites 500 // us and we need to retry a few times to get the LC to load properly 501 return resource.Retry(30*time.Second, func() *resource.RetryError { 502 err := resourceAwsLaunchConfigurationRead(d, meta) 503 if err != nil { 504 return resource.RetryableError(err) 505 } 506 return nil 507 }) 508 } 509 510 func resourceAwsLaunchConfigurationRead(d *schema.ResourceData, meta interface{}) error { 511 autoscalingconn := meta.(*AWSClient).autoscalingconn 512 ec2conn := meta.(*AWSClient).ec2conn 513 514 describeOpts := autoscaling.DescribeLaunchConfigurationsInput{ 515 LaunchConfigurationNames: []*string{aws.String(d.Id())}, 516 } 517 518 log.Printf("[DEBUG] launch configuration describe configuration: %s", describeOpts) 519 describConfs, err := autoscalingconn.DescribeLaunchConfigurations(&describeOpts) 520 if err != nil { 521 return fmt.Errorf("Error retrieving launch configuration: %s", err) 522 } 523 if len(describConfs.LaunchConfigurations) == 0 { 524 d.SetId("") 525 return nil 526 } 527 528 // Verify AWS returned our launch configuration 529 if *describConfs.LaunchConfigurations[0].LaunchConfigurationName != d.Id() { 530 return fmt.Errorf( 531 "Unable to find launch configuration: %#v", 532 describConfs.LaunchConfigurations) 533 } 534 535 lc := describConfs.LaunchConfigurations[0] 536 537 d.Set("key_name", lc.KeyName) 538 d.Set("image_id", lc.ImageId) 539 d.Set("instance_type", lc.InstanceType) 540 d.Set("name", lc.LaunchConfigurationName) 541 542 d.Set("iam_instance_profile", lc.IamInstanceProfile) 543 d.Set("ebs_optimized", lc.EbsOptimized) 544 d.Set("spot_price", lc.SpotPrice) 545 d.Set("enable_monitoring", lc.InstanceMonitoring.Enabled) 546 d.Set("security_groups", lc.SecurityGroups) 547 d.Set("associate_public_ip_address", lc.AssociatePublicIpAddress) 548 549 d.Set("vpc_classic_link_id", lc.ClassicLinkVPCId) 550 d.Set("vpc_classic_link_security_groups", lc.ClassicLinkVPCSecurityGroups) 551 552 if err := readLCBlockDevices(d, lc, ec2conn); err != nil { 553 return err 554 } 555 556 return nil 557 } 558 559 func resourceAwsLaunchConfigurationDelete(d *schema.ResourceData, meta interface{}) error { 560 autoscalingconn := meta.(*AWSClient).autoscalingconn 561 562 log.Printf("[DEBUG] Launch Configuration destroy: %v", d.Id()) 563 _, err := autoscalingconn.DeleteLaunchConfiguration( 564 &autoscaling.DeleteLaunchConfigurationInput{ 565 LaunchConfigurationName: aws.String(d.Id()), 566 }) 567 if err != nil { 568 autoscalingerr, ok := err.(awserr.Error) 569 if ok && (autoscalingerr.Code() == "InvalidConfiguration.NotFound" || autoscalingerr.Code() == "ValidationError") { 570 log.Printf("[DEBUG] Launch configuration (%s) not found", d.Id()) 571 return nil 572 } 573 574 return err 575 } 576 577 return nil 578 } 579 580 func readLCBlockDevices(d *schema.ResourceData, lc *autoscaling.LaunchConfiguration, ec2conn *ec2.EC2) error { 581 ibds, err := readBlockDevicesFromLaunchConfiguration(d, lc, ec2conn) 582 if err != nil { 583 return err 584 } 585 586 if err := d.Set("ebs_block_device", ibds["ebs"]); err != nil { 587 return err 588 } 589 if err := d.Set("ephemeral_block_device", ibds["ephemeral"]); err != nil { 590 return err 591 } 592 if ibds["root"] != nil { 593 if err := d.Set("root_block_device", []interface{}{ibds["root"]}); err != nil { 594 return err 595 } 596 } else { 597 d.Set("root_block_device", []interface{}{}) 598 } 599 600 return nil 601 } 602 603 func readBlockDevicesFromLaunchConfiguration(d *schema.ResourceData, lc *autoscaling.LaunchConfiguration, ec2conn *ec2.EC2) ( 604 map[string]interface{}, error) { 605 blockDevices := make(map[string]interface{}) 606 blockDevices["ebs"] = make([]map[string]interface{}, 0) 607 blockDevices["ephemeral"] = make([]map[string]interface{}, 0) 608 blockDevices["root"] = nil 609 if len(lc.BlockDeviceMappings) == 0 { 610 return nil, nil 611 } 612 rootDeviceName, err := fetchRootDeviceName(d.Get("image_id").(string), ec2conn) 613 if err != nil { 614 return nil, err 615 } 616 if rootDeviceName == nil { 617 // We do this so the value is empty so we don't have to do nil checks later 618 var blank string 619 rootDeviceName = &blank 620 } 621 for _, bdm := range lc.BlockDeviceMappings { 622 bd := make(map[string]interface{}) 623 if bdm.Ebs != nil && bdm.Ebs.DeleteOnTermination != nil { 624 bd["delete_on_termination"] = *bdm.Ebs.DeleteOnTermination 625 } 626 if bdm.Ebs != nil && bdm.Ebs.VolumeSize != nil { 627 bd["volume_size"] = *bdm.Ebs.VolumeSize 628 } 629 if bdm.Ebs != nil && bdm.Ebs.VolumeType != nil { 630 bd["volume_type"] = *bdm.Ebs.VolumeType 631 } 632 if bdm.Ebs != nil && bdm.Ebs.Iops != nil { 633 bd["iops"] = *bdm.Ebs.Iops 634 } 635 636 if bdm.DeviceName != nil && *bdm.DeviceName == *rootDeviceName { 637 blockDevices["root"] = bd 638 } else { 639 if bdm.Ebs != nil && bdm.Ebs.Encrypted != nil { 640 bd["encrypted"] = *bdm.Ebs.Encrypted 641 } 642 if bdm.DeviceName != nil { 643 bd["device_name"] = *bdm.DeviceName 644 } 645 if bdm.VirtualName != nil { 646 bd["virtual_name"] = *bdm.VirtualName 647 blockDevices["ephemeral"] = append(blockDevices["ephemeral"].([]map[string]interface{}), bd) 648 } else { 649 if bdm.Ebs != nil && bdm.Ebs.SnapshotId != nil { 650 bd["snapshot_id"] = *bdm.Ebs.SnapshotId 651 } 652 blockDevices["ebs"] = append(blockDevices["ebs"].([]map[string]interface{}), bd) 653 } 654 } 655 } 656 return blockDevices, nil 657 }