Skip to content

Commit cd61887

Browse files
authored
Merge pull request #1214 from mtulio/fix-hairpin-feat-tg-attrib
Fix hairpinning traffic on internal NLB by introducing TG attribute reconciler
2 parents 3b4c95b + 73428cd commit cd61887

File tree

7 files changed

+1474
-21
lines changed

7 files changed

+1474
-21
lines changed

docs/service_controller.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,56 @@ The service controller is responsible for watch for service and node object chan
3333
| service.beta.kubernetes.io/aws-load-balancer-healthcheck-protocol | [tcp\|http\|https] | tcp | NLB | Specifies the protocol to use for the target group health check. |
3434
| service.beta.kubernetes.io/aws-load-balancer-subnets | Comma-separated list | - | ELB,NLB | Specifies the Availability Zone configuration for the load balancer. The values are comma separated list of subnetID or subnetName from different AZs. |
3535
| service.beta.kubernetes.io/aws-load-balancer-target-node-labels | Comma-separated list of key=value | - | ELB,NLB | Specifies a comma-separated list of key-value pairs which will be used to select the target nodes for the load balancer. |
36+
| service.beta.kubernetes.io/aws-load-balancer-target-group-attributes | Comma-separated list of key=value | - | NLB | Specifies a comma-separated list of key-value pairs which will be applied as target group attributes. For example: "preserve_client_ip.enabled=false". The list of supported values is available [here](#tg-supported-attributes). |
37+
38+
39+
## Target group attributes for Service type-loadBalancer NLB <a name="tg-supported-attributes"></a>
40+
41+
The following target group attributes are supported by the controller using the annotation `service.beta.kubernetes.io/aws-load-balancer-target-group-attributes`:
42+
43+
| Attribute | Values | Description |
44+
| -- | -- | -- |
45+
| preserve_client_ip.enabled | [true\|false] | Whether to preserve client IP addresses when terminating connections at the target group level |
46+
| proxy_protocol_v2.enabled | [true\|false] | Whether to enable proxy protocol v2 on the target group |
47+
48+
**Format:** Attributes are specified as `key=value` pairs, separated by commas.
49+
50+
**Example:**
51+
```yaml
52+
service.beta.kubernetes.io/aws-load-balancer-target-group-attributes: preserve_client_ip.enabled=true,proxy_protocol_v2.enabled=false
53+
```
54+
55+
### Usage Example 1 - working with hairpin connection on internal NLB
56+
57+
The following Service example changes the Target Group Traffic Control attribute "Preserve client IP addresses" from the default (`true`, when target type is instance) to `false`:
58+
59+
```yaml
60+
apiVersion: v1
61+
kind: Service
62+
metadata:
63+
name: $SVC_NAME
64+
namespace: ${APP_NAMESPACE}
65+
annotations:
66+
service.beta.kubernetes.io/aws-load-balancer-type: nlb
67+
service.beta.kubernetes.io/aws-load-balancer-internal: true
68+
service.beta.kubernetes.io/aws-load-balancer-target-group-attributes: preserve_client_ip.enabled=false
69+
[...]
70+
```
71+
72+
### Usage Example 2 - working with hairpin connection on internal NLB tracking source IP address
73+
74+
The following example allow users to fine tune the Services for a backend which requires tracking the original source IP address of internal Load Balancers NLB with support of hairpin connections:
75+
76+
77+
```yaml
78+
apiVersion: v1
79+
kind: Service
80+
metadata:
81+
name: $SVC_NAME
82+
namespace: ${APP_NAMESPACE}
83+
annotations:
84+
service.beta.kubernetes.io/aws-load-balancer-type: nlb
85+
service.beta.kubernetes.io/aws-load-balancer-internal: true
86+
service.beta.kubernetes.io/aws-load-balancer-target-group-attributes: preserve_client_ip.enabled=false,proxy_protocol_v2.enabled=true
87+
[...]
88+
```

pkg/providers/v1/aws.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,12 @@ const ServiceAnnotationLoadBalancerHCTimeout = "service.beta.kubernetes.io/aws-l
216216
// service to specify, in seconds, the interval between health checks.
217217
const ServiceAnnotationLoadBalancerHCInterval = "service.beta.kubernetes.io/aws-load-balancer-healthcheck-interval"
218218

219+
// ServiceAnnotationLoadBalancerTargetGroupAttributes is the annotation used on the
220+
// service to specify a comma-separated list of key-value pairs which will be applied as
221+
// target group attributes.
222+
// For example: "preserve_client_ip.enabled=false,proxy_protocol_v2.enabled=true"
223+
const ServiceAnnotationLoadBalancerTargetGroupAttributes = "service.beta.kubernetes.io/aws-load-balancer-target-group-attributes"
224+
219225
// ServiceAnnotationLoadBalancerEIPAllocations is the annotation used on the
220226
// service to specify a comma separated list of EIP allocations to use as
221227
// static IP addresses for the NLB. Only supported on elbv2 (NLB)
@@ -267,6 +273,20 @@ const (
267273
regularAvailabilityZoneType = "availability-zone"
268274
)
269275

276+
// Target Group Attributes
277+
// https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2@main/types#TargetGroupAttribute
278+
const (
279+
// targetGroupAttributePreserveClientIPEnabled is the target group attribute preserve_client_ip.enabled.
280+
// Indicates whether client IP preservation is enabled.
281+
// Valid values are true or false.
282+
targetGroupAttributePreserveClientIPEnabled = "preserve_client_ip.enabled"
283+
284+
// targetGroupAttributeProxyProtocolV2Enabled is the target group attribute proxy_protocol_v2.enabled.
285+
// Indicates whether Proxy Protocol version 2 is enabled.
286+
// Valid values are true or false.
287+
targetGroupAttributeProxyProtocolV2Enabled = "proxy_protocol_v2.enabled"
288+
)
289+
270290
// awsTagNameMasterRoles is a set of well-known AWS tag names that indicate the instance is a master
271291
var awsTagNameMasterRoles = sets.NewString("kubernetes.io/role/master", "k8s.io/role/master")
272292

@@ -2147,6 +2167,14 @@ func (c *Cloud) EnsureLoadBalancer(ctx context.Context, clusterName string, apiS
21472167
klog.V(2).Infof("EnsureLoadBalancer(%v, %v, %v, %v, %v, %v, %v)",
21482168
clusterName, apiService.Namespace, apiService.Name, c.region, apiService.Spec.LoadBalancerIP, apiService.Spec.Ports, annotations)
21492169

2170+
// pre-flight validations for EnsureLoadBalancer.
2171+
if err := ensureLoadBalancerValidation(&awsValidationInput{
2172+
apiService: apiService,
2173+
annotations: annotations,
2174+
}); err != nil {
2175+
return nil, err
2176+
}
2177+
21502178
if apiService.Spec.SessionAffinity != v1.ServiceAffinityNone {
21512179
// ELB supports sticky sessions, but only when configured for HTTP/HTTPS
21522180
return nil, fmt.Errorf("unsupported load balancer affinity: %v", apiService.Spec.SessionAffinity)

pkg/providers/v1/aws_loadbalancer.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,14 @@ func (c *Cloud) ensureLoadBalancerv2(ctx context.Context, namespacedName types.N
400400
loadBalancer = &loadBalancers.LoadBalancers[0]
401401
}
402402
}
403+
404+
// Reconcile target group attributes.
405+
if _, present := annotations[ServiceAnnotationLoadBalancerTargetGroupAttributes]; present {
406+
if err := c.reconcileTargetGroupsAttributes(ctx, aws.ToString(loadBalancer.LoadBalancerArn), annotations); err != nil {
407+
return nil, fmt.Errorf("error reconciling target group attributes: %q", err)
408+
}
409+
}
410+
403411
return loadBalancer, nil
404412
}
405413

@@ -493,9 +501,156 @@ func (c *Cloud) reconcileLBAttributes(ctx context.Context, loadBalancerArn strin
493501
return fmt.Errorf("unable to update load balancer attributes during attribute sync: %q", err)
494502
}
495503
}
504+
505+
return nil
506+
}
507+
508+
// reconcileTargetGroupsAttributes reconciles the target group attributes for all target groups
509+
// associated with a load balancer to match the desired state specified in service annotations.
510+
// Only supported attributes by controller are reconciled.
511+
//
512+
// Parameters:
513+
// - ctx: context for AWS API calls with timeout and cancellation support
514+
// - lbARN: AWS load balancer ARN to identify which target groups to process
515+
// - annotations: service annotations containing desired target group attribute configuration
516+
//
517+
// Returns:
518+
// - error: validation errors, AWS API errors, or target group attribute update failures
519+
//
520+
// Documentation generated by Cursor AI
521+
func (c *Cloud) reconcileTargetGroupsAttributes(ctx context.Context, lbARN string, annotations map[string]string) error {
522+
if len(lbARN) == 0 {
523+
return fmt.Errorf("error updating target groups attributes: load balancer ARN is empty")
524+
}
525+
526+
describeTargetGroupsOutput, err := c.elbv2.DescribeTargetGroups(ctx, &elbv2.DescribeTargetGroupsInput{
527+
LoadBalancerArn: aws.String(lbARN),
528+
})
529+
if err != nil {
530+
return fmt.Errorf("error updating target groups attributes from load balancer %q: %w", lbARN, err)
531+
}
532+
533+
var errs []error
534+
for _, tg := range describeTargetGroupsOutput.TargetGroups {
535+
err := c.ensureTargetGroupAttributes(ctx, &tg, annotations)
536+
if err != nil {
537+
errs = append(errs, fmt.Errorf("error updating target group attributes for target group %q: %w", aws.ToString(tg.TargetGroupArn), err))
538+
}
539+
}
540+
if len(errs) > 0 {
541+
return fmt.Errorf("one or more errors occurred while updating target group attributes: %v", errs)
542+
}
543+
return nil
544+
}
545+
546+
// ensureTargetGroupAttributes ensures that the target group attributes for a specific
547+
// target group match the desired state specified in service annotations.
548+
//
549+
// Parameters:
550+
// - ctx: context for AWS API calls and cancellation
551+
// - tg: target group object containing ARN, protocol, and type information
552+
// - annotations: service annotations containing desired target group attributes
553+
//
554+
// Returns:
555+
// - error: validation errors, AWS API errors, or attribute building errors
556+
//
557+
// Documentation generated by Cursor AI
558+
func (c *Cloud) ensureTargetGroupAttributes(ctx context.Context, tg *elbv2types.TargetGroup, annotations map[string]string) error {
559+
if tg == nil {
560+
return fmt.Errorf("unable to reconcile target group attributes: target group is required")
561+
}
562+
563+
tgAttributes, err := c.elbv2.DescribeTargetGroupAttributes(ctx, &elbv2.DescribeTargetGroupAttributesInput{
564+
TargetGroupArn: tg.TargetGroupArn,
565+
})
566+
if err != nil {
567+
return fmt.Errorf("unable to retrieve target group attributes during attribute sync: %w", err)
568+
}
569+
570+
desiredTargetGroupAttributes, err := c.buildTargetGroupAttributes(tg, tgAttributes.Attributes, annotations)
571+
if err != nil {
572+
return fmt.Errorf("unable to build target group attributes: %w", err)
573+
}
574+
575+
if len(desiredTargetGroupAttributes) == 0 {
576+
return nil
577+
}
578+
klog.Infof("Updating attributes for target group %q", aws.ToString(tg.TargetGroupArn))
579+
580+
if _, err = c.elbv2.ModifyTargetGroupAttributes(ctx, &elbv2.ModifyTargetGroupAttributesInput{
581+
TargetGroupArn: tg.TargetGroupArn,
582+
Attributes: desiredTargetGroupAttributes,
583+
}); err != nil {
584+
return fmt.Errorf("unable to modify target group attributes during attribute sync: %w", err)
585+
}
586+
klog.Infof("Successfully updated target group attributes for %q", aws.ToString(tg.TargetGroupArn))
587+
496588
return nil
497589
}
498590

