Merge pull request #16088 from rlees85/ipv6-only-ec2-sd

discovery: Allow EC2 Service Discovery to work with IPv6-only instances
This commit is contained in:
Joe Adams 2026-05-11 23:09:34 -04:00 committed by GitHub
commit 63f2bf9b9a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 133 additions and 32 deletions

View File

@ -55,6 +55,7 @@ const (
ec2LabelInstanceType = ec2Label + "instance_type"
ec2LabelOwnerID = ec2Label + "owner_id"
ec2LabelPlatform = ec2Label + "platform"
ec2LabelDefaultIPv6Address = ec2Label + "default_ipv6_address"
ec2LabelPrimaryIPv6Addresses = ec2Label + "primary_ipv6_addresses"
ec2LabelPrimarySubnetID = ec2Label + "primary_subnet_id"
ec2LabelPrivateDNS = ec2Label + "private_dns_name"
@ -306,7 +307,9 @@ func (d *EC2Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error
for _, r := range p.Reservations {
for _, inst := range r.Instances {
if inst.PrivateIpAddress == nil {
defaultIPv6Addr, primaryIPv6Addrs, ipv6Addrs := getInstanceIPv6Addresses(&inst)
if inst.PrivateIpAddress == nil && defaultIPv6Addr == nil {
continue
}
@ -319,12 +322,20 @@ func (d *EC2Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error
labels[ec2LabelOwnerID] = model.LabelValue(*r.OwnerId)
}
labels[ec2LabelPrivateIP] = model.LabelValue(*inst.PrivateIpAddress)
if defaultIPv6Addr != nil {
labels[ec2LabelDefaultIPv6Address] = model.LabelValue(*defaultIPv6Addr)
}
if inst.PrivateIpAddress != nil {
labels[ec2LabelPrivateIP] = model.LabelValue(*inst.PrivateIpAddress)
labels[model.AddressLabel] = model.LabelValue(net.JoinHostPort(*inst.PrivateIpAddress, strconv.Itoa(d.cfg.Port)))
} else {
labels[model.AddressLabel] = model.LabelValue(net.JoinHostPort(*defaultIPv6Addr, strconv.Itoa(d.cfg.Port)))
}
if inst.PrivateDnsName != nil {
labels[ec2LabelPrivateDNS] = model.LabelValue(*inst.PrivateDnsName)
}
addr := net.JoinHostPort(*inst.PrivateIpAddress, strconv.Itoa(d.cfg.Port))
labels[model.AddressLabel] = model.LabelValue(addr)
if inst.Platform != "" {
labels[ec2LabelPlatform] = model.LabelValue(inst.Platform)
@ -334,6 +345,21 @@ func (d *EC2Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error
labels[ec2LabelPublicIP] = model.LabelValue(*inst.PublicIpAddress)
labels[ec2LabelPublicDNS] = model.LabelValue(*inst.PublicDnsName)
}
if primaryIPv6Addrs != nil {
labels[ec2LabelPrimaryIPv6Addresses] = model.LabelValue(
ec2LabelSeparator +
strings.Join(primaryIPv6Addrs, ec2LabelSeparator) +
ec2LabelSeparator)
}
if ipv6Addrs != nil {
labels[ec2LabelIPv6Addresses] = model.LabelValue(
ec2LabelSeparator +
strings.Join(ipv6Addrs, ec2LabelSeparator) +
ec2LabelSeparator)
}
labels[ec2LabelAMI] = model.LabelValue(*inst.ImageId)
labels[ec2LabelAZ] = model.LabelValue(*inst.Placement.AvailabilityZone)
azID, ok := d.azToAZID[*inst.Placement.AvailabilityZone]
@ -359,8 +385,6 @@ func (d *EC2Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error
labels[ec2LabelPrimarySubnetID] = model.LabelValue(*inst.SubnetId)
var subnets []string
var ipv6addrs []string
var primaryipv6addrs []string
subnetsMap := make(map[string]struct{})
for _, eni := range inst.NetworkInterfaces {
if eni.SubnetId == nil {
@ -371,36 +395,11 @@ func (d *EC2Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error
subnetsMap[*eni.SubnetId] = struct{}{}
subnets = append(subnets, *eni.SubnetId)
}
for _, ipv6addr := range eni.Ipv6Addresses {
ipv6addrs = append(ipv6addrs, *ipv6addr.Ipv6Address)
if *ipv6addr.IsPrimaryIpv6 {
// we might have to extend the slice with more than one element
// that could leave empty strings in the list which is intentional
// to keep the position/device index information
for int32(len(primaryipv6addrs)) <= *eni.Attachment.DeviceIndex {
primaryipv6addrs = append(primaryipv6addrs, "")
}
primaryipv6addrs[*eni.Attachment.DeviceIndex] = *ipv6addr.Ipv6Address
}
}
}
labels[ec2LabelSubnetID] = model.LabelValue(
ec2LabelSeparator +
strings.Join(subnets, ec2LabelSeparator) +
ec2LabelSeparator)
if len(ipv6addrs) > 0 {
labels[ec2LabelIPv6Addresses] = model.LabelValue(
ec2LabelSeparator +
strings.Join(ipv6addrs, ec2LabelSeparator) +
ec2LabelSeparator)
}
if len(primaryipv6addrs) > 0 {
labels[ec2LabelPrimaryIPv6Addresses] = model.LabelValue(
ec2LabelSeparator +
strings.Join(primaryipv6addrs, ec2LabelSeparator) +
ec2LabelSeparator)
}
}
for _, t := range inst.Tags {
@ -417,3 +416,39 @@ func (d *EC2Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error
return []*targetgroup.Group{tg}, nil
}
func getInstanceIPv6Addresses(i *ec2Types.Instance) (*string, []string, []string) {
var primaryIPv6Addrs []string
var ipv6Addrs []string
if i.VpcId != nil {
for _, eni := range i.NetworkInterfaces {
if eni.SubnetId == nil {
continue
}
for _, ipv6addr := range eni.Ipv6Addresses {
ipv6Addrs = append(ipv6Addrs, *ipv6addr.Ipv6Address)
if *ipv6addr.IsPrimaryIpv6 {
// we might have to extend the slice with more than one element
// that could leave empty strings in the list which is intentional
// to keep the position/device index information
for int32(len(primaryIPv6Addrs)) <= *eni.Attachment.DeviceIndex {
primaryIPv6Addrs = append(primaryIPv6Addrs, "")
}
primaryIPv6Addrs[*eni.Attachment.DeviceIndex] = *ipv6addr.Ipv6Address
}
}
}
// Find an IPv6 address we can use by default. Pick the first primary one if
// there is any available, if not then pick the first non-primary address.
for _, ipv6addr := range append(primaryIPv6Addrs, ipv6Addrs...) {
if ipv6addr != "" {
return &ipv6addr, primaryIPv6Addrs, ipv6Addrs
}
}
}
return nil, primaryIPv6Addrs, ipv6Addrs
}

View File

@ -108,7 +108,7 @@ func TestEC2DiscoveryRefresh(t *testing.T) {
expected []*targetgroup.Group
}{
{
name: "NoPrivateIp",
name: "NoPrivateIpOrIpv6",
ec2Data: &ec2DataStore{
region: "region-noprivateip",
azToAZID: map[string]string{
@ -351,6 +351,7 @@ func TestEC2DiscoveryRefresh(t *testing.T) {
"__meta_ec2_instance_type": model.LabelValue("instance-type-ipv6"),
"__meta_ec2_ipv6_addresses": model.LabelValue(",2001:db8:2::1:1,2001:db8:2::2:1,2001:db8:2::2:2,2001:db8:2::3:1,"),
"__meta_ec2_owner_id": model.LabelValue(""),
"__meta_ec2_default_ipv6_address": model.LabelValue("2001:db8:2::2:2"),
"__meta_ec2_primary_ipv6_addresses": model.LabelValue(",,2001:db8:2::2:2,,2001:db8:2::3:1,"),
"__meta_ec2_primary_subnet_id": model.LabelValue("azid-2"),
"__meta_ec2_private_ip": model.LabelValue("9.10.11.12"),
@ -362,6 +363,69 @@ func TestEC2DiscoveryRefresh(t *testing.T) {
},
},
},
{
name: "Ipv6-Only",
ec2Data: &ec2DataStore{
region: "region-ipv6-only",
azToAZID: map[string]string{
"azname-a": "azid-1",
"azname-b": "azid-2",
"azname-c": "azid-3",
},
instances: []ec2Types.Instance{
{
// just the minimum needed for the refresh work
ImageId: strptr("ami-ipv6-only"),
InstanceId: strptr("instance-id-ipv6-only"),
InstanceType: "instance-type-ipv6-only",
Placement: &ec2Types.Placement{AvailabilityZone: strptr("azname-b")},
State: &ec2Types.InstanceState{Name: "running"},
SubnetId: strptr("azid-2"),
VpcId: strptr("vpc-ipv6-only"),
// network interfaces
NetworkInterfaces: []ec2Types.InstanceNetworkInterface{
// interface without primary IPv6, index 0
{
Attachment: &ec2Types.InstanceNetworkInterfaceAttachment{
DeviceIndex: aws.Int32(0),
},
Ipv6Addresses: []ec2Types.InstanceIpv6Address{
{
Ipv6Address: strptr("2001:db8:2::1:1"),
IsPrimaryIpv6: boolptr(false),
},
},
SubnetId: strptr("azid-2"),
},
},
},
},
},
expected: []*targetgroup.Group{
{
Source: "region-ipv6-only",
Targets: []model.LabelSet{
{
"__address__": model.LabelValue("[2001:db8:2::1:1]:4242"),
"__meta_ec2_ami": model.LabelValue("ami-ipv6-only"),
"__meta_ec2_availability_zone": model.LabelValue("azname-b"),
"__meta_ec2_availability_zone_id": model.LabelValue("azid-2"),
"__meta_ec2_instance_id": model.LabelValue("instance-id-ipv6-only"),
"__meta_ec2_instance_state": model.LabelValue("running"),
"__meta_ec2_instance_type": model.LabelValue("instance-type-ipv6-only"),
"__meta_ec2_ipv6_addresses": model.LabelValue(",2001:db8:2::1:1,"),
"__meta_ec2_owner_id": model.LabelValue(""),
"__meta_ec2_default_ipv6_address": model.LabelValue("2001:db8:2::1:1"),
"__meta_ec2_primary_subnet_id": model.LabelValue("azid-2"),
"__meta_ec2_region": model.LabelValue("region-ipv6-only"),
"__meta_ec2_subnet_id": model.LabelValue(",azid-2,"),
"__meta_ec2_vpc_id": model.LabelValue("vpc-ipv6-only"),
},
},
},
},
},
} {
t.Run(tt.name, func(t *testing.T) {
client := newMockEC2Client(tt.ec2Data)

View File

@ -902,6 +902,7 @@ The following meta labels are available on targets during [relabeling](#relabel_
* `__meta_ec2_ipv6_addresses`: comma separated list of IPv6 addresses assigned to the instance's network interfaces, if present
* `__meta_ec2_owner_id`: the ID of the AWS account that owns the EC2 instance
* `__meta_ec2_platform`: the Operating System platform, set to 'windows' on Windows servers, absent otherwise
* `__meta_ec2_default_ipv6_address`: the first primary IPv6 address found if present, otherwise first non-primary IPv6 address, if present
* `__meta_ec2_primary_ipv6_addresses`: comma separated list of the Primary IPv6 addresses of the instance, if present. The list is ordered based on the position of each corresponding network interface in the attachment order.
* `__meta_ec2_primary_subnet_id`: the subnet ID of the primary network interface, if available
* `__meta_ec2_private_dns_name`: the private DNS name of the instance, if available
@ -1832,6 +1833,7 @@ The following meta labels are available on targets during [relabeling](#relabel_
* `__meta_ec2_ipv6_addresses`: comma separated list of IPv6 addresses assigned to the instance's network interfaces, if present
* `__meta_ec2_owner_id`: the ID of the AWS account that owns the EC2 instance
* `__meta_ec2_platform`: the Operating System platform, set to 'windows' on Windows servers, absent otherwise
* `__meta_ec2_default_ipv6_address`: the first primary IPv6 address found if present, otherwise first non-primary IPv6 address, if present
* `__meta_ec2_primary_ipv6_addresses`: comma separated list of the Primary IPv6 addresses of the instance, if present. The list is ordered based on the position of each corresponding network interface in the attachment order.
* `__meta_ec2_primary_subnet_id`: the subnet ID of the primary network interface, if available
* `__meta_ec2_private_dns_name`: the private DNS name of the instance, if available