Skip to content

Commit 8906067

Browse files
authored
Merge pull request #42685 from dhrkumar/f-lb-capacity-reservation
add capacity reservation support for alb and nlb
2 parents 2f4d542 + 0c6df18 commit 8906067

File tree

4 files changed

+242
-7
lines changed

4 files changed

+242
-7
lines changed

.changelog/42685.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
resource/aws_lb: Add `minimum_load_balancer_capacity` configuration block. This functionality requires the `elasticloadbalancing:DescribeCapacityReservations` and `elasticloadbalancing:ModifyCapacityReservation` IAM permissions
3+
```

internal/service/elbv2/load_balancer.go

Lines changed: 180 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,20 @@ func resourceLoadBalancer() *schema.Resource {
252252
Default: awstypes.LoadBalancerTypeEnumApplication,
253253
ValidateDiagFunc: enum.Validate[awstypes.LoadBalancerTypeEnum](),
254254
},
255+
"minimum_load_balancer_capacity": {
256+
Type: schema.TypeList,
257+
Optional: true,
258+
MaxItems: 1,
259+
DiffSuppressFunc: suppressIfLBTypeNot(awstypes.LoadBalancerTypeEnumApplication, awstypes.LoadBalancerTypeEnumNetwork),
260+
Elem: &schema.Resource{
261+
Schema: map[string]*schema.Schema{
262+
"capacity_units": {
263+
Type: schema.TypeInt,
264+
Required: true,
265+
},
266+
},
267+
},
268+
},
255269
names.AttrName: {
256270
Type: schema.TypeString,
257271
Optional: true,
@@ -445,6 +459,7 @@ func resourceLoadBalancerCreate(ctx context.Context, d *schema.ResourceData, met
445459
}
446460

447461
var attributes []awstypes.LoadBalancerAttribute
462+
var minCapacity *awstypes.MinimumLoadBalancerCapacity
448463

449464
if lbType == awstypes.LoadBalancerTypeEnumApplication || lbType == awstypes.LoadBalancerTypeEnumNetwork {
450465
if v, ok := d.GetOk("access_logs"); ok && len(v.([]any)) > 0 && v.([]any)[0] != nil {
@@ -455,6 +470,9 @@ func resourceLoadBalancerCreate(ctx context.Context, d *schema.ResourceData, met
455470
Value: flex.BoolValueToString(false),
456471
})
457472
}
473+
if v, ok := d.GetOk("minimum_load_balancer_capacity"); ok && len(v.([]any)) > 0 && v.([]any)[0] != nil {
474+
minCapacity = expandMinimumLoadBalancerCapacity(v.([]any))
475+
}
458476
}
459477

460478
if lbType == awstypes.LoadBalancerTypeEnumApplication {
@@ -470,6 +488,16 @@ func resourceLoadBalancerCreate(ctx context.Context, d *schema.ResourceData, met
470488

471489
attributes = append(attributes, loadBalancerAttributes.expand(d, lbType, false)...)
472490

491+
if minCapacity != nil {
492+
if err := modifyCapacityReservation(ctx, conn, d.Id(), minCapacity); err != nil {
493+
return sdkdiag.AppendFromErr(diags, err)
494+
}
495+
496+
if _, err := waitCapacityReservationProvisioned(ctx, conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil {
497+
return sdkdiag.AppendErrorf(diags, "waiting for ELBv2 Load Balancer (%s) capacity reservation provision: %s", d.Id(), err)
498+
}
499+
}
500+
473501
wait := false
474502
if len(attributes) > 0 {
475503
if err := modifyLoadBalancerAttributes(ctx, conn, d.Id(), attributes); err != nil {
@@ -564,6 +592,18 @@ func resourceLoadBalancerRead(ctx context.Context, d *schema.ResourceData, meta
564592

565593
loadBalancerAttributes.flatten(d, attributes)
566594

595+
if lb.Type == awstypes.LoadBalancerTypeEnumApplication || lb.Type == awstypes.LoadBalancerTypeEnumNetwork {
596+
capacity, err := findCapacityReservationByARN(ctx, conn, d.Id())
597+
598+
if err != nil {
599+
return sdkdiag.AppendErrorf(diags, "reading ELBv2 Load Balancer (%s) capacity reservation: %s", d.Id(), err)
600+
}
601+
602+
if err := d.Set("minimum_load_balancer_capacity", flattenMinimumLoadBalancerCapacity(capacity.MinimumLoadBalancerCapacity)); err != nil {
603+
return sdkdiag.AppendErrorf(diags, "setting minimum_load_balancer_capacity: %s", err)
604+
}
605+
}
606+
567607
return diags
568608
}
569609

@@ -677,6 +717,16 @@ func resourceLoadBalancerUpdate(ctx context.Context, d *schema.ResourceData, met
677717
}
678718
}
679719

720+
if d.HasChange("minimum_load_balancer_capacity") {
721+
if err := modifyCapacityReservation(ctx, conn, d.Id(), expandMinimumLoadBalancerCapacity(d.Get("minimum_load_balancer_capacity").([]any))); err != nil {
722+
return sdkdiag.AppendFromErr(diags, err)
723+
}
724+
725+
if _, err := waitCapacityReservationProvisioned(ctx, conn, d.Id(), d.Timeout(schema.TimeoutUpdate)); err != nil {
726+
return sdkdiag.AppendErrorf(diags, "waiting for ELBv2 Load Balancer (%s) capacity reservation provision: %s", d.Id(), err)
727+
}
728+
}
729+
680730
if _, err := waitLoadBalancerActive(ctx, conn, d.Id(), d.Timeout(schema.TimeoutUpdate)); err != nil {
681731
return sdkdiag.AppendErrorf(diags, "waiting for ELBv2 Load Balancer (%s) update: %s", d.Id(), err)
682732
}
@@ -717,7 +767,7 @@ func resourceLoadBalancerDelete(ctx context.Context, d *schema.ResourceData, met
717767
}
718768

719769
func modifyLoadBalancerAttributes(ctx context.Context, conn *elasticloadbalancingv2.Client, arn string, attributes []awstypes.LoadBalancerAttribute) error {
720-
input := &elasticloadbalancingv2.ModifyLoadBalancerAttributesInput{
770+
input := elasticloadbalancingv2.ModifyLoadBalancerAttributesInput{
721771
Attributes: attributes,
722772
LoadBalancerArn: aws.String(arn),
723773
}
@@ -728,7 +778,7 @@ func modifyLoadBalancerAttributes(ctx context.Context, conn *elasticloadbalancin
728778
return nil
729779
}
730780

731-
_, err := conn.ModifyLoadBalancerAttributes(ctx, input)
781+
_, err := conn.ModifyLoadBalancerAttributes(ctx, &input)
732782

733783
if err != nil {
734784
// "Validation error: Load balancer attribute key 'routing.http.desync_mitigation_mode' is not recognized"
@@ -750,6 +800,28 @@ func modifyLoadBalancerAttributes(ctx context.Context, conn *elasticloadbalancin
750800
}
751801
}
752802

803+
func modifyCapacityReservation(ctx context.Context, conn *elasticloadbalancingv2.Client, arn string, minCapacity *awstypes.MinimumLoadBalancerCapacity) error {
804+
resetCapacityReservation := false
805+
if minCapacity == nil {
806+
resetCapacityReservation = true
807+
} else if minCapacity.CapacityUnits == nil {
808+
resetCapacityReservation = true
809+
}
810+
input := elasticloadbalancingv2.ModifyCapacityReservationInput{
811+
LoadBalancerArn: aws.String(arn),
812+
MinimumLoadBalancerCapacity: minCapacity,
813+
ResetCapacityReservation: aws.Bool(resetCapacityReservation),
814+
}
815+
816+
_, err := conn.ModifyCapacityReservation(ctx, &input)
817+
818+
if err != nil {
819+
return fmt.Errorf("modifying ELBv2 Load Balancer (%s) capacity reservation: %w", arn, err)
820+
}
821+
822+
return nil
823+
}
824+
753825
type loadBalancerAttributeInfo struct {
754826
apiAttributeKey string
755827
tfType schema.ValueType
@@ -950,11 +1022,11 @@ func findLoadBalancerByARN(ctx context.Context, conn *elasticloadbalancingv2.Cli
9501022
}
9511023

9521024
func findLoadBalancerAttributesByARN(ctx context.Context, conn *elasticloadbalancingv2.Client, arn string) ([]awstypes.LoadBalancerAttribute, error) {
953-
input := &elasticloadbalancingv2.DescribeLoadBalancerAttributesInput{
1025+
input := elasticloadbalancingv2.DescribeLoadBalancerAttributesInput{
9541026
LoadBalancerArn: aws.String(arn),
9551027
}
9561028

957-
output, err := conn.DescribeLoadBalancerAttributes(ctx, input)
1029+
output, err := conn.DescribeLoadBalancerAttributes(ctx, &input)
9581030

9591031
if errs.IsA[*awstypes.LoadBalancerNotFoundException](err) {
9601032
return nil, &retry.NotFoundError{
@@ -974,6 +1046,31 @@ func findLoadBalancerAttributesByARN(ctx context.Context, conn *elasticloadbalan
9741046
return output.Attributes, nil
9751047
}
9761048

1049+
func findCapacityReservationByARN(ctx context.Context, conn *elasticloadbalancingv2.Client, arn string) (*elasticloadbalancingv2.DescribeCapacityReservationOutput, error) {
1050+
input := elasticloadbalancingv2.DescribeCapacityReservationInput{
1051+
LoadBalancerArn: aws.String(arn),
1052+
}
1053+
1054+
output, err := conn.DescribeCapacityReservation(ctx, &input)
1055+
1056+
if errs.IsA[*awstypes.LoadBalancerNotFoundException](err) {
1057+
return nil, &retry.NotFoundError{
1058+
LastError: err,
1059+
LastRequest: input,
1060+
}
1061+
}
1062+
1063+
if err != nil {
1064+
return nil, err
1065+
}
1066+
1067+
if output == nil {
1068+
return nil, tfresource.NewEmptyResultError(input)
1069+
}
1070+
1071+
return output, nil
1072+
}
1073+
9771074
func statusLoadBalancer(ctx context.Context, conn *elasticloadbalancingv2.Client, arn string) retry.StateRefreshFunc {
9781075
return func() (any, string, error) {
9791076
output, err := findLoadBalancerByARN(ctx, conn, arn)
@@ -990,6 +1087,29 @@ func statusLoadBalancer(ctx context.Context, conn *elasticloadbalancingv2.Client
9901087
}
9911088
}
9921089

1090+
func statusCapacityReservation(ctx context.Context, conn *elasticloadbalancingv2.Client, arn string) retry.StateRefreshFunc {
1091+
return func() (any, string, error) {
1092+
output, err := findCapacityReservationByARN(ctx, conn, arn)
1093+
1094+
if tfresource.NotFound(err) {
1095+
return nil, "", nil
1096+
}
1097+
1098+
if err != nil {
1099+
return nil, "", err
1100+
}
1101+
1102+
overallState := awstypes.CapacityReservationStateEnumProvisioned
1103+
for _, rs := range output.CapacityReservationState {
1104+
if rs.State.Code != awstypes.CapacityReservationStateEnumProvisioned {
1105+
overallState = rs.State.Code
1106+
}
1107+
}
1108+
1109+
return output, string(overallState), nil
1110+
}
1111+
}
1112+
9931113
func waitLoadBalancerActive(ctx context.Context, conn *elasticloadbalancingv2.Client, arn string, timeout time.Duration) (*awstypes.LoadBalancer, error) { //nolint:unparam
9941114
stateConf := &retry.StateChangeConf{
9951115
Pending: enum.Slice(awstypes.LoadBalancerStateEnumProvisioning, awstypes.LoadBalancerStateEnumFailed),
@@ -1076,6 +1196,25 @@ func waitForNLBNetworkInterfacesToDetach(ctx context.Context, conn *ec2.Client,
10761196
return err
10771197
}
10781198

1199+
func waitCapacityReservationProvisioned(ctx context.Context, conn *elasticloadbalancingv2.Client, lbArn string, timeout time.Duration) (*elasticloadbalancingv2.DescribeCapacityReservationOutput, error) { //nolint:unparam
1200+
stateConf := &retry.StateChangeConf{
1201+
Pending: enum.Slice(awstypes.CapacityReservationStateEnumPending, awstypes.CapacityReservationStateEnumFailed, awstypes.CapacityReservationStateEnumRebalancing),
1202+
Target: enum.Slice(awstypes.CapacityReservationStateEnumProvisioned),
1203+
Refresh: statusCapacityReservation(ctx, conn, lbArn),
1204+
Timeout: timeout,
1205+
MinTimeout: 10 * time.Second,
1206+
Delay: 30 * time.Second,
1207+
}
1208+
1209+
outputRaw, err := stateConf.WaitForStateContext(ctx)
1210+
1211+
if output, ok := outputRaw.(*elasticloadbalancingv2.DescribeCapacityReservationOutput); ok {
1212+
return output, err
1213+
}
1214+
1215+
return nil, err
1216+
}
1217+
10791218
func loadBalancerNameFromARN(s string) (string, error) {
10801219
v, err := arn.Parse(s)
10811220
if err != nil {
@@ -1464,13 +1603,47 @@ func expandIPAMPools(tfList []any) *awstypes.IpamPools {
14641603
return &apiObject
14651604
}
14661605

1467-
func flattenIPAMPools(ipamPools *awstypes.IpamPools) []any {
1468-
if ipamPools == nil {
1606+
func expandMinimumLoadBalancerCapacity(tfList []any) *awstypes.MinimumLoadBalancerCapacity {
1607+
if len(tfList) == 0 {
1608+
return nil
1609+
}
1610+
1611+
var apiObject awstypes.MinimumLoadBalancerCapacity
1612+
1613+
for _, tfMapRaw := range tfList {
1614+
tfMap, ok := tfMapRaw.(map[string]any)
1615+
1616+
if !ok {
1617+
continue
1618+
}
1619+
1620+
if v, ok := tfMap["capacity_units"].(int); ok && v != 0 {
1621+
apiObject.CapacityUnits = aws.Int32(int32(v))
1622+
}
1623+
}
1624+
1625+
return &apiObject
1626+
}
1627+
1628+
func flattenIPAMPools(apiObject *awstypes.IpamPools) []any {
1629+
if apiObject == nil {
1630+
return nil
1631+
}
1632+
1633+
tfMap := map[string]any{
1634+
"ipv4_ipam_pool_id": aws.ToString(apiObject.Ipv4IpamPoolId),
1635+
}
1636+
1637+
return []any{tfMap}
1638+
}
1639+
1640+
func flattenMinimumLoadBalancerCapacity(apiObject *awstypes.MinimumLoadBalancerCapacity) []any {
1641+
if apiObject == nil {
14691642
return nil
14701643
}
14711644

14721645
tfMap := map[string]any{
1473-
"ipv4_ipam_pool_id": aws.ToString(ipamPools.Ipv4IpamPoolId),
1646+
"capacity_units": aws.ToInt32(apiObject.CapacityUnits),
14741647
}
14751648

14761649
return []any{tfMap}

internal/service/elbv2/load_balancer_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2239,6 +2239,42 @@ func TestAccELBV2LoadBalancer_ALB_updateXffClientPort(t *testing.T) {
22392239
})
22402240
}
22412241

2242+
func TestAccELBV2LoadBalancer_updateCapacityReservation(t *testing.T) {
2243+
ctx := acctest.Context(t)
2244+
var conf awstypes.LoadBalancer
2245+
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
2246+
resourceName := "aws_lb.test"
2247+
2248+
resource.ParallelTest(t, resource.TestCase{
2249+
PreCheck: func() { acctest.PreCheck(ctx, t) },
2250+
ErrorCheck: acctest.ErrorCheck(t, names.ELBV2ServiceID),
2251+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
2252+
CheckDestroy: testAccCheckLoadBalancerDestroy(ctx),
2253+
Steps: []resource.TestStep{
2254+
{
2255+
Config: testAccLoadBalancerConfig_capacityReservation(rName, 100),
2256+
Check: resource.ComposeAggregateTestCheckFunc(
2257+
testAccCheckLoadBalancerExists(ctx, resourceName, &conf),
2258+
resource.TestCheckResourceAttr(resourceName, "minimum_load_balancer_capacity.#", "1"),
2259+
resource.TestCheckResourceAttr(resourceName, "minimum_load_balancer_capacity.0.capacity_units", "100"),
2260+
),
2261+
},
2262+
{
2263+
ResourceName: resourceName,
2264+
ImportState: true,
2265+
ImportStateVerify: true,
2266+
},
2267+
{
2268+
Config: testAccLoadBalancerConfig_basic(rName),
2269+
Check: resource.ComposeAggregateTestCheckFunc(
2270+
testAccCheckLoadBalancerExists(ctx, resourceName, &conf),
2271+
resource.TestCheckResourceAttr(resourceName, "minimum_load_balancer_capacity.#", "0"),
2272+
),
2273+
},
2274+
},
2275+
})
2276+
}
2277+
22422278
func testAccCheckLoadBalancerNotRecreated(i, j *awstypes.LoadBalancer) resource.TestCheckFunc {
22432279
return func(s *terraform.State) error {
22442280
if aws.ToString(i.LoadBalancerArn) != aws.ToString(j.LoadBalancerArn) {
@@ -3584,3 +3620,21 @@ resource "aws_lb" "test" {
35843620
}
35853621
`, rName, zs))
35863622
}
3623+
3624+
func testAccLoadBalancerConfig_capacityReservation(rName string, unit int) string {
3625+
return acctest.ConfigCompose(testAccLoadBalancerConfig_baseInternal(rName, 2), fmt.Sprintf(`
3626+
resource "aws_lb" "test" {
3627+
name = %[1]q
3628+
internal = true
3629+
security_groups = [aws_security_group.test.id]
3630+
subnets = slice(aws_subnet.test[*].id, 0, 2)
3631+
3632+
idle_timeout = 30
3633+
enable_deletion_protection = false
3634+
3635+
minimum_load_balancer_capacity {
3636+
capacity_units = %[2]d
3637+
}
3638+
}
3639+
`, rName, unit))
3640+
}