591+
// buildTargetGroupAttributes builds the list of target group attributes that need to be modified
592+
// based on the Service annotation, and current attribute values, calculating only the attributes
593+
// to be changed.
594+
//
595+
// Supported values to annotation ServiceAnnotationLoadBalancerTargetGroupAttributes:
596+
// - preserve_client_ip.enabled=true|false - whether to preserve client IP addresses
597+
// - proxy_protocol_v2.enabled=true|false - whether to enable proxy protocol v2
598+
599+
// Behavior when no annotations provided or removed:
600+
// - Target groups preserves the last set values, and skips any changes.
601+
//
602+
// Parameters:
603+
// - tg: target group object
604+
// - tgAttributes: current target group attributes from AWS resource
605+
// - annotations: service annotations containing desired attribute values
606+
//
607+
// Returns:
608+
// - []elbv2types.TargetGroupAttribute: list of attributes that need to be modified
609+
// - error: validation errors, parsing errors, or AWS restrictions
610+
//
611+
// Documentation generated by Cursor AI
612+
func (c *Cloud) buildTargetGroupAttributes(tg *elbv2types.TargetGroup, tgAttributes []elbv2types.TargetGroupAttribute, annotations map[string]string) ([]elbv2types.TargetGroupAttribute, error) {
613+
errPrefix := "error building target group attributes"
614+
if tg == nil {
615+
return nil, fmt.Errorf("%s: target group is nil", errPrefix)
616+
}
617+
if tgAttributes == nil {
618+
return nil, fmt.Errorf("%s: target group attributes are nil", errPrefix)
619+
}
620+
621+
// existingAttributes are current target group attributes from AWS.
622+
existingAttributes := make(map[string]string, len(tgAttributes))
623+
for _, attr := range tgAttributes {
624+
existingAttributes[aws.ToString(attr.Key)] = aws.ToString(attr.Value)
625+
}
626+
627+
// annotationAttributes are the user-defined attributes set through annotations.
628+
annotationAttributes := getKeyValuePropertiesFromAnnotation(annotations, ServiceAnnotationLoadBalancerTargetGroupAttributes)
629+
630+
// Calculate attribute difference between current and desired state.
631+
var diff []elbv2types.TargetGroupAttribute
632+
for attrKey, attrValue := range annotationAttributes {
633+
// Skip non-supported attributes by controller.
634+
if _, ok := existingAttributes[attrKey]; !ok {
635+
klog.V(2).Infof("Skipping non-supported target group attribute %q", attrKey)
636+
continue
637+
}
638+
639+
// Calculate the target value: annotation override > current value.
640+
if attrValue == existingAttributes[attrKey] {
641+
klog.V(2).Infof("Skipping changes to target group attribute %q, values are the same: %q", attrKey, attrValue)
642+
continue
643+
}
644+
klog.V(2).Infof("Setting from annotation the target group attribute %q value from %q to %q", attrKey, existingAttributes[attrKey], attrValue)
645+
646+
diff = append(diff, elbv2types.TargetGroupAttribute{
647+
Key: aws.String(attrKey),
648+
Value: aws.String(attrValue),
649+
})
650+
}
651+
return diff, nil
652+
}
653+
499654
var invalidELBV2NameRegex = regexp.MustCompile("[^[:alnum:]]")
500655

501656
// buildTargetGroupName will build unique name for targetGroup of service & port.

0 commit comments

Comments
 (0)