diff --git a/.changelog/35121.txt b/.changelog/35121.txt new file mode 100644 index 000000000000..2bdb24cc8efd --- /dev/null +++ b/.changelog/35121.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_lb_listener: Prevent duplicate listener creation by checking for existing listeners with same port and protocol before creation +``` diff --git a/internal/service/elbv2/listener.go b/internal/service/elbv2/listener.go index d8e20f9233a3..c3e72ba34444 100644 --- a/internal/service/elbv2/listener.go +++ b/internal/service/elbv2/listener.go @@ -570,6 +570,44 @@ func resourceListenerCreate(ctx context.Context, d *schema.ResourceData, meta an conn := meta.(*conns.AWSClient).ELBV2Client(ctx) lbARN := d.Get("load_balancer_arn").(string) + + // Check for duplicate listener on same load balancer with same port/protocol + if v, ok := d.GetOk(names.AttrPort); ok { + port := int32(v.(int)) + + // Determine protocol (using same logic as creation) + var protocol awstypes.ProtocolEnum + if v, ok := d.GetOk(names.AttrProtocol); ok { + protocol = awstypes.ProtocolEnum(v.(string)) + } else if strings.Contains(lbARN, "loadbalancer/app/") { + // Default to HTTP for Application Load Balancers if no protocol specified + if _, ok := d.GetOk(names.AttrCertificateARN); ok { + protocol = awstypes.ProtocolEnumHttps + } else { + protocol = awstypes.ProtocolEnumHttp + } + } + + // Only check for duplicates if we have a protocol to check + if protocol != "" { + // Query existing listeners on this load balancer + describeInput := &elasticloadbalancingv2.DescribeListenersInput{ + LoadBalancerArn: aws.String(lbARN), + } + existingListener, err := findListener(ctx, conn, describeInput, func(listener *awstypes.Listener) bool { + return aws.ToInt32(listener.Port) == port && listener.Protocol == protocol + }) + + if err != nil && !tfresource.NotFound(err) { + return sdkdiag.AppendErrorf(diags, "reading ELBv2 Listeners for Load Balancer (%s): %s", lbARN, err) + } + + if existingListener != nil { + return sdkdiag.AppendErrorf(diags, "ELBv2 Listener on port %d with protocol %s already exists on Load Balancer (%s)", port, protocol, lbARN) + } + } + } + input := &elasticloadbalancingv2.CreateListenerInput{ LoadBalancerArn: aws.String(lbARN), Tags: getTagsIn(ctx), diff --git a/internal/service/elbv2/listener_test.go b/internal/service/elbv2/listener_test.go index 475c5c2c1150..512c76f06c8c 100644 --- a/internal/service/elbv2/listener_test.go +++ b/internal/service/elbv2/listener_test.go @@ -289,6 +289,100 @@ func TestAccELBV2Listener_disappears(t *testing.T) { }) } +func TestAccELBV2Listener_duplicate(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ELBV2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckListenerDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccListenerConfig_duplicate(rName), + ExpectError: regexache.MustCompile(`already exists`), + }, + }, + }) +} + +func TestAccELBV2Listener_duplicate_ALB_protocolDefaulting_HTTP(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ELBV2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckListenerDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccListenerConfig_duplicate_ALB_protocolDefaulting_HTTP(rName), + ExpectError: regexache.MustCompile(`already exists`), + }, + }, + }) +} + +func TestAccELBV2Listener_duplicate_ALB_protocolDefaulting_HTTPS(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + key := acctest.TLSRSAPrivateKeyPEM(t, 2048) + certificate := acctest.TLSRSAX509SelfSignedCertificatePEM(t, key, "example.com") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ELBV2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckListenerDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccListenerConfig_duplicate_ALB_protocolDefaulting_HTTPS(rName, key, certificate), + ExpectError: regexache.MustCompile(`already exists`), + }, + }, + }) +} + +func TestAccELBV2Listener_duplicate_NLB_TCP(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ELBV2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckListenerDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccListenerConfig_duplicate_NLB_TCP(rName), + ExpectError: regexache.MustCompile(`already exists`), + }, + }, + }) +} + +func TestAccELBV2Listener_duplicate_NLB_TLS(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + key := acctest.TLSRSAPrivateKeyPEM(t, 2048) + certificate := acctest.TLSRSAX509SelfSignedCertificatePEM(t, key, "example.com") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ELBV2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckListenerDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccListenerConfig_duplicate_NLB_TLS(rName, key, certificate), + ExpectError: regexache.MustCompile(`already exists`), + }, + }, + }) +} + func TestAccELBV2Listener_Forward_update(t *testing.T) { ctx := acctest.Context(t) var conf awstypes.Listener @@ -4835,3 +4929,317 @@ resource "aws_lb_target_group" "test" { } `, rName)) } + +func testAccListenerConfig_duplicate(rName string) string { + return acctest.ConfigCompose(testAccListenerConfig_base(rName), fmt.Sprintf(` +resource "aws_lb_listener" "test" { + load_balancer_arn = aws_lb.test.id + protocol = "HTTP" + port = "80" + + default_action { + target_group_arn = aws_lb_target_group.test.arn + type = "forward" + } +} + +resource "aws_lb_listener" "duplicate" { + load_balancer_arn = aws_lb.test.id + protocol = "HTTP" + port = "80" + + default_action { + target_group_arn = aws_lb_target_group.test.arn + type = "forward" + } +} + +resource "aws_lb" "test" { + name = %[1]q + internal = true + security_groups = [aws_security_group.test.id] + subnets = aws_subnet.test[*].id + + idle_timeout = 30 + enable_deletion_protection = false + + tags = { + Name = %[1]q + } +} + +resource "aws_lb_target_group" "test" { + name = %[1]q + port = 8080 + protocol = "HTTP" + vpc_id = aws_vpc.test.id + + health_check { + path = "/health" + interval = 60 + port = 8081 + protocol = "HTTP" + timeout = 3 + healthy_threshold = 3 + unhealthy_threshold = 3 + matcher = "200-299" + } + + tags = { + Name = %[1]q + } +} +`, rName)) +} + +func testAccListenerConfig_duplicate_ALB_protocolDefaulting_HTTP(rName string) string { + return acctest.ConfigCompose(testAccListenerConfig_base(rName), fmt.Sprintf(` +resource "aws_lb_listener" "test" { + load_balancer_arn = aws_lb.test.id + port = "80" + + default_action { + target_group_arn = aws_lb_target_group.test.arn + type = "forward" + } +} + +resource "aws_lb_listener" "duplicate" { + load_balancer_arn = aws_lb.test.id + port = "80" + + default_action { + target_group_arn = aws_lb_target_group.test.arn + type = "forward" + } +} + +resource "aws_lb" "test" { + name = %[1]q + internal = true + security_groups = [aws_security_group.test.id] + subnets = aws_subnet.test[*].id + + idle_timeout = 30 + enable_deletion_protection = false + + tags = { + Name = %[1]q + } +} + +resource "aws_lb_target_group" "test" { + name = %[1]q + port = 8080 + protocol = "HTTP" + vpc_id = aws_vpc.test.id + + health_check { + path = "/health" + interval = 60 + port = 8081 + protocol = "HTTP" + timeout = 3 + healthy_threshold = 3 + unhealthy_threshold = 3 + matcher = "200-299" + } + + tags = { + Name = %[1]q + } +} +`, rName)) +} + +func testAccListenerConfig_duplicate_ALB_protocolDefaulting_HTTPS(rName, key, certificate string) string { + return acctest.ConfigCompose(testAccListenerConfig_base(rName), fmt.Sprintf(` +resource "aws_iam_server_certificate" "test" { + name = %[1]q + certificate_body = "%[2]s" + private_key = "%[3]s" +} + +resource "aws_lb_listener" "test" { + load_balancer_arn = aws_lb.test.id + port = "443" + certificate_arn = aws_iam_server_certificate.test.arn + + default_action { + target_group_arn = aws_lb_target_group.test.arn + type = "forward" + } +} + +resource "aws_lb_listener" "duplicate" { + load_balancer_arn = aws_lb.test.id + port = "443" + certificate_arn = aws_iam_server_certificate.test.arn + + default_action { + target_group_arn = aws_lb_target_group.test.arn + type = "forward" + } +} + +resource "aws_lb" "test" { + name = %[1]q + internal = true + security_groups = [aws_security_group.test.id] + subnets = aws_subnet.test[*].id + + idle_timeout = 30 + enable_deletion_protection = false + + tags = { + Name = %[1]q + } +} + +resource "aws_lb_target_group" "test" { + name = %[1]q + port = 8080 + protocol = "HTTP" + vpc_id = aws_vpc.test.id + + health_check { + path = "/health" + interval = 60 + port = 8081 + protocol = "HTTP" + timeout = 3 + healthy_threshold = 3 + unhealthy_threshold = 3 + matcher = "200-299" + } + + tags = { + Name = %[1]q + } +} +`, rName, acctest.TLSPEMEscapeNewlines(certificate), acctest.TLSPEMEscapeNewlines(key))) +} + +func testAccListenerConfig_duplicate_NLB_TCP(rName string) string { + return acctest.ConfigCompose(testAccListenerConfig_base(rName), fmt.Sprintf(` +resource "aws_lb_listener" "test" { + load_balancer_arn = aws_lb.test.id + protocol = "TCP" + port = "80" + + default_action { + target_group_arn = aws_lb_target_group.test.arn + type = "forward" + } +} + +resource "aws_lb_listener" "duplicate" { + load_balancer_arn = aws_lb.test.id + protocol = "TCP" + port = "80" + + default_action { + target_group_arn = aws_lb_target_group.test.arn + type = "forward" + } +} + +resource "aws_lb" "test" { + name = %[1]q + internal = true + load_balancer_type = "network" + subnets = aws_subnet.test[*].id + + enable_deletion_protection = false + + tags = { + Name = %[1]q + } +} + +resource "aws_lb_target_group" "test" { + name = %[1]q + port = 8080 + protocol = "TCP" + vpc_id = aws_vpc.test.id + + health_check { + protocol = "TCP" + interval = 30 + healthy_threshold = 3 + unhealthy_threshold = 3 + } + + tags = { + Name = %[1]q + } +} +`, rName)) +} + +func testAccListenerConfig_duplicate_NLB_TLS(rName, key, certificate string) string { + return acctest.ConfigCompose(testAccListenerConfig_base(rName), fmt.Sprintf(` +resource "aws_iam_server_certificate" "test" { + name = %[1]q + certificate_body = "%[2]s" + private_key = "%[3]s" +} + +resource "aws_lb_listener" "test" { + load_balancer_arn = aws_lb.test.id + protocol = "TLS" + port = "443" + certificate_arn = aws_iam_server_certificate.test.arn + ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06" + + default_action { + target_group_arn = aws_lb_target_group.test.arn + type = "forward" + } +} + +resource "aws_lb_listener" "duplicate" { + load_balancer_arn = aws_lb.test.id + protocol = "TLS" + port = "443" + certificate_arn = aws_iam_server_certificate.test.arn + ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06" + + default_action { + target_group_arn = aws_lb_target_group.test.arn + type = "forward" + } +} + +resource "aws_lb" "test" { + name = %[1]q + internal = true + load_balancer_type = "network" + subnets = aws_subnet.test[*].id + + enable_deletion_protection = false + + tags = { + Name = %[1]q + } +} + +resource "aws_lb_target_group" "test" { + name = %[1]q + port = 8080 + protocol = "TCP" + vpc_id = aws_vpc.test.id + + health_check { + protocol = "TCP" + interval = 30 + healthy_threshold = 3 + unhealthy_threshold = 3 + } + + tags = { + Name = %[1]q + } +} +`, rName, acctest.TLSPEMEscapeNewlines(certificate), acctest.TLSPEMEscapeNewlines(key))) +}