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