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  }