github.com/jsoriano/terraform@v0.6.7-0.20151026070445-8b70867fdd95/builtin/providers/aws/resource_aws_elb.go (about)

     1  package aws
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"log"
     7  	"regexp"
     8  	"strings"
     9  
    10  	"github.com/aws/aws-sdk-go/aws"
    11  	"github.com/aws/aws-sdk-go/aws/awserr"
    12  	"github.com/aws/aws-sdk-go/service/elb"
    13  	"github.com/hashicorp/terraform/helper/hashcode"
    14  	"github.com/hashicorp/terraform/helper/resource"
    15  	"github.com/hashicorp/terraform/helper/schema"
    16  )
    17  
    18  func resourceAwsElb() *schema.Resource {
    19  	return &schema.Resource{
    20  		Create: resourceAwsElbCreate,
    21  		Read:   resourceAwsElbRead,
    22  		Update: resourceAwsElbUpdate,
    23  		Delete: resourceAwsElbDelete,
    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: validateElbName,
    32  			},
    33  
    34  			"internal": &schema.Schema{
    35  				Type:     schema.TypeBool,
    36  				Optional: true,
    37  				ForceNew: true,
    38  				Computed: true,
    39  			},
    40  
    41  			"cross_zone_load_balancing": &schema.Schema{
    42  				Type:     schema.TypeBool,
    43  				Optional: true,
    44  			},
    45  
    46  			"availability_zones": &schema.Schema{
    47  				Type:     schema.TypeSet,
    48  				Elem:     &schema.Schema{Type: schema.TypeString},
    49  				Optional: true,
    50  				ForceNew: true,
    51  				Computed: true,
    52  				Set:      schema.HashString,
    53  			},
    54  
    55  			"instances": &schema.Schema{
    56  				Type:     schema.TypeSet,
    57  				Elem:     &schema.Schema{Type: schema.TypeString},
    58  				Optional: true,
    59  				Computed: true,
    60  				Set:      schema.HashString,
    61  			},
    62  
    63  			"security_groups": &schema.Schema{
    64  				Type:     schema.TypeSet,
    65  				Elem:     &schema.Schema{Type: schema.TypeString},
    66  				Optional: true,
    67  				Computed: true,
    68  				Set:      schema.HashString,
    69  			},
    70  
    71  			"source_security_group": &schema.Schema{
    72  				Type:     schema.TypeString,
    73  				Optional: true,
    74  				Computed: true,
    75  			},
    76  
    77  			"subnets": &schema.Schema{
    78  				Type:     schema.TypeSet,
    79  				Elem:     &schema.Schema{Type: schema.TypeString},
    80  				Optional: true,
    81  				ForceNew: true,
    82  				Computed: true,
    83  				Set:      schema.HashString,
    84  			},
    85  
    86  			"idle_timeout": &schema.Schema{
    87  				Type:     schema.TypeInt,
    88  				Optional: true,
    89  				Default:  60,
    90  			},
    91  
    92  			"connection_draining": &schema.Schema{
    93  				Type:     schema.TypeBool,
    94  				Optional: true,
    95  				Default:  false,
    96  			},
    97  
    98  			"connection_draining_timeout": &schema.Schema{
    99  				Type:     schema.TypeInt,
   100  				Optional: true,
   101  				Default:  300,
   102  			},
   103  
   104  			"listener": &schema.Schema{
   105  				Type:     schema.TypeSet,
   106  				Required: true,
   107  				Elem: &schema.Resource{
   108  					Schema: map[string]*schema.Schema{
   109  						"instance_port": &schema.Schema{
   110  							Type:     schema.TypeInt,
   111  							Required: true,
   112  						},
   113  
   114  						"instance_protocol": &schema.Schema{
   115  							Type:     schema.TypeString,
   116  							Required: true,
   117  						},
   118  
   119  						"lb_port": &schema.Schema{
   120  							Type:     schema.TypeInt,
   121  							Required: true,
   122  						},
   123  
   124  						"lb_protocol": &schema.Schema{
   125  							Type:     schema.TypeString,
   126  							Required: true,
   127  						},
   128  
   129  						"ssl_certificate_id": &schema.Schema{
   130  							Type:     schema.TypeString,
   131  							Optional: true,
   132  						},
   133  					},
   134  				},
   135  				Set: resourceAwsElbListenerHash,
   136  			},
   137  
   138  			"health_check": &schema.Schema{
   139  				Type:     schema.TypeSet,
   140  				Optional: true,
   141  				Computed: true,
   142  				Elem: &schema.Resource{
   143  					Schema: map[string]*schema.Schema{
   144  						"healthy_threshold": &schema.Schema{
   145  							Type:     schema.TypeInt,
   146  							Required: true,
   147  						},
   148  
   149  						"unhealthy_threshold": &schema.Schema{
   150  							Type:     schema.TypeInt,
   151  							Required: true,
   152  						},
   153  
   154  						"target": &schema.Schema{
   155  							Type:     schema.TypeString,
   156  							Required: true,
   157  						},
   158  
   159  						"interval": &schema.Schema{
   160  							Type:     schema.TypeInt,
   161  							Required: true,
   162  						},
   163  
   164  						"timeout": &schema.Schema{
   165  							Type:     schema.TypeInt,
   166  							Required: true,
   167  						},
   168  					},
   169  				},
   170  				Set: resourceAwsElbHealthCheckHash,
   171  			},
   172  
   173  			"dns_name": &schema.Schema{
   174  				Type:     schema.TypeString,
   175  				Computed: true,
   176  			},
   177  
   178  			"zone_id": &schema.Schema{
   179  				Type:     schema.TypeString,
   180  				Computed: true,
   181  			},
   182  
   183  			"tags": tagsSchema(),
   184  		},
   185  	}
   186  }
   187  
   188  func resourceAwsElbCreate(d *schema.ResourceData, meta interface{}) error {
   189  	elbconn := meta.(*AWSClient).elbconn
   190  
   191  	// Expand the "listener" set to aws-sdk-go compat []*elb.Listener
   192  	listeners, err := expandListeners(d.Get("listener").(*schema.Set).List())
   193  	if err != nil {
   194  		return err
   195  	}
   196  
   197  	var elbName string
   198  	if v, ok := d.GetOk("name"); ok {
   199  		elbName = v.(string)
   200  	} else {
   201  		elbName = resource.PrefixedUniqueId("tf-lb-")
   202  		d.Set("name", elbName)
   203  	}
   204  
   205  	tags := tagsFromMapELB(d.Get("tags").(map[string]interface{}))
   206  	// Provision the elb
   207  	elbOpts := &elb.CreateLoadBalancerInput{
   208  		LoadBalancerName: aws.String(elbName),
   209  		Listeners:        listeners,
   210  		Tags:             tags,
   211  	}
   212  
   213  	if scheme, ok := d.GetOk("internal"); ok && scheme.(bool) {
   214  		elbOpts.Scheme = aws.String("internal")
   215  	}
   216  
   217  	if v, ok := d.GetOk("availability_zones"); ok {
   218  		elbOpts.AvailabilityZones = expandStringList(v.(*schema.Set).List())
   219  	}
   220  
   221  	if v, ok := d.GetOk("security_groups"); ok {
   222  		elbOpts.SecurityGroups = expandStringList(v.(*schema.Set).List())
   223  	}
   224  
   225  	if v, ok := d.GetOk("subnets"); ok {
   226  		elbOpts.Subnets = expandStringList(v.(*schema.Set).List())
   227  	}
   228  
   229  	log.Printf("[DEBUG] ELB create configuration: %#v", elbOpts)
   230  	if _, err := elbconn.CreateLoadBalancer(elbOpts); err != nil {
   231  		return fmt.Errorf("Error creating ELB: %s", err)
   232  	}
   233  
   234  	// Assign the elb's unique identifier for use later
   235  	d.SetId(elbName)
   236  	log.Printf("[INFO] ELB ID: %s", d.Id())
   237  
   238  	// Enable partial mode and record what we set
   239  	d.Partial(true)
   240  	d.SetPartial("name")
   241  	d.SetPartial("internal")
   242  	d.SetPartial("availability_zones")
   243  	d.SetPartial("listener")
   244  	d.SetPartial("security_groups")
   245  	d.SetPartial("subnets")
   246  
   247  	d.Set("tags", tagsToMapELB(tags))
   248  
   249  	return resourceAwsElbUpdate(d, meta)
   250  }
   251  
   252  func resourceAwsElbRead(d *schema.ResourceData, meta interface{}) error {
   253  	elbconn := meta.(*AWSClient).elbconn
   254  	elbName := d.Id()
   255  
   256  	// Retrieve the ELB properties for updating the state
   257  	describeElbOpts := &elb.DescribeLoadBalancersInput{
   258  		LoadBalancerNames: []*string{aws.String(elbName)},
   259  	}
   260  
   261  	describeResp, err := elbconn.DescribeLoadBalancers(describeElbOpts)
   262  	if err != nil {
   263  		if isLoadBalancerNotFound(err) {
   264  			// The ELB is gone now, so just remove it from the state
   265  			d.SetId("")
   266  			return nil
   267  		}
   268  
   269  		return fmt.Errorf("Error retrieving ELB: %s", err)
   270  	}
   271  	if len(describeResp.LoadBalancerDescriptions) != 1 {
   272  		return fmt.Errorf("Unable to find ELB: %#v", describeResp.LoadBalancerDescriptions)
   273  	}
   274  
   275  	describeAttrsOpts := &elb.DescribeLoadBalancerAttributesInput{
   276  		LoadBalancerName: aws.String(elbName),
   277  	}
   278  	describeAttrsResp, err := elbconn.DescribeLoadBalancerAttributes(describeAttrsOpts)
   279  	if err != nil {
   280  		if isLoadBalancerNotFound(err) {
   281  			// The ELB is gone now, so just remove it from the state
   282  			d.SetId("")
   283  			return nil
   284  		}
   285  
   286  		return fmt.Errorf("Error retrieving ELB: %s", err)
   287  	}
   288  
   289  	lbAttrs := describeAttrsResp.LoadBalancerAttributes
   290  
   291  	lb := describeResp.LoadBalancerDescriptions[0]
   292  
   293  	d.Set("name", *lb.LoadBalancerName)
   294  	d.Set("dns_name", *lb.DNSName)
   295  	d.Set("zone_id", *lb.CanonicalHostedZoneNameID)
   296  	d.Set("internal", *lb.Scheme == "internal")
   297  	d.Set("availability_zones", lb.AvailabilityZones)
   298  	d.Set("instances", flattenInstances(lb.Instances))
   299  	d.Set("listener", flattenListeners(lb.ListenerDescriptions))
   300  	d.Set("security_groups", lb.SecurityGroups)
   301  	if lb.SourceSecurityGroup != nil {
   302  		d.Set("source_security_group", lb.SourceSecurityGroup.GroupName)
   303  	}
   304  	d.Set("subnets", lb.Subnets)
   305  	d.Set("idle_timeout", lbAttrs.ConnectionSettings.IdleTimeout)
   306  	d.Set("connection_draining", lbAttrs.ConnectionDraining.Enabled)
   307  	d.Set("connection_draining_timeout", lbAttrs.ConnectionDraining.Timeout)
   308  
   309  	resp, err := elbconn.DescribeTags(&elb.DescribeTagsInput{
   310  		LoadBalancerNames: []*string{lb.LoadBalancerName},
   311  	})
   312  
   313  	var et []*elb.Tag
   314  	if len(resp.TagDescriptions) > 0 {
   315  		et = resp.TagDescriptions[0].Tags
   316  	}
   317  	d.Set("tags", tagsToMapELB(et))
   318  	// There's only one health check, so save that to state as we
   319  	// currently can
   320  	if *lb.HealthCheck.Target != "" {
   321  		d.Set("health_check", flattenHealthCheck(lb.HealthCheck))
   322  	}
   323  
   324  	return nil
   325  }
   326  
   327  func resourceAwsElbUpdate(d *schema.ResourceData, meta interface{}) error {
   328  	elbconn := meta.(*AWSClient).elbconn
   329  
   330  	d.Partial(true)
   331  
   332  	if d.HasChange("listener") {
   333  		o, n := d.GetChange("listener")
   334  		os := o.(*schema.Set)
   335  		ns := n.(*schema.Set)
   336  
   337  		remove, _ := expandListeners(os.Difference(ns).List())
   338  		add, _ := expandListeners(ns.Difference(os).List())
   339  
   340  		if len(remove) > 0 {
   341  			ports := make([]*int64, 0, len(remove))
   342  			for _, listener := range remove {
   343  				ports = append(ports, listener.LoadBalancerPort)
   344  			}
   345  
   346  			deleteListenersOpts := &elb.DeleteLoadBalancerListenersInput{
   347  				LoadBalancerName:  aws.String(d.Id()),
   348  				LoadBalancerPorts: ports,
   349  			}
   350  
   351  			_, err := elbconn.DeleteLoadBalancerListeners(deleteListenersOpts)
   352  			if err != nil {
   353  				return fmt.Errorf("Failure removing outdated ELB listeners: %s", err)
   354  			}
   355  		}
   356  
   357  		if len(add) > 0 {
   358  			createListenersOpts := &elb.CreateLoadBalancerListenersInput{
   359  				LoadBalancerName: aws.String(d.Id()),
   360  				Listeners:        add,
   361  			}
   362  
   363  			_, err := elbconn.CreateLoadBalancerListeners(createListenersOpts)
   364  			if err != nil {
   365  				return fmt.Errorf("Failure adding new or updated ELB listeners: %s", err)
   366  			}
   367  		}
   368  
   369  		d.SetPartial("listener")
   370  	}
   371  
   372  	// If we currently have instances, or did have instances,
   373  	// we want to figure out what to add and remove from the load
   374  	// balancer
   375  	if d.HasChange("instances") {
   376  		o, n := d.GetChange("instances")
   377  		os := o.(*schema.Set)
   378  		ns := n.(*schema.Set)
   379  		remove := expandInstanceString(os.Difference(ns).List())
   380  		add := expandInstanceString(ns.Difference(os).List())
   381  
   382  		if len(add) > 0 {
   383  			registerInstancesOpts := elb.RegisterInstancesWithLoadBalancerInput{
   384  				LoadBalancerName: aws.String(d.Id()),
   385  				Instances:        add,
   386  			}
   387  
   388  			_, err := elbconn.RegisterInstancesWithLoadBalancer(&registerInstancesOpts)
   389  			if err != nil {
   390  				return fmt.Errorf("Failure registering instances with ELB: %s", err)
   391  			}
   392  		}
   393  		if len(remove) > 0 {
   394  			deRegisterInstancesOpts := elb.DeregisterInstancesFromLoadBalancerInput{
   395  				LoadBalancerName: aws.String(d.Id()),
   396  				Instances:        remove,
   397  			}
   398  
   399  			_, err := elbconn.DeregisterInstancesFromLoadBalancer(&deRegisterInstancesOpts)
   400  			if err != nil {
   401  				return fmt.Errorf("Failure deregistering instances from ELB: %s", err)
   402  			}
   403  		}
   404  
   405  		d.SetPartial("instances")
   406  	}
   407  
   408  	if d.HasChange("cross_zone_load_balancing") || d.HasChange("idle_timeout") {
   409  		attrs := elb.ModifyLoadBalancerAttributesInput{
   410  			LoadBalancerName: aws.String(d.Get("name").(string)),
   411  			LoadBalancerAttributes: &elb.LoadBalancerAttributes{
   412  				CrossZoneLoadBalancing: &elb.CrossZoneLoadBalancing{
   413  					Enabled: aws.Bool(d.Get("cross_zone_load_balancing").(bool)),
   414  				},
   415  				ConnectionSettings: &elb.ConnectionSettings{
   416  					IdleTimeout: aws.Int64(int64(d.Get("idle_timeout").(int))),
   417  				},
   418  			},
   419  		}
   420  
   421  		_, err := elbconn.ModifyLoadBalancerAttributes(&attrs)
   422  		if err != nil {
   423  			return fmt.Errorf("Failure configuring ELB attributes: %s", err)
   424  		}
   425  
   426  		d.SetPartial("cross_zone_load_balancing")
   427  		d.SetPartial("idle_timeout")
   428  		d.SetPartial("connection_draining_timeout")
   429  	}
   430  
   431  	// We have to do these changes separately from everything else since
   432  	// they have some weird undocumented rules. You can't set the timeout
   433  	// without having connection draining to true, so we set that to true,
   434  	// set the timeout, then reset it to false if requested.
   435  	if d.HasChange("connection_draining") || d.HasChange("connection_draining_timeout") {
   436  		// We do timeout changes first since they require us to set draining
   437  		// to true for a hot second.
   438  		if d.HasChange("connection_draining_timeout") {
   439  			attrs := elb.ModifyLoadBalancerAttributesInput{
   440  				LoadBalancerName: aws.String(d.Get("name").(string)),
   441  				LoadBalancerAttributes: &elb.LoadBalancerAttributes{
   442  					ConnectionDraining: &elb.ConnectionDraining{
   443  						Enabled: aws.Bool(true),
   444  						Timeout: aws.Int64(int64(d.Get("connection_draining_timeout").(int))),
   445  					},
   446  				},
   447  			}
   448  
   449  			_, err := elbconn.ModifyLoadBalancerAttributes(&attrs)
   450  			if err != nil {
   451  				return fmt.Errorf("Failure configuring ELB attributes: %s", err)
   452  			}
   453  
   454  			d.SetPartial("connection_draining_timeout")
   455  		}
   456  
   457  		// Then we always set connection draining even if there is no change.
   458  		// This lets us reset to "false" if requested even with a timeout
   459  		// change.
   460  		attrs := elb.ModifyLoadBalancerAttributesInput{
   461  			LoadBalancerName: aws.String(d.Get("name").(string)),
   462  			LoadBalancerAttributes: &elb.LoadBalancerAttributes{
   463  				ConnectionDraining: &elb.ConnectionDraining{
   464  					Enabled: aws.Bool(d.Get("connection_draining").(bool)),
   465  				},
   466  			},
   467  		}
   468  
   469  		_, err := elbconn.ModifyLoadBalancerAttributes(&attrs)
   470  		if err != nil {
   471  			return fmt.Errorf("Failure configuring ELB attributes: %s", err)
   472  		}
   473  
   474  		d.SetPartial("connection_draining")
   475  	}
   476  
   477  	if d.HasChange("health_check") {
   478  		vs := d.Get("health_check").(*schema.Set).List()
   479  		if len(vs) > 0 {
   480  			check := vs[0].(map[string]interface{})
   481  			configureHealthCheckOpts := elb.ConfigureHealthCheckInput{
   482  				LoadBalancerName: aws.String(d.Id()),
   483  				HealthCheck: &elb.HealthCheck{
   484  					HealthyThreshold:   aws.Int64(int64(check["healthy_threshold"].(int))),
   485  					UnhealthyThreshold: aws.Int64(int64(check["unhealthy_threshold"].(int))),
   486  					Interval:           aws.Int64(int64(check["interval"].(int))),
   487  					Target:             aws.String(check["target"].(string)),
   488  					Timeout:            aws.Int64(int64(check["timeout"].(int))),
   489  				},
   490  			}
   491  			_, err := elbconn.ConfigureHealthCheck(&configureHealthCheckOpts)
   492  			if err != nil {
   493  				return fmt.Errorf("Failure configuring health check for ELB: %s", err)
   494  			}
   495  			d.SetPartial("health_check")
   496  		}
   497  	}
   498  
   499  	if d.HasChange("security_groups") {
   500  		groups := d.Get("security_groups").(*schema.Set).List()
   501  
   502  		applySecurityGroupsOpts := elb.ApplySecurityGroupsToLoadBalancerInput{
   503  			LoadBalancerName: aws.String(d.Id()),
   504  			SecurityGroups:   expandStringList(groups),
   505  		}
   506  
   507  		_, err := elbconn.ApplySecurityGroupsToLoadBalancer(&applySecurityGroupsOpts)
   508  		if err != nil {
   509  			return fmt.Errorf("Failure applying security groups to ELB: %s", err)
   510  		}
   511  
   512  		d.SetPartial("security_groups")
   513  	}
   514  
   515  	if err := setTagsELB(elbconn, d); err != nil {
   516  		return err
   517  	}
   518  
   519  	d.SetPartial("tags")
   520  	d.Partial(false)
   521  
   522  	return resourceAwsElbRead(d, meta)
   523  }
   524  
   525  func resourceAwsElbDelete(d *schema.ResourceData, meta interface{}) error {
   526  	elbconn := meta.(*AWSClient).elbconn
   527  
   528  	log.Printf("[INFO] Deleting ELB: %s", d.Id())
   529  
   530  	// Destroy the load balancer
   531  	deleteElbOpts := elb.DeleteLoadBalancerInput{
   532  		LoadBalancerName: aws.String(d.Id()),
   533  	}
   534  	if _, err := elbconn.DeleteLoadBalancer(&deleteElbOpts); err != nil {
   535  		return fmt.Errorf("Error deleting ELB: %s", err)
   536  	}
   537  
   538  	return nil
   539  }
   540  
   541  func resourceAwsElbHealthCheckHash(v interface{}) int {
   542  	var buf bytes.Buffer
   543  	m := v.(map[string]interface{})
   544  	buf.WriteString(fmt.Sprintf("%d-", m["healthy_threshold"].(int)))
   545  	buf.WriteString(fmt.Sprintf("%d-", m["unhealthy_threshold"].(int)))
   546  	buf.WriteString(fmt.Sprintf("%s-", m["target"].(string)))
   547  	buf.WriteString(fmt.Sprintf("%d-", m["interval"].(int)))
   548  	buf.WriteString(fmt.Sprintf("%d-", m["timeout"].(int)))
   549  
   550  	return hashcode.String(buf.String())
   551  }
   552  
   553  func resourceAwsElbListenerHash(v interface{}) int {
   554  	var buf bytes.Buffer
   555  	m := v.(map[string]interface{})
   556  	buf.WriteString(fmt.Sprintf("%d-", m["instance_port"].(int)))
   557  	buf.WriteString(fmt.Sprintf("%s-",
   558  		strings.ToLower(m["instance_protocol"].(string))))
   559  	buf.WriteString(fmt.Sprintf("%d-", m["lb_port"].(int)))
   560  	buf.WriteString(fmt.Sprintf("%s-",
   561  		strings.ToLower(m["lb_protocol"].(string))))
   562  
   563  	if v, ok := m["ssl_certificate_id"]; ok {
   564  		buf.WriteString(fmt.Sprintf("%s-", v.(string)))
   565  	}
   566  
   567  	return hashcode.String(buf.String())
   568  }
   569  
   570  func isLoadBalancerNotFound(err error) bool {
   571  	elberr, ok := err.(awserr.Error)
   572  	return ok && elberr.Code() == "LoadBalancerNotFound"
   573  }
   574  
   575  func validateElbName(v interface{}, k string) (ws []string, errors []error) {
   576  	value := v.(string)
   577  	if !regexp.MustCompile(`^[0-9A-Za-z-]+$`).MatchString(value) {
   578  		errors = append(errors, fmt.Errorf(
   579  			"only alphanumeric characters and hyphens allowed in %q: %q",
   580  			k, value))
   581  	}
   582  	if len(value) > 32 {
   583  		errors = append(errors, fmt.Errorf(
   584  			"%q cannot be longer than 32 characters: %q", k, value))
   585  	}
   586  	if regexp.MustCompile(`^-`).MatchString(value) {
   587  		errors = append(errors, fmt.Errorf(
   588  			"%q cannot begin with a hyphen: %q", k, value))
   589  	}
   590  	if regexp.MustCompile(`-$`).MatchString(value) {
   591  		errors = append(errors, fmt.Errorf(
   592  			"%q cannot end with a hyphen: %q", k, value))
   593  	}
   594  	return
   595  
   596  }