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