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

Don't force recreation of aws_lb_target_groups on health check modifications #28018

Merged
merged 6 commits into from
Dec 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changelog/28018.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
```release-note:bug
resource/aws_lb_target_group: Don't force recreation on `health_check` attribute changes
```

```release-note:bug
resource/aws_lb_target_group: Allow `interval` to be updated for TCP health checks
```

```release-note:bug
resource/aws_lb_target_group: Allow `timeout` to be set for TCP health checks
```

```release-note:bug
resource/aws_lb_target_group: Allow `healthy_threshold` and `unhealthy_threshold` to be set to different values for TCP health checks.
```
41 changes: 5 additions & 36 deletions internal/service/elbv2/target_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -662,10 +662,11 @@ func resourceTargetGroupUpdate(d *schema.ResourceData, meta interface{}) error {
healthCheck := healthChecks[0].(map[string]interface{})

params = &elbv2.ModifyTargetGroupInput{
TargetGroupArn: aws.String(d.Id()),
HealthCheckEnabled: aws.Bool(healthCheck["enabled"].(bool)),
HealthyThresholdCount: aws.Int64(int64(healthCheck["healthy_threshold"].(int))),
UnhealthyThresholdCount: aws.Int64(int64(healthCheck["unhealthy_threshold"].(int))),
TargetGroupArn: aws.String(d.Id()),
HealthCheckEnabled: aws.Bool(healthCheck["enabled"].(bool)),
HealthCheckIntervalSeconds: aws.Int64(int64(healthCheck["interval"].(int))),
HealthyThresholdCount: aws.Int64(int64(healthCheck["healthy_threshold"].(int))),
UnhealthyThresholdCount: aws.Int64(int64(healthCheck["unhealthy_threshold"].(int))),
}

t := healthCheck["timeout"].(int)
Expand All @@ -686,7 +687,6 @@ func resourceTargetGroupUpdate(d *schema.ResourceData, meta interface{}) error {
}
}
params.HealthCheckPath = aws.String(healthCheck["path"].(string))
params.HealthCheckIntervalSeconds = aws.Int64(int64(healthCheck["interval"].(int)))
}
if d.Get("target_type").(string) != elbv2.TargetTypeEnumLambda {
params.HealthCheckPort = aws.String(healthCheck["port"].(string))
Expand Down Expand Up @@ -1157,15 +1157,6 @@ func resourceTargetGroupCustomizeDiff(_ context.Context, diff *schema.ResourceDi
if m := healthCheck["path"].(string); m != "" {
return fmt.Errorf("%s: health_check.path is not supported for target_groups with TCP protocol", diff.Id())
}
// Cannot set custom timeout on TCP health checks
if t := healthCheck["timeout"].(int); t != 0 && diff.Id() == "" {
// timeout has a default value, so only check this if this is a network
// LB and is a first run
return fmt.Errorf("%s: health_check.timeout is not supported for target_groups with TCP protocol", diff.Id())
}
if healthCheck["healthy_threshold"].(int) != healthCheck["unhealthy_threshold"].(int) {
return fmt.Errorf("%s: health_check.healthy_threshold %d and health_check.unhealthy_threshold %d must be the same for target_groups with TCP protocol", diff.Id(), healthCheck["healthy_threshold"].(int), healthCheck["unhealthy_threshold"].(int))
}
}
}

Expand All @@ -1183,28 +1174,6 @@ func resourceTargetGroupCustomizeDiff(_ context.Context, diff *schema.ResourceDi
return nil
}

if protocol == elbv2.ProtocolEnumTcp {
if diff.HasChange("health_check.0.interval") {
if err := diff.ForceNew("health_check.0.interval"); err != nil {
return err
}
}
// The health_check configuration block protocol argument has Default: HTTP, however the block
// itself is Computed: true. When not configured, a TLS (Network LB) Target Group will default
// to health check protocol TLS. We do not want to trigger recreation in this scenario.
// ResourceDiff will show 0 changed keys for the configuration block, which we can use to ensure
// there was an actual change to trigger the ForceNew.
if diff.HasChange("health_check.0.protocol") && len(diff.GetChangedKeysPrefix("health_check.0")) != 0 {
if err := diff.ForceNew("health_check.0.protocol"); err != nil {
return err
}
}
if diff.HasChange("health_check.0.timeout") {
if err := diff.ForceNew("health_check.0.timeout"); err != nil {
return err
}
}
}
return nil
}

