Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add stickiness support for NLB target_groups #15295

Merged
merged 10 commits into from
Oct 9, 2020
57 changes: 21 additions & 36 deletions aws/resource_aws_lb_target_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,14 +149,22 @@ func resourceAwsLbTargetGroup() *schema.Resource {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{
"lb_cookie",
"lb_cookie", // Only for ALBs
"source_ip", // Only for NLBs
}, false),
},
"cookie_duration": {
Type: schema.TypeInt,
Optional: true,
Default: 86400,
ValidateFunc: validation.IntBetween(0, 604800),
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
madpipeline marked this conversation as resolved.
Show resolved Hide resolved
switch d.Get("protocol").(string) {
case elbv2.ProtocolEnumTcp, elbv2.ProtocolEnumUdp, elbv2.ProtocolEnumTcpUdp, elbv2.ProtocolEnumTls:
return true
}
return false
},
},
},
},
Expand Down Expand Up @@ -434,11 +442,7 @@ func resourceAwsLbTargetGroupUpdate(d *schema.ResourceData, meta interface{}) er
})
}

// In CustomizeDiff we allow LB stickiness to be declared for TCP target
// groups, so long as it's not enabled. This allows for better support for
// modules, but also means we need to completely skip sending the data to the
// API if it's defined on a TCP target group.
if d.HasChange("stickiness") && d.Get("protocol") != elbv2.ProtocolEnumTcp {
if d.HasChange("stickiness") {
stickinessBlocks := d.Get("stickiness").([]interface{})
if len(stickinessBlocks) == 1 {
stickiness := stickinessBlocks[0].(map[string]interface{})
Expand All @@ -451,11 +455,16 @@ func resourceAwsLbTargetGroupUpdate(d *schema.ResourceData, meta interface{}) er
&elbv2.TargetGroupAttribute{
Key: aws.String("stickiness.type"),
Value: aws.String(stickiness["type"].(string)),
},
&elbv2.TargetGroupAttribute{
Key: aws.String("stickiness.lb_cookie.duration_seconds"),
Value: aws.String(fmt.Sprintf("%d", stickiness["cookie_duration"].(int))),
})

switch d.Get("protocol").(string) {
case elbv2.ProtocolEnumHttp, elbv2.ProtocolEnumHttps:
attrs = append(attrs,
&elbv2.TargetGroupAttribute{
Key: aws.String("stickiness.lb_cookie.duration_seconds"),
Value: aws.String(fmt.Sprintf("%d", stickiness["cookie_duration"].(int))),
})
}
} else if len(stickinessBlocks) == 0 {
attrs = append(attrs, &elbv2.TargetGroupAttribute{
Key: aws.String("stickiness.enabled"),
Expand Down Expand Up @@ -654,23 +663,8 @@ func flattenAwsLbTargetGroupResource(d *schema.ResourceData, meta interface{}, t
}
}

// We only read in the stickiness attributes if the target group is not
// TCP-based. This ensures we don't end up causing a spurious diff if someone
// has defined the stickiness block on a TCP target group (albeit with
// false), for which this update would clobber the state coming from config
// for.
//
// This is a workaround to support module design where the module needs to
// support HTTP and TCP target groups.
switch {
case aws.StringValue(targetGroup.Protocol) != elbv2.ProtocolEnumTcp:
if err = flattenAwsLbTargetGroupStickiness(d, attrResp.Attributes); err != nil {
return err
}
case aws.StringValue(targetGroup.Protocol) == elbv2.ProtocolEnumTcp && len(d.Get("stickiness").([]interface{})) < 1:
if err = d.Set("stickiness", []interface{}{}); err != nil {
return fmt.Errorf("error setting stickiness: %s", err)
}
if err = flattenAwsLbTargetGroupStickiness(d, attrResp.Attributes); err != nil {
return err
}

tags, err := keyvaluetags.Elbv2ListTags(elbconn, d.Id())
Expand Down Expand Up @@ -725,15 +719,6 @@ func flattenAwsLbTargetGroupStickiness(d *schema.ResourceData, attributes []*elb

func resourceAwsLbTargetGroupCustomizeDiff(_ context.Context, diff *schema.ResourceDiff, v interface{}) error {
protocol := diff.Get("protocol").(string)
if protocol == elbv2.ProtocolEnumTcp {
// TCP load balancers do not support stickiness
if stickinessBlocks := diff.Get("stickiness").([]interface{}); len(stickinessBlocks) == 1 {
stickiness := stickinessBlocks[0].(map[string]interface{})
if val := stickiness["enabled"].(bool); val {
return fmt.Errorf("Network Load Balancers do not support Stickiness")
}
}
}

// Network Load Balancers have many special qwirks to them.
// See http://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_CreateTargetGroup.html
Expand Down
204 changes: 193 additions & 11 deletions aws/resource_aws_lb_target_group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -964,7 +964,7 @@ protocol = "TCP"
})
}

func TestAccAWSLBTargetGroup_stickinessWithTCPDisabled(t *testing.T) {
func TestAccAWSLBTargetGroup_stickinessDefaultNLB(t *testing.T) {
var conf elbv2.TargetGroup
resourceName := "aws_lb_target_group.test"

Expand All @@ -975,11 +975,135 @@ func TestAccAWSLBTargetGroup_stickinessWithTCPDisabled(t *testing.T) {
CheckDestroy: testAccCheckAWSLBTargetGroupDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSLBTargetGroupConfig_stickinessWithTCP(false),
Config: testAccAWSLBTargetGroupConfig_stickinessDefault("TCP"),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSLBTargetGroupExists(resourceName, &conf),
resource.TestCheckResourceAttr(resourceName, "stickiness.#", "1"),
resource.TestCheckResourceAttr(resourceName, "stickiness.0.enabled", "false"),
resource.TestCheckResourceAttr(resourceName, "stickiness.0.type", "source_ip"),
),
},
{
Config: testAccAWSLBTargetGroupConfig_stickinessDefault("UDP"),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSLBTargetGroupExists(resourceName, &conf),
resource.TestCheckResourceAttr(resourceName, "stickiness.#", "1"),
resource.TestCheckResourceAttr(resourceName, "stickiness.0.enabled", "false"),
resource.TestCheckResourceAttr(resourceName, "stickiness.0.type", "source_ip"),
),
},
{
Config: testAccAWSLBTargetGroupConfig_stickinessDefault("TCP_UDP"),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSLBTargetGroupExists(resourceName, &conf),
resource.TestCheckResourceAttr(resourceName, "stickiness.#", "1"),
resource.TestCheckResourceAttr(resourceName, "stickiness.0.enabled", "false"),
resource.TestCheckResourceAttr(resourceName, "stickiness.0.type", "source_ip"),
),
},
},
})
}

