Skip to content
Open
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 docs/guide/service/annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,21 @@ on the load balancer.
service.beta.kubernetes.io/aws-load-balancer-eip-allocations: eipalloc-xyz, eipalloc-zzz
```

- <a name="eip-tags">`service.beta.kubernetes.io/aws-load-balancer-eip-tags`</a> specifies a set of AWS tags to dynamically select unassociated Elastic IPs for an internet-facing NLB.

!!!note
- This configuration is optional, and you can use it to assign static IP addresses to your NLB by tag selection.
- You must specify tags in the format `key1=value1,key2=value2`.
- The controller will select unassociated EIPs matching all tags and assign them to the NLB subnets.
- NLB must be internet-facing.
- You must tag your EIPs in AWS before using this annotation.
- The number of matching, unassociated EIPs must be at least the number of subnets.

!!!example
```
service.beta.kubernetes.io/aws-load-balancer-eip-tags: k8s-cluster=my-prod-cluster,purpose=ingress-nlb
```


- <a name="private-ipv4-addresses">`service.beta.kubernetes.io/aws-load-balancer-private-ipv4-addresses`</a> specifies a list of private IPv4 addresses for an internal NLB.

Expand Down
1 change: 1 addition & 0 deletions pkg/annotations/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ const (
SvcLBSuffixTargetGroupAttributes = "aws-load-balancer-target-group-attributes"
SvcLBSuffixSubnets = "aws-load-balancer-subnets"
SvcLBSuffixEIPAllocations = "aws-load-balancer-eip-allocations"
SvcLBSuffixEIPTags = "aws-load-balancer-eip-tags"
SvcLBSuffixPrivateIpv4Addresses = "aws-load-balancer-private-ipv4-addresses"
SvcLBSuffixIpv6Addresses = "aws-load-balancer-ipv6-addresses"
SvcLBSuffixALPNPolicy = "aws-load-balancer-alpn-policy"
Expand Down
15 changes: 15 additions & 0 deletions pkg/aws/services/ec2.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ type EC2 interface {
DescribeAvailabilityZonesWithContext(ctx context.Context, input *ec2.DescribeAvailabilityZonesInput) (*ec2.DescribeAvailabilityZonesOutput, error)
DescribeVpcsWithContext(ctx context.Context, input *ec2.DescribeVpcsInput) (*ec2.DescribeVpcsOutput, error)
DescribeInstancesWithContext(ctx context.Context, input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error)

// DescribeAddressesAsList wraps the DescribeAddresses API, aggregates results into a list.
DescribeAddressesAsList(ctx context.Context, input *ec2.DescribeAddressesInput) ([]types.Address, error)
}

// NewEC2 constructs new EC2 implementation.
Expand All @@ -50,6 +53,18 @@ type ec2Client struct {
awsClientsProvider provider.AWSClientsProvider
}

func (c *ec2Client) DescribeAddressesAsList(ctx context.Context, input *ec2.DescribeAddressesInput) ([]types.Address, error) {
client, err := c.awsClientsProvider.GetEC2Client(ctx, "DescribeAddresses")
if err != nil {
return nil, err
}
output, err := client.DescribeAddresses(ctx, input)
if err != nil {
return nil, err
}
return output.Addresses, nil
}

func (c *ec2Client) DescribeInstancesWithContext(ctx context.Context, input *ec2.DescribeInstancesInput) (*ec2.DescribeInstancesOutput, error) {
client, err := c.awsClientsProvider.GetEC2Client(ctx, "DescribeInstances")
if err != nil {
Expand Down
49 changes: 49 additions & 0 deletions pkg/service/model_build_load_balancer.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"sort"
"strconv"

"github.com/aws/aws-sdk-go-v2/service/ec2"

awssdk "github.com/aws/aws-sdk-go-v2/aws"
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/pkg/errors"
Expand Down Expand Up @@ -335,13 +337,60 @@ func (t *defaultModelBuildTask) buildLoadBalancerTags(ctx context.Context) (map[
func (t *defaultModelBuildTask) buildLoadBalancerSubnetMappings(_ context.Context, ipAddressType elbv2model.IPAddressType, scheme elbv2model.LoadBalancerScheme, ec2Subnets []ec2types.Subnet, enablePrefixForIpv6SourceNat elbv2model.EnablePrefixForIpv6SourceNat) ([]elbv2model.SubnetMapping, error) {
var eipAllocation []string
eipConfigured := t.annotationParser.ParseStringSliceAnnotation(annotations.SvcLBSuffixEIPAllocations, &eipAllocation, t.service.Annotations)
var eipTags string
eipTagsConfigured := t.annotationParser.ParseStringAnnotation(annotations.SvcLBSuffixEIPTags, &eipTags, t.service.Annotations)
if eipConfigured {
if scheme != elbv2model.LoadBalancerSchemeInternetFacing {
return nil, errors.Errorf("EIP allocations can only be set for internet facing load balancers")
}
if len(eipAllocation) != len(ec2Subnets) {
return nil, errors.Errorf("count of EIP allocations (%d) and subnets (%d) must match", len(eipAllocation), len(ec2Subnets))
}
} else if eipTagsConfigured {
if scheme != elbv2model.LoadBalancerSchemeInternetFacing {
return nil, errors.Errorf("EIP tags can only be set for internet facing load balancers")
}
// Parse tags string into map
tagMap := make(map[string]string)
for _, kv := range regexp.MustCompile(`,`).Split(eipTags, -1) {
parts := regexp.MustCompile(`=`).Split(kv, 2)
if len(parts) == 2 {
tagMap[parts[0]] = parts[1]
}
}
// Build EC2 filter for DescribeAddresses
var filters []ec2types.Filter
for k, v := range tagMap {
filters = append(filters, ec2types.Filter{
Name: awssdk.String(fmt.Sprintf("tag:%s", k)),
Values: []string{v},
})
}
filters = append(filters, ec2types.Filter{
Name: awssdk.String("domain"),
Values: []string{"vpc"},
})
addresses, err := t.ec2Client.DescribeAddressesAsList(context.Background(), &ec2.DescribeAddressesInput{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be good to have a cache in front of it as I don't expect this result to change often.
example:

type webACLNameToArnMapper struct {
wafv2Client services.WAFv2
cache *cache.Expiring
cacheTTL time.Duration
cacheMutex sync.RWMutex
}

Filters: filters,
})
if err != nil {
return nil, errors.Wrap(err, "failed to describe EIPs by tags")
}
// Filter unassociated EIPs client-side
var unassociatedAddresses []ec2types.Address
for _, addr := range addresses {
if addr.AssociationId == nil || awssdk.ToString(addr.AssociationId) == "" {
unassociatedAddresses = append(unassociatedAddresses, addr)
}
}
if len(unassociatedAddresses) < len(ec2Subnets) {
return nil, errors.Errorf("not enough unassociated EIPs with specified tags; needed %d, found %d", len(ec2Subnets), len(unassociatedAddresses))
}
eipAllocation = make([]string, len(ec2Subnets))
for i := range ec2Subnets {
eipAllocation[i] = awssdk.ToString(unassociatedAddresses[i].AllocationId)
}
eipConfigured = true
}

var ipv4Addresses []netip.Addr
Expand Down
Loading