website/docs/r/lb.html.markdown

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ This resource supports the following arguments:
117117
* `ip_address_type` - (Optional) Type of IP addresses used by the subnets for your load balancer. The possible values depend upon the load balancer type: `ipv4` (all load balancer types), `dualstack` (all load balancer types), and `dualstack-without-public-ipv4` (type `application` only).
118118
* `ipam_pools` (Optional). The IPAM pools to use with the load balancer. Only valid for Load Balancers of type `application`. See [ipam_pools](#ipam_pools) for more information.
119119
* `load_balancer_type` - (Optional) Type of load balancer to create. Possible values are `application`, `gateway`, or `network`. The default value is `application`.
120+
* `minimum_load_balancer_capacity` - (Optional) Minimum capacity for a load balancer. Only valid for Load Balancers of type `application` or `network`.
120121
* `name` - (Optional) Name of the LB. This name must be unique within your AWS account, can have a maximum of 32 characters, must contain only alphanumeric characters or hyphens, and must not begin or end with a hyphen. If not specified, Terraform will autogenerate a name beginning with `tf-lb`.
121122
* `name_prefix` - (Optional) Creates a unique name beginning with the specified prefix. Conflicts with `name`.
122123
* `security_groups` - (Optional) 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.
@@ -146,6 +147,10 @@ This resource supports the following arguments:
146147

147148
* `ipv4_ipam_pool_id` - (Required) The ID of the IPv4 IPAM pool.
148149

150+
### minimum_load_balancer_capacity
151+
152+
* `capacity_units` - (Required) The number of capacity units.
153+
149154
### subnet_mapping
150155

151156
* `subnet_id` - (Required) ID of the subnet of which to attach to the load balancer. You can specify only one subnet per Availability Zone.

0 commit comments

Comments
 (0)