func TestAccAWSLBTargetGroup_stickinessDefaultALB(t *testing.T) {
var conf elbv2.TargetGroup
resourceName := "aws_lb_target_group.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
IDRefreshName: resourceName,
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSLBTargetGroupDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSLBTargetGroupConfig_stickinessDefault("HTTP"),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSLBTargetGroupExists(resourceName, &conf),
resource.TestCheckResourceAttr(resourceName, "stickiness.#", "1"),
resource.TestCheckResourceAttr(resourceName, "stickiness.0.enabled", "false"),
resource.TestCheckResourceAttr(resourceName, "stickiness.0.type", "lb_cookie"),
),
},
},
})
}

func TestAccAWSLBTargetGroup_stickinessValidNLB(t *testing.T) {
var conf elbv2.TargetGroup
resourceName := "aws_lb_target_group.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
IDRefreshName: resourceName,
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSLBTargetGroupDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSLBTargetGroupConfig_stickinessValidity("TCP", "source_ip", false),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSLBTargetGroupExists(resourceName, &conf),
resource.TestCheckResourceAttr(resourceName, "stickiness.#", "1"),
resource.TestCheckResourceAttr(resourceName, "stickiness.0.enabled", "false"),
resource.TestCheckResourceAttr(resourceName, "stickiness.0.type", "source_ip"),
),
},
{
Config: testAccAWSLBTargetGroupConfig_stickinessValidity("TCP", "source_ip", true),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSLBTargetGroupExists(resourceName, &conf),
resource.TestCheckResourceAttr(resourceName, "stickiness.#", "1"),
resource.TestCheckResourceAttr(resourceName, "stickiness.0.enabled", "true"),
resource.TestCheckResourceAttr(resourceName, "stickiness.0.type", "source_ip"),
),
},
{
Config: testAccAWSLBTargetGroupConfig_stickinessValidity("UDP", "source_ip", true),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSLBTargetGroupExists(resourceName, &conf),
resource.TestCheckResourceAttr(resourceName, "stickiness.#", "1"),
resource.TestCheckResourceAttr(resourceName, "stickiness.0.enabled", "true"),
resource.TestCheckResourceAttr(resourceName, "stickiness.0.type", "source_ip"),
),
},
{
Config: testAccAWSLBTargetGroupConfig_stickinessValidity("TCP_UDP", "source_ip", true),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSLBTargetGroupExists(resourceName, &conf),
resource.TestCheckResourceAttr(resourceName, "stickiness.#", "1"),
resource.TestCheckResourceAttr(resourceName, "stickiness.0.enabled", "true"),
resource.TestCheckResourceAttr(resourceName, "stickiness.0.type", "source_ip"),
),
},
},
})
}

