github.com/jrperritt/terraform@v0.1.1-0.20170525065507-96f391dafc38/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": { 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": { 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": { 65 Type: schema.TypeString, 66 Required: true, 67 ForceNew: true, 68 }, 69 70 "instance_type": { 71 Type: schema.TypeString, 72 Required: true, 73 ForceNew: true, 74 }, 75 76 "iam_instance_profile": { 77 Type: schema.TypeString, 78 Optional: true, 79 ForceNew: true, 80 }, 81 82 "key_name": { 83 Type: schema.TypeString, 84 Optional: true, 85 Computed: true, 86 ForceNew: true, 87 }, 88 89 "user_data": { 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": { 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": { 113 Type: schema.TypeString, 114 Optional: true, 115 ForceNew: true, 116 }, 117 118 "vpc_classic_link_security_groups": { 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": { 127 Type: schema.TypeBool, 128 Optional: true, 129 ForceNew: true, 130 Default: false, 131 }, 132 133 "spot_price": { 134 Type: schema.TypeString, 135 Optional: true, 136 ForceNew: true, 137 }, 138 139 "ebs_optimized": { 140 Type: schema.TypeBool, 141 Optional: true, 142 ForceNew: true, 143 Computed: true, 144 }, 145 146 "placement_tenancy": { 147 Type: schema.TypeString, 148 Optional: true, 149 ForceNew: true, 150 }, 151 152 "enable_monitoring": { 153 Type: schema.TypeBool, 154 Optional: true, 155 ForceNew: true, 156 Default: true, 157 }, 158 159 "ebs_block_device": { 160 Type: schema.TypeSet, 161 Optional: true, 162 Computed: true, 163 Elem: &schema.Resource{ 164 Schema: map[string]*schema.Schema{ 165 "delete_on_termination": { 166 Type: schema.TypeBool, 167 Optional: true, 168 Default: true, 169 ForceNew: true, 170 }, 171 172 "device_name": { 173 Type: schema.TypeString, 174 Required: true, 175 ForceNew: true, 176 }, 177 178 "iops": { 179 Type: schema.TypeInt, 180 Optional: true, 181 Computed: true, 182 ForceNew: true, 183 }, 184 185 "snapshot_id": { 186 Type: schema.TypeString, 187 Optional: true, 188 Computed: true, 189 ForceNew: true, 190 }, 191 192 "volume_size": { 193 Type: schema.TypeInt, 194 Optional: true, 195 Computed: true, 196 ForceNew: true, 197 }, 198 199 "volume_type": { 200 Type: schema.TypeString, 201 Optional: true, 202 Computed: true, 203 ForceNew: true, 204 }, 205 206 "encrypted": { 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": { 224 Type: schema.TypeSet, 225 Optional: true, 226 ForceNew: true, 227 Elem: &schema.Resource{ 228 Schema: map[string]*schema.Schema{ 229 "device_name": { 230 Type: schema.TypeString, 231 Required: true, 232 }, 233 234 "virtual_name": { 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": { 250 Type: schema.TypeList, 251 Optional: true, 252 Computed: true, 253 MaxItems: 1, 254 Elem: &schema.Resource{ 255 // "You can only modify the volume size, volume type, and Delete on 256 // Termination flag on the block device mapping entry for the root 257 // device volume." - bit.ly/ec2bdmap 258 Schema: map[string]*schema.Schema{ 259 "delete_on_termination": { 260 Type: schema.TypeBool, 261 Optional: true, 262 Default: true, 263 ForceNew: true, 264 }, 265 266 "iops": { 267 Type: schema.TypeInt, 268 Optional: true, 269 Computed: true, 270 ForceNew: true, 271 }, 272 273 "volume_size": { 274 Type: schema.TypeInt, 275 Optional: true, 276 Computed: true, 277 ForceNew: true, 278 }, 279 280 "volume_type": { 281 Type: schema.TypeString, 282 Optional: true, 283 Computed: true, 284 ForceNew: true, 285 }, 286 }, 287 }, 288 }, 289 }, 290 } 291 } 292 293 func resourceAwsLaunchConfigurationCreate(d *schema.ResourceData, meta interface{}) error { 294 autoscalingconn := meta.(*AWSClient).autoscalingconn 295 ec2conn := meta.(*AWSClient).ec2conn 296 297 createLaunchConfigurationOpts := autoscaling.CreateLaunchConfigurationInput{ 298 LaunchConfigurationName: aws.String(d.Get("name").(string)), 299 ImageId: aws.String(d.Get("image_id").(string)), 300 InstanceType: aws.String(d.Get("instance_type").(string)), 301 EbsOptimized: aws.Bool(d.Get("ebs_optimized").(bool)), 302 } 303 304 if v, ok := d.GetOk("user_data"); ok { 305 userData := base64Encode([]byte(v.(string))) 306 createLaunchConfigurationOpts.UserData = aws.String(userData) 307 } 308 309 createLaunchConfigurationOpts.InstanceMonitoring = &autoscaling.InstanceMonitoring{ 310 Enabled: aws.Bool(d.Get("enable_monitoring").(bool)), 311 } 312 313 if v, ok := d.GetOk("iam_instance_profile"); ok { 314 createLaunchConfigurationOpts.IamInstanceProfile = aws.String(v.(string)) 315 } 316 317 if v, ok := d.GetOk("placement_tenancy"); ok { 318 createLaunchConfigurationOpts.PlacementTenancy = aws.String(v.(string)) 319 } 320 321 if v, ok := d.GetOk("associate_public_ip_address"); ok { 322 createLaunchConfigurationOpts.AssociatePublicIpAddress = aws.Bool(v.(bool)) 323 } 324 325 if v, ok := d.GetOk("key_name"); ok { 326 createLaunchConfigurationOpts.KeyName = aws.String(v.(string)) 327 } 328 if v, ok := d.GetOk("spot_price"); ok { 329 createLaunchConfigurationOpts.SpotPrice = aws.String(v.(string)) 330 } 331 332 if v, ok := d.GetOk("security_groups"); ok { 333 createLaunchConfigurationOpts.SecurityGroups = expandStringList( 334 v.(*schema.Set).List(), 335 ) 336 } 337 338 if v, ok := d.GetOk("vpc_classic_link_id"); ok { 339 createLaunchConfigurationOpts.ClassicLinkVPCId = aws.String(v.(string)) 340 } 341 342 if v, ok := d.GetOk("vpc_classic_link_security_groups"); ok { 343 createLaunchConfigurationOpts.ClassicLinkVPCSecurityGroups = expandStringList( 344 v.(*schema.Set).List(), 345 ) 346 } 347 348 var blockDevices []*autoscaling.BlockDeviceMapping 349 350 // We'll use this to detect if we're declaring it incorrectly as an ebs_block_device. 351 rootDeviceName, err := fetchRootDeviceName(d.Get("image_id").(string), ec2conn) 352 if err != nil { 353 return err 354 } 355 if rootDeviceName == nil { 356 // We do this so the value is empty so we don't have to do nil checks later 357 var blank string 358 rootDeviceName = &blank 359 } 360 361 if v, ok := d.GetOk("ebs_block_device"); ok { 362 vL := v.(*schema.Set).List() 363 for _, v := range vL { 364 bd := v.(map[string]interface{}) 365 ebs := &autoscaling.Ebs{ 366 DeleteOnTermination: aws.Bool(bd["delete_on_termination"].(bool)), 367 } 368 369 if v, ok := bd["snapshot_id"].(string); ok && v != "" { 370 ebs.SnapshotId = aws.String(v) 371 } 372 373 if v, ok := bd["encrypted"].(bool); ok && v { 374 ebs.Encrypted = aws.Bool(v) 375 } 376 377 if v, ok := bd["volume_size"].(int); ok && v != 0 { 378 ebs.VolumeSize = aws.Int64(int64(v)) 379 } 380 381 if v, ok := bd["volume_type"].(string); ok && v != "" { 382 ebs.VolumeType = aws.String(v) 383 } 384 385 if v, ok := bd["iops"].(int); ok && v > 0 { 386 ebs.Iops = aws.Int64(int64(v)) 387 } 388 389 if *aws.String(bd["device_name"].(string)) == *rootDeviceName { 390 return fmt.Errorf("Root device (%s) declared as an 'ebs_block_device'. Use 'root_block_device' keyword.", *rootDeviceName) 391 } 392 393 blockDevices = append(blockDevices, &autoscaling.BlockDeviceMapping{ 394 DeviceName: aws.String(bd["device_name"].(string)), 395 Ebs: ebs, 396 }) 397 } 398 } 399 400 if v, ok := d.GetOk("ephemeral_block_device"); ok { 401 vL := v.(*schema.Set).List() 402 for _, v := range vL { 403 bd := v.(map[string]interface{}) 404 blockDevices = append(blockDevices, &autoscaling.BlockDeviceMapping{ 405 DeviceName: aws.String(bd["device_name"].(string)), 406 VirtualName: aws.String(bd["virtual_name"].(string)), 407 }) 408 } 409 } 410 411 if v, ok := d.GetOk("root_block_device"); ok { 412 vL := v.([]interface{}) 413 for _, v := range vL { 414 bd := v.(map[string]interface{}) 415 ebs := &autoscaling.Ebs{ 416 DeleteOnTermination: aws.Bool(bd["delete_on_termination"].(bool)), 417 } 418 419 if v, ok := bd["volume_size"].(int); ok && v != 0 { 420 ebs.VolumeSize = aws.Int64(int64(v)) 421 } 422 423 if v, ok := bd["volume_type"].(string); ok && v != "" { 424 ebs.VolumeType = aws.String(v) 425 } 426 427 if v, ok := bd["iops"].(int); ok && v > 0 { 428 ebs.Iops = aws.Int64(int64(v)) 429 } 430 431 if dn, err := fetchRootDeviceName(d.Get("image_id").(string), ec2conn); err == nil { 432 if dn == nil { 433 return fmt.Errorf( 434 "Expected to find a Root Device name for AMI (%s), but got none", 435 d.Get("image_id").(string)) 436 } 437 blockDevices = append(blockDevices, &autoscaling.BlockDeviceMapping{ 438 DeviceName: dn, 439 Ebs: ebs, 440 }) 441 } else { 442 return err 443 } 444 } 445 } 446 447 if len(blockDevices) > 0 { 448 createLaunchConfigurationOpts.BlockDeviceMappings = blockDevices 449 } 450 451 var lcName string 452 if v, ok := d.GetOk("name"); ok { 453 lcName = v.(string) 454 } else if v, ok := d.GetOk("name_prefix"); ok { 455 lcName = resource.PrefixedUniqueId(v.(string)) 456 } else { 457 lcName = resource.UniqueId() 458 } 459 createLaunchConfigurationOpts.LaunchConfigurationName = aws.String(lcName) 460 461 log.Printf( 462 "[DEBUG] autoscaling create launch configuration: %s", createLaunchConfigurationOpts) 463 464 // IAM profiles can take ~10 seconds to propagate in AWS: 465 // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#launch-instance-with-role-console 466 err = resource.Retry(90*time.Second, func() *resource.RetryError { 467 _, err := autoscalingconn.CreateLaunchConfiguration(&createLaunchConfigurationOpts) 468 if err != nil { 469 if awsErr, ok := err.(awserr.Error); ok { 470 if strings.Contains(awsErr.Message(), "Invalid IamInstanceProfile") { 471 return resource.RetryableError(err) 472 } 473 if strings.Contains(awsErr.Message(), "You are not authorized to perform this operation") { 474 return resource.RetryableError(err) 475 } 476 } 477 return resource.NonRetryableError(err) 478 } 479 return nil 480 }) 481 482 if err != nil { 483 return fmt.Errorf("Error creating launch configuration: %s", err) 484 } 485 486 d.SetId(lcName) 487 log.Printf("[INFO] launch configuration ID: %s", d.Id()) 488 489 // We put a Retry here since sometimes eventual consistency bites 490 // us and we need to retry a few times to get the LC to load properly 491 return resource.Retry(30*time.Second, func() *resource.RetryError { 492 err := resourceAwsLaunchConfigurationRead(d, meta) 493 if err != nil { 494 return resource.RetryableError(err) 495 } 496 return nil 497 }) 498 } 499 500 func resourceAwsLaunchConfigurationRead(d *schema.ResourceData, meta interface{}) error { 501 autoscalingconn := meta.(*AWSClient).autoscalingconn 502 ec2conn := meta.(*AWSClient).ec2conn 503 504 describeOpts := autoscaling.DescribeLaunchConfigurationsInput{ 505 LaunchConfigurationNames: []*string{aws.String(d.Id())}, 506 } 507 508 log.Printf("[DEBUG] launch configuration describe configuration: %s", describeOpts) 509 describConfs, err := autoscalingconn.DescribeLaunchConfigurations(&describeOpts) 510 if err != nil { 511 return fmt.Errorf("Error retrieving launch configuration: %s", err) 512 } 513 if len(describConfs.LaunchConfigurations) == 0 { 514 d.SetId("") 515 return nil 516 } 517 518 // Verify AWS returned our launch configuration 519 if *describConfs.LaunchConfigurations[0].LaunchConfigurationName != d.Id() { 520 return fmt.Errorf( 521 "Unable to find launch configuration: %#v", 522 describConfs.LaunchConfigurations) 523 } 524 525 lc := describConfs.LaunchConfigurations[0] 526 527 d.Set("key_name", lc.KeyName) 528 d.Set("image_id", lc.ImageId) 529 d.Set("instance_type", lc.InstanceType) 530 d.Set("name", lc.LaunchConfigurationName) 531 532 d.Set("iam_instance_profile", lc.IamInstanceProfile) 533 d.Set("ebs_optimized", lc.EbsOptimized) 534 d.Set("spot_price", lc.SpotPrice) 535 d.Set("enable_monitoring", lc.InstanceMonitoring.Enabled) 536 d.Set("security_groups", lc.SecurityGroups) 537 d.Set("associate_public_ip_address", lc.AssociatePublicIpAddress) 538 539 d.Set("vpc_classic_link_id", lc.ClassicLinkVPCId) 540 d.Set("vpc_classic_link_security_groups", lc.ClassicLinkVPCSecurityGroups) 541 542 if err := readLCBlockDevices(d, lc, ec2conn); err != nil { 543 return err 544 } 545 546 return nil 547 } 548 549 func resourceAwsLaunchConfigurationDelete(d *schema.ResourceData, meta interface{}) error { 550 autoscalingconn := meta.(*AWSClient).autoscalingconn 551 552 log.Printf("[DEBUG] Launch Configuration destroy: %v", d.Id()) 553 _, err := autoscalingconn.DeleteLaunchConfiguration( 554 &autoscaling.DeleteLaunchConfigurationInput{ 555 LaunchConfigurationName: aws.String(d.Id()), 556 }) 557 if err != nil { 558 autoscalingerr, ok := err.(awserr.Error) 559 if ok && (autoscalingerr.Code() == "InvalidConfiguration.NotFound" || autoscalingerr.Code() == "ValidationError") { 560 log.Printf("[DEBUG] Launch configuration (%s) not found", d.Id()) 561 return nil 562 } 563 564 return err 565 } 566 567 return nil 568 } 569 570 func readLCBlockDevices(d *schema.ResourceData, lc *autoscaling.LaunchConfiguration, ec2conn *ec2.EC2) error { 571 ibds, err := readBlockDevicesFromLaunchConfiguration(d, lc, ec2conn) 572 if err != nil { 573 return err 574 } 575 576 if err := d.Set("ebs_block_device", ibds["ebs"]); err != nil { 577 return err 578 } 579 if err := d.Set("ephemeral_block_device", ibds["ephemeral"]); err != nil { 580 return err 581 } 582 if ibds["root"] != nil { 583 if err := d.Set("root_block_device", []interface{}{ibds["root"]}); err != nil { 584 return err 585 } 586 } else { 587 d.Set("root_block_device", []interface{}{}) 588 } 589 590 return nil 591 } 592 593 func readBlockDevicesFromLaunchConfiguration(d *schema.ResourceData, lc *autoscaling.LaunchConfiguration, ec2conn *ec2.EC2) ( 594 map[string]interface{}, error) { 595 blockDevices := make(map[string]interface{}) 596 blockDevices["ebs"] = make([]map[string]interface{}, 0) 597 blockDevices["ephemeral"] = make([]map[string]interface{}, 0) 598 blockDevices["root"] = nil 599 if len(lc.BlockDeviceMappings) == 0 { 600 return nil, nil 601 } 602 rootDeviceName, err := fetchRootDeviceName(d.Get("image_id").(string), ec2conn) 603 if err != nil { 604 return nil, err 605 } 606 if rootDeviceName == nil { 607 // We do this so the value is empty so we don't have to do nil checks later 608 var blank string 609 rootDeviceName = &blank 610 } 611 for _, bdm := range lc.BlockDeviceMappings { 612 bd := make(map[string]interface{}) 613 if bdm.Ebs != nil && bdm.Ebs.DeleteOnTermination != nil { 614 bd["delete_on_termination"] = *bdm.Ebs.DeleteOnTermination 615 } 616 if bdm.Ebs != nil && bdm.Ebs.VolumeSize != nil { 617 bd["volume_size"] = *bdm.Ebs.VolumeSize 618 } 619 if bdm.Ebs != nil && bdm.Ebs.VolumeType != nil { 620 bd["volume_type"] = *bdm.Ebs.VolumeType 621 } 622 if bdm.Ebs != nil && bdm.Ebs.Iops != nil { 623 bd["iops"] = *bdm.Ebs.Iops 624 } 625 626 if bdm.DeviceName != nil && *bdm.DeviceName == *rootDeviceName { 627 blockDevices["root"] = bd 628 } else { 629 if bdm.Ebs != nil && bdm.Ebs.Encrypted != nil { 630 bd["encrypted"] = *bdm.Ebs.Encrypted 631 } 632 if bdm.DeviceName != nil { 633 bd["device_name"] = *bdm.DeviceName 634 } 635 if bdm.VirtualName != nil { 636 bd["virtual_name"] = *bdm.VirtualName 637 blockDevices["ephemeral"] = append(blockDevices["ephemeral"].([]map[string]interface{}), bd) 638 } else { 639 if bdm.Ebs != nil && bdm.Ebs.SnapshotId != nil { 640 bd["snapshot_id"] = *bdm.Ebs.SnapshotId 641 } 642 blockDevices["ebs"] = append(blockDevices["ebs"].([]map[string]interface{}), bd) 643 } 644 } 645 } 646 return blockDevices, nil 647 }