diff --git a/docs/tutorials/aws-sd.md b/docs/tutorials/aws-sd.md index c3e0e8814..5e6b753be 100644 --- a/docs/tutorials/aws-sd.md +++ b/docs/tutorials/aws-sd.md @@ -304,6 +304,32 @@ spec: This will set the TTL for the DNS record to 60 seconds. +## IPv6 Support + +If your Kubernetes cluster is configured with IPv6 support, such as an [EKS cluster with IPv6 support](https://docs.aws.amazon.com/eks/latest/userguide/deploy-ipv6-cluster.html), ExternalDNS can +also create AAAA DNS records. + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: nginx + annotations: + external-dns.alpha.kubernetes.io/hostname: nginx.external-dns-test.my-org.com + external-dns.alpha.kubernetes.io/ttl: "60" +spec: + ipFamilies: + - "IPv6" + type: NodePort + ports: + - port: 80 + name: http + targetPort: 80 + selector: + app: nginx +``` + +:information_source: The AWS-SD provider does not currently support dualstack load balancers and will only create A records for these at this time. See the AWS provider and the [AWS Load Balancer Controller Tutorial](./aws-load-balancer-controller.md) for dualstack load balancer support. ## Clean up diff --git a/provider/awssd/aws_sd.go b/provider/awssd/aws_sd.go index bd1ec496c..babe48593 100644 --- a/provider/awssd/aws_sd.go +++ b/provider/awssd/aws_sd.go @@ -41,6 +41,7 @@ const ( sdNamespaceTypePrivate = "private" sdInstanceAttrIPV4 = "AWS_INSTANCE_IPV4" + sdInstanceAttrIPV6 = "AWS_INSTANCE_IPV6" sdInstanceAttrCname = "AWS_INSTANCE_CNAME" sdInstanceAttrAlias = "AWS_ALIAS_DNS_NAME" ) @@ -186,10 +187,15 @@ func (p *AWSSDProvider) instancesToEndpoint(ns *sdtypes.NamespaceSummary, srv *s newEndpoint.RecordType = endpoint.RecordTypeCNAME newEndpoint.Targets = append(newEndpoint.Targets, inst.Attributes[sdInstanceAttrAlias]) - // IP-based target + // IPv4-based target } else if inst.Attributes[sdInstanceAttrIPV4] != "" { newEndpoint.RecordType = endpoint.RecordTypeA newEndpoint.Targets = append(newEndpoint.Targets, inst.Attributes[sdInstanceAttrIPV4]) + + // IPv6-based target + } else if inst.Attributes[sdInstanceAttrIPV6] != "" { + newEndpoint.RecordType = endpoint.RecordTypeAAAA + newEndpoint.Targets = append(newEndpoint.Targets, inst.Attributes[sdInstanceAttrIPV6]) } else { log.Warnf("Invalid instance \"%v\" found in service \"%v\"", inst, srv.Name) } @@ -485,15 +491,18 @@ func (p *AWSSDProvider) RegisterInstance(ctx context.Context, service *sdtypes.S attr := make(map[string]string) - if ep.RecordType == endpoint.RecordTypeCNAME { + switch ep.RecordType { + case endpoint.RecordTypeCNAME: if p.isAWSLoadBalancer(target) { attr[sdInstanceAttrAlias] = target } else { attr[sdInstanceAttrCname] = target } - } else if ep.RecordType == endpoint.RecordTypeA { + case endpoint.RecordTypeA: attr[sdInstanceAttrIPV4] = target - } else { + case endpoint.RecordTypeAAAA: + attr[sdInstanceAttrIPV6] = target + default: return fmt.Errorf("invalid endpoint type (%v)", ep) } @@ -597,16 +606,17 @@ func (p *AWSSDProvider) parseHostname(hostname string) (namespace string, servic // determine service routing policy based on endpoint type func (p *AWSSDProvider) routingPolicyFromEndpoint(ep *endpoint.Endpoint) sdtypes.RoutingPolicy { - if ep.RecordType == endpoint.RecordTypeA { + if ep.RecordType == endpoint.RecordTypeA || ep.RecordType == endpoint.RecordTypeAAAA { return sdtypes.RoutingPolicyMultivalue } return sdtypes.RoutingPolicyWeighted } -// determine service type (A, CNAME) from given endpoint +// determine service type (A, AAAA, CNAME) from given endpoint func (p *AWSSDProvider) serviceTypeFromEndpoint(ep *endpoint.Endpoint) sdtypes.RecordType { - if ep.RecordType == endpoint.RecordTypeCNAME { + switch ep.RecordType { + case endpoint.RecordTypeCNAME: // FIXME service type is derived from the first target only. Theoretically this may be problem. // But I don't see a scenario where one endpoint contains targets of different types. if p.isAWSLoadBalancer(ep.Targets[0]) { @@ -614,8 +624,11 @@ func (p *AWSSDProvider) serviceTypeFromEndpoint(ep *endpoint.Endpoint) sdtypes.R return sdtypes.RecordTypeA } return sdtypes.RecordTypeCname + case endpoint.RecordTypeAAAA: + return sdtypes.RecordTypeAaaa + default: + return sdtypes.RecordTypeA } - return sdtypes.RecordTypeA } // determine if a given hostname belongs to an AWS load balancer diff --git a/provider/awssd/aws_sd_test.go b/provider/awssd/aws_sd_test.go index 4481b0927..2600cd4b7 100644 --- a/provider/awssd/aws_sd_test.go +++ b/provider/awssd/aws_sd_test.go @@ -307,6 +307,19 @@ func TestAWSSDProvider_Records(t *testing.T) { }}, }, }, + "aaaa-srv": { + Id: aws.String("aaaa-srv"), + Name: aws.String("service4"), + Description: aws.String("owner-id"), + DnsConfig: &sdtypes.DnsConfig{ + NamespaceId: aws.String("private"), + RoutingPolicy: sdtypes.RoutingPolicyWeighted, + DnsRecords: []sdtypes.DnsRecord{{ + Type: sdtypes.RecordTypeAaaa, + TTL: aws.Int64(100), + }}, + }, + }, }, } @@ -341,12 +354,21 @@ func TestAWSSDProvider_Records(t *testing.T) { }, }, }, + "aaaa-srv": { + "0000:0000:0000:0000:abcd:abcd:abcd:abcd": { + Id: aws.String("0000:0000:0000:0000:abcd:abcd:abcd:abcd"), + Attributes: map[string]string{ + sdInstanceAttrIPV6: "0000:0000:0000:0000:abcd:abcd:abcd:abcd", + }, + }, + }, } expectedEndpoints := []*endpoint.Endpoint{ {DNSName: "service1.private.com", Targets: endpoint.Targets{"1.2.3.4", "1.2.3.5"}, RecordType: endpoint.RecordTypeA, RecordTTL: 100, Labels: map[string]string{endpoint.AWSSDDescriptionLabel: "owner-id"}}, {DNSName: "service2.private.com", Targets: endpoint.Targets{"load-balancer.us-east-1.elb.amazonaws.com"}, RecordType: endpoint.RecordTypeCNAME, RecordTTL: 100, Labels: map[string]string{endpoint.AWSSDDescriptionLabel: "owner-id"}}, {DNSName: "service3.private.com", Targets: endpoint.Targets{"cname.target.com"}, RecordType: endpoint.RecordTypeCNAME, RecordTTL: 80, Labels: map[string]string{endpoint.AWSSDDescriptionLabel: "owner-id"}}, + {DNSName: "service4.private.com", Targets: endpoint.Targets{"0000:0000:0000:0000:abcd:abcd:abcd:abcd"}, RecordType: endpoint.RecordTypeAAAA, RecordTTL: 100, Labels: map[string]string{endpoint.AWSSDDescriptionLabel: "owner-id"}}, } api := &AWSSDClientStub{ @@ -557,6 +579,28 @@ func TestAWSSDProvider_CreateService(t *testing.T) { NamespaceId: aws.String("private"), } + // AAAA type + provider.CreateService(context.Background(), aws.String("private"), aws.String("AAAA-srv"), &endpoint.Endpoint{ + Labels: map[string]string{ + endpoint.AWSSDDescriptionLabel: "AAAA-srv", + }, + RecordType: endpoint.RecordTypeAAAA, + RecordTTL: 60, + Targets: endpoint.Targets{"::1234:5678:"}, + }) + expectedServices["AAAA-srv"] = &sdtypes.Service{ + Name: aws.String("AAAA-srv"), + Description: aws.String("AAAA-srv"), + DnsConfig: &sdtypes.DnsConfig{ + RoutingPolicy: sdtypes.RoutingPolicyMultivalue, + DnsRecords: []sdtypes.DnsRecord{{ + Type: sdtypes.RecordTypeAaaa, + TTL: aws.Int64(60), + }}, + }, + NamespaceId: aws.String("private"), + } + // CNAME type provider.CreateService(context.Background(), aws.String("private"), aws.String("CNAME-srv"), &endpoint.Endpoint{ Labels: map[string]string{ @@ -768,6 +812,19 @@ func TestAWSSDProvider_RegisterInstance(t *testing.T) { }}, }, }, + "aaaa-srv": { + Id: aws.String("aaaa-srv"), + Name: aws.String("service4"), + Description: aws.String("owner-id"), + DnsConfig: &sdtypes.DnsConfig{ + NamespaceId: aws.String("private"), + RoutingPolicy: sdtypes.RoutingPolicyWeighted, + DnsRecords: []sdtypes.DnsRecord{{ + Type: sdtypes.RecordTypeAaaa, + TTL: aws.Int64(100), + }}, + }, + }, }, } @@ -781,7 +838,7 @@ func TestAWSSDProvider_RegisterInstance(t *testing.T) { expectedInstances := make(map[string]*sdtypes.Instance) - // IP-based instance + // IPv4-based instance provider.RegisterInstance(context.Background(), services["private"]["a-srv"], &endpoint.Endpoint{ RecordType: endpoint.RecordTypeA, DNSName: "service1.private.com.", @@ -849,6 +906,20 @@ func TestAWSSDProvider_RegisterInstance(t *testing.T) { }, } + // IPv6-based instance + provider.RegisterInstance(context.Background(), services["private"]["aaaa-srv"], &endpoint.Endpoint{ + RecordType: endpoint.RecordTypeAAAA, + DNSName: "service4.private.com.", + RecordTTL: 300, + Targets: endpoint.Targets{"0000:0000:0000:0000:abcd:abcd:abcd:abcd"}, + }) + expectedInstances["0000:0000:0000:0000:abcd:abcd:abcd:abcd"] = &sdtypes.Instance{ + Id: aws.String("0000:0000:0000:0000:abcd:abcd:abcd:abcd"), + Attributes: map[string]string{ + sdInstanceAttrIPV6: "0000:0000:0000:0000:abcd:abcd:abcd:abcd", + }, + } + // validate instances for _, srvInst := range api.instances { for id, inst := range srvInst {