github.com/peterbale/terraform@v0.9.0-beta2.0.20170315142748-5723acd55547/builtin/providers/aws/resource_aws_elb.go (about)

     1  package aws
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"log"
     7  	"regexp"
     8  	"strconv"
     9  	"strings"
    10  	"time"
    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/ec2"
    15  	"github.com/aws/aws-sdk-go/service/elb"
    16  	"github.com/hashicorp/terraform/helper/hashcode"
    17  	"github.com/hashicorp/terraform/helper/resource"
    18  	"github.com/hashicorp/terraform/helper/schema"
    19  )
    20  
    21  func resourceAwsElb() *schema.Resource {
    22  	return &schema.Resource{
    23  		Create: resourceAwsElbCreate,
    24  		Read:   resourceAwsElbRead,
    25  		Update: resourceAwsElbUpdate,
    26  		Delete: resourceAwsElbDelete,
    27  		Importer: &schema.ResourceImporter{
    28  			State: schema.ImportStatePassthrough,
    29  		},
    30  
    31  		Schema: map[string]*schema.Schema{
    32  			"name": &schema.Schema{
    33  				Type:         schema.TypeString,
    34  				Optional:     true,
    35  				Computed:     true,
    36  				ForceNew:     true,
    37  				ValidateFunc: validateElbName,
    38  			},
    39  
    40  			"internal": &schema.Schema{
    41  				Type:     schema.TypeBool,
    42  				Optional: true,
    43  				ForceNew: true,
    44  				Computed: true,
    45  			},
    46  
    47  			"cross_zone_load_balancing": &schema.Schema{
    48  				Type:     schema.TypeBool,
    49  				Optional: true,
    50  				Default:  true,
    51  			},
    52  
    53  			"availability_zones": &schema.Schema{
    54  				Type:     schema.TypeSet,
    55  				Elem:     &schema.Schema{Type: schema.TypeString},
    56  				Optional: true,
    57  				Computed: true,
    58  				Set:      schema.HashString,
    59  			},
    60  
    61  			"instances": &schema.Schema{
    62  				Type:     schema.TypeSet,
    63  				Elem:     &schema.Schema{Type: schema.TypeString},
    64  				Optional: true,
    65  				Computed: true,
    66  				Set:      schema.HashString,
    67  			},
    68  
    69  			"security_groups": &schema.Schema{
    70  				Type:     schema.TypeSet,
    71  				Elem:     &schema.Schema{Type: schema.TypeString},
    72  				Optional: true,
    73  				Computed: true,
    74  				Set:      schema.HashString,
    75  			},
    76  
    77  			"source_security_group": &schema.Schema{
    78  				Type:     schema.TypeString,
    79  				Optional: true,
    80  				Computed: true,
    81  			},
    82  
    83  			"source_security_group_id": &schema.Schema{
    84  				Type:     schema.TypeString,
    85  				Computed: true,
    86  			},
    87  
    88  			"subnets": &schema.Schema{
    89  				Type:     schema.TypeSet,
    90  				Elem:     &schema.Schema{Type: schema.TypeString},
    91  				Optional: true,
    92  				Computed: true,
    93  				Set:      schema.HashString,
    94  			},
    95  
    96  			"idle_timeout": &schema.Schema{
    97  				Type:         schema.TypeInt,
    98  				Optional:     true,
    99  				Default:      60,
   100  				ValidateFunc: validateIntegerInRange(1, 3600),
   101  			},
   102  
   103  			"connection_draining": &schema.Schema{
   104  				Type:     schema.TypeBool,
   105  				Optional: true,
   106  				Default:  false,
   107  			},
   108  
   109  			"connection_draining_timeout": &schema.Schema{
   110  				Type:     schema.TypeInt,
   111  				Optional: true,
   112  				Default:  300,
   113  			},
   114  
   115  			"access_logs": &schema.Schema{
   116  				Type:     schema.TypeList,
   117  				Optional: true,
   118  				MaxItems: 1,
   119  				Elem: &schema.Resource{
   120  					Schema: map[string]*schema.Schema{
   121  						"interval": &schema.Schema{
   122  							Type:         schema.TypeInt,
   123  							Optional:     true,
   124  							Default:      60,
   125  							ValidateFunc: validateAccessLogsInterval,
   126  						},
   127  						"bucket": &schema.Schema{
   128  							Type:     schema.TypeString,
   129  							Required: true,
   130  						},
   131  						"bucket_prefix": &schema.Schema{
   132  							Type:     schema.TypeString,
   133  							Optional: true,
   134  						},
   135  						"enabled": &schema.Schema{
   136  							Type:     schema.TypeBool,
   137  							Optional: true,
   138  							Default:  true,
   139  						},
   140  					},
   141  				},
   142  			},
   143  
   144  			"listener": &schema.Schema{
   145  				Type:     schema.TypeSet,
   146  				Required: true,
   147  				Elem: &schema.Resource{
   148  					Schema: map[string]*schema.Schema{
   149  						"instance_port": &schema.Schema{
   150  							Type:         schema.TypeInt,
   151  							Required:     true,
   152  							ValidateFunc: validateIntegerInRange(1, 65535),
   153  						},
   154  
   155  						"instance_protocol": &schema.Schema{
   156  							Type:         schema.TypeString,
   157  							Required:     true,
   158  							ValidateFunc: validateListenerProtocol,
   159  						},
   160  
   161  						"lb_port": &schema.Schema{
   162  							Type:         schema.TypeInt,
   163  							Required:     true,
   164  							ValidateFunc: validateIntegerInRange(1, 65535),
   165  						},
   166  
   167  						"lb_protocol": &schema.Schema{
   168  							Type:         schema.TypeString,
   169  							Required:     true,
   170  							ValidateFunc: validateListenerProtocol,
   171  						},
   172  
   173  						"ssl_certificate_id": &schema.Schema{
   174  							Type:     schema.TypeString,
   175  							Optional: true,
   176  						},
   177  					},
   178  				},
   179  				Set: resourceAwsElbListenerHash,
   180  			},
   181  
   182  			"health_check": &schema.Schema{
   183  				Type:     schema.TypeList,
   184  				Optional: true,
   185  				Computed: true,
   186  				MaxItems: 1,
   187  				Elem: &schema.Resource{
   188  					Schema: map[string]*schema.Schema{
   189  						"healthy_threshold": &schema.Schema{
   190  							Type:         schema.TypeInt,
   191  							Required:     true,
   192  							ValidateFunc: validateIntegerInRange(2, 10),
   193  						},
   194  
   195  						"unhealthy_threshold": &schema.Schema{
   196  							Type:         schema.TypeInt,
   197  							Required:     true,
   198  							ValidateFunc: validateIntegerInRange(2, 10),
   199  						},
   200  
   201  						"target": &schema.Schema{
   202  							Type:         schema.TypeString,
   203  							Required:     true,
   204  							ValidateFunc: validateHeathCheckTarget,
   205  						},
   206  
   207  						"interval": &schema.Schema{
   208  							Type:         schema.TypeInt,
   209  							Required:     true,
   210  							ValidateFunc: validateIntegerInRange(5, 300),
   211  						},
   212  
   213  						"timeout": &schema.Schema{
   214  							Type:         schema.TypeInt,
   215  							Required:     true,
   216  							ValidateFunc: validateIntegerInRange(2, 60),
   217  						},
   218  					},
   219  				},
   220  			},
   221  
   222  			"dns_name": &schema.Schema{
   223  				Type:     schema.TypeString,
   224  				Computed: true,
   225  			},
   226  
   227  			"zone_id": &schema.Schema{
   228  				Type:     schema.TypeString,
   229  				Computed: true,
   230  			},
   231  
   232  			"tags": tagsSchema(),
   233  		},
   234  	}
   235  }
   236  
   237  func resourceAwsElbCreate(d *schema.ResourceData, meta interface{}) error {
   238  	elbconn := meta.(*AWSClient).elbconn
   239  
   240  	// Expand the "listener" set to aws-sdk-go compat []*elb.Listener
   241  	listeners, err := expandListeners(d.Get("listener").(*schema.Set).List())
   242  	if err != nil {
   243  		return err
   244  	}
   245  
   246  	var elbName string
   247  	if v, ok := d.GetOk("name"); ok {
   248  		elbName = v.(string)
   249  	} else {
   250  		elbName = resource.PrefixedUniqueId("tf-lb-")
   251  		d.Set("name", elbName)
   252  	}
   253  
   254  	tags := tagsFromMapELB(d.Get("tags").(map[string]interface{}))
   255  	// Provision the elb
   256  	elbOpts := &elb.CreateLoadBalancerInput{
   257  		LoadBalancerName: aws.String(elbName),
   258  		Listeners:        listeners,
   259  		Tags:             tags,
   260  	}
   261  
   262  	if scheme, ok := d.GetOk("internal"); ok && scheme.(bool) {
   263  		elbOpts.Scheme = aws.String("internal")
   264  	}
   265  
   266  	if v, ok := d.GetOk("availability_zones"); ok {
   267  		elbOpts.AvailabilityZones = expandStringList(v.(*schema.Set).List())
   268  	}
   269  
   270  	if v, ok := d.GetOk("security_groups"); ok {
   271  		elbOpts.SecurityGroups = expandStringList(v.(*schema.Set).List())
   272  	}
   273  
   274  	if v, ok := d.GetOk("subnets"); ok {
   275  		elbOpts.Subnets = expandStringList(v.(*schema.Set).List())
   276  	}
   277  
   278  	log.Printf("[DEBUG] ELB create configuration: %#v", elbOpts)
   279  	err = resource.Retry(1*time.Minute, func() *resource.RetryError {
   280  		_, err := elbconn.CreateLoadBalancer(elbOpts)
   281  
   282  		if err != nil {
   283  			if awsErr, ok := err.(awserr.Error); ok {
   284  				// Check for IAM SSL Cert error, eventual consistancy issue
   285  				if awsErr.Code() == "CertificateNotFound" {
   286  					return resource.RetryableError(
   287  						fmt.Errorf("[WARN] Error creating ELB Listener with SSL Cert, retrying: %s", err))
   288  				}
   289  			}
   290  			return resource.NonRetryableError(err)
   291  		}
   292  		return nil
   293  	})
   294  
   295  	if err != nil {
   296  		return err
   297  	}
   298  
   299  	// Assign the elb's unique identifier for use later
   300  	d.SetId(elbName)
   301  	log.Printf("[INFO] ELB ID: %s", d.Id())
   302  
   303  	// Enable partial mode and record what we set
   304  	d.Partial(true)
   305  	d.SetPartial("name")
   306  	d.SetPartial("internal")
   307  	d.SetPartial("availability_zones")
   308  	d.SetPartial("listener")
   309  	d.SetPartial("security_groups")
   310  	d.SetPartial("subnets")
   311  
   312  	d.Set("tags", tagsToMapELB(tags))
   313  
   314  	return resourceAwsElbUpdate(d, meta)
   315  }
   316  
   317  func resourceAwsElbRead(d *schema.ResourceData, meta interface{}) error {
   318  	elbconn := meta.(*AWSClient).elbconn
   319  	elbName := d.Id()
   320  
   321  	// Retrieve the ELB properties for updating the state
   322  	describeElbOpts := &elb.DescribeLoadBalancersInput{
   323  		LoadBalancerNames: []*string{aws.String(elbName)},
   324  	}
   325  
   326  	describeResp, err := elbconn.DescribeLoadBalancers(describeElbOpts)
   327  	if err != nil {
   328  		if isLoadBalancerNotFound(err) {
   329  			// The ELB is gone now, so just remove it from the state
   330  			d.SetId("")
   331  			return nil
   332  		}
   333  
   334  		return fmt.Errorf("Error retrieving ELB: %s", err)
   335  	}
   336  	if len(describeResp.LoadBalancerDescriptions) != 1 {
   337  		return fmt.Errorf("Unable to find ELB: %#v", describeResp.LoadBalancerDescriptions)
   338  	}
   339  
   340  	describeAttrsOpts := &elb.DescribeLoadBalancerAttributesInput{
   341  		LoadBalancerName: aws.String(elbName),
   342  	}
   343  	describeAttrsResp, err := elbconn.DescribeLoadBalancerAttributes(describeAttrsOpts)
   344  	if err != nil {
   345  		if isLoadBalancerNotFound(err) {
   346  			// The ELB is gone now, so just remove it from the state
   347  			d.SetId("")
   348  			return nil
   349  		}
   350  
   351  		return fmt.Errorf("Error retrieving ELB: %s", err)
   352  	}
   353  
   354  	lbAttrs := describeAttrsResp.LoadBalancerAttributes
   355  
   356  	lb := describeResp.LoadBalancerDescriptions[0]
   357  
   358  	d.Set("name", lb.LoadBalancerName)
   359  	d.Set("dns_name", lb.DNSName)
   360  	d.Set("zone_id", lb.CanonicalHostedZoneNameID)
   361  
   362  	var scheme bool
   363  	if lb.Scheme != nil {
   364  		scheme = *lb.Scheme == "internal"
   365  	}
   366  	d.Set("internal", scheme)
   367  	d.Set("availability_zones", flattenStringList(lb.AvailabilityZones))
   368  	d.Set("instances", flattenInstances(lb.Instances))
   369  	d.Set("listener", flattenListeners(lb.ListenerDescriptions))
   370  	d.Set("security_groups", flattenStringList(lb.SecurityGroups))
   371  	if lb.SourceSecurityGroup != nil {
   372  		group := lb.SourceSecurityGroup.GroupName
   373  		if lb.SourceSecurityGroup.OwnerAlias != nil && *lb.SourceSecurityGroup.OwnerAlias != "" {
   374  			group = aws.String(*lb.SourceSecurityGroup.OwnerAlias + "/" + *lb.SourceSecurityGroup.GroupName)
   375  		}
   376  		d.Set("source_security_group", group)
   377  
   378  		// Manually look up the ELB Security Group ID, since it's not provided
   379  		var elbVpc string
   380  		if lb.VPCId != nil {
   381  			elbVpc = *lb.VPCId
   382  			sgId, err := sourceSGIdByName(meta, *lb.SourceSecurityGroup.GroupName, elbVpc)
   383  			if err != nil {
   384  				return fmt.Errorf("[WARN] Error looking up ELB Security Group ID: %s", err)
   385  			} else {
   386  				d.Set("source_security_group_id", sgId)
   387  			}
   388  		}
   389  	}
   390  	d.Set("subnets", flattenStringList(lb.Subnets))
   391  	if lbAttrs.ConnectionSettings != nil {
   392  		d.Set("idle_timeout", lbAttrs.ConnectionSettings.IdleTimeout)
   393  	}
   394  	d.Set("connection_draining", lbAttrs.ConnectionDraining.Enabled)
   395  	d.Set("connection_draining_timeout", lbAttrs.ConnectionDraining.Timeout)
   396  	d.Set("cross_zone_load_balancing", lbAttrs.CrossZoneLoadBalancing.Enabled)
   397  	if lbAttrs.AccessLog != nil {
   398  		// The AWS API does not allow users to remove access_logs, only disable them.
   399  		// During creation of the ELB, Terraform sets the access_logs to disabled,
   400  		// so there should not be a case where lbAttrs.AccessLog above is nil.
   401  
   402  		// Here we do not record the remove value of access_log if:
   403  		// - there is no access_log block in the configuration
   404  		// - the remote access_logs are disabled
   405  		//
   406  		// This indicates there is no access_log in the configuration.
   407  		// - externally added access_logs will be enabled, so we'll detect the drift
   408  		// - locally added access_logs will be in the config, so we'll add to the
   409  		// API/state
   410  		// See https://github.com/hashicorp/terraform/issues/10138
   411  		_, n := d.GetChange("access_logs")
   412  		elbal := lbAttrs.AccessLog
   413  		nl := n.([]interface{})
   414  		if len(nl) == 0 && !*elbal.Enabled {
   415  			elbal = nil
   416  		}
   417  		if err := d.Set("access_logs", flattenAccessLog(elbal)); err != nil {
   418  			return err
   419  		}
   420  	}
   421  
   422  	resp, err := elbconn.DescribeTags(&elb.DescribeTagsInput{
   423  		LoadBalancerNames: []*string{lb.LoadBalancerName},
   424  	})
   425  
   426  	var et []*elb.Tag
   427  	if len(resp.TagDescriptions) > 0 {
   428  		et = resp.TagDescriptions[0].Tags
   429  	}
   430  	d.Set("tags", tagsToMapELB(et))
   431  
   432  	// There's only one health check, so save that to state as we
   433  	// currently can
   434  	if *lb.HealthCheck.Target != "" {
   435  		d.Set("health_check", flattenHealthCheck(lb.HealthCheck))
   436  	}
   437  
   438  	return nil
   439  }
   440  
   441  func resourceAwsElbUpdate(d *schema.ResourceData, meta interface{}) error {
   442  	elbconn := meta.(*AWSClient).elbconn
   443  
   444  	d.Partial(true)
   445  
   446  	if d.HasChange("listener") {
   447  		o, n := d.GetChange("listener")
   448  		os := o.(*schema.Set)
   449  		ns := n.(*schema.Set)
   450  
   451  		remove, _ := expandListeners(os.Difference(ns).List())
   452  		add, _ := expandListeners(ns.Difference(os).List())
   453  
   454  		if len(remove) > 0 {
   455  			ports := make([]*int64, 0, len(remove))
   456  			for _, listener := range remove {
   457  				ports = append(ports, listener.LoadBalancerPort)
   458  			}
   459  
   460  			deleteListenersOpts := &elb.DeleteLoadBalancerListenersInput{
   461  				LoadBalancerName:  aws.String(d.Id()),
   462  				LoadBalancerPorts: ports,
   463  			}
   464  
   465  			log.Printf("[DEBUG] ELB Delete Listeners opts: %s", deleteListenersOpts)
   466  			_, err := elbconn.DeleteLoadBalancerListeners(deleteListenersOpts)
   467  			if err != nil {
   468  				return fmt.Errorf("Failure removing outdated ELB listeners: %s", err)
   469  			}
   470  		}
   471  
   472  		if len(add) > 0 {
   473  			createListenersOpts := &elb.CreateLoadBalancerListenersInput{
   474  				LoadBalancerName: aws.String(d.Id()),
   475  				Listeners:        add,
   476  			}
   477  
   478  			// Occasionally AWS will error with a 'duplicate listener', without any
   479  			// other listeners on the ELB. Retry here to eliminate that.
   480  			err := resource.Retry(1*time.Minute, func() *resource.RetryError {
   481  				log.Printf("[DEBUG] ELB Create Listeners opts: %s", createListenersOpts)
   482  				if _, err := elbconn.CreateLoadBalancerListeners(createListenersOpts); err != nil {
   483  					if awsErr, ok := err.(awserr.Error); ok {
   484  						if awsErr.Code() == "DuplicateListener" {
   485  							log.Printf("[DEBUG] Duplicate listener found for ELB (%s), retrying", d.Id())
   486  							return resource.RetryableError(awsErr)
   487  						}
   488  						if awsErr.Code() == "CertificateNotFound" && strings.Contains(awsErr.Message(), "Server Certificate not found for the key: arn") {
   489  							log.Printf("[DEBUG] SSL Cert not found for given ARN, retrying")
   490  							return resource.RetryableError(awsErr)
   491  						}
   492  					}
   493  
   494  					// Didn't recognize the error, so shouldn't retry.
   495  					return resource.NonRetryableError(err)
   496  				}
   497  				// Successful creation
   498  				return nil
   499  			})
   500  			if err != nil {
   501  				return fmt.Errorf("Failure adding new or updated ELB listeners: %s", err)
   502  			}
   503  		}
   504  
   505  		d.SetPartial("listener")
   506  	}
   507  
   508  	// If we currently have instances, or did have instances,
   509  	// we want to figure out what to add and remove from the load
   510  	// balancer
   511  	if d.HasChange("instances") {
   512  		o, n := d.GetChange("instances")
   513  		os := o.(*schema.Set)
   514  		ns := n.(*schema.Set)
   515  		remove := expandInstanceString(os.Difference(ns).List())
   516  		add := expandInstanceString(ns.Difference(os).List())
   517  
   518  		if len(add) > 0 {
   519  			registerInstancesOpts := elb.RegisterInstancesWithLoadBalancerInput{
   520  				LoadBalancerName: aws.String(d.Id()),
   521  				Instances:        add,
   522  			}
   523  
   524  			_, err := elbconn.RegisterInstancesWithLoadBalancer(&registerInstancesOpts)
   525  			if err != nil {
   526  				return fmt.Errorf("Failure registering instances with ELB: %s", err)
   527  			}
   528  		}
   529  		if len(remove) > 0 {
   530  			deRegisterInstancesOpts := elb.DeregisterInstancesFromLoadBalancerInput{
   531  				LoadBalancerName: aws.String(d.Id()),
   532  				Instances:        remove,
   533  			}
   534  
   535  			_, err := elbconn.DeregisterInstancesFromLoadBalancer(&deRegisterInstancesOpts)
   536  			if err != nil {
   537  				return fmt.Errorf("Failure deregistering instances from ELB: %s", err)
   538  			}
   539  		}
   540  
   541  		d.SetPartial("instances")
   542  	}
   543  
   544  	if d.HasChange("cross_zone_load_balancing") || d.HasChange("idle_timeout") || d.HasChange("access_logs") {
   545  		attrs := elb.ModifyLoadBalancerAttributesInput{
   546  			LoadBalancerName: aws.String(d.Get("name").(string)),
   547  			LoadBalancerAttributes: &elb.LoadBalancerAttributes{
   548  				CrossZoneLoadBalancing: &elb.CrossZoneLoadBalancing{
   549  					Enabled: aws.Bool(d.Get("cross_zone_load_balancing").(bool)),
   550  				},
   551  				ConnectionSettings: &elb.ConnectionSettings{
   552  					IdleTimeout: aws.Int64(int64(d.Get("idle_timeout").(int))),
   553  				},
   554  			},
   555  		}
   556  
   557  		logs := d.Get("access_logs").([]interface{})
   558  		if len(logs) == 1 {
   559  			l := logs[0].(map[string]interface{})
   560  			accessLog := &elb.AccessLog{
   561  				Enabled:      aws.Bool(l["enabled"].(bool)),
   562  				EmitInterval: aws.Int64(int64(l["interval"].(int))),
   563  				S3BucketName: aws.String(l["bucket"].(string)),
   564  			}
   565  
   566  			if l["bucket_prefix"] != "" {
   567  				accessLog.S3BucketPrefix = aws.String(l["bucket_prefix"].(string))
   568  			}
   569  
   570  			attrs.LoadBalancerAttributes.AccessLog = accessLog
   571  		} else if len(logs) == 0 {
   572  			// disable access logs
   573  			attrs.LoadBalancerAttributes.AccessLog = &elb.AccessLog{
   574  				Enabled: aws.Bool(false),
   575  			}
   576  		}
   577  
   578  		log.Printf("[DEBUG] ELB Modify Load Balancer Attributes Request: %#v", attrs)
   579  		_, err := elbconn.ModifyLoadBalancerAttributes(&attrs)
   580  		if err != nil {
   581  			return fmt.Errorf("Failure configuring ELB attributes: %s", err)
   582  		}
   583  
   584  		d.SetPartial("cross_zone_load_balancing")
   585  		d.SetPartial("idle_timeout")
   586  		d.SetPartial("connection_draining_timeout")
   587  	}
   588  
   589  	// We have to do these changes separately from everything else since
   590  	// they have some weird undocumented rules. You can't set the timeout
   591  	// without having connection draining to true, so we set that to true,
   592  	// set the timeout, then reset it to false if requested.
   593  	if d.HasChange("connection_draining") || d.HasChange("connection_draining_timeout") {
   594  		// We do timeout changes first since they require us to set draining
   595  		// to true for a hot second.
   596  		if d.HasChange("connection_draining_timeout") {
   597  			attrs := elb.ModifyLoadBalancerAttributesInput{
   598  				LoadBalancerName: aws.String(d.Get("name").(string)),
   599  				LoadBalancerAttributes: &elb.LoadBalancerAttributes{
   600  					ConnectionDraining: &elb.ConnectionDraining{
   601  						Enabled: aws.Bool(true),
   602  						Timeout: aws.Int64(int64(d.Get("connection_draining_timeout").(int))),
   603  					},
   604  				},
   605  			}
   606  
   607  			_, err := elbconn.ModifyLoadBalancerAttributes(&attrs)
   608  			if err != nil {
   609  				return fmt.Errorf("Failure configuring ELB attributes: %s", err)
   610  			}
   611  
   612  			d.SetPartial("connection_draining_timeout")
   613  		}
   614  
   615  		// Then we always set connection draining even if there is no change.
   616  		// This lets us reset to "false" if requested even with a timeout
   617  		// change.
   618  		attrs := elb.ModifyLoadBalancerAttributesInput{
   619  			LoadBalancerName: aws.String(d.Get("name").(string)),
   620  			LoadBalancerAttributes: &elb.LoadBalancerAttributes{
   621  				ConnectionDraining: &elb.ConnectionDraining{
   622  					Enabled: aws.Bool(d.Get("connection_draining").(bool)),
   623  				},
   624  			},
   625  		}
   626  
   627  		_, err := elbconn.ModifyLoadBalancerAttributes(&attrs)
   628  		if err != nil {
   629  			return fmt.Errorf("Failure configuring ELB attributes: %s", err)
   630  		}
   631  
   632  		d.SetPartial("connection_draining")
   633  	}
   634  
   635  	if d.HasChange("health_check") {
   636  		hc := d.Get("health_check").([]interface{})
   637  		if len(hc) > 0 {
   638  			check := hc[0].(map[string]interface{})
   639  			configureHealthCheckOpts := elb.ConfigureHealthCheckInput{
   640  				LoadBalancerName: aws.String(d.Id()),
   641  				HealthCheck: &elb.HealthCheck{
   642  					HealthyThreshold:   aws.Int64(int64(check["healthy_threshold"].(int))),
   643  					UnhealthyThreshold: aws.Int64(int64(check["unhealthy_threshold"].(int))),
   644  					Interval:           aws.Int64(int64(check["interval"].(int))),
   645  					Target:             aws.String(check["target"].(string)),
   646  					Timeout:            aws.Int64(int64(check["timeout"].(int))),
   647  				},
   648  			}
   649  			_, err := elbconn.ConfigureHealthCheck(&configureHealthCheckOpts)
   650  			if err != nil {
   651  				return fmt.Errorf("Failure configuring health check for ELB: %s", err)
   652  			}
   653  			d.SetPartial("health_check")
   654  		}
   655  	}
   656  
   657  	if d.HasChange("security_groups") {
   658  		groups := d.Get("security_groups").(*schema.Set).List()
   659  
   660  		applySecurityGroupsOpts := elb.ApplySecurityGroupsToLoadBalancerInput{
   661  			LoadBalancerName: aws.String(d.Id()),
   662  			SecurityGroups:   expandStringList(groups),
   663  		}
   664  
   665  		_, err := elbconn.ApplySecurityGroupsToLoadBalancer(&applySecurityGroupsOpts)
   666  		if err != nil {
   667  			return fmt.Errorf("Failure applying security groups to ELB: %s", err)
   668  		}
   669  
   670  		d.SetPartial("security_groups")
   671  	}
   672  
   673  	if d.HasChange("availability_zones") {
   674  		o, n := d.GetChange("availability_zones")
   675  		os := o.(*schema.Set)
   676  		ns := n.(*schema.Set)
   677  
   678  		removed := expandStringList(os.Difference(ns).List())
   679  		added := expandStringList(ns.Difference(os).List())
   680  
   681  		if len(added) > 0 {
   682  			enableOpts := &elb.EnableAvailabilityZonesForLoadBalancerInput{
   683  				LoadBalancerName:  aws.String(d.Id()),
   684  				AvailabilityZones: added,
   685  			}
   686  
   687  			log.Printf("[DEBUG] ELB enable availability zones opts: %s", enableOpts)
   688  			_, err := elbconn.EnableAvailabilityZonesForLoadBalancer(enableOpts)
   689  			if err != nil {
   690  				return fmt.Errorf("Failure enabling ELB availability zones: %s", err)
   691  			}
   692  		}
   693  
   694  		if len(removed) > 0 {
   695  			disableOpts := &elb.DisableAvailabilityZonesForLoadBalancerInput{
   696  				LoadBalancerName:  aws.String(d.Id()),
   697  				AvailabilityZones: removed,
   698  			}
   699  
   700  			log.Printf("[DEBUG] ELB disable availability zones opts: %s", disableOpts)
   701  			_, err := elbconn.DisableAvailabilityZonesForLoadBalancer(disableOpts)
   702  			if err != nil {
   703  				return fmt.Errorf("Failure disabling ELB availability zones: %s", err)
   704  			}
   705  		}
   706  
   707  		d.SetPartial("availability_zones")
   708  	}
   709  
   710  	if d.HasChange("subnets") {
   711  		o, n := d.GetChange("subnets")
   712  		os := o.(*schema.Set)
   713  		ns := n.(*schema.Set)
   714  
   715  		removed := expandStringList(os.Difference(ns).List())
   716  		added := expandStringList(ns.Difference(os).List())
   717  
   718  		if len(removed) > 0 {
   719  			detachOpts := &elb.DetachLoadBalancerFromSubnetsInput{
   720  				LoadBalancerName: aws.String(d.Id()),
   721  				Subnets:          removed,
   722  			}
   723  
   724  			log.Printf("[DEBUG] ELB detach subnets opts: %s", detachOpts)
   725  			_, err := elbconn.DetachLoadBalancerFromSubnets(detachOpts)
   726  			if err != nil {
   727  				return fmt.Errorf("Failure removing ELB subnets: %s", err)
   728  			}
   729  		}
   730  
   731  		if len(added) > 0 {
   732  			attachOpts := &elb.AttachLoadBalancerToSubnetsInput{
   733  				LoadBalancerName: aws.String(d.Id()),
   734  				Subnets:          added,
   735  			}
   736  
   737  			log.Printf("[DEBUG] ELB attach subnets opts: %s", attachOpts)
   738  			err := resource.Retry(1*time.Minute, func() *resource.RetryError {
   739  				_, err := elbconn.AttachLoadBalancerToSubnets(attachOpts)
   740  				if err != nil {
   741  					if awsErr, ok := err.(awserr.Error); ok {
   742  						// eventually consistent issue with removing a subnet in AZ1 and
   743  						// immediately adding a new one in the same AZ
   744  						if awsErr.Code() == "InvalidConfigurationRequest" && strings.Contains(awsErr.Message(), "cannot be attached to multiple subnets in the same AZ") {
   745  							log.Printf("[DEBUG] retrying az association")
   746  							return resource.RetryableError(awsErr)
   747  						}
   748  					}
   749  					return resource.NonRetryableError(err)
   750  				}
   751  				return nil
   752  			})
   753  			if err != nil {
   754  				return fmt.Errorf("Failure adding ELB subnets: %s", err)
   755  			}
   756  		}
   757  
   758  		d.SetPartial("subnets")
   759  	}
   760  
   761  	if err := setTagsELB(elbconn, d); err != nil {
   762  		return err
   763  	}
   764  
   765  	d.SetPartial("tags")
   766  	d.Partial(false)
   767  
   768  	return resourceAwsElbRead(d, meta)
   769  }
   770  
   771  func resourceAwsElbDelete(d *schema.ResourceData, meta interface{}) error {
   772  	elbconn := meta.(*AWSClient).elbconn
   773  
   774  	log.Printf("[INFO] Deleting ELB: %s", d.Id())
   775  
   776  	// Destroy the load balancer
   777  	deleteElbOpts := elb.DeleteLoadBalancerInput{
   778  		LoadBalancerName: aws.String(d.Id()),
   779  	}
   780  	if _, err := elbconn.DeleteLoadBalancer(&deleteElbOpts); err != nil {
   781  		return fmt.Errorf("Error deleting ELB: %s", err)
   782  	}
   783  
   784  	return nil
   785  }
   786  
   787  func resourceAwsElbListenerHash(v interface{}) int {
   788  	var buf bytes.Buffer
   789  	m := v.(map[string]interface{})
   790  	buf.WriteString(fmt.Sprintf("%d-", m["instance_port"].(int)))
   791  	buf.WriteString(fmt.Sprintf("%s-",
   792  		strings.ToLower(m["instance_protocol"].(string))))
   793  	buf.WriteString(fmt.Sprintf("%d-", m["lb_port"].(int)))
   794  	buf.WriteString(fmt.Sprintf("%s-",
   795  		strings.ToLower(m["lb_protocol"].(string))))
   796  
   797  	if v, ok := m["ssl_certificate_id"]; ok {
   798  		buf.WriteString(fmt.Sprintf("%s-", v.(string)))
   799  	}
   800  
   801  	return hashcode.String(buf.String())
   802  }
   803  
   804  func isLoadBalancerNotFound(err error) bool {
   805  	elberr, ok := err.(awserr.Error)
   806  	return ok && elberr.Code() == "LoadBalancerNotFound"
   807  }
   808  
   809  func sourceSGIdByName(meta interface{}, sg, vpcId string) (string, error) {
   810  	conn := meta.(*AWSClient).ec2conn
   811  	var filters []*ec2.Filter
   812  	var sgFilterName, sgFilterVPCID *ec2.Filter
   813  	sgFilterName = &ec2.Filter{
   814  		Name:   aws.String("group-name"),
   815  		Values: []*string{aws.String(sg)},
   816  	}
   817  
   818  	if vpcId != "" {
   819  		sgFilterVPCID = &ec2.Filter{
   820  			Name:   aws.String("vpc-id"),
   821  			Values: []*string{aws.String(vpcId)},
   822  		}
   823  	}
   824  
   825  	filters = append(filters, sgFilterName)
   826  
   827  	if sgFilterVPCID != nil {
   828  		filters = append(filters, sgFilterVPCID)
   829  	}
   830  
   831  	req := &ec2.DescribeSecurityGroupsInput{
   832  		Filters: filters,
   833  	}
   834  	resp, err := conn.DescribeSecurityGroups(req)
   835  	if err != nil {
   836  		if ec2err, ok := err.(awserr.Error); ok {
   837  			if ec2err.Code() == "InvalidSecurityGroupID.NotFound" ||
   838  				ec2err.Code() == "InvalidGroup.NotFound" {
   839  				resp = nil
   840  				err = nil
   841  			}
   842  		}
   843  
   844  		if err != nil {
   845  			log.Printf("Error on ELB SG look up: %s", err)
   846  			return "", err
   847  		}
   848  	}
   849  
   850  	if resp == nil || len(resp.SecurityGroups) == 0 {
   851  		return "", fmt.Errorf("No security groups found for name %s and vpc id %s", sg, vpcId)
   852  	}
   853  
   854  	group := resp.SecurityGroups[0]
   855  	return *group.GroupId, nil
   856  }
   857  
   858  func validateAccessLogsInterval(v interface{}, k string) (ws []string, errors []error) {
   859  	value := v.(int)
   860  
   861  	// Check if the value is either 5 or 60 (minutes).
   862  	if value != 5 && value != 60 {
   863  		errors = append(errors, fmt.Errorf(
   864  			"%q contains an invalid Access Logs interval \"%d\". "+
   865  				"Valid intervals are either 5 or 60 (minutes).",
   866  			k, value))
   867  	}
   868  	return
   869  }
   870  
   871  func validateHeathCheckTarget(v interface{}, k string) (ws []string, errors []error) {
   872  	value := v.(string)
   873  
   874  	// Parse the Health Check target value.
   875  	matches := regexp.MustCompile(`\A(\w+):(\d+)(.+)?\z`).FindStringSubmatch(value)
   876  
   877  	// Check if the value contains a valid target.
   878  	if matches == nil || len(matches) < 1 {
   879  		errors = append(errors, fmt.Errorf(
   880  			"%q contains an invalid Health Check: %s",
   881  			k, value))
   882  
   883  		// Invalid target? Return immediately,
   884  		// there is no need to collect other
   885  		// errors.
   886  		return
   887  	}
   888  
   889  	// Check if the value contains a valid protocol.
   890  	if !isValidProtocol(matches[1]) {
   891  		errors = append(errors, fmt.Errorf(
   892  			"%q contains an invalid Health Check protocol %q. "+
   893  				"Valid protocols are either %q, %q, %q, or %q.",
   894  			k, matches[1], "TCP", "SSL", "HTTP", "HTTPS"))
   895  	}
   896  
   897  	// Check if the value contains a valid port range.
   898  	port, _ := strconv.Atoi(matches[2])
   899  	if port < 1 || port > 65535 {
   900  		errors = append(errors, fmt.Errorf(
   901  			"%q contains an invalid Health Check target port \"%d\". "+
   902  				"Valid port is in the range from 1 to 65535 inclusive.",
   903  			k, port))
   904  	}
   905  
   906  	switch strings.ToLower(matches[1]) {
   907  	case "tcp", "ssl":
   908  		// Check if value is in the form <PROTOCOL>:<PORT> for TCP and/or SSL.
   909  		if matches[3] != "" {
   910  			errors = append(errors, fmt.Errorf(
   911  				"%q cannot contain a path in the Health Check target: %s",
   912  				k, value))
   913  		}
   914  		break
   915  	case "http", "https":
   916  		// Check if value is in the form <PROTOCOL>:<PORT>/<PATH> for HTTP and/or HTTPS.
   917  		if matches[3] == "" {
   918  			errors = append(errors, fmt.Errorf(
   919  				"%q must contain a path in the Health Check target: %s",
   920  				k, value))
   921  		}
   922  
   923  		// Cannot be longer than 1024 multibyte characters.
   924  		if len([]rune(matches[3])) > 1024 {
   925  			errors = append(errors, fmt.Errorf("%q cannot contain a path longer "+
   926  				"than 1024 characters in the Health Check target: %s",
   927  				k, value))
   928  		}
   929  		break
   930  	}
   931  
   932  	return
   933  }
   934  
   935  func validateListenerProtocol(v interface{}, k string) (ws []string, errors []error) {
   936  	value := v.(string)
   937  
   938  	if !isValidProtocol(value) {
   939  		errors = append(errors, fmt.Errorf(
   940  			"%q contains an invalid Listener protocol %q. "+
   941  				"Valid protocols are either %q, %q, %q, or %q.",
   942  			k, value, "TCP", "SSL", "HTTP", "HTTPS"))
   943  	}
   944  	return
   945  }
   946  
   947  func isValidProtocol(s string) bool {
   948  	if s == "" {
   949  		return false
   950  	}
   951  	s = strings.ToLower(s)
   952  
   953  	validProtocols := map[string]bool{
   954  		"http":  true,
   955  		"https": true,
   956  		"ssl":   true,
   957  		"tcp":   true,
   958  	}
   959  
   960  	if _, ok := validProtocols[s]; !ok {
   961  		return false
   962  	}
   963  
   964  	return true
   965  }