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