Expand Down
155 changes: 14 additions & 141 deletions internal/service/elbv2/target_group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -222,38 +222,6 @@ func TestAccELBV2TargetGroup_ProtocolVersion_grpcUpdate(t *testing.T) {
})
}

func TestAccELBV2TargetGroup_HealthCheck_tcp(t *testing.T) {
var targetGroup1, targetGroup2 elbv2.TargetGroup
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
resourceName := "aws_lb_target_group.test"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
ErrorCheck: acctest.ErrorCheck(t, elbv2.EndpointsID),
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
CheckDestroy: testAccCheckTargetGroupDestroy,
Steps: []resource.TestStep{
{
Config: testAccTargetGroupConfig_typeTCPIntervalUpdated(rName),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckTargetGroupExists(resourceName, &targetGroup1),
resource.TestCheckResourceAttr(resourceName, "health_check.#", "1"),
resource.TestCheckResourceAttr(resourceName, "health_check.0.protocol", "TCP"),
),
},
{
Config: testAccTargetGroupConfig_typeTCPHTTPHealthCheck(rName, "/", 5),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckTargetGroupExists(resourceName, &targetGroup2),
testAccCheckTargetGroupRecreated(&targetGroup1, &targetGroup2),
resource.TestCheckResourceAttr(resourceName, "health_check.#", "1"),
resource.TestCheckResourceAttr(resourceName, "health_check.0.protocol", "HTTPS"),
),
},
},
})
}

func TestAccELBV2TargetGroup_ipAddressType(t *testing.T) {
var targetGroup1 elbv2.TargetGroup
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
Expand Down Expand Up @@ -646,17 +614,12 @@ interval = 10
port = 8081
protocol = "TCP"
matcher = "200"
`
healthCheckInvalid3 := `
interval = 10
port = 8081
protocol = "TCP"
timeout = 4
`
healthCheckValid := `
interval = 10
port = 8081
protocol = "TCP"
timeout = 4
`

resource.ParallelTest(t, resource.TestCase{
Expand All @@ -673,10 +636,6 @@ protocol = "TCP"
Config: testAccTargetGroupConfig_nlbDefaults(rName, healthCheckInvalid2),
ExpectError: regexp.MustCompile("health_check.matcher is not supported for target_groups with TCP protocol"),
},
{
Config: testAccTargetGroupConfig_nlbDefaults(rName, healthCheckInvalid3),
ExpectError: regexp.MustCompile("health_check.timeout is not supported for target_groups with TCP protocol"),
},
{
Config: testAccTargetGroupConfig_nlbDefaults(rName, healthCheckValid),
Check: resource.ComposeAggregateTestCheckFunc(
Expand All @@ -692,7 +651,7 @@ protocol = "TCP"
resource.TestCheckResourceAttr(resourceName, "health_check.0.interval", "10"),
resource.TestCheckResourceAttr(resourceName, "health_check.0.port", "8081"),
resource.TestCheckResourceAttr(resourceName, "health_check.0.protocol", "TCP"),
resource.TestCheckResourceAttr(resourceName, "health_check.0.timeout", "10"),
resource.TestCheckResourceAttr(resourceName, "health_check.0.timeout", "4"),
resource.TestCheckResourceAttr(resourceName, "health_check.0.healthy_threshold", "3"),
resource.TestCheckResourceAttr(resourceName, "health_check.0.unhealthy_threshold", "3"),
resource.TestCheckResourceAttr(resourceName, "tags.%", "1"),
Expand Down Expand Up @@ -781,8 +740,8 @@ func TestAccELBV2TargetGroup_Name_prefix(t *testing.T) {
})
}

func TestAccELBV2TargetGroup_NetworkLB_targetGroup(t *testing.T) {
var targetGroup1, targetGroup2, targetGroup3 elbv2.TargetGroup
func TestAccELBV2TargetGroup_NetworkLB_tcpHealthCheckUpdated(t *testing.T) {
var targetGroup1, targetGroup2 elbv2.TargetGroup
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
resourceName := "aws_lb_target_group.test"

Expand Down Expand Up @@ -817,11 +776,7 @@ func TestAccELBV2TargetGroup_NetworkLB_targetGroup(t *testing.T) {
),
},
{
Config: testAccTargetGroupConfig_typeTCPInvalidThreshold(rName),
ExpectError: regexp.MustCompile(`health_check\.healthy_threshold [0-9]+ and health_check\.unhealthy_threshold [0-9]+ must be the same for target_groups with TCP protocol`),
},
{
Config: testAccTargetGroupConfig_typeTCPThresholdUpdated(rName),
Config: testAccTargetGroupConfig_typeTCPHealthCheckUpdated(rName),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckTargetGroupExists(resourceName, &targetGroup2),
testAccCheckTargetGroupNotRecreated(&targetGroup1, &targetGroup2),
Expand All @@ -833,23 +788,16 @@ func TestAccELBV2TargetGroup_NetworkLB_targetGroup(t *testing.T) {
resource.TestCheckResourceAttr(resourceName, "deregistration_delay", "200"),
resource.TestCheckResourceAttr(resourceName, "health_check.#", "1"),
resource.TestCheckResourceAttr(resourceName, "health_check.0.enabled", "true"),
resource.TestCheckResourceAttr(resourceName, "health_check.0.interval", "10"),
resource.TestCheckResourceAttr(resourceName, "health_check.0.port", "traffic-port"),
resource.TestCheckResourceAttr(resourceName, "health_check.0.interval", "20"),
resource.TestCheckResourceAttr(resourceName, "health_check.0.port", "8081"),
resource.TestCheckResourceAttr(resourceName, "health_check.0.protocol", "TCP"),
resource.TestCheckResourceAttr(resourceName, "health_check.0.timeout", "10"),
resource.TestCheckResourceAttr(resourceName, "health_check.0.healthy_threshold", "5"),
resource.TestCheckResourceAttr(resourceName, "health_check.0.timeout", "15"),
resource.TestCheckResourceAttr(resourceName, "health_check.0.healthy_threshold", "4"),
resource.TestCheckResourceAttr(resourceName, "health_check.0.unhealthy_threshold", "5"),
resource.TestCheckResourceAttr(resourceName, "tags.%", "1"),
resource.TestCheckResourceAttr(resourceName, "tags.Name", rName),
),
},
{
Config: testAccTargetGroupConfig_typeTCPIntervalUpdated(rName),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckTargetGroupExists(resourceName, &targetGroup3),
testAccCheckTargetGroupRecreated(&targetGroup2, &targetGroup3),
),
},
},
})
}
Expand Down Expand Up @@ -2913,7 +2861,7 @@ resource "aws_vpc" "test" {
`, rName)
}

