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