func TestAccAWSLBTargetGroup_stickinessValidALB(t *testing.T) {
var conf elbv2.TargetGroup
resourceName := "aws_lb_target_group.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
IDRefreshName: resourceName,
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSLBTargetGroupDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSLBTargetGroupConfig_stickinessValidity("HTTP", "lb_cookie", true),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSLBTargetGroupExists(resourceName, &conf),
resource.TestCheckResourceAttr(resourceName, "stickiness.#", "1"),
resource.TestCheckResourceAttr(resourceName, "stickiness.0.enabled", "true"),
resource.TestCheckResourceAttr(resourceName, "stickiness.0.type", "lb_cookie"),
resource.TestCheckResourceAttr(resourceName, "stickiness.0.cookie_duration", "86400"),
),
},
{
Config: testAccAWSLBTargetGroupConfig_stickinessValidity("HTTPS", "lb_cookie", true),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckAWSLBTargetGroupExists(resourceName, &conf),
resource.TestCheckResourceAttr(resourceName, "stickiness.#", "1"),
resource.TestCheckResourceAttr(resourceName, "stickiness.0.enabled", "true"),
resource.TestCheckResourceAttr(resourceName, "stickiness.0.type", "lb_cookie"),
resource.TestCheckResourceAttr(resourceName, "stickiness.0.cookie_duration", "86400"),
),
Expand All @@ -988,16 +1112,55 @@ func TestAccAWSLBTargetGroup_stickinessWithTCPDisabled(t *testing.T) {
})
}

func TestAccAWSLBTargetGroup_stickinessWithTCPEnabledShouldError(t *testing.T) {
func TestAccAWSLBTargetGroup_stickinessInvalidNLB(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSLBTargetGroupDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSLBTargetGroupConfig_stickinessValidity("TCP", "lb_cookie", true),
ExpectError: regexp.MustCompile("Stickiness type 'lb_cookie' is not supported for target groups with"),
},
{
Config: testAccAWSLBTargetGroupConfig_stickinessValidity("UDP", "lb_cookie", true),
ExpectError: regexp.MustCompile("Stickiness type 'lb_cookie' is not supported for target groups with"),
},
{
Config: testAccAWSLBTargetGroupConfig_stickinessValidity("TCP_UDP", "lb_cookie", true),
ExpectError: regexp.MustCompile("Stickiness type 'lb_cookie' is not supported for target groups with"),
},
{
Config: testAccAWSLBTargetGroupConfig_stickinessValidity("TCP_UDP", "lb_cookie", false),
PlanOnly: true,
ExpectNonEmptyPlan: true,
},
},
})
}

func TestAccAWSLBTargetGroup_stickinessInvalidALB(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSLBTargetGroupDestroy,
Steps: []resource.TestStep{
{
Config: testAccAWSLBTargetGroupConfig_stickinessWithTCP(true),
PlanOnly: true,
ExpectError: regexp.MustCompile("Network Load Balancers do not support Stickiness"),
Config: testAccAWSLBTargetGroupConfig_stickinessValidity("HTTP", "source_ip", true),
ExpectError: regexp.MustCompile("Stickiness type 'source_ip' is not supported for target groups with"),
},
{
Config: testAccAWSLBTargetGroupConfig_stickinessValidity("HTTPS", "source_ip", true),
ExpectError: regexp.MustCompile("Stickiness type 'source_ip' is not supported for target groups with"),
},
{
Config: testAccAWSLBTargetGroupConfig_stickinessValidity("TLS", "lb_cookie", true),
ExpectError: regexp.MustCompile("Stickiness type 'lb_cookie' is not supported for target groups with"),
},
{
Config: testAccAWSLBTargetGroupConfig_stickinessValidity("TCP_UDP", "lb_cookie", false),
PlanOnly: true,
ExpectNonEmptyPlan: true,
},
},
})
Expand Down Expand Up @@ -1923,16 +2086,35 @@ resource "aws_vpc" "test" {
}
`

func testAccAWSLBTargetGroupConfig_stickinessWithTCP(enabled bool) string {
func testAccAWSLBTargetGroupConfig_stickinessDefault(protocol string) string {
return fmt.Sprintf(`
resource "aws_lb_target_group" "test" {
name_prefix = "tf-"
port = 25
protocol = %q
vpc_id = aws_vpc.test.id
}

