Skip to content

feat(aws): support alias=A and alias=AAAA to create single record type#5997

Merged
k8s-ci-robot merged 10 commits into
kubernetes-sigs:masterfrom
u-kai:feat/disable-aaaa-aws-record
May 17, 2026
Merged

feat(aws): support alias=A and alias=AAAA to create single record type#5997
k8s-ci-robot merged 10 commits into
kubernetes-sigs:masterfrom
u-kai:feat/disable-aaaa-aws-record

Conversation

@u-kai
Copy link
Copy Markdown
Member

@u-kai u-kai commented Dec 7, 2025

What does it do ?

Fixes: #5815

This PR extends the existing alias annotation to support selective control over A and AAAA alias record creation:

  • alias: "A" - creates only an A ALIAS record (IPv4 only)
  • alias: "AAAA" - creates only an AAAA ALIAS record (IPv6 only)
    When a CNAME endpoint creates alias records by aws provider, external-dns automatically creates both A and AAAA alias records by default. These new values allow users to create only a specific record type on a per-endpoint basis.

Motivation

Issue #5815 requested the ability to selectively disable AAAA alias records for specific use cases. Currently, the only option is the global --exclude-record-types=AAAA flag, which affects all records across all providers.

This feature is particularly important for same-zone alias records where CNAME endpoints point to other records within the same hosted zone. While AWS managed services (like ELBs, CloudFront) typically support both IPv4 and IPv6, custom applications and services within the same zone often have different connectivity requirements.

Unlike AWS managed resources which are generally dual-stack capable, user-managed records in the same zone frequently have single-stack limitations, making this granular control essential for proper DNS configuration.

This enhancement provides the targeted control needed for these scenarios while maintaining full backward compatibility.

More

  • Yes, this PR title follows Conventional Commits
  • Yes, I added unit tests
  • Yes, I updated end user documentation accordingly

…r-specific options)

Signed-off-by: u-kai <76635578+u-kai@users.noreply.github.com>
@k8s-ci-robot k8s-ci-robot added the provider Issues or PRs related to a provider label Dec 7, 2025
@k8s-ci-robot k8s-ci-robot requested a review from szuecs December 7, 2025 06:39
@k8s-ci-robot k8s-ci-robot added the needs-ok-to-test Indicates a PR that requires an org member to verify it is safe to test. label Dec 7, 2025
@k8s-ci-robot
Copy link
Copy Markdown
Contributor

Hi @u-kai. Thanks for your PR.

I'm waiting for a github.com member to verify that this patch is reasonable to test. If it is, they should reply with /ok-to-test on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should join the org to skip this step.

Once the patch is verified, the new status will be reflected by the ok-to-test label.

I understand the commands that are listed here.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@k8s-ci-robot k8s-ci-robot added size/M Denotes a PR that changes 30-99 lines, ignoring generated files. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. labels Dec 7, 2025
@mloiseleur
Copy link
Copy Markdown
Collaborator

@u-kai thanks for this interesting PR.

Wdyt of re-use existing annotation, external-dns.alpha.kubernetes.io/alias, (doc here) instead of using a new one ?

It could be configurable like this :

  • external-dns.alpha.kubernetes.io/alias: false => no aliases
  • external-dns.alpha.kubernetes.io/alias: true => A & AAAA aliases
  • external-dns.alpha.kubernetes.io/alias: "A" => only A aliases
  • external-dns.alpha.kubernetes.io/alias: "AAAA" => only AAAA aliases

@u-kai
Copy link
Copy Markdown
Member Author

u-kai commented Dec 7, 2025

@mloiseleur
Thank you for the suggestion! I hadn’t thought about reusing the existing annotation at all — that’s a great idea. Your proposal looks much cleaner and more flexible, so I’ll switch to this approach.

@ivankatliarchuk
Copy link
Copy Markdown
Member

/ok-to-test

@k8s-ci-robot k8s-ci-robot added ok-to-test Indicates a non-member PR verified by an org member that is safe to test. and removed needs-ok-to-test Indicates a PR that requires an org member to verify it is safe to test. labels Dec 8, 2025
@coveralls
Copy link
Copy Markdown

coveralls commented Dec 8, 2025

