external-dns 与 kube-ingress-aws-controller 集成指南:在 AWS 上为 ALB/NLB Ingress 自动管理 Route53 记录
external-dns 与 kube-ingress-aws-controller 集成指南:在 AWS 上为 ALB/NLB Ingress 自动管理 Route53 记录
导读
本教程面向在 AWS EKS 集群中使用 Zalando 系 Ingress 方案(kube-ingress-aws-controller 自动创建 ALB/NLB,配合 Skipper 做 HTTP 路由)的开发者,讲解如何把 external-dns 接入该体系,实现"Ingress 一创建、DNS 自动生效"的端到端流程。读完本文你将掌握:以 --source=ingress 模式对接 Ingress 对象并生成 Route53 ALIAS 记录、用 --ingress-class 收敛源范围、通过注解切换 NLB 与 Dualstack 负载均衡、以及借助 --source=skipper-routegroup 处理 Skipper RouteGroup CRD 的完整实战方案,并理解 external-dns 在 source/ingress.go 与 source/skipper_routegroup.go 中对应的底层实现原理。
前置准备:先按 AWS 教程完成基础部署
在接入 kube-ingress-aws-controller 之前,请先按照 AWS 教程 完成 external-dns 在 AWS 上的基础配置,包括:
- 创建允许修改 Route53 的 IAM 策略(
route53:ChangeResourceRecordSets、route53:ListResourceRecordSets、route53:ListTagsForResources、route53:ListHostedZones); - 通过 Node IAM Role、静态凭证、IRSA(推荐)或 EKS Pod Identity 之一授予凭证;
- 创建托管区(Hosted Zone)并将域名的 NS 指向 AWS 分配的 nameserver;
- 部署 external-dns Deployment(RBAC 或非 RBAC 两种形态)。
在 Kubernetes 集群中运行 external-dns 时,需要指定 --source=ingress 参数,让 external-dns 从 Ingress 对象中提取 hostname 作为 DNS 记录的来源:
args:
- --source=ingress
- --provider=aws
- --policy=upsert-only
- --registry=txt
- --txt-owner-id=my-hostedzone-identifier
从源码看,--source=ingress 对应的 NewIngressSource 会通过 kubeinformers.NewSharedInformerFactoryWithOptions 建立 Ingress 资源的 shared informer,监听 add/update/delete 事件;Endpoints 方法在每次同步时遍历缓存的 Ingress,从中生成 *endpoint.Endpoint 列表,最终由控制器与 Route53 比对并执行变更。
可选:用 ingress-class 过滤源
如果你的集群中存在多个 Ingress Controller(例如同时运行 ingress-nginx 与 kube-ingress-aws-controller),可以通过 --ingress-class 参数限定 external-dns 只消费指定 class 的 Ingress,避免误管理其它控制器创建的对象。该参数并非必需,但推荐在混合环境中启用。
其底层逻辑位于 filterByIngressClass:当配置了 ingressClassNames 时,external-dns 会先匹配 spec.ingressClassName 字段,若不满足再回退匹配 kubernetes.io/ingress.class 注解(IngressClassAnnotationKey)。同时 NewIngressSource 会校验:--ingress-class 与基于 kubernetes.io/ingress.class 的注解过滤器互斥,二者不能同时配置。
部署示例应用:echoserver
创建下面的示例应用,用来演示 external-dns 如何为 kube-ingress-aws-controller 创建的 Ingress 生成 DNS 记录:
apiVersion: apps/v1
kind: Deployment
metadata:
name: echoserver
spec:
replicas: 1
selector:
matchLabels:
app: echoserver
template:
metadata:
labels:
app: echoserver
spec:
containers:
- image: gcr.io/google_containers/echoserver:1.4
imagePullPolicy: Always
name: echoserver
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: echoserver
spec:
ports:
- port: 80
targetPort: 8080
protocol: TCP
type: ClusterIP
selector:
app: echoserver
注意这里的 Service 类型是 ClusterIP 而非 LoadBalancer。这是因为在 kube-ingress-aws-controller 架构中,HTTP 路由统一交给共享的 Skipper 实例完成:Skipper 以 hostNetwork 模式运行,直接接收来自 AWS 负载均衡器(ALB/NLB)的流量,因此每个应用不需要独立的 LoadBalancer Service。ALB 或 NLB 会按需创建,并在默认情况下为所有 Ingress 共享。
Ingress 示例:基于 host 的多域名路由
创建下面的 Ingress 将 echoserver 暴露到公网:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: echoserver
spec:
ingressClassName: skipper
rules:
- host: echoserver.mycluster.example.org
http: &echoserver_root
paths:
- path: /
backend:
service:
name: echoserver
port:
number: 80
pathType: Prefix
- host: echoserver.example.org
http: *echoserver_root
应用后,kube-ingress-aws-controller 会在 AWS 上创建一个(IPv4)ALB,将流量转发给 Skipper,再由 Skipper 转发到 echoserver 应用。
当指定了 --source=ingress 时,external-dns 会基于 Ingress 对象中 spec.rules[].host 的取值创建 DNS 记录。上述示例将为 echoserver.mycluster.example.org 和 echoserver.example.org 各创建两条别名记录(A 和 AAAA),四条记录全部指向与该 Ingress 关联的 ALB。由于 ALB 是纯 IPv4 的,AAAA 别名记录实际不起作用。
从 endpointsFromIngress 的实现可以看到 hostname 的完整提取路径:优先遍历 ing.Spec.Rules 中非空的 rule.Host,再遍历 ing.Spec.TLS[].Hosts(若未通过 --ignore-ingress-tls-spec 禁用),最后收集 external-dns.kubernetes.io/hostname 注解中的主机名;目标(target)则来自 targetsFromIngressStatus,即读取 Ingress status.loadBalancer.ingress 中由控制器写入的 IP 与 Hostname。
禁止生成 AAAA 记录
如果希望 external-dns 完全不创建 AAAA 记录,可以追加命令行参数:
--exclude-record-types=AAAA
该参数对应配置项 ExcludeDNSRecordTypes,定义在 pkg/apis/externaldns/types.go 并由 --exclude-record-types 标志位绑定(可多次指定以排除多种类型,见 types.go)。被排除的记录类型会从管理范围内剔除(参见 controller/controller.go 的 ExcludeRecordTypes 字段)。请注意:此参数会全局禁用 AAAA 记录创建,即使负载均衡器启用了 dualstack 也不例外。
基于注解的多别名 Ingress
上述示例利用了 YAML 锚点(anchor)特性,避免为多个使用完全相同路径的 host 重复编写 http 段。如果该 Ingress 只代理一个后端 Service,可以改写成下面这种更简洁的形式:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
external-dns.kubernetes.io/hostname: echoserver.mycluster.example.org, echoserver.example.org
name: echoserver
spec:
ingressClassName: skipper
rules:
- http:
paths:
- path: /
backend:
service:
name: echoserver
port:
number: 80
pathType: Prefix
这里创建了一条对任何 hostname 都生效的默认路径,并利用 external-dns.kubernetes.io/hostname 注解为最终生成的 ALB 创建多个别名记录。注解中的多个主机名以英文逗号分隔,对应源码中的 HostnamesFromAnnotations → extractHostnamesFromAnnotations → SplitHostnameAnnotation:该函数会去除首尾空白并按逗号切分主机名列表。
使用 NLB 替代 ALB
AWS 同时提供 ALB 和 NLB 两种负载均衡器,kube-ingress-aws-controller 通过 zalando.org/aws-load-balancer-type 注解(默认值为 alb)决定创建哪种类型。将该注解设置为 nlb,external-dns 就会为目标生成 NLB 对应的别名记录:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
zalando.org/aws-load-balancer-type: nlb
name: echoserver
spec:
ingressClassName: skipper
rules:
- host: echoserver.example.org
http:
paths:
- path: /
backend:
service:
name: echoserver
port:
number: 80
pathType: Prefix
应用上述 Ingress 后,kube-ingress-aws-controller 会创建 NLB,并把负载均衡器信息写入 Ingress 的 status 字段,你可以用 kubectl get ingress -o yaml 观察:
status:
loadBalancer:
ingress:
- hostname: kube-ing-lb-atedkrlml7iu-1681027139.$region.elb.amazonaws.com
external-dns 会为 echoserver.example.org 创建 A 和 AAAA 两条别名记录,并借助别名记录自动维护 NLB 的 IP 地址——当 NLB 实例变化时无需修改 DNS 记录本身。
Dualstack 负载均衡器
AWS 为 ALB 和 NLB 同时支持纯 IPv4 与 "dualstack"(同时支持 IPv4 和 IPv6)两种网络接口。kube-ingress-aws-controller 通过 alb.ingress.kubernetes.io/ip-address-type 注解(默认值为 ipv4)控制这一行为:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
alb.ingress.kubernetes.io/ip-address-type: dualstack
name: echoserver
spec:
ingressClassName: skipper
rules:
- host: echoserver.example.org
http:
paths:
- path: /
backend:
service:
name: echoserver
port:
number: 80
pathType: Prefix
上述 Ingress 会创建一个具有 dualstack 网络接口的 ALB。需要注意:external-dns 默认会同时创建 A 和 AAAA 两种别名记录,无论该注解如何设置;若希望只创建 A 记录,仍使用前文提到的 --exclude-record-types=AAAA 参数。
可选进阶:Skipper RouteGroup
kube-ingress-aws-controller、Skipper 与 external-dns 三方都支持 RouteGroup CRD。RouteGroup 允许在 Skipper 中实现复杂的路由能力。external-dns 需要以 --source=skipper-routegroup 参数启动,才能处理 RouteGroup 对象:
--source=skipper-routegroup
从源码可以确认,skipper-routegroup 是一个独立的 Source 类型(见 source/types/types.go 的 SkipperRouteGroup Type),对应实现为 routeGroupSource。其 NewRouteGroupSource 在启动时会先探测 RouteGroup CRD 是否已安装:如果 routegroups.zalando.org 资源缺失,会直接返回错误 --source=skipper-routegroup requires the RouteGroup CRD (routegroups.zalando.org) to be installed,避免启动后长时间空转。
第一步:安装 RouteGroup CRD
首先需要将 RouteGroup CRD 应用到集群:
kubectl apply -f https://github.com/zalando/skipper/blob/HEAD/dataclients/kubernetes/deploy/apply/routegroups_crd.yaml
第二步:配置 RBAC 权限
你需要授予所有三个控制器读取 routegroup 资源的权限,并授予 kube-ingress-aws-controller 更新 routegroup status 字段的权限。如果你启用了 RBAC,可以为三个控制器共用以下 ClusterRole:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kube-ingress-aws-controller
rules:
- apiGroups:
- extensions
- networking.k8s.io
resources:
- ingresses
verbs:
- get
- list
- watch
- apiGroups:
- extensions
- networking.k8s.io
resources:
- ingresses/status
verbs:
- patch
- update
- apiGroups:
- zalando.org
resources:
- routegroups
verbs:
- get
- list
- watch
- apiGroups:
- zalando.org
resources:
- routegroups/status
verbs:
- patch
- update
可以参考 Zalando kubernetes-on-aws 项目中三份现成的 RBAC 清单:kube-ingress-aws-controller 的 01-rbac.yaml、skipper 的 rbac.yaml、external-dns 的 01-rbac.yaml。对本仓库而言,external-dns 侧所需的最小权限主要体现在上述规则的 routegroups 部分——注意 external-dns 只读 routegroups(get/list/watch),不需要写权限。
第三步:一个带 HTTPS 重定向的 RouteGroup 示例
RouteGroup 能力很丰富,这里仅展示一个带自定义 HTTPS 重定向的简单示例(包含一个应用后端和一个 shunt 后端):
apiVersion: zalando.org/v1
kind: RouteGroup
metadata:
name: my-route-group
spec:
backends:
- name: my-backend
type: service
serviceName: my-service
servicePort: 80
- name: redirectShunt
type: shunt
defaultBackends:
- backendName: my-service
routes:
- pathSubtree: /
- pathSubtree: /
predicates:
- Header("X-Forwarded-Proto", "http")
filters:
- redirectTo(302, "https:")
backends:
- redirectShunt
external-dns 对 RouteGroup 的处理逻辑位于 endpointsFromRouteGroup:它会从 rg.Spec.Hosts 中提取显式声明的 host,并从 external-dns.kubernetes.io/hostname 注解中提取主机名;目标取自 targetsFromRouteGroupStatus(即 RouteGroup status.loadBalancer.routeGroup 中的 IP/Hostname),与 Ingress 的 status 读取方式保持一致。上述示例中两个 pathSubtree: / 路由通过 predicate 区分明文 HTTP 请求并执行 302 重定向到 HTTPS,其余流量默认转发到 my-service 后端。
验证与故障排查
- 确认源已生效:查看 external-dns 日志,应能看到
Endpoints generated from ingress: <namespace>/<name>级别的 Debug 输出(对应 source/ingress.go)。 - 确认 DNS 记录已生成:在 AWS Console 的 Route53 中检查目标托管区,应出现 A/AAAA 别名记录以及以
heritage=external-dns开头的 TXT 记录;TXTT 记录表明这些别名记录由 external-dns 托管,因此与其它方式管理的记录共存是安全的。 - 确认 LoadBalancer status 已写入:external-dns 依赖 Ingress/RouteGroup 的
status.loadBalancer.ingress获取目标地址,如果记录未生成,先检查 kube-ingress-aws-controller 是否正常回写 status,以及 RBAC 是否允许其patch/update对应 status 子资源。 - 检查 CRD 依赖:使用
--source=skipper-routegroup时若启动失败,按 source/skipper_routegroup.go 的逻辑检查routegroups.zalando.orgCRD 是否已安装。