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