Coverage Report for CI Build 25616170037

Coverage increased (+0.01%) to 80.596%

Details

  • Coverage increased (+0.01%) from the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • 20 coverage regressions across 1 file.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

20 previously-covered lines in 1 file lost coverage.

File Lines Losing Coverage Coverage
aws/aws.go 20 89.04%

Coverage Stats

Coverage Status
Relevant Lines: 21393
Covered Lines: 17242
Line Coverage: 80.6%
Coverage Strength: 1450.86 hits per line

💛 - Coveralls

Copy link
Copy Markdown
Member

@ivankatliarchuk ivankatliarchuk left a comment

Choose a reason for hiding this comment

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

Documentation with use cases for this properties is missing, and how it was tested on a real cluster

Comment thread provider/aws/aws.go Outdated
return endpoints, nil
}

func aliasDisableARecord(ep *endpoint.Endpoint) bool {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

My opinion, we will benefit to have a generic,centralised method on Endpoint object for all annotations that contains booleans like true/false

Copy link
Copy Markdown
Member

@ivankatliarchuk ivankatliarchuk Dec 8, 2025

Choose a reason for hiding this comment

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

something like

func (ep *endpoint.Endpoint) GetProviderSpecificBool(key string bool) bool {
	val, ok := ep.GetProviderSpecificProperty(key)
	if !ok {
		return false
	}
	// Normalize whitespace and case; accept common truthy values
	v := strings.TrimSpace(strings.ToLower(val))
	switch v {
	case "1", "t", "true", "yes", "y":
		return true
	case "0", "f", "false", "no", "n":
		return false
	default:		
		return false
	}
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

That does sound like a good idea.
However, in this change we’re going to use values other than just true/false, as @mloiseleur mentioned, and I’m a bit concerned that introducing a generic, centralized method here would make the scope of this PR too large.
I’d prefer to handle that refactor in a separate PR that can apply the change across the whole codebase. What do you think?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ok

…ewAaaaIfNeeded

Signed-off-by: u-kai <76635578+u-kai@users.noreply.github.com>
@u-kai
Copy link
Copy Markdown
Member Author

u-kai commented Dec 9, 2025

@mloiseleur @ivankatliarchuk

While creating comprehensive tests for supporting annotations like external-dns.alpha.kubernetes.io/alias: "AAAA", I encountered an inconsistent behavior in the existing code.

Should I include this fix in this PR, or would it be better to submit it as a separate PR?
In the latter case, I would work on this PR after the new PR is merged first.

Discovered Issue

When a ProviderSpecific property alias=true exists on record types that don't support alias records
(like MX records):

  1. Alias processing continues even after alias property deletion

    • The alias property is deleted at aws.go, but the alias variable remains true
    • This results in unnecessary evaluateTargetHealth=false being added
  2. TTL value gets unintentionally modified

    • When RecordTTL is configured, it gets fixed to 300

Reproduction

ep := &endpoint.Endpoint{
    RecordType: endpoint.RecordTypeMX,
    RecordTTL:  600,
    ProviderSpecific: endpoint.ProviderSpecific{
        {Name: "alias", Value: "true"},
    },
}
// Expected: TTL=600, ProviderSpecific=empty
// But result is: TTL=300, evaluateTargetHealth=false gets added

While this affects cases with incorrectly configured ProviderSpecific properties, I believe the behavior should be consistent.

@mloiseleur
Copy link
Copy Markdown
Collaborator

Should I include this fix in this PR, or would it be better to submit it as a separate PR?

It's better as a separate PR. It's easier to review & test this way.
It's also more clear for Changelog / Release notes.

@u-kai
Copy link
Copy Markdown
Member Author

u-kai commented Dec 12, 2025

@mloiseleur

I’ve submitted the fix in a separate PR here: #6017.
When you have a moment, I’d appreciate it if you could take a look.

@ivankatliarchuk
Copy link
Copy Markdown
Member

Refactoring, bug fixes, new features - all should be in they own PRs. We are trying to de-resk releases. Way too many issues opened ;-)

Copy link
Copy Markdown
Member

@ivankatliarchuk ivankatliarchuk left a comment

Choose a reason for hiding this comment

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

No update to:

  • docs/tutorials/aws.md
  • docs/examples/aws.md

This should include:

New provider-specific annotations:
external-dns.alpha.kubernetes.io/aws-alias-disable-a: "true"
external-dns.alpha.kubernetes.io/aws-alias-disable-aaaa: "true"

or 

external-dns.alpha.kubernetes.io/alias-disable-a: "true"
external-dns.alpha.kubernetes.io/alias-disable-aaaa: "true"

And more important, what are the use cases, as at the moment this is just added annotation for no reasons

Comment thread provider/aws/aws.go Outdated
}
if enableAandAAAA {
// Add a new endpoint for the AAAA record
aliasCnameAaaaEndpoints = append(aliasCnameAaaaEndpoints, &endpoint.Endpoint{
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Make sure not to use &endpoint.Endpoint{}, but methods available in the package. This will create tech debt for us.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd say it should be clone or similar function in Endpoint package, to make sure we do not mutate original

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

aEp := ep.DeepCopy()

Comment thread provider/aws/aws.go Outdated

func aliasDisableARecord(ep *endpoint.Endpoint) bool {
disable, ok := ep.GetProviderSpecificProperty(providerSpecificAliasDisableA)
return ok && disable == "true"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Use constants instead of strings for "true".

Comment thread provider/aws/aws.go Outdated
disableAlias := disableA && disableAaaa
enableAandAAAA := !disableA && !disableAaaa

if ep.RecordType == endpoint.RecordTypeCNAME && !disableAlias {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Something does not sound here. This most likely could and should be a method on it's own, so we could add tests just for this method

something like

log.Debugf("Modifying endpoint: %v, changing record type from %s to %s", ep, ....)
if ep.RecordType == endpoint.RecordTypeCNAME && strings.CutPrefix(k, disableAliasPrefix) {
   epModified = .....modify me...
   or 
   epC = ep.DeepCopy()
   epC.RecordType = modifyMe(......)
}

@ivankatliarchuk
Copy link
Copy Markdown
Member

I'm unsure how big is the problem. At the moment this change affects only AWS. It could be that same behaviour is relevant for other providers as well. In such case we could have annotation without a provider prefix, and modify type in post processor wrapper https://github.com/kubernetes-sigs/external-dns/blob/master/source/wrappers/post_processor.go

@ivankatliarchuk
Copy link
Copy Markdown
Member

@u-kai thanks for this interesting PR.

Wdyt of re-use existing annotation, external-dns.alpha.kubernetes.io/alias, (doc here) instead of using a new one ?

It could be configurable like this :

* `external-dns.alpha.kubernetes.io/alias: false` => no aliases

* `external-dns.alpha.kubernetes.io/alias: true` => A & AAAA aliases

* `external-dns.alpha.kubernetes.io/alias: "A"` => only A aliases

* `external-dns.alpha.kubernetes.io/alias: "AAAA"` => only AAAA aliases

I'm not sure about 4 state solution, need to double check. Same time this feets wrapper case, so it should not be in aws provider.

@ivankatliarchuk
Copy link
Copy Markdown
Member

Would be nice to get some sort of mermaid diagram https://mermaid.live/edit or similar before and after change behavoir hightlight.

@u-kai
Copy link
Copy Markdown
Member Author

u-kai commented Dec 20, 2025

@ivankatliarchuk

My understanding is that alias itself is an AWS-specific feature.
However, in the current codebase, some alias-related behavior has already
leaked into non-provider layers (e.g. txt-registry).

Given that, I think it’s worth taking a step back and clarifying
where alias-related logic should live. I’ll revisit this part.

@u-kai
Copy link
Copy Markdown
Member Author

u-kai commented Dec 20, 2025

@ivankatliarchuk

I spent some time reading through the codebase, and my current understanding is:

  • Alias itself is an AWS-specific concern, but due to historical reasons it has leaked into the txt-registry.
    Ideally, I would like to push alias-related logic back into the AWS provider.
  • However, doing so would change the TXT record naming, and without a careful migration strategy
    this would be difficult to do safely.
  • On the other hand, embedding AWS-specific logic into Endpoint seems problematic as well,
    since it would make Endpoint depend on a concrete provider.
    That also makes it unclear, going forward, whether similar logic should live in Endpoint or in providers.

Given these trade-offs, I think it’s not obvious what the right approach is here yet.
I’d like to take a step back and think through this more carefully.

To make the trade-offs more explicit, here is how I see the current vs desired dependency graph:
(Current: txt-registry contains AWS alias special-casing; Desired: txt-registry stays provider-agnostic and AWS alias logic is fully contained in the AWS provider.)

Current

flowchart LR
    EndpointPkg["endpoint package
(provider-agnostic)"]

    TxtRegistry["registry/txt
(TXT Registry)"]

    AwsProvider["provider/aws
(AWS provider)"]

    ProviderIface["provider interface"]

    %% code dependencies (imports)
    TxtRegistry -->|depends on| EndpointPkg
    TxtRegistry -->|depends on| ProviderIface

    AwsProvider -->|depends on| ProviderIface
    AwsProvider -->|depends on| EndpointPkg

    %% problematic knowledge leak
    TxtRegistry -->|knows AWS alias semantics| AwsProvider

    %% emphasis
    TxtRegistry:::problem
Loading

My desired

flowchart LR
    EndpointPkg["endpoint package
(provider-agnostic)"]

    TxtRegistry["registry/txt
(TXT Registry)"]

    AwsProvider["provider/aws
(AWS provider)"]

    ProviderIface["provider interface"]


    %% code dependencies (imports)
    TxtRegistry -->|depends on| EndpointPkg
    TxtRegistry -->|depends on| ProviderIface

    AwsProvider -->|depends on| ProviderIface
    AwsProvider -->|depends on| EndpointPkg
Loading

@u-kai
Copy link
Copy Markdown
Member Author

u-kai commented Dec 20, 2025

Personally, even if it takes time, I’d like to focus on eventually removing
alias-specific details from the TXT registry.
I’m not yet sure whether this is fully achievable in practice, but I think
it’s a direction worth exploring.

@k8s-ci-robot k8s-ci-robot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label Feb 7, 2026
@u-kai u-kai changed the title feat(aws): add aws/alias-disable-a and aws/alias-disable-aaaa provider-specific options feat(aws): support alias=A and alias=AAAA to create single record type Feb 7, 2026
u-kai added 2 commits February 7, 2026 21:25
Signed-off-by: u-kai <76635578+u-kai@users.noreply.github.com>
Signed-off-by: u-kai <76635578+u-kai@users.noreply.github.com>
@u-kai
Copy link
Copy Markdown
Member Author

u-kai commented Feb 7, 2026

I introduced AliasType to represent the intent at the Endpoint level (A-only / AAAA-only / both / none).

Today, alias semantics are effectively only implemented by the AWS provider, and other providers have very different behaviors and constraints. Based on the provider docs, trying to centralize too much alias logic in endpoint would likely cause abstraction leaks.

So the Endpoint only carries the minimal, provider-agnostic intent, and each provider is responsible for mapping that intent to its own record model.

@ivankatliarchuk
Copy link
Copy Markdown
Member

I have similar concerns here as in the previous PR #6017

While the implementation itself looks reasonable, it feels like we’re again compensating at the provider layer for situations that should ideally be handled earlier. Carrying this logic into the provider risks increasing hidden behavior and makes it harder to reason about correctness across providers.

As a suggestion, it may be worth revisiting whether these cases could be normalized before provider-specific processing, rather than teaching each provider how to interpret this annotations.

Basically ExternalDNS should enforce provider-agnostic semantics before provider code runs. The proposed responsibility is wrong from my view and does not scale.

@ivankatliarchuk
Copy link
Copy Markdown
Member

We don't have specific normalization middleware at the moment. We could reuse one of the most approapriate middleware or create new one.

@ivankatliarchuk
Copy link
Copy Markdown
Member

If this contributing docs missing designs https://github.com/kubernetes-sigs/external-dns/blob/master/docs/contributing/source-wrappers.md worth to update them as well.

If the value of this annotation is `true`, specifies that CNAME records generated by the
resource should instead be alias records.

Additionally, you can set the value to `A` or `AAAA` to create only one type of alias record:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Missing full example, hard to understand how to configure it.

example

apiVersion: v1
kind: RESOURCE
metadata:
  name: app-assets-dns
  namespace: default
  annotations:
    xternal-dns.alpha.kubernetes.io/ABRAKADABRA: ABRAKADABRA

# Result:
# DNS -> TARGET (CNAME)

Signed-off-by: u-kai <76635578+u-kai@users.noreply.github.com>
@u-kai u-kai force-pushed the feat/disable-aaaa-aws-record branch from 304115a to c65b5f5 Compare February 11, 2026 05:20
@u-kai
Copy link
Copy Markdown
Member Author

u-kai commented Feb 11, 2026

Validation of invalid combinations is being handled separately in the CheckEndpoint PR.

Given that, I’m not entirely sure what additional responsibility we should move into the common layer in this PR. From my perspective, this change is intentionally kept minimal and only exposes the intent, while leaving provider-specific interpretation to each provider.

If there are specific cases you think should still be normalized earlier, I’d be happy to discuss them.

@ivankatliarchuk
Copy link
Copy Markdown
Member

Is this supported/not supported?

@k8s-ci-robot k8s-ci-robot added the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Mar 30, 2026
@k8s-ci-robot k8s-ci-robot removed the needs-rebase Indicates a PR cannot be merged because it has merge conflicts with HEAD. label Apr 22, 2026
@u-kai u-kai force-pushed the feat/disable-aaaa-aws-record branch from d7de208 to 33fea72 Compare April 22, 2026 13:00
@u-kai
Copy link
Copy Markdown
Member Author

u-kai commented Apr 22, 2026

Rebased onto the latest master. The implementation is done — the issue will be resolved once this PR is merged.

Comment thread registry/txt/registry.go
Comment on lines +300 to +303
func shouldUseCNAMEForTxtRecord(ep *endpoint.Endpoint) bool {
aliasType := ep.GetAliasProperty()
return (aliasType == endpoint.AliasTrue || aliasType == endpoint.AliasA) && ep.RecordType == endpoint.RecordTypeA
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@u-kai Thanks for the rebase. You have addressed many comments 👍 .
I have still one, though: What should be the behavior of this func when aliasType is endpoint.AliasAAAA ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@mloiseleur
Thanks for the comments!

AliasAAAA doesn't need any special handling in shouldUseCNAMEForTxtRecord.

The CNAME encoding for alias A records may exist for backward compatibility: before #3910, alias records were represented as RecordTypeCNAME in external-dns.
When that PR changed them to RecordTypeA, the TXT ownership records had to keep using "cname" as the encoded type.

The alias AAAA record was never represented as CNAME, so its TXT record should use "AAAA" as the record type.

@mloiseleur
Copy link
Copy Markdown
Collaborator

/lgtm

@k8s-ci-robot k8s-ci-robot added the lgtm "Looks good to me", indicates that a PR is ready to be merged. label May 10, 2026
@ivankatliarchuk
Copy link
Copy Markdown
Member

I'm on the fence. This functionality is not AWS specific. Should not be in aws provider

@u-kai
Copy link
Copy Markdown
Member Author

u-kai commented May 17, 2026

The alias intent (annotation parsing, preferAlias, AliasType definition) is already centralized in the common layer.

But each provider handles that intent differently — for example,
AWS creates both A and AAAA records for a CNAME alias because LBs are dual-stack, while other providers may behave differently.

Is there a specific part you think should be moved earlier that we're missing?

@ivankatliarchuk
Copy link
Copy Markdown
Member

ok. let's have a look

/approve

@k8s-ci-robot
Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: ivankatliarchuk

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@k8s-ci-robot k8s-ci-robot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label May 17, 2026
@k8s-ci-robot k8s-ci-robot merged commit 48cfd5d into kubernetes-sigs:master May 17, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. cncf-cla: yes Indicates the PR's author has signed the CNCF CLA. docs lgtm "Looks good to me", indicates that a PR is ready to be merged. ok-to-test Indicates a non-member PR verified by an org member that is safe to test. provider Issues or PRs related to a provider registry Issues or PRs related to a registry size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(aws): Add ProviderSpecific option to disable A/AAAA Alias record creation for Route53

5 participants