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