external-dns 与 kube-ingress-aws-controller 集成指南:在 AWS 上为 ALB/NLB Ingress 自动管理 Route53 记录

原创2026-09-24 13:08:461,899 阅读
文章标签:云原生

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 后端。

验证与故障排查

  1. 确认源已生效:查看 external-dns 日志,应能看到 Endpoints generated from ingress: <namespace>/<name> 级别的 Debug 输出(对应 source/ingress.go)。
  2. 确认 DNS 记录已生成:在 AWS Console 的 Route53 中检查目标托管区,应出现 A/AAAA 别名记录以及以 heritage=external-dns 开头的 TXT 记录;TXTT 记录表明这些别名记录由 external-dns 托管,因此与其它方式管理的记录共存是安全的。
  3. 确认 LoadBalancer status 已写入:external-dns 依赖 Ingress/RouteGroup 的 status.loadBalancer.ingress 获取目标地址,如果记录未生成,先检查 kube-ingress-aws-controller 是否正常回写 status,以及 RBAC 是否允许其 patch/update 对应 status 子资源。
  4. 检查 CRD 依赖:使用 --source=skipper-routegroup 时若启动失败,按 source/skipper_routegroup.go 的逻辑检查 routegroups.zalando.org CRD 是否已安装。
登录后查看全文
external-dns