github.com/ottenhoff/terraform@v0.7.0-rc1.0.20160607213102-ac2d195cc560/builtin/providers/aws/resource_aws_autoscaling_group.go (about) 1 package aws 2 3 import ( 4 "fmt" 5 "log" 6 "strings" 7 "time" 8 9 "github.com/hashicorp/terraform/helper/resource" 10 "github.com/hashicorp/terraform/helper/schema" 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/elb" 16 ) 17 18 func resourceAwsAutoscalingGroup() *schema.Resource { 19 return &schema.Resource{ 20 Create: resourceAwsAutoscalingGroupCreate, 21 Read: resourceAwsAutoscalingGroupRead, 22 Update: resourceAwsAutoscalingGroupUpdate, 23 Delete: resourceAwsAutoscalingGroupDelete, 24 Importer: &schema.ResourceImporter{ 25 State: schema.ImportStatePassthrough, 26 }, 27 28 Schema: map[string]*schema.Schema{ 29 "name": &schema.Schema{ 30 Type: schema.TypeString, 31 Optional: true, 32 Computed: true, 33 ForceNew: true, 34 ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { 35 // https://github.com/boto/botocore/blob/9f322b1/botocore/data/autoscaling/2011-01-01/service-2.json#L1862-L1873 36 value := v.(string) 37 if len(value) > 255 { 38 errors = append(errors, fmt.Errorf( 39 "%q cannot be longer than 255 characters", k)) 40 } 41 return 42 }, 43 }, 44 45 "launch_configuration": &schema.Schema{ 46 Type: schema.TypeString, 47 Required: true, 48 }, 49 50 "desired_capacity": &schema.Schema{ 51 Type: schema.TypeInt, 52 Optional: true, 53 Computed: true, 54 }, 55 56 "min_elb_capacity": &schema.Schema{ 57 Type: schema.TypeInt, 58 Optional: true, 59 }, 60 61 "min_size": &schema.Schema{ 62 Type: schema.TypeInt, 63 Required: true, 64 }, 65 66 "max_size": &schema.Schema{ 67 Type: schema.TypeInt, 68 Required: true, 69 }, 70 71 "default_cooldown": &schema.Schema{ 72 Type: schema.TypeInt, 73 Optional: true, 74 Computed: true, 75 }, 76 77 "force_delete": &schema.Schema{ 78 Type: schema.TypeBool, 79 Optional: true, 80 Default: false, 81 }, 82 83 "health_check_grace_period": &schema.Schema{ 84 Type: schema.TypeInt, 85 Optional: true, 86 Default: 300, 87 }, 88 89 "health_check_type": &schema.Schema{ 90 Type: schema.TypeString, 91 Optional: true, 92 Computed: true, 93 }, 94 95 "availability_zones": &schema.Schema{ 96 Type: schema.TypeSet, 97 Optional: true, 98 Computed: true, 99 Elem: &schema.Schema{Type: schema.TypeString}, 100 Set: schema.HashString, 101 }, 102 103 "placement_group": &schema.Schema{ 104 Type: schema.TypeString, 105 Optional: true, 106 }, 107 108 "load_balancers": &schema.Schema{ 109 Type: schema.TypeSet, 110 Optional: true, 111 Elem: &schema.Schema{Type: schema.TypeString}, 112 Set: schema.HashString, 113 }, 114 115 "vpc_zone_identifier": &schema.Schema{ 116 Type: schema.TypeSet, 117 Optional: true, 118 Computed: true, 119 Elem: &schema.Schema{Type: schema.TypeString}, 120 Set: schema.HashString, 121 }, 122 123 "termination_policies": &schema.Schema{ 124 Type: schema.TypeList, 125 Optional: true, 126 Elem: &schema.Schema{Type: schema.TypeString}, 127 }, 128 129 "wait_for_capacity_timeout": &schema.Schema{ 130 Type: schema.TypeString, 131 Optional: true, 132 Default: "10m", 133 ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { 134 value := v.(string) 135 duration, err := time.ParseDuration(value) 136 if err != nil { 137 errors = append(errors, fmt.Errorf( 138 "%q cannot be parsed as a duration: %s", k, err)) 139 } 140 if duration < 0 { 141 errors = append(errors, fmt.Errorf( 142 "%q must be greater than zero", k)) 143 } 144 return 145 }, 146 }, 147 148 "wait_for_elb_capacity": &schema.Schema{ 149 Type: schema.TypeInt, 150 Optional: true, 151 }, 152 153 "enabled_metrics": &schema.Schema{ 154 Type: schema.TypeSet, 155 Optional: true, 156 Elem: &schema.Schema{Type: schema.TypeString}, 157 Set: schema.HashString, 158 }, 159 160 "metrics_granularity": &schema.Schema{ 161 Type: schema.TypeString, 162 Optional: true, 163 Default: "1Minute", 164 }, 165 166 "tag": autoscalingTagsSchema(), 167 }, 168 } 169 } 170 171 func resourceAwsAutoscalingGroupCreate(d *schema.ResourceData, meta interface{}) error { 172 conn := meta.(*AWSClient).autoscalingconn 173 174 var autoScalingGroupOpts autoscaling.CreateAutoScalingGroupInput 175 176 var asgName string 177 if v, ok := d.GetOk("name"); ok { 178 asgName = v.(string) 179 } else { 180 asgName = resource.PrefixedUniqueId("tf-asg-") 181 d.Set("name", asgName) 182 } 183 184 autoScalingGroupOpts.AutoScalingGroupName = aws.String(asgName) 185 autoScalingGroupOpts.LaunchConfigurationName = aws.String(d.Get("launch_configuration").(string)) 186 autoScalingGroupOpts.MinSize = aws.Int64(int64(d.Get("min_size").(int))) 187 autoScalingGroupOpts.MaxSize = aws.Int64(int64(d.Get("max_size").(int))) 188 189 // Availability Zones are optional if VPC Zone Identifer(s) are specified 190 if v, ok := d.GetOk("availability_zones"); ok && v.(*schema.Set).Len() > 0 { 191 autoScalingGroupOpts.AvailabilityZones = expandStringList(v.(*schema.Set).List()) 192 } 193 194 if v, ok := d.GetOk("tag"); ok { 195 autoScalingGroupOpts.Tags = autoscalingTagsFromMap( 196 setToMapByKey(v.(*schema.Set), "key"), d.Get("name").(string)) 197 } 198 199 if v, ok := d.GetOk("default_cooldown"); ok { 200 autoScalingGroupOpts.DefaultCooldown = aws.Int64(int64(v.(int))) 201 } 202 203 if v, ok := d.GetOk("health_check_type"); ok && v.(string) != "" { 204 autoScalingGroupOpts.HealthCheckType = aws.String(v.(string)) 205 } 206 207 if v, ok := d.GetOk("desired_capacity"); ok { 208 autoScalingGroupOpts.DesiredCapacity = aws.Int64(int64(v.(int))) 209 } 210 211 if v, ok := d.GetOk("health_check_grace_period"); ok { 212 autoScalingGroupOpts.HealthCheckGracePeriod = aws.Int64(int64(v.(int))) 213 } 214 215 if v, ok := d.GetOk("placement_group"); ok { 216 autoScalingGroupOpts.PlacementGroup = aws.String(v.(string)) 217 } 218 219 if v, ok := d.GetOk("load_balancers"); ok && v.(*schema.Set).Len() > 0 { 220 autoScalingGroupOpts.LoadBalancerNames = expandStringList( 221 v.(*schema.Set).List()) 222 } 223 224 if v, ok := d.GetOk("vpc_zone_identifier"); ok && v.(*schema.Set).Len() > 0 { 225 autoScalingGroupOpts.VPCZoneIdentifier = expandVpcZoneIdentifiers(v.(*schema.Set).List()) 226 } 227 228 if v, ok := d.GetOk("termination_policies"); ok && len(v.([]interface{})) > 0 { 229 autoScalingGroupOpts.TerminationPolicies = expandStringList(v.([]interface{})) 230 } 231 232 log.Printf("[DEBUG] AutoScaling Group create configuration: %#v", autoScalingGroupOpts) 233 _, err := conn.CreateAutoScalingGroup(&autoScalingGroupOpts) 234 if err != nil { 235 return fmt.Errorf("Error creating Autoscaling Group: %s", err) 236 } 237 238 d.SetId(d.Get("name").(string)) 239 log.Printf("[INFO] AutoScaling Group ID: %s", d.Id()) 240 241 if err := waitForASGCapacity(d, meta, capacitySatifiedCreate); err != nil { 242 return err 243 } 244 245 if _, ok := d.GetOk("enabled_metrics"); ok { 246 metricsErr := enableASGMetricsCollection(d, conn) 247 if metricsErr != nil { 248 return metricsErr 249 } 250 } 251 252 return resourceAwsAutoscalingGroupRead(d, meta) 253 } 254 255 func resourceAwsAutoscalingGroupRead(d *schema.ResourceData, meta interface{}) error { 256 conn := meta.(*AWSClient).autoscalingconn 257 258 g, err := getAwsAutoscalingGroup(d.Id(), conn) 259 if err != nil { 260 return err 261 } 262 if g == nil { 263 log.Printf("[INFO] Autoscaling Group %q not found", d.Id()) 264 d.SetId("") 265 return nil 266 } 267 268 d.Set("availability_zones", flattenStringList(g.AvailabilityZones)) 269 d.Set("default_cooldown", g.DefaultCooldown) 270 d.Set("desired_capacity", g.DesiredCapacity) 271 d.Set("health_check_grace_period", g.HealthCheckGracePeriod) 272 d.Set("health_check_type", g.HealthCheckType) 273 d.Set("launch_configuration", g.LaunchConfigurationName) 274 d.Set("load_balancers", flattenStringList(g.LoadBalancerNames)) 275 d.Set("min_size", g.MinSize) 276 d.Set("max_size", g.MaxSize) 277 d.Set("placement_group", g.PlacementGroup) 278 d.Set("name", g.AutoScalingGroupName) 279 d.Set("tag", autoscalingTagDescriptionsToSlice(g.Tags)) 280 d.Set("vpc_zone_identifier", strings.Split(*g.VPCZoneIdentifier, ",")) 281 282 // If no termination polices are explicitly configured and the upstream state 283 // is only using the "Default" policy, clear the state to make it consistent 284 // with the default AWS create API behavior. 285 _, ok := d.GetOk("termination_policies") 286 if !ok && len(g.TerminationPolicies) == 1 && *g.TerminationPolicies[0] == "Default" { 287 d.Set("termination_policies", []interface{}{}) 288 } else { 289 d.Set("termination_policies", flattenStringList(g.TerminationPolicies)) 290 } 291 292 if g.EnabledMetrics != nil { 293 if err := d.Set("enabled_metrics", flattenAsgEnabledMetrics(g.EnabledMetrics)); err != nil { 294 log.Printf("[WARN] Error setting metrics for (%s): %s", d.Id(), err) 295 } 296 d.Set("metrics_granularity", g.EnabledMetrics[0].Granularity) 297 } 298 299 return nil 300 } 301 302 func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{}) error { 303 conn := meta.(*AWSClient).autoscalingconn 304 shouldWaitForCapacity := false 305 306 opts := autoscaling.UpdateAutoScalingGroupInput{ 307 AutoScalingGroupName: aws.String(d.Id()), 308 } 309 310 if d.HasChange("default_cooldown") { 311 opts.DefaultCooldown = aws.Int64(int64(d.Get("default_cooldown").(int))) 312 } 313 314 if d.HasChange("desired_capacity") { 315 opts.DesiredCapacity = aws.Int64(int64(d.Get("desired_capacity").(int))) 316 shouldWaitForCapacity = true 317 } 318 319 if d.HasChange("launch_configuration") { 320 opts.LaunchConfigurationName = aws.String(d.Get("launch_configuration").(string)) 321 } 322 323 if d.HasChange("min_size") { 324 opts.MinSize = aws.Int64(int64(d.Get("min_size").(int))) 325 shouldWaitForCapacity = true 326 } 327 328 if d.HasChange("max_size") { 329 opts.MaxSize = aws.Int64(int64(d.Get("max_size").(int))) 330 } 331 332 if d.HasChange("health_check_grace_period") { 333 opts.HealthCheckGracePeriod = aws.Int64(int64(d.Get("health_check_grace_period").(int))) 334 } 335 336 if d.HasChange("health_check_type") { 337 opts.HealthCheckGracePeriod = aws.Int64(int64(d.Get("health_check_grace_period").(int))) 338 opts.HealthCheckType = aws.String(d.Get("health_check_type").(string)) 339 } 340 341 if d.HasChange("vpc_zone_identifier") { 342 opts.VPCZoneIdentifier = expandVpcZoneIdentifiers(d.Get("vpc_zone_identifier").(*schema.Set).List()) 343 } 344 345 if d.HasChange("availability_zones") { 346 if v, ok := d.GetOk("availability_zones"); ok && v.(*schema.Set).Len() > 0 { 347 opts.AvailabilityZones = expandStringList(v.(*schema.Set).List()) 348 } 349 } 350 351 if d.HasChange("placement_group") { 352 opts.PlacementGroup = aws.String(d.Get("placement_group").(string)) 353 } 354 355 if d.HasChange("termination_policies") { 356 // If the termination policy is set to null, we need to explicitly set 357 // it back to "Default", or the API won't reset it for us. 358 if v, ok := d.GetOk("termination_policies"); ok && len(v.([]interface{})) > 0 { 359 opts.TerminationPolicies = expandStringList(v.([]interface{})) 360 } else { 361 log.Printf("[DEBUG] Explictly setting null termination policy to 'Default'") 362 opts.TerminationPolicies = aws.StringSlice([]string{"Default"}) 363 } 364 } 365 366 if err := setAutoscalingTags(conn, d); err != nil { 367 return err 368 } else { 369 d.SetPartial("tag") 370 } 371 372 log.Printf("[DEBUG] AutoScaling Group update configuration: %#v", opts) 373 _, err := conn.UpdateAutoScalingGroup(&opts) 374 if err != nil { 375 d.Partial(true) 376 return fmt.Errorf("Error updating Autoscaling group: %s", err) 377 } 378 379 if d.HasChange("load_balancers") { 380 381 o, n := d.GetChange("load_balancers") 382 if o == nil { 383 o = new(schema.Set) 384 } 385 if n == nil { 386 n = new(schema.Set) 387 } 388 389 os := o.(*schema.Set) 390 ns := n.(*schema.Set) 391 remove := expandStringList(os.Difference(ns).List()) 392 add := expandStringList(ns.Difference(os).List()) 393 394 if len(remove) > 0 { 395 _, err := conn.DetachLoadBalancers(&autoscaling.DetachLoadBalancersInput{ 396 AutoScalingGroupName: aws.String(d.Id()), 397 LoadBalancerNames: remove, 398 }) 399 if err != nil { 400 return fmt.Errorf("[WARN] Error updating Load Balancers for AutoScaling Group (%s), error: %s", d.Id(), err) 401 } 402 } 403 404 if len(add) > 0 { 405 _, err := conn.AttachLoadBalancers(&autoscaling.AttachLoadBalancersInput{ 406 AutoScalingGroupName: aws.String(d.Id()), 407 LoadBalancerNames: add, 408 }) 409 if err != nil { 410 return fmt.Errorf("[WARN] Error updating Load Balancers for AutoScaling Group (%s), error: %s", d.Id(), err) 411 } 412 } 413 } 414 415 if shouldWaitForCapacity { 416 waitForASGCapacity(d, meta, capacitySatifiedUpdate) 417 } 418 419 if d.HasChange("enabled_metrics") { 420 updateASGMetricsCollection(d, conn) 421 } 422 423 return resourceAwsAutoscalingGroupRead(d, meta) 424 } 425 426 func resourceAwsAutoscalingGroupDelete(d *schema.ResourceData, meta interface{}) error { 427 conn := meta.(*AWSClient).autoscalingconn 428 429 // Read the autoscaling group first. If it doesn't exist, we're done. 430 // We need the group in order to check if there are instances attached. 431 // If so, we need to remove those first. 432 g, err := getAwsAutoscalingGroup(d.Id(), conn) 433 if err != nil { 434 return err 435 } 436 if g == nil { 437 log.Printf("[INFO] Autoscaling Group %q not found", d.Id()) 438 d.SetId("") 439 return nil 440 } 441 if len(g.Instances) > 0 || *g.DesiredCapacity > 0 { 442 if err := resourceAwsAutoscalingGroupDrain(d, meta); err != nil { 443 return err 444 } 445 } 446 447 log.Printf("[DEBUG] AutoScaling Group destroy: %v", d.Id()) 448 deleteopts := autoscaling.DeleteAutoScalingGroupInput{ 449 AutoScalingGroupName: aws.String(d.Id()), 450 ForceDelete: aws.Bool(d.Get("force_delete").(bool)), 451 } 452 453 // We retry the delete operation to handle InUse/InProgress errors coming 454 // from scaling operations. We should be able to sneak in a delete in between 455 // scaling operations within 5m. 456 err = resource.Retry(5*time.Minute, func() *resource.RetryError { 457 if _, err := conn.DeleteAutoScalingGroup(&deleteopts); err != nil { 458 if awserr, ok := err.(awserr.Error); ok { 459 switch awserr.Code() { 460 case "InvalidGroup.NotFound": 461 // Already gone? Sure! 462 return nil 463 case "ResourceInUse", "ScalingActivityInProgress": 464 // These are retryable 465 return resource.RetryableError(awserr) 466 } 467 } 468 // Didn't recognize the error, so shouldn't retry. 469 return resource.NonRetryableError(err) 470 } 471 // Successful delete 472 return nil 473 }) 474 if err != nil { 475 return err 476 } 477 478 return resource.Retry(5*time.Minute, func() *resource.RetryError { 479 if g, _ = getAwsAutoscalingGroup(d.Id(), conn); g != nil { 480 return resource.RetryableError( 481 fmt.Errorf("Auto Scaling Group still exists")) 482 } 483 return nil 484 }) 485 } 486 487 func getAwsAutoscalingGroup( 488 asgName string, 489 conn *autoscaling.AutoScaling) (*autoscaling.Group, error) { 490 491 describeOpts := autoscaling.DescribeAutoScalingGroupsInput{ 492 AutoScalingGroupNames: []*string{aws.String(asgName)}, 493 } 494 495 log.Printf("[DEBUG] AutoScaling Group describe configuration: %#v", describeOpts) 496 describeGroups, err := conn.DescribeAutoScalingGroups(&describeOpts) 497 if err != nil { 498 autoscalingerr, ok := err.(awserr.Error) 499 if ok && autoscalingerr.Code() == "InvalidGroup.NotFound" { 500 return nil, nil 501 } 502 503 return nil, fmt.Errorf("Error retrieving AutoScaling groups: %s", err) 504 } 505 506 // Search for the autoscaling group 507 for idx, asc := range describeGroups.AutoScalingGroups { 508 if *asc.AutoScalingGroupName == asgName { 509 return describeGroups.AutoScalingGroups[idx], nil 510 } 511 } 512 513 return nil, nil 514 } 515 516 func resourceAwsAutoscalingGroupDrain(d *schema.ResourceData, meta interface{}) error { 517 conn := meta.(*AWSClient).autoscalingconn 518 519 if d.Get("force_delete").(bool) { 520 log.Printf("[DEBUG] Skipping ASG drain, force_delete was set.") 521 return nil 522 } 523 524 // First, set the capacity to zero so the group will drain 525 log.Printf("[DEBUG] Reducing autoscaling group capacity to zero") 526 opts := autoscaling.UpdateAutoScalingGroupInput{ 527 AutoScalingGroupName: aws.String(d.Id()), 528 DesiredCapacity: aws.Int64(0), 529 MinSize: aws.Int64(0), 530 MaxSize: aws.Int64(0), 531 } 532 if _, err := conn.UpdateAutoScalingGroup(&opts); err != nil { 533 return fmt.Errorf("Error setting capacity to zero to drain: %s", err) 534 } 535 536 // Next, wait for the autoscale group to drain 537 log.Printf("[DEBUG] Waiting for group to have zero instances") 538 return resource.Retry(10*time.Minute, func() *resource.RetryError { 539 g, err := getAwsAutoscalingGroup(d.Id(), conn) 540 if err != nil { 541 return resource.NonRetryableError(err) 542 } 543 if g == nil { 544 log.Printf("[INFO] Autoscaling Group %q not found", d.Id()) 545 d.SetId("") 546 return nil 547 } 548 549 if len(g.Instances) == 0 { 550 return nil 551 } 552 553 return resource.RetryableError( 554 fmt.Errorf("group still has %d instances", len(g.Instances))) 555 }) 556 } 557 558 func enableASGMetricsCollection(d *schema.ResourceData, conn *autoscaling.AutoScaling) error { 559 props := &autoscaling.EnableMetricsCollectionInput{ 560 AutoScalingGroupName: aws.String(d.Id()), 561 Granularity: aws.String(d.Get("metrics_granularity").(string)), 562 Metrics: expandStringList(d.Get("enabled_metrics").(*schema.Set).List()), 563 } 564 565 log.Printf("[INFO] Enabling metrics collection for the ASG: %s", d.Id()) 566 _, metricsErr := conn.EnableMetricsCollection(props) 567 if metricsErr != nil { 568 return metricsErr 569 } 570 571 return nil 572 } 573 574 func updateASGMetricsCollection(d *schema.ResourceData, conn *autoscaling.AutoScaling) error { 575 576 o, n := d.GetChange("enabled_metrics") 577 if o == nil { 578 o = new(schema.Set) 579 } 580 if n == nil { 581 n = new(schema.Set) 582 } 583 584 os := o.(*schema.Set) 585 ns := n.(*schema.Set) 586 587 disableMetrics := os.Difference(ns) 588 if disableMetrics.Len() != 0 { 589 props := &autoscaling.DisableMetricsCollectionInput{ 590 AutoScalingGroupName: aws.String(d.Id()), 591 Metrics: expandStringList(disableMetrics.List()), 592 } 593 594 _, err := conn.DisableMetricsCollection(props) 595 if err != nil { 596 return fmt.Errorf("Failure to Disable metrics collection types for ASG %s: %s", d.Id(), err) 597 } 598 } 599 600 enabledMetrics := ns.Difference(os) 601 if enabledMetrics.Len() != 0 { 602 props := &autoscaling.EnableMetricsCollectionInput{ 603 AutoScalingGroupName: aws.String(d.Id()), 604 Metrics: expandStringList(enabledMetrics.List()), 605 } 606 607 _, err := conn.EnableMetricsCollection(props) 608 if err != nil { 609 return fmt.Errorf("Failure to Enable metrics collection types for ASG %s: %s", d.Id(), err) 610 } 611 } 612 613 return nil 614 } 615 616 // Returns a mapping of the instance states of all the ELBs attached to the 617 // provided ASG. 618 // 619 // Nested like: lbName -> instanceId -> instanceState 620 func getLBInstanceStates(g *autoscaling.Group, meta interface{}) (map[string]map[string]string, error) { 621 lbInstanceStates := make(map[string]map[string]string) 622 elbconn := meta.(*AWSClient).elbconn 623 624 for _, lbName := range g.LoadBalancerNames { 625 lbInstanceStates[*lbName] = make(map[string]string) 626 opts := &elb.DescribeInstanceHealthInput{LoadBalancerName: lbName} 627 r, err := elbconn.DescribeInstanceHealth(opts) 628 if err != nil { 629 return nil, err 630 } 631 for _, is := range r.InstanceStates { 632 if is.InstanceId == nil || is.State == nil { 633 continue 634 } 635 lbInstanceStates[*lbName][*is.InstanceId] = *is.State 636 } 637 } 638 639 return lbInstanceStates, nil 640 } 641 642 func expandVpcZoneIdentifiers(list []interface{}) *string { 643 strs := make([]string, len(list)) 644 for _, s := range list { 645 strs = append(strs, s.(string)) 646 } 647 return aws.String(strings.Join(strs, ",")) 648 }