func testAccTargetGroupConfig_typeTCPIntervalUpdated(rName string) string {
func testAccTargetGroupConfig_typeTCPHealthCheckUpdated(rName string) string {
return fmt.Sprintf(`
resource "aws_lb_target_group" "test" {
name = %[1]q
Expand All @@ -2924,76 +2872,11 @@ resource "aws_lb_target_group" "test" {
deregistration_delay = 200

health_check {
interval = 30
port = "traffic-port"
interval = 20
port = "8081"
protocol = "TCP"
healthy_threshold = 5
unhealthy_threshold = 5
}

tags = {
Name = %[1]q
}
}

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

tags = {
Name = %[1]q
}
}
`, rName)
}

func testAccTargetGroupConfig_typeTCPInvalidThreshold(rName string) string {
return fmt.Sprintf(`
resource "aws_lb_target_group" "test" {
name = %[1]q
port = 8082
protocol = "TCP"
vpc_id = aws_vpc.test.id

deregistration_delay = 200

health_check {
interval = 10
port = "traffic-port"
protocol = "TCP"
healthy_threshold = 3
unhealthy_threshold = 4
}

tags = {
Name = %[1]q
}
}

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

tags = {
Name = %[1]q
}
}
`, rName)
}

func testAccTargetGroupConfig_typeTCPThresholdUpdated(rName string) string {
return fmt.Sprintf(`
resource "aws_lb_target_group" "test" {
name = %[1]q
port = 8082
protocol = "TCP"
vpc_id = aws_vpc.test.id

deregistration_delay = 200

health_check {
interval = 10
port = "traffic-port"
protocol = "TCP"
healthy_threshold = 5
timeout = 15
healthy_threshold = 4
unhealthy_threshold = 5
}

Expand Down Expand Up @@ -3386,16 +3269,6 @@ func testAccCheckTargetGroupNotRecreated(i, j *elbv2.TargetGroup) resource.TestC
}
}

func testAccCheckTargetGroupRecreated(i, j *elbv2.TargetGroup) resource.TestCheckFunc {
return func(s *terraform.State) error {
if aws.StringValue(i.TargetGroupArn) == aws.StringValue(j.TargetGroupArn) {
return fmt.Errorf("ELBv2 Target Group (%s) not recreated", aws.StringValue(i.TargetGroupArn))
}

return nil
}
}

func testAccTargetGroupConfig_nlbDefaults(rName, healthCheckBlock string) string {
return fmt.Sprintf(`
resource "aws_lb_target_group" "test" {
Expand Down
12 changes: 6 additions & 6 deletions website/docs/r/lb_target_group.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,14 @@ The following arguments are supported:
~> **Note:** The Health Check parameters you can set vary by the `protocol` of the Target Group. Many parameters cannot be set to custom values for `network` load balancers at this time. See http://docs.aws.amazon.com/elasticloadbalancing/latest/APIReference/API_CreateTargetGroup.html for a complete reference. Keep in mind, that health checks produce actual requests to the backend. The underlying function is invoked when `target_type` is set to `lambda`.

* `enabled` - (Optional) Whether health checks are enabled. Defaults to `true`.
* `healthy_threshold` - (Optional) Number of consecutive health checks successes required before considering an unhealthy target healthy. Defaults to 3.
* `interval` - (Optional) Approximate amount of time, in seconds, between health checks of an individual target. Minimum value 5 seconds, Maximum value 300 seconds. For `lambda` target groups, it needs to be greater as the `timeout` of the underlying `lambda`. Default 30 seconds.
* `healthy_threshold` - (Optional) Number of consecutive health check successes required before considering a target healthy. The range is 2-10. Defaults to 3.
* `interval` - (Optional) Approximate amount of time, in seconds, between health checks of an individual target. The range is 5-300. For `lambda` target groups, it needs to be greater than the timeout of the underlying `lambda`. Defaults to 30.
* `matcher` (May be required) Response codes to use when checking for a healthy responses from a target. You can specify multiple values (for example, "200,202" for HTTP(s) or "0,12" for GRPC) or a range of values (for example, "200-299" or "0-99"). Required for HTTP/HTTPS/GRPC ALB. Only applies to Application Load Balancers (i.e., HTTP/HTTPS/GRPC) not Network Load Balancers (i.e., TCP).
* `path` - (May be required) Destination for the health check request. Required for HTTP/HTTPS ALB and HTTP NLB. Only applies to HTTP/HTTPS.
* `port` - (Optional) Port to use to connect with the target. Valid values are either ports 1-65535, or `traffic-port`. Defaults to `traffic-port`.
* `protocol` - (Optional) Protocol to use to connect with the target. Defaults to `HTTP`. Not applicable when `target_type` is `lambda`.
* `timeout` - (Optional) Amount of time, in seconds, during which no response means a failed health check. For Application Load Balancers, the range is 2 to 120 seconds, and the default is 5 seconds for the `instance` target type and 30 seconds for the `lambda` target type. For Network Load Balancers, you cannot set a custom value, and the default is 10 seconds for TCP and HTTPS health checks and 5 seconds for HTTP health checks.
* `unhealthy_threshold` - (Optional) Number of consecutive health check failures required before considering the target unhealthy. For Network Load Balancers, this value must be the same as the `healthy_threshold`. Defaults to 3.
* `port` - (Optional) The port the load balancer uses when performing health checks on targets. Default is traffic-port.
* `protocol` - (Optional) Protocol the load balancer uses when performing health checks on targets. Must be either `TCP`, `HTTP`, or `HTTPS`. The TCP protocol is not supported for health checks if the protocol of the target group is HTTP or HTTPS. Defaults to HTTP.
* `timeout` - (optional) Amount of time, in seconds, during which no response from a target means a failed health check. The range is 2–120 seconds. For target groups with a protocol of HTTP, the default is 6 seconds. For target groups with a protocol of TCP, TLS or HTTPS, the default is 10 seconds. For target groups with a protocol of GENEVE, the default is 5 seconds. If the target type is lambda, the default is 30 seconds.
* `unhealthy_threshold` - (Optional) Number of consecutive health check failures required before considering a target unhealthy. The range is 2-10. Defaults to 3.

### stickiness

Expand Down