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  }