diff --git a/.changelog/33205.txt b/.changelog/33205.txt new file mode 100644 index 00000000000..e4eedefc568 --- /dev/null +++ b/.changelog/33205.txt @@ -0,0 +1,19 @@ +```release-note:enhancement +resource/aws_lb: Allow the number of `subnets` for Network Load Balancers to be increased without recreating the resource +``` + +```release-note:enhancement +resource/aws_lb: Add plan-time validation that exactly one of either `subnets` or `subnet_mapping` is configured +``` + +```release-note:enhancement +resource/aws_lb: Allow the number of `subnet_mapping`s for Network Load Balancers to be increased without recreating the resource +``` + +```release-note:enhancement +resource/aws_lb: Allow the number of `subnet_mapping`s for Application Load Balancers to be changed without recreating the resource +``` + +```release-note:bug +resource/aws_lb: Correct in-place update of `security_groups` for Network Load Balancers when the new value is Computed +``` \ No newline at end of file diff --git a/internal/service/elbv2/load_balancer.go b/internal/service/elbv2/load_balancer.go index 5ed5b623a11..37270c7df7e 100644 --- a/internal/service/elbv2/load_balancer.go +++ b/internal/service/elbv2/load_balancer.go @@ -51,7 +51,9 @@ func ResourceLoadBalancer() *schema.Resource { }, CustomizeDiff: customdiff.Sequence( + customizeDiffALB, customizeDiffNLB, + customizeDiffGWLB, verify.SetTagsDiff, ), @@ -227,18 +229,15 @@ func ResourceLoadBalancer() *schema.Resource { Type: schema.TypeSet, Optional: true, Computed: true, - ForceNew: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "allocation_id": { Type: schema.TypeString, Optional: true, - ForceNew: true, }, "ipv6_address": { Type: schema.TypeString, Optional: true, - ForceNew: true, ValidateFunc: validation.IsIPv6Address, }, "outpost_id": { @@ -248,22 +247,22 @@ func ResourceLoadBalancer() *schema.Resource { "private_ipv4_address": { Type: schema.TypeString, Optional: true, - ForceNew: true, ValidateFunc: validation.IsIPv4Address, }, "subnet_id": { Type: schema.TypeString, Required: true, - ForceNew: true, }, }, }, + ExactlyOneOf: []string{"subnet_mapping", "subnets"}, }, "subnets": { - Type: schema.TypeSet, - Optional: true, - Computed: true, - Elem: &schema.Schema{Type: schema.TypeString}, + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + ExactlyOneOf: []string{"subnet_mapping", "subnets"}, }, names.AttrTags: tftags.TagsSchema(), names.AttrTagsAll: tftags.TagsSchemaComputed(), @@ -532,10 +531,21 @@ func resourceLoadBalancerUpdate(ctx context.Context, d *schema.ResourceData, met } } - if d.HasChange("subnets") { + if d.HasChanges("subnet_mapping", "subnets") { input := &elbv2.SetSubnetsInput{ LoadBalancerArn: aws.String(d.Id()), - Subnets: flex.ExpandStringSet(d.Get("subnets").(*schema.Set)), + } + + if d.HasChange("subnet_mapping") { + if v, ok := d.GetOk("subnet_mapping"); ok && v.(*schema.Set).Len() > 0 { + input.SubnetMappings = expandSubnetMappings(v.(*schema.Set).List()) + } + } + + if d.HasChange("subnets") { + if v, ok := d.GetOk("subnets"); ok { + input.Subnets = flex.ExpandStringSet(v.(*schema.Set)) + } } _, err := conn.SetSubnetsWithContext(ctx, input) @@ -953,28 +963,27 @@ func loadBalancerNameFromARN(s string) (string, error) { return matches[1], nil } -func flattenSubnetsFromAvailabilityZones(availabilityZones []*elbv2.AvailabilityZone) []string { - return tfslices.ApplyToAll(availabilityZones, func(v *elbv2.AvailabilityZone) string { - return aws.StringValue(v.SubnetId) +func flattenSubnetsFromAvailabilityZones(apiObjects []*elbv2.AvailabilityZone) []string { + return tfslices.ApplyToAll(apiObjects, func(apiObject *elbv2.AvailabilityZone) string { + return aws.StringValue(apiObject.SubnetId) }) } -func flattenSubnetMappingsFromAvailabilityZones(availabilityZones []*elbv2.AvailabilityZone) []map[string]interface{} { - l := make([]map[string]interface{}, 0) - for _, availabilityZone := range availabilityZones { - m := make(map[string]interface{}) - m["subnet_id"] = aws.StringValue(availabilityZone.SubnetId) - m["outpost_id"] = aws.StringValue(availabilityZone.OutpostId) - - for _, loadBalancerAddress := range availabilityZone.LoadBalancerAddresses { - m["allocation_id"] = aws.StringValue(loadBalancerAddress.AllocationId) - m["private_ipv4_address"] = aws.StringValue(loadBalancerAddress.PrivateIPv4Address) - m["ipv6_address"] = aws.StringValue(loadBalancerAddress.IPv6Address) +func flattenSubnetMappingsFromAvailabilityZones(apiObjects []*elbv2.AvailabilityZone) []map[string]interface{} { + return tfslices.ApplyToAll(apiObjects, func(apiObject *elbv2.AvailabilityZone) map[string]interface{} { + tfMap := map[string]interface{}{ + "outpost_id": aws.StringValue(apiObject.OutpostId), + "subnet_id": aws.StringValue(apiObject.SubnetId), + } + if apiObjects := apiObject.LoadBalancerAddresses; len(apiObjects) > 0 { + apiObject := apiObjects[0] + tfMap["allocation_id"] = aws.StringValue(apiObject.AllocationId) + tfMap["ipv6_address"] = aws.StringValue(apiObject.IPv6Address) + tfMap["private_ipv4_address"] = aws.StringValue(apiObject.PrivateIPv4Address) } - l = append(l, m) - } - return l + return tfMap + }) } func SuffixFromARN(arn *string) string { @@ -999,7 +1008,7 @@ func customizeDiffNLB(_ context.Context, diff *schema.ResourceDiff, v interface{ // The current criteria for determining if the operation should be ForceNew: // - lb of type "network" // - existing resource (id is not "") - // - there are actual changes to be made in the subnets + // - there are subnet removals // OR security groups are being added where none currently exist // OR all security groups are being removed // @@ -1016,25 +1025,139 @@ func customizeDiffNLB(_ context.Context, diff *schema.ResourceDiff, v interface{ return nil } - // Get diff for subnets. - o, n := diff.GetChange("subnets") - os, ns := o.(*schema.Set), n.(*schema.Set) + config := diff.GetRawConfig() + + // Subnet diffs. + // Check for changes here -- SetNewComputed will modify HasChange. + hasSubnetMappingChanges, hasSubnetsChanges := diff.HasChange("subnet_mapping"), diff.HasChange("subnets") + if hasSubnetMappingChanges { + if v := config.GetAttr("subnet_mapping"); v.IsWhollyKnown() { + o, n := diff.GetChange("subnet_mapping") + os, ns := o.(*schema.Set), n.(*schema.Set) + + deltaN := ns.Len() - os.Len() + switch { + case deltaN == 0: + // No change in number of subnet mappings, but one of the mappings did change. + fallthrough + case deltaN < 0: + // Subnet mappings removed. + if err := diff.ForceNew("subnet_mapping"); err != nil { + return err + } + case deltaN > 0: + // Subnet mappings added. Ensure that the previous mappings didn't change. + if ns.Intersection(os).Len() != os.Len() { + if err := diff.ForceNew("subnet_mapping"); err != nil { + return err + } + } + } + } - if add, del := ns.Difference(os).List(), os.Difference(ns).List(); len(del) > 0 || len(add) > 0 { - if err := diff.ForceNew("subnets"); err != nil { + if err := diff.SetNewComputed("subnets"); err != nil { + return err + } + } + if hasSubnetsChanges { + if v := config.GetAttr("subnets"); v.IsWhollyKnown() { + o, n := diff.GetChange("subnets") + os, ns := o.(*schema.Set), n.(*schema.Set) + + // In-place increase in number of subnets only. + if deltaN := ns.Len() - os.Len(); deltaN <= 0 { + if err := diff.ForceNew("subnets"); err != nil { + return err + } + } + } + + if err := diff.SetNewComputed("subnet_mapping"); err != nil { return err } } // Get diff for security groups. - o, n = diff.GetChange("security_groups") - os, ns = o.(*schema.Set), n.(*schema.Set) + if diff.HasChange("security_groups") { + if v := config.GetAttr("security_groups"); v.IsWhollyKnown() { + o, n := diff.GetChange("security_groups") + os, ns := o.(*schema.Set), n.(*schema.Set) + + if (os.Len() == 0 && ns.Len() > 0) || (ns.Len() == 0 && os.Len() > 0) { + if err := diff.ForceNew("security_groups"); err != nil { + return err + } + } + } + } + + return nil +} + +func customizeDiffALB(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { + if lbType := diff.Get("load_balancer_type").(string); lbType != elbv2.LoadBalancerTypeEnumApplication { + return nil + } + + if diff.Id() == "" { + return nil + } + + config := diff.GetRawConfig() + + // Subnet diffs. + // Check for changes here -- SetNewComputed will modify HasChange. + hasSubnetMappingChanges, hasSubnetsChanges := diff.HasChange("subnet_mapping"), diff.HasChange("subnets") + if hasSubnetMappingChanges { + if v := config.GetAttr("subnet_mapping"); v.IsWhollyKnown() { + o, n := diff.GetChange("subnet_mapping") + os, ns := o.(*schema.Set), n.(*schema.Set) + + deltaN := ns.Len() - os.Len() + switch { + case deltaN == 0: + // No change in number of subnet mappings, but one of the mappings did change. + if err := diff.ForceNew("subnet_mapping"); err != nil { + return err + } + case deltaN < 0: + // Subnet mappings removed. Ensure that the remaining mappings didn't change. + if os.Intersection(ns).Len() != ns.Len() { + if err := diff.ForceNew("subnet_mapping"); err != nil { + return err + } + } + case deltaN > 0: + // Subnet mappings added. Ensure that the previous mappings didn't change. + if ns.Intersection(os).Len() != os.Len() { + if err := diff.ForceNew("subnet_mapping"); err != nil { + return err + } + } + } + } - if (os.Len() == 0 && ns.Len() > 0) || (ns.Len() == 0 && os.Len() > 0) { - if err := diff.ForceNew("security_groups"); err != nil { + if err := diff.SetNewComputed("subnets"); err != nil { return err } } + if hasSubnetsChanges { + if err := diff.SetNewComputed("subnet_mapping"); err != nil { + return err + } + } + + return nil +} + +func customizeDiffGWLB(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { + if lbType := diff.Get("load_balancer_type").(string); lbType != elbv2.LoadBalancerTypeEnumGateway { + return nil + } + + if diff.Id() == "" { + return nil + } return nil } diff --git a/internal/service/elbv2/load_balancer_test.go b/internal/service/elbv2/load_balancer_test.go index 3f5bcae5930..5a5febece8e 100644 --- a/internal/service/elbv2/load_balancer_test.go +++ b/internal/service/elbv2/load_balancer_test.go @@ -123,7 +123,7 @@ func TestAccELBV2LoadBalancer_NLB_basic(t *testing.T) { CheckDestroy: testAccCheckLoadBalancerDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccLoadBalancerConfig_nlb(rName, false), + Config: testAccLoadBalancerConfig_nlbBasic(rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckLoadBalancerExists(ctx, resourceName, &conf), resource.TestCheckResourceAttr(resourceName, "access_logs.#", "1"), @@ -137,6 +137,8 @@ func TestAccELBV2LoadBalancer_NLB_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "load_balancer_type", "network"), resource.TestCheckResourceAttr(resourceName, "name", rName), resource.TestCheckResourceAttr(resourceName, "name_prefix", ""), + resource.TestCheckResourceAttr(resourceName, "subnet_mapping.#", "1"), + resource.TestCheckResourceAttr(resourceName, "subnets.#", "1"), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), resource.TestCheckResourceAttrSet(resourceName, "zone_id"), @@ -610,7 +612,7 @@ func TestAccELBV2LoadBalancer_NetworkLoadBalancer_updateCrossZone(t *testing.T) CheckDestroy: testAccCheckLoadBalancerDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccLoadBalancerConfig_nlb(rName, true), + Config: testAccLoadBalancerConfig_nlbCrossZone(rName, true), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckLoadBalancerExists(ctx, resourceName, &pre), testAccCheckLoadBalancerAttribute(ctx, resourceName, "load_balancing.cross_zone.enabled", "true"), @@ -618,7 +620,7 @@ func TestAccELBV2LoadBalancer_NetworkLoadBalancer_updateCrossZone(t *testing.T) ), }, { - Config: testAccLoadBalancerConfig_nlb(rName, false), + Config: testAccLoadBalancerConfig_nlbCrossZone(rName, false), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckLoadBalancerExists(ctx, resourceName, &mid), testAccCheckLoadBalancerAttribute(ctx, resourceName, "load_balancing.cross_zone.enabled", "false"), @@ -627,7 +629,7 @@ func TestAccELBV2LoadBalancer_NetworkLoadBalancer_updateCrossZone(t *testing.T) ), }, { - Config: testAccLoadBalancerConfig_nlb(rName, true), + Config: testAccLoadBalancerConfig_nlbCrossZone(rName, true), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckLoadBalancerExists(ctx, resourceName, &post), testAccCheckLoadBalancerAttribute(ctx, resourceName, "load_balancing.cross_zone.enabled", "true"), @@ -918,7 +920,7 @@ func TestAccELBV2LoadBalancer_ApplicationLoadBalancer_updatedSecurityGroups(t *t }) } -func TestAccELBV2LoadBalancer_ApplicationLoadBalancer_updateSubnets(t *testing.T) { +func TestAccELBV2LoadBalancer_ApplicationLoadBalancer_addSubnet(t *testing.T) { ctx := acctest.Context(t) var pre, post elbv2.LoadBalancer rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -935,17 +937,19 @@ func TestAccELBV2LoadBalancer_ApplicationLoadBalancer_updateSubnets(t *testing.T CheckDestroy: testAccCheckLoadBalancerDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccLoadBalancerConfig_basic(rName), + Config: testAccLoadBalancerConfig_subnetCount(rName, 2), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckLoadBalancerExists(ctx, resourceName, &pre), + resource.TestCheckResourceAttr(resourceName, "subnet_mapping.#", "2"), resource.TestCheckResourceAttr(resourceName, "subnets.#", "2"), ), }, { - Config: testAccLoadBalancerConfig_updateSubnets(rName), + Config: testAccLoadBalancerConfig_subnetCount(rName, 3), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckLoadBalancerExists(ctx, resourceName, &post), testAccCheckLoadBalancerNotRecreated(&pre, &post), + resource.TestCheckResourceAttr(resourceName, "subnet_mapping.#", "3"), resource.TestCheckResourceAttr(resourceName, "subnets.#", "3"), ), }, @@ -953,6 +957,117 @@ func TestAccELBV2LoadBalancer_ApplicationLoadBalancer_updateSubnets(t *testing.T }) } +func TestAccELBV2LoadBalancer_ApplicationLoadBalancer_deleteSubnet(t *testing.T) { + ctx := acctest.Context(t) + var pre, post elbv2.LoadBalancer + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_lb.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + // GovCloud Regions don't always have 3 AZs. + acctest.PreCheckPartitionNot(t, names.USGovCloudPartitionID) + }, + ErrorCheck: acctest.ErrorCheck(t, elbv2.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckLoadBalancerDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccLoadBalancerConfig_subnetCount(rName, 3), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckLoadBalancerExists(ctx, resourceName, &pre), + resource.TestCheckResourceAttr(resourceName, "subnet_mapping.#", "3"), + resource.TestCheckResourceAttr(resourceName, "subnets.#", "3"), + ), + }, + { + Config: testAccLoadBalancerConfig_subnetCount(rName, 2), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckLoadBalancerExists(ctx, resourceName, &post), + testAccCheckLoadBalancerNotRecreated(&pre, &post), + resource.TestCheckResourceAttr(resourceName, "subnet_mapping.#", "2"), + resource.TestCheckResourceAttr(resourceName, "subnets.#", "2"), + ), + }, + }, + }) +} + +func TestAccELBV2LoadBalancer_ApplicationLoadBalancer_addSubnetMapping(t *testing.T) { + ctx := acctest.Context(t) + var pre, post elbv2.LoadBalancer + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_lb.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + // GovCloud Regions don't always have 3 AZs. + acctest.PreCheckPartitionNot(t, names.USGovCloudPartitionID) + }, + ErrorCheck: acctest.ErrorCheck(t, elbv2.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckLoadBalancerDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccLoadBalancerConfig_subnetMappingCount(rName, 2), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckLoadBalancerExists(ctx, resourceName, &pre), + resource.TestCheckResourceAttr(resourceName, "subnet_mapping.#", "2"), + resource.TestCheckResourceAttr(resourceName, "subnets.#", "2"), + ), + }, + { + Config: testAccLoadBalancerConfig_subnetMappingCount(rName, 3), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckLoadBalancerExists(ctx, resourceName, &post), + testAccCheckLoadBalancerNotRecreated(&pre, &post), + resource.TestCheckResourceAttr(resourceName, "subnet_mapping.#", "3"), + resource.TestCheckResourceAttr(resourceName, "subnets.#", "3"), + ), + }, + }, + }) +} + +func TestAccELBV2LoadBalancer_ApplicationLoadBalancer_deleteSubnetMapping(t *testing.T) { + ctx := acctest.Context(t) + var pre, post elbv2.LoadBalancer + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_lb.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + // GovCloud Regions don't always have 3 AZs. + acctest.PreCheckPartitionNot(t, names.USGovCloudPartitionID) + }, + ErrorCheck: acctest.ErrorCheck(t, elbv2.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckLoadBalancerDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccLoadBalancerConfig_subnetMappingCount(rName, 3), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckLoadBalancerExists(ctx, resourceName, &pre), + resource.TestCheckResourceAttr(resourceName, "subnet_mapping.#", "3"), + resource.TestCheckResourceAttr(resourceName, "subnets.#", "3"), + ), + }, + { + Config: testAccLoadBalancerConfig_subnetMappingCount(rName, 2), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckLoadBalancerExists(ctx, resourceName, &post), + testAccCheckLoadBalancerNotRecreated(&pre, &post), + resource.TestCheckResourceAttr(resourceName, "subnet_mapping.#", "2"), + resource.TestCheckResourceAttr(resourceName, "subnets.#", "2"), + ), + }, + }, + }) +} + // TestAccELBV2LoadBalancer_noSecurityGroup regression tests the issue in #8264, // where if an ALB is created without a security group, a default one // is assigned. @@ -1431,7 +1546,7 @@ func TestAccELBV2LoadBalancer_NetworkLoadBalancer_enforcePrivateLink(t *testing. }) } -func TestAccELBV2LoadBalancer_NetworkLoadBalancer_updateSubnets(t *testing.T) { +func TestAccELBV2LoadBalancer_NetworkLoadBalancer_addSubnet(t *testing.T) { ctx := acctest.Context(t) var pre, post elbv2.LoadBalancer rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -1448,17 +1563,89 @@ func TestAccELBV2LoadBalancer_NetworkLoadBalancer_updateSubnets(t *testing.T) { CheckDestroy: testAccCheckLoadBalancerDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccLoadBalancerConfig_nlbSubnets(rName, 2), + Config: testAccLoadBalancerConfig_nlbSubnetCount(rName, 2), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckLoadBalancerExists(ctx, resourceName, &pre), resource.TestCheckResourceAttr(resourceName, "subnets.#", "2"), ), }, { - Config: testAccLoadBalancerConfig_nlbSubnets(rName, 3), + Config: testAccLoadBalancerConfig_nlbSubnetCount(rName, 3), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckLoadBalancerExists(ctx, resourceName, &post), + testAccCheckLoadBalancerNotRecreated(&post, &pre), + resource.TestCheckResourceAttr(resourceName, "subnets.#", "3"), + ), + }, + }, + }) +} + +func TestAccELBV2LoadBalancer_NetworkLoadBalancer_deleteSubnet(t *testing.T) { + ctx := acctest.Context(t) + var pre, post elbv2.LoadBalancer + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_lb.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + // GovCloud Regions don't always have 3 AZs. + acctest.PreCheckPartitionNot(t, names.USGovCloudPartitionID) + }, + ErrorCheck: acctest.ErrorCheck(t, elbv2.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckLoadBalancerDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccLoadBalancerConfig_nlbSubnetCount(rName, 3), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckLoadBalancerExists(ctx, resourceName, &pre), + resource.TestCheckResourceAttr(resourceName, "subnets.#", "3"), + ), + }, + { + Config: testAccLoadBalancerConfig_nlbSubnetCount(rName, 2), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckLoadBalancerExists(ctx, resourceName, &post), testAccCheckLoadBalancerRecreated(&post, &pre), + resource.TestCheckResourceAttr(resourceName, "subnets.#", "2"), + ), + }, + }, + }) +} + +func TestAccELBV2LoadBalancer_NetworkLoadBalancer_addSubnetMapping(t *testing.T) { + ctx := acctest.Context(t) + var pre, post elbv2.LoadBalancer + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_lb.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + // GovCloud Regions don't always have 3 AZs. + acctest.PreCheckPartitionNot(t, names.USGovCloudPartitionID) + }, + ErrorCheck: acctest.ErrorCheck(t, elbv2.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckLoadBalancerDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccLoadBalancerConfig_nlbSubnetMappingCount(rName, false, 2), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckLoadBalancerExists(ctx, resourceName, &pre), + resource.TestCheckResourceAttr(resourceName, "subnet_mapping.#", "2"), + resource.TestCheckResourceAttr(resourceName, "subnets.#", "2"), + ), + }, + { + Config: testAccLoadBalancerConfig_nlbSubnetMappingCount(rName, false, 3), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckLoadBalancerExists(ctx, resourceName, &post), + testAccCheckLoadBalancerNotRecreated(&pre, &post), + resource.TestCheckResourceAttr(resourceName, "subnet_mapping.#", "3"), resource.TestCheckResourceAttr(resourceName, "subnets.#", "3"), ), }, @@ -1466,6 +1653,43 @@ func TestAccELBV2LoadBalancer_NetworkLoadBalancer_updateSubnets(t *testing.T) { }) } +func TestAccELBV2LoadBalancer_NetworkLoadBalancer_deleteSubnetMapping(t *testing.T) { + ctx := acctest.Context(t) + var pre, post elbv2.LoadBalancer + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_lb.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + // GovCloud Regions don't always have 3 AZs. + acctest.PreCheckPartitionNot(t, names.USGovCloudPartitionID) + }, + ErrorCheck: acctest.ErrorCheck(t, elbv2.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckLoadBalancerDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccLoadBalancerConfig_nlbSubnetMappingCount(rName, false, 3), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckLoadBalancerExists(ctx, resourceName, &pre), + resource.TestCheckResourceAttr(resourceName, "subnet_mapping.#", "3"), + resource.TestCheckResourceAttr(resourceName, "subnets.#", "3"), + ), + }, + { + Config: testAccLoadBalancerConfig_nlbSubnetMappingCount(rName, false, 2), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckLoadBalancerExists(ctx, resourceName, &post), + testAccCheckLoadBalancerRecreated(&pre, &post), + resource.TestCheckResourceAttr(resourceName, "subnet_mapping.#", "2"), + resource.TestCheckResourceAttr(resourceName, "subnets.#", "2"), + ), + }, + }, + }) +} + func TestAccELBV2LoadBalancer_updateDesyncMitigationMode(t *testing.T) { ctx := acctest.Context(t) var pre, mid, post elbv2.LoadBalancer @@ -1803,7 +2027,11 @@ resource "aws_security_group" "test" { } func testAccLoadBalancerConfig_basic(rName string) string { - return acctest.ConfigCompose(testAccLoadBalancerConfig_baseInternal(rName, 2), fmt.Sprintf(` + return testAccLoadBalancerConfig_subnetCount(rName, 2) +} + +func testAccLoadBalancerConfig_subnetCount(rName string, subnetCount int) string { + return acctest.ConfigCompose(testAccLoadBalancerConfig_baseInternal(rName, subnetCount), fmt.Sprintf(` resource "aws_lb" "test" { name = %[1]q internal = true @@ -1816,6 +2044,30 @@ resource "aws_lb" "test" { `, rName)) } +func testAccLoadBalancerConfig_subnetMappingCount(rName string, subnetCount int) string { + return acctest.ConfigCompose(testAccLoadBalancerConfig_baseInternal(rName, subnetCount), fmt.Sprintf(` +resource "aws_lb" "test" { + name = %[1]q + internal = true + security_groups = [aws_security_group.test.id] + + idle_timeout = 30 + enable_deletion_protection = false + + dynamic "subnet_mapping" { + for_each = aws_subnet.test[*] + content { + subnet_id = subnet_mapping.value.id + } + } + + tags = { + Name = %[1]q + } +} +`, rName)) +} + func testAccLoadBalancerConfig_nameGenerated(rName string) string { return acctest.ConfigCompose(testAccLoadBalancerConfig_baseInternal(rName, 2), fmt.Sprintf(` resource "aws_lb" "test" { @@ -2169,8 +2421,16 @@ resource "aws_lb" "test" { `, rName, wafFailOpen)) } -func testAccLoadBalancerConfig_nlb(rName string, cz bool) string { - return acctest.ConfigCompose(acctest.ConfigVPCWithSubnets(rName, 1), fmt.Sprintf(` +func testAccLoadBalancerConfig_nlbBasic(rName string) string { + return testAccLoadBalancerConfig_nlbSubnetMappingCount(rName, false, 1) +} + +func testAccLoadBalancerConfig_nlbCrossZone(rName string, cz bool) string { + return testAccLoadBalancerConfig_nlbSubnetMappingCount(rName, cz, 1) +} + +func testAccLoadBalancerConfig_nlbSubnetMappingCount(rName string, cz bool, subnetCount int) string { + return acctest.ConfigCompose(acctest.ConfigVPCWithSubnets(rName, subnetCount), fmt.Sprintf(` resource "aws_lb" "test" { name = %[1]q internal = true @@ -2179,8 +2439,11 @@ resource "aws_lb" "test" { enable_deletion_protection = false enable_cross_zone_load_balancing = %[2]t - subnet_mapping { - subnet_id = aws_subnet.test[0].id + dynamic "subnet_mapping" { + for_each = aws_subnet.test[*] + content { + subnet_id = subnet_mapping.value.id + } } tags = { @@ -2446,20 +2709,6 @@ resource "aws_security_group" "test2" { `, rName)) } -func testAccLoadBalancerConfig_updateSubnets(rName string) string { - return acctest.ConfigCompose(testAccLoadBalancerConfig_baseInternal(rName, 3), fmt.Sprintf(` -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 -} -`, rName)) -} - func testAccLoadBalancerConfig_albNoSecurityGroups(rName string) string { return acctest.ConfigCompose(acctest.ConfigVPCWithSubnets(rName, 2), fmt.Sprintf(` resource "aws_lb" "test" { @@ -2670,7 +2919,7 @@ resource "aws_lb" "test" { `, rName, n, enforcePrivateLink)) } -func testAccLoadBalancerConfig_nlbSubnets(rName string, subnetCount int) string { +func testAccLoadBalancerConfig_nlbSubnetCount(rName string, subnetCount int) string { return acctest.ConfigCompose(acctest.ConfigVPCWithSubnets(rName, subnetCount), fmt.Sprintf(` resource "aws_lb" "test" { name = %[1]q diff --git a/website/docs/r/lb.html.markdown b/website/docs/r/lb.html.markdown index fd43f7b625e..2f1128ae16d 100644 --- a/website/docs/r/lb.html.markdown +++ b/website/docs/r/lb.html.markdown @@ -123,12 +123,10 @@ Terraform will autogenerate a name beginning with `tf-lb`. * `name_prefix` - (Optional) Creates a unique name beginning with the specified prefix. Conflicts with `name`. * `security_groups` - (Optional) A list of security group IDs to assign to the LB. Only valid for Load Balancers of type `application` or `network`. For load balancers of type `network` security groups cannot be added if none are currently present, and cannot all be removed once added. If either of these conditions are met, this will force a recreation of the resource. * `preserve_host_header` - (Optional) Indicates whether the Application Load Balancer should preserve the Host header in the HTTP request and send it to the target without any change. Defaults to `false`. -* `subnet_mapping` - (Optional) A subnet mapping block as documented below. -* `subnets` - (Optional) A list of subnet IDs to attach to the LB. Subnets -cannot be updated for Load Balancers of type `network`. Changing this value -for load balancers of type `network` will force a recreation of the resource. -* `xff_header_processing_mode` - (Optional) Determines how the load balancer modifies the `X-Forwarded-For` header in the HTTP request before sending the request to the target. The possible values are `append`, `preserve`, and `remove`. Only valid for Load Balancers of type `application`. The default is `append`. +* `subnet_mapping` - (Optional) A subnet mapping block as documented below. For Load Balancers of type `network` subnet mappings can only be added. +* `subnets` - (Optional) A list of subnet IDs to attach to the LB. For Load Balancers of type `network` subnets can only be added (see [Availability Zones](https://docs.aws.amazon.com/elasticloadbalancing/latest/network/network-load-balancers.html#availability-zones)), deleting a subnet for load balancers of type `network` will force a recreation of the resource. * `tags` - (Optional) A map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. +* `xff_header_processing_mode` - (Optional) Determines how the load balancer modifies the `X-Forwarded-For` header in the HTTP request before sending the request to the target. The possible values are `append`, `preserve`, and `remove`. Only valid for Load Balancers of type `application`. The default is `append`. ### access_logs