terraform/builtin/providers/aws/resource_aws_elb.go

797 lines
22 KiB
Go

package aws
import (
"bytes"
"fmt"
"log"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/elb"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsElb() *schema.Resource {
return &schema.Resource{
Create: resourceAwsElbCreate,
Read: resourceAwsElbRead,
Update: resourceAwsElbUpdate,
Delete: resourceAwsElbDelete,
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ValidateFunc: validateElbName,
},
"internal": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
Computed: true,
},
"cross_zone_load_balancing": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"availability_zones": &schema.Schema{
Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
Computed: true,
Set: schema.HashString,
},
"instances": &schema.Schema{
Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
Computed: true,
Set: schema.HashString,
},
"security_groups": &schema.Schema{
Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
Computed: true,
Set: schema.HashString,
},
"source_security_group": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"source_security_group_id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"subnets": &schema.Schema{
Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
Computed: true,
Set: schema.HashString,
},
"idle_timeout": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 60,
},
"connection_draining": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"connection_draining_timeout": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 300,
},
"access_logs": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"interval": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 60,
},
"bucket": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"bucket_prefix": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
},
},
"listener": &schema.Schema{
Type: schema.TypeSet,
Required: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"instance_port": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"instance_protocol": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"lb_port": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"lb_protocol": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"ssl_certificate_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
},
Set: resourceAwsElbListenerHash,
},
"health_check": &schema.Schema{
Type: schema.TypeList,
Optional: true,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"healthy_threshold": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"unhealthy_threshold": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"target": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"interval": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
"timeout": &schema.Schema{
Type: schema.TypeInt,
Required: true,
},
},
},
},
"dns_name": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"zone_id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"tags": tagsSchema(),
},
}
}
func resourceAwsElbCreate(d *schema.ResourceData, meta interface{}) error {
elbconn := meta.(*AWSClient).elbconn
// Expand the "listener" set to aws-sdk-go compat []*elb.Listener
listeners, err := expandListeners(d.Get("listener").(*schema.Set).List())
if err != nil {
return err
}
var elbName string
if v, ok := d.GetOk("name"); ok {
elbName = v.(string)
} else {
elbName = resource.PrefixedUniqueId("tf-lb-")
d.Set("name", elbName)
}
tags := tagsFromMapELB(d.Get("tags").(map[string]interface{}))
// Provision the elb
elbOpts := &elb.CreateLoadBalancerInput{
LoadBalancerName: aws.String(elbName),
Listeners: listeners,
Tags: tags,
}
if scheme, ok := d.GetOk("internal"); ok && scheme.(bool) {
elbOpts.Scheme = aws.String("internal")
}
if v, ok := d.GetOk("availability_zones"); ok {
elbOpts.AvailabilityZones = expandStringList(v.(*schema.Set).List())
}
if v, ok := d.GetOk("security_groups"); ok {
elbOpts.SecurityGroups = expandStringList(v.(*schema.Set).List())
}
if v, ok := d.GetOk("subnets"); ok {
elbOpts.Subnets = expandStringList(v.(*schema.Set).List())
}
log.Printf("[DEBUG] ELB create configuration: %#v", elbOpts)
err = resource.Retry(1*time.Minute, func() *resource.RetryError {
_, err := elbconn.CreateLoadBalancer(elbOpts)
if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
// Check for IAM SSL Cert error, eventual consistancy issue
if awsErr.Code() == "CertificateNotFound" {
return resource.RetryableError(
fmt.Errorf("[WARN] Error creating ELB Listener with SSL Cert, retrying: %s", err))
}
}
return resource.NonRetryableError(err)
}
return nil
})
if err != nil {
return err
}
// Assign the elb's unique identifier for use later
d.SetId(elbName)
log.Printf("[INFO] ELB ID: %s", d.Id())
// Enable partial mode and record what we set
d.Partial(true)
d.SetPartial("name")
d.SetPartial("internal")
d.SetPartial("availability_zones")
d.SetPartial("listener")
d.SetPartial("security_groups")
d.SetPartial("subnets")
d.Set("tags", tagsToMapELB(tags))
return resourceAwsElbUpdate(d, meta)
}
func resourceAwsElbRead(d *schema.ResourceData, meta interface{}) error {
elbconn := meta.(*AWSClient).elbconn
elbName := d.Id()
// Retrieve the ELB properties for updating the state
describeElbOpts := &elb.DescribeLoadBalancersInput{
LoadBalancerNames: []*string{aws.String(elbName)},
}
describeResp, err := elbconn.DescribeLoadBalancers(describeElbOpts)
if err != nil {
if isLoadBalancerNotFound(err) {
// The ELB is gone now, so just remove it from the state
d.SetId("")
return nil
}
return fmt.Errorf("Error retrieving ELB: %s", err)
}
if len(describeResp.LoadBalancerDescriptions) != 1 {
return fmt.Errorf("Unable to find ELB: %#v", describeResp.LoadBalancerDescriptions)
}
describeAttrsOpts := &elb.DescribeLoadBalancerAttributesInput{
LoadBalancerName: aws.String(elbName),
}
describeAttrsResp, err := elbconn.DescribeLoadBalancerAttributes(describeAttrsOpts)
if err != nil {
if isLoadBalancerNotFound(err) {
// The ELB is gone now, so just remove it from the state
d.SetId("")
return nil
}
return fmt.Errorf("Error retrieving ELB: %s", err)
}
lbAttrs := describeAttrsResp.LoadBalancerAttributes
lb := describeResp.LoadBalancerDescriptions[0]
d.Set("name", *lb.LoadBalancerName)
d.Set("dns_name", *lb.DNSName)
d.Set("zone_id", *lb.CanonicalHostedZoneNameID)
d.Set("internal", *lb.Scheme == "internal")
d.Set("availability_zones", flattenStringList(lb.AvailabilityZones))
d.Set("instances", flattenInstances(lb.Instances))
d.Set("listener", flattenListeners(lb.ListenerDescriptions))
d.Set("security_groups", flattenStringList(lb.SecurityGroups))
if lb.SourceSecurityGroup != nil {
group := lb.SourceSecurityGroup.GroupName
if lb.SourceSecurityGroup.OwnerAlias != nil && *lb.SourceSecurityGroup.OwnerAlias != "" {
group = aws.String(*lb.SourceSecurityGroup.OwnerAlias + "/" + *lb.SourceSecurityGroup.GroupName)
}
d.Set("source_security_group", group)
// Manually look up the ELB Security Group ID, since it's not provided
var elbVpc string
if lb.VPCId != nil {
elbVpc = *lb.VPCId
sgId, err := sourceSGIdByName(meta, *lb.SourceSecurityGroup.GroupName, elbVpc)
if err != nil {
return fmt.Errorf("[WARN] Error looking up ELB Security Group ID: %s", err)
} else {
d.Set("source_security_group_id", sgId)
}
}
}
d.Set("subnets", flattenStringList(lb.Subnets))
d.Set("idle_timeout", lbAttrs.ConnectionSettings.IdleTimeout)
d.Set("connection_draining", lbAttrs.ConnectionDraining.Enabled)
d.Set("connection_draining_timeout", lbAttrs.ConnectionDraining.Timeout)
d.Set("cross_zone_load_balancing", lbAttrs.CrossZoneLoadBalancing.Enabled)
if lbAttrs.AccessLog != nil {
if err := d.Set("access_logs", flattenAccessLog(lbAttrs.AccessLog)); err != nil {
return err
}
}
resp, err := elbconn.DescribeTags(&elb.DescribeTagsInput{
LoadBalancerNames: []*string{lb.LoadBalancerName},
})
var et []*elb.Tag
if len(resp.TagDescriptions) > 0 {
et = resp.TagDescriptions[0].Tags
}
d.Set("tags", tagsToMapELB(et))
// There's only one health check, so save that to state as we
// currently can
if *lb.HealthCheck.Target != "" {
d.Set("health_check", flattenHealthCheck(lb.HealthCheck))
}
return nil
}
func resourceAwsElbUpdate(d *schema.ResourceData, meta interface{}) error {
elbconn := meta.(*AWSClient).elbconn
d.Partial(true)
if d.HasChange("listener") {
o, n := d.GetChange("listener")
os := o.(*schema.Set)
ns := n.(*schema.Set)
remove, _ := expandListeners(os.Difference(ns).List())
add, _ := expandListeners(ns.Difference(os).List())
if len(remove) > 0 {
ports := make([]*int64, 0, len(remove))
for _, listener := range remove {
ports = append(ports, listener.LoadBalancerPort)
}
deleteListenersOpts := &elb.DeleteLoadBalancerListenersInput{
LoadBalancerName: aws.String(d.Id()),
LoadBalancerPorts: ports,
}
log.Printf("[DEBUG] ELB Delete Listeners opts: %s", deleteListenersOpts)
_, err := elbconn.DeleteLoadBalancerListeners(deleteListenersOpts)
if err != nil {
return fmt.Errorf("Failure removing outdated ELB listeners: %s", err)
}
}
if len(add) > 0 {
createListenersOpts := &elb.CreateLoadBalancerListenersInput{
LoadBalancerName: aws.String(d.Id()),
Listeners: add,
}
// Occasionally AWS will error with a 'duplicate listener', without any
// other listeners on the ELB. Retry here to eliminate that.
err := resource.Retry(1*time.Minute, func() *resource.RetryError {
log.Printf("[DEBUG] ELB Create Listeners opts: %s", createListenersOpts)
if _, err := elbconn.CreateLoadBalancerListeners(createListenersOpts); err != nil {
if awsErr, ok := err.(awserr.Error); ok {
if awsErr.Code() == "DuplicateListener" {
log.Printf("[DEBUG] Duplicate listener found for ELB (%s), retrying", d.Id())
return resource.RetryableError(awsErr)
}
if awsErr.Code() == "CertificateNotFound" && strings.Contains(awsErr.Message(), "Server Certificate not found for the key: arn") {
log.Printf("[DEBUG] SSL Cert not found for given ARN, retrying")
return resource.RetryableError(awsErr)
}
}
// Didn't recognize the error, so shouldn't retry.
return resource.NonRetryableError(err)
}
// Successful creation
return nil
})
if err != nil {
return fmt.Errorf("Failure adding new or updated ELB listeners: %s", err)
}
}
d.SetPartial("listener")
}
// If we currently have instances, or did have instances,
// we want to figure out what to add and remove from the load
// balancer
if d.HasChange("instances") {
o, n := d.GetChange("instances")
os := o.(*schema.Set)
ns := n.(*schema.Set)
remove := expandInstanceString(os.Difference(ns).List())
add := expandInstanceString(ns.Difference(os).List())
if len(add) > 0 {
registerInstancesOpts := elb.RegisterInstancesWithLoadBalancerInput{
LoadBalancerName: aws.String(d.Id()),
Instances: add,
}
_, err := elbconn.RegisterInstancesWithLoadBalancer(&registerInstancesOpts)
if err != nil {
return fmt.Errorf("Failure registering instances with ELB: %s", err)
}
}
if len(remove) > 0 {
deRegisterInstancesOpts := elb.DeregisterInstancesFromLoadBalancerInput{
LoadBalancerName: aws.String(d.Id()),
Instances: remove,
}
_, err := elbconn.DeregisterInstancesFromLoadBalancer(&deRegisterInstancesOpts)
if err != nil {
return fmt.Errorf("Failure deregistering instances from ELB: %s", err)
}
}
d.SetPartial("instances")
}
if d.HasChange("cross_zone_load_balancing") || d.HasChange("idle_timeout") || d.HasChange("access_logs") {
attrs := elb.ModifyLoadBalancerAttributesInput{
LoadBalancerName: aws.String(d.Get("name").(string)),
LoadBalancerAttributes: &elb.LoadBalancerAttributes{
CrossZoneLoadBalancing: &elb.CrossZoneLoadBalancing{
Enabled: aws.Bool(d.Get("cross_zone_load_balancing").(bool)),
},
ConnectionSettings: &elb.ConnectionSettings{
IdleTimeout: aws.Int64(int64(d.Get("idle_timeout").(int))),
},
},
}
logs := d.Get("access_logs").([]interface{})
if len(logs) > 1 {
return fmt.Errorf("Only one access logs config per ELB is supported")
} else if len(logs) == 1 {
log := logs[0].(map[string]interface{})
accessLog := &elb.AccessLog{
Enabled: aws.Bool(true),
EmitInterval: aws.Int64(int64(log["interval"].(int))),
S3BucketName: aws.String(log["bucket"].(string)),
}
if log["bucket_prefix"] != "" {
accessLog.S3BucketPrefix = aws.String(log["bucket_prefix"].(string))
}
attrs.LoadBalancerAttributes.AccessLog = accessLog
} else if len(logs) == 0 {
// disable access logs
attrs.LoadBalancerAttributes.AccessLog = &elb.AccessLog{
Enabled: aws.Bool(false),
}
}
log.Printf("[DEBUG] ELB Modify Load Balancer Attributes Request: %#v", attrs)
_, err := elbconn.ModifyLoadBalancerAttributes(&attrs)
if err != nil {
return fmt.Errorf("Failure configuring ELB attributes: %s", err)
}
d.SetPartial("cross_zone_load_balancing")
d.SetPartial("idle_timeout")
d.SetPartial("connection_draining_timeout")
}
// We have to do these changes separately from everything else since
// they have some weird undocumented rules. You can't set the timeout
// without having connection draining to true, so we set that to true,
// set the timeout, then reset it to false if requested.
if d.HasChange("connection_draining") || d.HasChange("connection_draining_timeout") {
// We do timeout changes first since they require us to set draining
// to true for a hot second.
if d.HasChange("connection_draining_timeout") {
attrs := elb.ModifyLoadBalancerAttributesInput{
LoadBalancerName: aws.String(d.Get("name").(string)),
LoadBalancerAttributes: &elb.LoadBalancerAttributes{
ConnectionDraining: &elb.ConnectionDraining{
Enabled: aws.Bool(true),
Timeout: aws.Int64(int64(d.Get("connection_draining_timeout").(int))),
},
},
}
_, err := elbconn.ModifyLoadBalancerAttributes(&attrs)
if err != nil {
return fmt.Errorf("Failure configuring ELB attributes: %s", err)
}
d.SetPartial("connection_draining_timeout")
}
// Then we always set connection draining even if there is no change.
// This lets us reset to "false" if requested even with a timeout
// change.
attrs := elb.ModifyLoadBalancerAttributesInput{
LoadBalancerName: aws.String(d.Get("name").(string)),
LoadBalancerAttributes: &elb.LoadBalancerAttributes{
ConnectionDraining: &elb.ConnectionDraining{
Enabled: aws.Bool(d.Get("connection_draining").(bool)),
},
},
}
_, err := elbconn.ModifyLoadBalancerAttributes(&attrs)
if err != nil {
return fmt.Errorf("Failure configuring ELB attributes: %s", err)
}
d.SetPartial("connection_draining")
}
if d.HasChange("health_check") {
hc := d.Get("health_check").([]interface{})
if len(hc) > 1 {
return fmt.Errorf("Only one health check per ELB is supported")
} else if len(hc) > 0 {
check := hc[0].(map[string]interface{})
configureHealthCheckOpts := elb.ConfigureHealthCheckInput{
LoadBalancerName: aws.String(d.Id()),
HealthCheck: &elb.HealthCheck{
HealthyThreshold: aws.Int64(int64(check["healthy_threshold"].(int))),
UnhealthyThreshold: aws.Int64(int64(check["unhealthy_threshold"].(int))),
Interval: aws.Int64(int64(check["interval"].(int))),
Target: aws.String(check["target"].(string)),
Timeout: aws.Int64(int64(check["timeout"].(int))),
},
}
_, err := elbconn.ConfigureHealthCheck(&configureHealthCheckOpts)
if err != nil {
return fmt.Errorf("Failure configuring health check for ELB: %s", err)
}
d.SetPartial("health_check")
}
}
if d.HasChange("security_groups") {
groups := d.Get("security_groups").(*schema.Set).List()
applySecurityGroupsOpts := elb.ApplySecurityGroupsToLoadBalancerInput{
LoadBalancerName: aws.String(d.Id()),
SecurityGroups: expandStringList(groups),
}
_, err := elbconn.ApplySecurityGroupsToLoadBalancer(&applySecurityGroupsOpts)
if err != nil {
return fmt.Errorf("Failure applying security groups to ELB: %s", err)
}
d.SetPartial("security_groups")
}
if d.HasChange("availability_zones") {
o, n := d.GetChange("availability_zones")
os := o.(*schema.Set)
ns := n.(*schema.Set)
removed := expandStringList(os.Difference(ns).List())
added := expandStringList(ns.Difference(os).List())
if len(added) > 0 {
enableOpts := &elb.EnableAvailabilityZonesForLoadBalancerInput{
LoadBalancerName: aws.String(d.Id()),
AvailabilityZones: added,
}
log.Printf("[DEBUG] ELB enable availability zones opts: %s", enableOpts)
_, err := elbconn.EnableAvailabilityZonesForLoadBalancer(enableOpts)
if err != nil {
return fmt.Errorf("Failure enabling ELB availability zones: %s", err)
}
}
if len(removed) > 0 {
disableOpts := &elb.DisableAvailabilityZonesForLoadBalancerInput{
LoadBalancerName: aws.String(d.Id()),
AvailabilityZones: removed,
}
log.Printf("[DEBUG] ELB disable availability zones opts: %s", disableOpts)
_, err := elbconn.DisableAvailabilityZonesForLoadBalancer(disableOpts)
if err != nil {
return fmt.Errorf("Failure disabling ELB availability zones: %s", err)
}
}
d.SetPartial("availability_zones")
}
if d.HasChange("subnets") {
o, n := d.GetChange("subnets")
os := o.(*schema.Set)
ns := n.(*schema.Set)
removed := expandStringList(os.Difference(ns).List())
added := expandStringList(ns.Difference(os).List())
if len(added) > 0 {
attachOpts := &elb.AttachLoadBalancerToSubnetsInput{
LoadBalancerName: aws.String(d.Id()),
Subnets: added,
}
log.Printf("[DEBUG] ELB attach subnets opts: %s", attachOpts)
_, err := elbconn.AttachLoadBalancerToSubnets(attachOpts)
if err != nil {
return fmt.Errorf("Failure adding ELB subnets: %s", err)
}
}
if len(removed) > 0 {
detachOpts := &elb.DetachLoadBalancerFromSubnetsInput{
LoadBalancerName: aws.String(d.Id()),
Subnets: removed,
}
log.Printf("[DEBUG] ELB detach subnets opts: %s", detachOpts)
_, err := elbconn.DetachLoadBalancerFromSubnets(detachOpts)
if err != nil {
return fmt.Errorf("Failure removing ELB subnets: %s", err)
}
}
d.SetPartial("subnets")
}
if err := setTagsELB(elbconn, d); err != nil {
return err
}
d.SetPartial("tags")
d.Partial(false)
return resourceAwsElbRead(d, meta)
}
func resourceAwsElbDelete(d *schema.ResourceData, meta interface{}) error {
elbconn := meta.(*AWSClient).elbconn
log.Printf("[INFO] Deleting ELB: %s", d.Id())
// Destroy the load balancer
deleteElbOpts := elb.DeleteLoadBalancerInput{
LoadBalancerName: aws.String(d.Id()),
}
if _, err := elbconn.DeleteLoadBalancer(&deleteElbOpts); err != nil {
return fmt.Errorf("Error deleting ELB: %s", err)
}
return nil
}
func resourceAwsElbListenerHash(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%d-", m["instance_port"].(int)))
buf.WriteString(fmt.Sprintf("%s-",
strings.ToLower(m["instance_protocol"].(string))))
buf.WriteString(fmt.Sprintf("%d-", m["lb_port"].(int)))
buf.WriteString(fmt.Sprintf("%s-",
strings.ToLower(m["lb_protocol"].(string))))
if v, ok := m["ssl_certificate_id"]; ok {
buf.WriteString(fmt.Sprintf("%s-", v.(string)))
}
return hashcode.String(buf.String())
}
func isLoadBalancerNotFound(err error) bool {
elberr, ok := err.(awserr.Error)
return ok && elberr.Code() == "LoadBalancerNotFound"
}
func sourceSGIdByName(meta interface{}, sg, vpcId string) (string, error) {
conn := meta.(*AWSClient).ec2conn
var filters []*ec2.Filter
var sgFilterName, sgFilterVPCID *ec2.Filter
sgFilterName = &ec2.Filter{
Name: aws.String("group-name"),
Values: []*string{aws.String(sg)},
}
if vpcId != "" {
sgFilterVPCID = &ec2.Filter{
Name: aws.String("vpc-id"),
Values: []*string{aws.String(vpcId)},
}
}
filters = append(filters, sgFilterName)
if sgFilterVPCID != nil {
filters = append(filters, sgFilterVPCID)
}
req := &ec2.DescribeSecurityGroupsInput{
Filters: filters,
}
resp, err := conn.DescribeSecurityGroups(req)
if err != nil {
if ec2err, ok := err.(awserr.Error); ok {
if ec2err.Code() == "InvalidSecurityGroupID.NotFound" ||
ec2err.Code() == "InvalidGroup.NotFound" {
resp = nil
err = nil
}
}
if err != nil {
log.Printf("Error on ELB SG look up: %s", err)
return "", err
}
}
if resp == nil || len(resp.SecurityGroups) == 0 {
return "", fmt.Errorf("No security groups found for name %s and vpc id %s", sg, vpcId)
}
group := resp.SecurityGroups[0]
return *group.GroupId, nil
}