Skip to content
125 changes: 125 additions & 0 deletions docs/tutorials/aws-load-balancer-controller.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,128 @@ spec:

The above Ingress object will result in the creation of an ALB with a dualstack
interface.

## Frontend Network Load Balancer (NLB)

The AWS Load Balancer Controller supports [fronting ALBs with an NLB][6] for improved performance
and static IP addresses. When this feature is enabled, the controller creates both an ALB and an
NLB, resulting in two hostnames in the Ingress status.

[6]: https://kubernetes-sigs.github.io/aws-load-balancer-controller/latest/guide/ingress/annotations/#enable-frontend-nlb

### Known Issue with Internal ALBs

When using an internal ALB (`alb.ingress.kubernetes.io/scheme: internal`) with frontend NLB,
ExternalDNS may create DNS records pointing to the ALB instead of the NLB due to alphabetical
ordering:

- Internal ALB hostname: `internal-k8s-myapp-alb.us-east-1.elb.amazonaws.com`
- NLB hostname: `k8s-myapp-nlb-123456789.elb.us-east-1.amazonaws.com`

When multiple targets exist, Route53 selects the first one alphabetically, which incorrectly
selects the internal ALB. See [issue #5661][7] for details.

[7]: https://github.com/kubernetes-sigs/external-dns/issues/5661

### Workarounds

There are several approaches to ensure DNS records point to the correct (NLB) target:

#### Option 1: Combine load balancer naming with target annotation (Recommended)

Use [`alb.ingress.kubernetes.io/load-balancer-name`][8] to create predictable hostnames, then
explicitly reference the NLB using [`external-dns.alpha.kubernetes.io/target`][9]:

[8]: https://kubernetes-sigs.github.io/aws-load-balancer-controller/latest/guide/ingress/annotations/#load-balancer-name
[9]: https://kubernetes-sigs.github.io/external-dns/latest/docs/annotations/annotations/#external-dnsalphakubernetesiotarget

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
alb.ingress.kubernetes.io/scheme: internal
alb.ingress.kubernetes.io/enable-frontend-nlb: "true"
alb.ingress.kubernetes.io/frontend-nlb-scheme: internal
alb.ingress.kubernetes.io/load-balancer-name: myapp-alb
external-dns.alpha.kubernetes.io/target: k8s-myapp-nlb.elb.us-east-1.amazonaws.com
name: echoserver
spec:
ingressClassName: alb
rules:
- host: echoserver.example.org
http:
paths:
- path: /
backend:
service:
name: echoserver
port:
number: 80
pathType: Prefix
```

**Benefits**:

- Predictable, consistent load balancer naming across environments
- Explicit control over which target ExternalDNS uses
- Works reliably with internal ALBs
- No need to lookup auto-generated NLB names

**NLB hostname pattern**: When you set `load-balancer-name: myapp-alb`, the NLB hostname
becomes `k8s-myapp-nlb.elb.<region>.amazonaws.com` (note the `-nlb` suffix).

**ALB internal hostname pattern**: When you set `load-balancer-name: myapp-alb`, the ALB hostname
becomes `internal-myapp-nlb.<region>.elb.amazonaws.com` (note the `internal-` suffix).

#### Option 2: Use the target annotation only

If you cannot control the load balancer name, explicitly specify the NLB hostname:

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
alb.ingress.kubernetes.io/scheme: internal
alb.ingress.kubernetes.io/enable-frontend-nlb: "true"
alb.ingress.kubernetes.io/frontend-nlb-scheme: internal
external-dns.alpha.kubernetes.io/target: k8s-myapp-nlb-123456789.elb.us-east-1.amazonaws.com
name: echoserver
spec:
ingressClassName: alb
rules:
- host: echoserver.example.org
http:
paths:
- path: /
backend:
service:
name: echoserver
port:
number: 80
pathType: Prefix
```

**Note**: You'll need to lookup the auto-generated NLB hostname after the controller creates it.

#### Option 3: Use a DNSEndpoint resource

Create a [`DNSEndpoint`][10] custom resource to explicitly define the DNS record:

```yaml
apiVersion: externaldns.k8s.io/v1alpha1
kind: DNSEndpoint
metadata:
name: echoserver-dns
spec:
endpoints:
- dnsName: echoserver.example.org
recordType: CNAME
targets:
- k8s-myapp-nlb-123456789.elb.us-east-1.amazonaws.com
```

This approach is useful when you want to manage DNS records independently of the Ingress resource.

[10]:https://kubernetes-sigs.github.io/external-dns/latest/docs/tutorials/crd/
76 changes: 76 additions & 0 deletions source/ingress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1611,6 +1611,82 @@ func TestIngressWithConfiguration(t *testing.T) {
{DNSName: "abc.example.com", Targets: endpoint.Targets{"1.2.3.4"}, RecordType: endpoint.RecordTypeA},
},
},
{
title: "ingress with when AWS ALB controller and NLB type generates two targets for CNAME",
ingresses: []*networkv1.Ingress{
{
ObjectMeta: metav1.ObjectMeta{
Name: "my-ingress",
Namespace: "default",
Annotations: map[string]string{
"alb.ingress.kubernetes.io/enable-frontend-nlb": "true",
"alb.ingress.kubernetes.io/frontend-nlb-scheme": "internal",
},
},
Spec: networkv1.IngressSpec{
IngressClassName: testutils.ToPtr("alb"),
Rules: []networkv1.IngressRule{
{Host: "some.subdomain.mydomain.com"},
},
},
Status: networkv1.IngressStatus{
LoadBalancer: networkv1.IngressLoadBalancerStatus{
Ingress: []networkv1.IngressLoadBalancerIngress{
{Hostname: "internal-k8s-some-domain.us-east-1.elb.amazonaws.com"},
{Hostname: "k8s-another-domain-nlb-123456789.elb.us-east-1.amazonaws.com"},
},
},
},
},
},
expected: []*endpoint.Endpoint{
{
DNSName: "some.subdomain.mydomain.com",
RecordType: endpoint.RecordTypeCNAME,
Targets: endpoint.Targets{
"internal-k8s-some-domain.us-east-1.elb.amazonaws.com",
"k8s-another-domain-nlb-123456789.elb.us-east-1.amazonaws.com",
},
},
},
},
{
title: "ingress with when AWS ALB controller and NLB with target annotation and CNAME with single target",
ingresses: []*networkv1.Ingress{
{
ObjectMeta: metav1.ObjectMeta{
Name: "my-ingress",
Namespace: "default",
Annotations: map[string]string{
"alb.ingress.kubernetes.io/enable-frontend-nlb": "true",
"alb.ingress.kubernetes.io/frontend-nlb-scheme": "internal",
annotations.TargetKey: "k8s-another-domain-nlb-123456789.elb.us-east-1.amazonaws.com",
},
},
Spec: networkv1.IngressSpec{
IngressClassName: testutils.ToPtr("alb"),
Rules: []networkv1.IngressRule{
{Host: "some.subdomain.mydomain.com"},
},
},
Status: networkv1.IngressStatus{
LoadBalancer: networkv1.IngressLoadBalancerStatus{
Ingress: []networkv1.IngressLoadBalancerIngress{
{Hostname: "internal-k8s-some-domain.us-east-1.elb.amazonaws.com"},
{Hostname: "k8s-another-domain-nlb-123456789.elb.us-east-1.amazonaws.com"},
},
},
},
},
},
expected: []*endpoint.Endpoint{
{
DNSName: "some.subdomain.mydomain.com",
RecordType: endpoint.RecordTypeCNAME,
Targets: endpoint.Targets{"k8s-another-domain-nlb-123456789.elb.us-east-1.amazonaws.com"},
},
},
},
} {
t.Run(tt.title, func(t *testing.T) {
kubeClient := fake.NewClientset()
Expand Down
Loading