github.com/turtlemonvh/terraform@v0.6.9-0.20151204001754-8e40b6b855e8/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 Computed: true, 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 Elem: &schema.Schema{Type: schema.TypeString}, 96 Set: schema.HashString, 97 }, 98 99 "placement_group": &schema.Schema{ 100 Type: schema.TypeString, 101 Optional: true, 102 }, 103 104 "load_balancers": &schema.Schema{ 105 Type: schema.TypeSet, 106 Optional: true, 107 Elem: &schema.Schema{Type: schema.TypeString}, 108 Set: schema.HashString, 109 }, 110 111 "vpc_zone_identifier": &schema.Schema{ 112 Type: schema.TypeSet, 113 Optional: true, 114 Computed: true, 115 Elem: &schema.Schema{Type: schema.TypeString}, 116 Set: schema.HashString, 117 }, 118 119 "termination_policies": &schema.Schema{ 120 Type: schema.TypeList, 121 Optional: true, 122 Elem: &schema.Schema{Type: schema.TypeString}, 123 }, 124 125 "wait_for_capacity_timeout": &schema.Schema{ 126 Type: schema.TypeString, 127 Optional: true, 128 Default: "10m", 129 ValidateFunc: func(v interface{}, k string) (ws []string, errors []error) { 130 value := v.(string) 131 duration, err := time.ParseDuration(value) 132 if err != nil { 133 errors = append(errors, fmt.Errorf( 134 "%q cannot be parsed as a duration: %s", k, err)) 135 } 136 if duration < 0 { 137 errors = append(errors, fmt.Errorf( 138 "%q must be greater than zero", k)) 139 } 140 return 141 }, 142 }, 143 144 "tag": autoscalingTagsSchema(), 145 }, 146 } 147 } 148 149 func resourceAwsAutoscalingGroupCreate(d *schema.ResourceData, meta interface{}) error { 150 conn := meta.(*AWSClient).autoscalingconn 151 152 var autoScalingGroupOpts autoscaling.CreateAutoScalingGroupInput 153 154 var asgName string 155 if v, ok := d.GetOk("name"); ok { 156 asgName = v.(string) 157 } else { 158 asgName = resource.PrefixedUniqueId("tf-asg-") 159 d.Set("name", asgName) 160 } 161 162 autoScalingGroupOpts.AutoScalingGroupName = aws.String(asgName) 163 autoScalingGroupOpts.LaunchConfigurationName = aws.String(d.Get("launch_configuration").(string)) 164 autoScalingGroupOpts.MinSize = aws.Int64(int64(d.Get("min_size").(int))) 165 autoScalingGroupOpts.MaxSize = aws.Int64(int64(d.Get("max_size").(int))) 166 167 // Availability Zones are optional if VPC Zone Identifer(s) are specified 168 if v, ok := d.GetOk("availability_zones"); ok && v.(*schema.Set).Len() > 0 { 169 autoScalingGroupOpts.AvailabilityZones = expandStringList(v.(*schema.Set).List()) 170 } 171 172 if v, ok := d.GetOk("tag"); ok { 173 autoScalingGroupOpts.Tags = autoscalingTagsFromMap( 174 setToMapByKey(v.(*schema.Set), "key"), d.Get("name").(string)) 175 } 176 177 if v, ok := d.GetOk("default_cooldown"); ok { 178 autoScalingGroupOpts.DefaultCooldown = aws.Int64(int64(v.(int))) 179 } 180 181 if v, ok := d.GetOk("health_check_type"); ok && v.(string) != "" { 182 autoScalingGroupOpts.HealthCheckType = aws.String(v.(string)) 183 } 184 185 if v, ok := d.GetOk("desired_capacity"); ok { 186 autoScalingGroupOpts.DesiredCapacity = aws.Int64(int64(v.(int))) 187 } 188 189 if v, ok := d.GetOk("health_check_grace_period"); ok { 190 autoScalingGroupOpts.HealthCheckGracePeriod = aws.Int64(int64(v.(int))) 191 } 192 193 if v, ok := d.GetOk("placement_group"); ok { 194 autoScalingGroupOpts.PlacementGroup = aws.String(v.(string)) 195 } 196 197 if v, ok := d.GetOk("load_balancers"); ok && v.(*schema.Set).Len() > 0 { 198 autoScalingGroupOpts.LoadBalancerNames = expandStringList( 199 v.(*schema.Set).List()) 200 } 201 202 if v, ok := d.GetOk("vpc_zone_identifier"); ok && v.(*schema.Set).Len() > 0 { 203 autoScalingGroupOpts.VPCZoneIdentifier = expandVpcZoneIdentifiers(v.(*schema.Set).List()) 204 } 205 206 if v, ok := d.GetOk("termination_policies"); ok && len(v.([]interface{})) > 0 { 207 autoScalingGroupOpts.TerminationPolicies = expandStringList(v.([]interface{})) 208 } 209 210 log.Printf("[DEBUG] AutoScaling Group create configuration: %#v", autoScalingGroupOpts) 211 _, err := conn.CreateAutoScalingGroup(&autoScalingGroupOpts) 212 if err != nil { 213 return fmt.Errorf("Error creating Autoscaling Group: %s", err) 214 } 215 216 d.SetId(d.Get("name").(string)) 217 log.Printf("[INFO] AutoScaling Group ID: %s", d.Id()) 218 219 if err := waitForASGCapacity(d, meta); err != nil { 220 return err 221 } 222 223 return resourceAwsAutoscalingGroupRead(d, meta) 224 } 225 226 func resourceAwsAutoscalingGroupRead(d *schema.ResourceData, meta interface{}) error { 227 g, err := getAwsAutoscalingGroup(d, meta) 228 if err != nil { 229 return err 230 } 231 if g == nil { 232 return nil 233 } 234 235 d.Set("availability_zones", g.AvailabilityZones) 236 d.Set("default_cooldown", g.DefaultCooldown) 237 d.Set("desired_capacity", g.DesiredCapacity) 238 d.Set("health_check_grace_period", g.HealthCheckGracePeriod) 239 d.Set("health_check_type", g.HealthCheckType) 240 d.Set("launch_configuration", g.LaunchConfigurationName) 241 d.Set("load_balancers", g.LoadBalancerNames) 242 d.Set("min_size", g.MinSize) 243 d.Set("max_size", g.MaxSize) 244 d.Set("placement_group", g.PlacementGroup) 245 d.Set("name", g.AutoScalingGroupName) 246 d.Set("tag", g.Tags) 247 d.Set("vpc_zone_identifier", strings.Split(*g.VPCZoneIdentifier, ",")) 248 d.Set("termination_policies", g.TerminationPolicies) 249 250 return nil 251 } 252 253 func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{}) error { 254 conn := meta.(*AWSClient).autoscalingconn 255 256 opts := autoscaling.UpdateAutoScalingGroupInput{ 257 AutoScalingGroupName: aws.String(d.Id()), 258 } 259 260 if d.HasChange("default_cooldown") { 261 opts.DefaultCooldown = aws.Int64(int64(d.Get("default_cooldown").(int))) 262 } 263 264 if d.HasChange("desired_capacity") { 265 opts.DesiredCapacity = aws.Int64(int64(d.Get("desired_capacity").(int))) 266 } 267 268 if d.HasChange("launch_configuration") { 269 opts.LaunchConfigurationName = aws.String(d.Get("launch_configuration").(string)) 270 } 271 272 if d.HasChange("min_size") { 273 opts.MinSize = aws.Int64(int64(d.Get("min_size").(int))) 274 } 275 276 if d.HasChange("max_size") { 277 opts.MaxSize = aws.Int64(int64(d.Get("max_size").(int))) 278 } 279 280 if d.HasChange("health_check_grace_period") { 281 opts.HealthCheckGracePeriod = aws.Int64(int64(d.Get("health_check_grace_period").(int))) 282 } 283 284 if d.HasChange("health_check_type") { 285 opts.HealthCheckGracePeriod = aws.Int64(int64(d.Get("health_check_grace_period").(int))) 286 opts.HealthCheckType = aws.String(d.Get("health_check_type").(string)) 287 } 288 289 if d.HasChange("vpc_zone_identifier") { 290 opts.VPCZoneIdentifier = expandVpcZoneIdentifiers(d.Get("vpc_zone_identifier").(*schema.Set).List()) 291 } 292 293 if d.HasChange("availability_zones") { 294 if v, ok := d.GetOk("availability_zones"); ok && v.(*schema.Set).Len() > 0 { 295 opts.AvailabilityZones = expandStringList(d.Get("availability_zones").(*schema.Set).List()) 296 } 297 } 298 299 if d.HasChange("placement_group") { 300 opts.PlacementGroup = aws.String(d.Get("placement_group").(string)) 301 } 302 303 if d.HasChange("termination_policies") { 304 // If the termination policy is set to null, we need to explicitly set 305 // it back to "Default", or the API won't reset it for us. 306 // This means GetOk() will fail us on the zero check. 307 v := d.Get("termination_policies") 308 if len(v.([]interface{})) > 0 { 309 opts.TerminationPolicies = expandStringList(v.([]interface{})) 310 } else { 311 // Policies is a slice of string pointers, so build one. 312 // Maybe there's a better idiom for this? 313 log.Printf("[DEBUG] Explictly setting null termination policy to 'Default'") 314 pol := "Default" 315 s := make([]*string, 1, 1) 316 s[0] = &pol 317 opts.TerminationPolicies = s 318 } 319 } 320 321 if err := setAutoscalingTags(conn, d); err != nil { 322 return err 323 } else { 324 d.SetPartial("tag") 325 } 326 327 log.Printf("[DEBUG] AutoScaling Group update configuration: %#v", opts) 328 _, err := conn.UpdateAutoScalingGroup(&opts) 329 if err != nil { 330 d.Partial(true) 331 return fmt.Errorf("Error updating Autoscaling group: %s", err) 332 } 333 334 if d.HasChange("load_balancers") { 335 336 o, n := d.GetChange("load_balancers") 337 if o == nil { 338 o = new(schema.Set) 339 } 340 if n == nil { 341 n = new(schema.Set) 342 } 343 344 os := o.(*schema.Set) 345 ns := n.(*schema.Set) 346 remove := expandStringList(os.Difference(ns).List()) 347 add := expandStringList(ns.Difference(os).List()) 348 349 if len(remove) > 0 { 350 _, err := conn.DetachLoadBalancers(&autoscaling.DetachLoadBalancersInput{ 351 AutoScalingGroupName: aws.String(d.Id()), 352 LoadBalancerNames: remove, 353 }) 354 if err != nil { 355 return fmt.Errorf("[WARN] Error updating Load Balancers for AutoScaling Group (%s), error: %s", d.Id(), err) 356 } 357 } 358 359 if len(add) > 0 { 360 _, err := conn.AttachLoadBalancers(&autoscaling.AttachLoadBalancersInput{ 361 AutoScalingGroupName: aws.String(d.Id()), 362 LoadBalancerNames: add, 363 }) 364 if err != nil { 365 return fmt.Errorf("[WARN] Error updating Load Balancers for AutoScaling Group (%s), error: %s", d.Id(), err) 366 } 367 } 368 } 369 370 return resourceAwsAutoscalingGroupRead(d, meta) 371 } 372 373 func resourceAwsAutoscalingGroupDelete(d *schema.ResourceData, meta interface{}) error { 374 conn := meta.(*AWSClient).autoscalingconn 375 376 // Read the autoscaling group first. If it doesn't exist, we're done. 377 // We need the group in order to check if there are instances attached. 378 // If so, we need to remove those first. 379 g, err := getAwsAutoscalingGroup(d, meta) 380 if err != nil { 381 return err 382 } 383 if g == nil { 384 return nil 385 } 386 if len(g.Instances) > 0 || *g.DesiredCapacity > 0 { 387 if err := resourceAwsAutoscalingGroupDrain(d, meta); err != nil { 388 return err 389 } 390 } 391 392 log.Printf("[DEBUG] AutoScaling Group destroy: %v", d.Id()) 393 deleteopts := autoscaling.DeleteAutoScalingGroupInput{ 394 AutoScalingGroupName: aws.String(d.Id()), 395 ForceDelete: aws.Bool(d.Get("force_delete").(bool)), 396 } 397 398 // We retry the delete operation to handle InUse/InProgress errors coming 399 // from scaling operations. We should be able to sneak in a delete in between 400 // scaling operations within 5m. 401 err = resource.Retry(5*time.Minute, func() error { 402 if _, err := conn.DeleteAutoScalingGroup(&deleteopts); err != nil { 403 if awserr, ok := err.(awserr.Error); ok { 404 switch awserr.Code() { 405 case "InvalidGroup.NotFound": 406 // Already gone? Sure! 407 return nil 408 case "ResourceInUse", "ScalingActivityInProgress": 409 // These are retryable 410 return awserr 411 } 412 } 413 // Didn't recognize the error, so shouldn't retry. 414 return resource.RetryError{Err: err} 415 } 416 // Successful delete 417 return nil 418 }) 419 if err != nil { 420 return err 421 } 422 423 return resource.Retry(5*time.Minute, func() error { 424 if g, _ = getAwsAutoscalingGroup(d, meta); g != nil { 425 return fmt.Errorf("Auto Scaling Group still exists") 426 } 427 return nil 428 }) 429 } 430 431 func getAwsAutoscalingGroup( 432 d *schema.ResourceData, 433 meta interface{}) (*autoscaling.Group, error) { 434 conn := meta.(*AWSClient).autoscalingconn 435 436 describeOpts := autoscaling.DescribeAutoScalingGroupsInput{ 437 AutoScalingGroupNames: []*string{aws.String(d.Id())}, 438 } 439 440 log.Printf("[DEBUG] AutoScaling Group describe configuration: %#v", describeOpts) 441 describeGroups, err := conn.DescribeAutoScalingGroups(&describeOpts) 442 if err != nil { 443 autoscalingerr, ok := err.(awserr.Error) 444 if ok && autoscalingerr.Code() == "InvalidGroup.NotFound" { 445 d.SetId("") 446 return nil, nil 447 } 448 449 return nil, fmt.Errorf("Error retrieving AutoScaling groups: %s", err) 450 } 451 452 // Search for the autoscaling group 453 for idx, asc := range describeGroups.AutoScalingGroups { 454 if *asc.AutoScalingGroupName == d.Id() { 455 return describeGroups.AutoScalingGroups[idx], nil 456 } 457 } 458 459 // ASG not found 460 d.SetId("") 461 return nil, nil 462 } 463 464 func resourceAwsAutoscalingGroupDrain(d *schema.ResourceData, meta interface{}) error { 465 conn := meta.(*AWSClient).autoscalingconn 466 467 if d.Get("force_delete").(bool) { 468 log.Printf("[DEBUG] Skipping ASG drain, force_delete was set.") 469 return nil 470 } 471 472 // First, set the capacity to zero so the group will drain 473 log.Printf("[DEBUG] Reducing autoscaling group capacity to zero") 474 opts := autoscaling.UpdateAutoScalingGroupInput{ 475 AutoScalingGroupName: aws.String(d.Id()), 476 DesiredCapacity: aws.Int64(0), 477 MinSize: aws.Int64(0), 478 MaxSize: aws.Int64(0), 479 } 480 if _, err := conn.UpdateAutoScalingGroup(&opts); err != nil { 481 return fmt.Errorf("Error setting capacity to zero to drain: %s", err) 482 } 483 484 // Next, wait for the autoscale group to drain 485 log.Printf("[DEBUG] Waiting for group to have zero instances") 486 return resource.Retry(10*time.Minute, func() error { 487 g, err := getAwsAutoscalingGroup(d, meta) 488 if err != nil { 489 return resource.RetryError{Err: err} 490 } 491 if g == nil { 492 return nil 493 } 494 495 if len(g.Instances) == 0 { 496 return nil 497 } 498 499 return fmt.Errorf("group still has %d instances", len(g.Instances)) 500 }) 501 } 502 503 // Waits for a minimum number of healthy instances to show up as healthy in the 504 // ASG before continuing. Waits up to `waitForASGCapacityTimeout` for 505 // "desired_capacity", or "min_size" if desired capacity is not specified. 506 // 507 // If "min_elb_capacity" is specified, will also wait for that number of 508 // instances to show up InService in all attached ELBs. See "Waiting for 509 // Capacity" in docs for more discussion of the feature. 510 func waitForASGCapacity(d *schema.ResourceData, meta interface{}) error { 511 wantASG := d.Get("min_size").(int) 512 if v := d.Get("desired_capacity").(int); v > 0 { 513 wantASG = v 514 } 515 wantELB := d.Get("min_elb_capacity").(int) 516 517 wait, err := time.ParseDuration(d.Get("wait_for_capacity_timeout").(string)) 518 if err != nil { 519 return err 520 } 521 522 if wait == 0 { 523 log.Printf("[DEBUG] Capacity timeout set to 0, skipping capacity waiting.") 524 return nil 525 } 526 527 log.Printf("[DEBUG] Waiting %s for capacity: %d ASG, %d ELB", 528 wait, wantASG, wantELB) 529 530 return resource.Retry(wait, func() error { 531 g, err := getAwsAutoscalingGroup(d, meta) 532 if err != nil { 533 return resource.RetryError{Err: err} 534 } 535 if g == nil { 536 return nil 537 } 538 lbis, err := getLBInstanceStates(g, meta) 539 if err != nil { 540 return resource.RetryError{Err: err} 541 } 542 543 haveASG := 0 544 haveELB := 0 545 546 for _, i := range g.Instances { 547 if i.HealthStatus == nil || i.InstanceId == nil || i.LifecycleState == nil { 548 continue 549 } 550 551 if !strings.EqualFold(*i.HealthStatus, "Healthy") { 552 continue 553 } 554 555 if !strings.EqualFold(*i.LifecycleState, "InService") { 556 continue 557 } 558 559 haveASG++ 560 561 if wantELB > 0 { 562 inAllLbs := true 563 for _, states := range lbis { 564 state, ok := states[*i.InstanceId] 565 if !ok || !strings.EqualFold(state, "InService") { 566 inAllLbs = false 567 } 568 } 569 if inAllLbs { 570 haveELB++ 571 } 572 } 573 } 574 575 log.Printf("[DEBUG] %q Capacity: %d/%d ASG, %d/%d ELB", 576 d.Id(), haveASG, wantASG, haveELB, wantELB) 577 578 if haveASG >= wantASG && haveELB >= wantELB { 579 return nil 580 } 581 582 return fmt.Errorf("Still need to wait for more healthy instances. This could mean instances failed to launch. See Scaling History for more information.") 583 }) 584 } 585 586 // Returns a mapping of the instance states of all the ELBs attached to the 587 // provided ASG. 588 // 589 // Nested like: lbName -> instanceId -> instanceState 590 func getLBInstanceStates(g *autoscaling.Group, meta interface{}) (map[string]map[string]string, error) { 591 lbInstanceStates := make(map[string]map[string]string) 592 elbconn := meta.(*AWSClient).elbconn 593 594 for _, lbName := range g.LoadBalancerNames { 595 lbInstanceStates[*lbName] = make(map[string]string) 596 opts := &elb.DescribeInstanceHealthInput{LoadBalancerName: lbName} 597 r, err := elbconn.DescribeInstanceHealth(opts) 598 if err != nil { 599 return nil, err 600 } 601 for _, is := range r.InstanceStates { 602 if is.InstanceId == nil || is.State == nil { 603 continue 604 } 605 lbInstanceStates[*lbName][*is.InstanceId] = *is.State 606 } 607 } 608 609 return lbInstanceStates, nil 610 } 611 612 func expandVpcZoneIdentifiers(list []interface{}) *string { 613 strs := make([]string, len(list)) 614 for _, s := range list { 615 strs = append(strs, s.(string)) 616 } 617 return aws.String(strings.Join(strs, ",")) 618 }