resource "aws_vpc" "test" {
cidr_block = "10.0.0.0/16"

tags = {
Name = "testAccAWSLBTargetGroupConfig_stickinessDefault"
}
}
`, protocol)
}

func testAccAWSLBTargetGroupConfig_stickinessValidity(protocol, stickyType string, enabled bool) string {
return fmt.Sprintf(`
resource "aws_lb_target_group" "test" {
name_prefix = "tf-"
port = 25
protocol = "TCP"
protocol = %q
vpc_id = aws_vpc.test.id

stickiness {
type = "lb_cookie"
type = %q
enabled = %t
}
}
Expand All @@ -1941,8 +2123,8 @@ resource "aws_vpc" "test" {
cidr_block = "10.0.0.0/16"

tags = {
Name = "testAccAWSLBTargetGroupConfig_namePrefix"
Name = "testAccAWSLBTargetGroupConfig_stickinessValidity"
}
}
`, enabled)
`, protocol, stickyType, enabled)
}
8 changes: 4 additions & 4 deletions website/docs/r/lb_target_group.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ The following arguments are supported:
* `load_balancing_algorithm_type` - (Optional) Determines how the load balancer selects targets when routing requests. Only applicable for Application Load Balancer Target Groups. The value is `round_robin` or `least_outstanding_requests`. The default is `round_robin`.
* `lambda_multi_value_headers_enabled` - (Optional) Boolean whether the request and response headers exchanged between the load balancer and the Lambda function include arrays of values or strings. Only applies when `target_type` is `lambda`.
* `proxy_protocol_v2` - (Optional) Boolean to enable / disable support for proxy protocol v2 on Network Load Balancers. See [doc](https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-target-groups.html#proxy-protocol) for more information.
* `stickiness` - (Optional) A Stickiness block. Stickiness blocks are documented below. `stickiness` is only valid if used with Load Balancers of type `Application`
* `health_check` - (Optional) A Health Check block. Health Check blocks are documented below.
* `stickiness` - (Optional, Maximum of 1) A Stickiness block. Stickiness blocks are documented below.
* `health_check` - (Optional, Maximum of 1) A Health Check block. Health Check blocks are documented below.
* `target_type` - (Optional, Forces new resource) The type of target that you must specify when registering targets with this target group.
The possible values are `instance` (targets are specified by instance ID) or `ip` (targets are specified by IP address) or `lambda` (targets are specified by lambda arn).
The default is `instance`. Note that you can't specify targets for a target group using both instance IDs and IP addresses.
Expand All @@ -81,8 +81,8 @@ You can't specify publicly routable IP addresses.

Stickiness Blocks (`stickiness`) support the following:

* `type` - (Required) The type of sticky sessions. The only current possible value is `lb_cookie`.
* `cookie_duration` - (Optional) The time period, in seconds, during which requests from a client should be routed to the same target. After this time period expires, the load balancer-generated cookie is considered stale. The range is 1 second to 1 week (604800 seconds). The default value is 1 day (86400 seconds).
* `type` - (Required) The type of sticky sessions. The only current possible values are `lb_cookie` for ALBs and `source_ip` for NLBs.
* `cookie_duration` - (Optional) Only used when the type is `lb_cookie`. The time period, in seconds, during which requests from a client should be routed to the same target. After this time period expires, the load balancer-generated cookie is considered stale. The range is 1 second to 1 week (604800 seconds). The default value is 1 day (86400 seconds).
* `enabled` - (Optional) Boolean to enable / disable `stickiness`. Default is `true`

~> **NOTE:** To help facilitate the authoring of modules that support target groups of any protocol, you can define `stickiness` regardless of the protocol chosen. However, for `TCP` target groups, `enabled` must be `false`.
Expand Down