关于 external-dns
面向 Kubernetes 运维人员的 External-DNS 操作技能,提供多 DNS 提供商的配置、最佳实践与故障排查,重点涵盖 Azure 和 Cloudflare。
主要功能
- 01配置 Azure、Cloudflare、AWS 和 Google DNS
- 02设置服务与 Ingress 注解
- 03提供环境配置建议
- 04排查身份验证与 DNS 同步问题
适用场景
- 01为 Kubernetes 服务配置自动 DNS 管理
- 02部署 External-DNS 到 Azure 或 Cloudflare
- 03诊断 DNS 记录未同步或权限错误
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ julianobarbosa/ claude-code-skills --skill external-dns安装后在 Claude Code 中输入/external-dns确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/azure-dns.mdcloudflare.mdtroubleshooting.md详细介绍
External-DNS 技能
在 Kubernetes 集群中执行完整的 External-DNS 操作,实现 DNS 自动管理。
概述
External-DNS 会将对外暴露的 Kubernetes Service 和 Ingress 与 DNS 提供商同步,从而免去手动管理 DNS 记录。本技能涵盖多个 DNS 提供商的配置、最佳实践和故障排除,重点介绍 Azure 和 Cloudflare。
提供商快速参考
| 提供商 | 身份验证方法 | 状态 | 参考资料 |
|---|---|---|---|
| Azure DNS | Workload Identity(推荐)或 Service Principal | 稳定 | references/azure-dns.md |
| Cloudflare | API Token | 测试版 | references/cloudflare.md |
| AWS Route53 | IRSA(推荐)或访问密钥 | 稳定 | 下文 |
| Google Cloud DNS | Workload Identity | 稳定 | 下文 |
Helm Values 基本结构
# kubernetes-sigs/external-dns chart (v1.18.0+)
fullnameOverride: external-dns
provider:
name: <provider> # azure, cloudflare, aws, google
# Sources to watch
sources:
- service
- ingress
# Domain restrictions
domainFilters:
- example.com
# Policy: sync (creates/updates/deletes) or upsert-only (creates/updates only)
policy: upsert-only # Recommended for production
# Sync interval
interval: "5m"
# TXT record ownership (MUST be unique per cluster)
txtOwnerId: "aks-cluster-name"
txtPrefix: "_externaldns."
# Logging
logLevel: info # debug, info, warning, error
logFormat: json
# Resources
resources:
requests:
memory: "64Mi"
cpu: "25m"
limits:
memory: "128Mi"
# cpu: REMOVED per best practice (no CPU limits)
# Security context
securityContext:
runAsNonRoot: true
runAsUser: 65534
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
# Prometheus metrics
serviceMonitor:
enabled: true
interval: 30s
Azure DNS 配置
Workload Identity(推荐)
provider:
name: azure
serviceAccount:
labels:
azure.workload.identity/use: "true"
annotations:
azure.workload.identity/client-id: "<MANAGED_IDENTITY_CLIENT_ID>"
podLabels:
azure.workload.identity/use: "true"
env:
- name: AZURE_TENANT_ID
value: "<TENANT_ID>"
- name: AZURE_SUBSCRIPTION_ID
value: "<SUBSCRIPTION_ID>"
- name: AZURE_RESOURCE_GROUP
value: "<DNS_ZONE_RESOURCE_GROUP>"
domainFilters:
- example.com
txtOwnerId: "aks-cluster-name"
policy: upsert-only
interval: "5m"
Azure RBAC 所需权限
# Assign DNS Zone Contributor role to the managed identity
az role assignment create \
--role "DNS Zone Contributor" \
--assignee "<MANAGED_IDENTITY_OBJECT_ID>" \
--scope "/subscriptions/<SUB_ID>/resourceGroups/<RG>/providers/Microsoft.Network/dnszones/<ZONE>"
# For Private DNS Zones
az role assignment create \
--role "Private DNS Zone Contributor" \
--assignee "<MANAGED_IDENTITY_OBJECT_ID>" \
--scope "/subscriptions/<SUB_ID>/resourceGroups/<RG>/providers/Microsoft.Network/privateDnsZones/<ZONE>"
Service Principal 替代方案
provider:
name: azure
env:
- name: AZURE_TENANT_ID
value: "<TENANT_ID>"
- name: AZURE_SUBSCRIPTION_ID
value: "<SUBSCRIPTION_ID>"
- name: AZURE_RESOURCE_GROUP
value: "<DNS_ZONE_RESOURCE_GROUP>"
- name: AZURE_CLIENT_ID
valueFrom:
secretKeyRef:
name: azure-credentials
key: client-id
- name: AZURE_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: azure-credentials
key: client-secret
Cloudflare 配置
provider:
name: cloudflare
env:
- name: CF_API_TOKEN
valueFrom:
secretKeyRef:
name: cloudflare-api-token
key: cloudflare_api_token
extraArgs:
cloudflare-proxied: true # Enable CDN/DDoS protection
cloudflare-dns-records-per-page: 5000 # Optimize API calls
domainFilters:
- example.com
txtOwnerId: "aks-cluster-name"
policy: upsert-only
Cloudflare API Token 权限
- Zone:Read - 列出区域
- DNS:Edit - 创建/更新/删除 DNS 记录
- Zone Resources:所有区域或指定区域
AWS Route53 配置(IRSA)
provider:
name: aws
env:
- name: AWS_DEFAULT_REGION
value: "us-east-1"
serviceAccount:
annotations:
eks.amazonaws.com/role-arn: "arn:aws:iam::<ACCOUNT_ID>:role/external-dns"
extraArgs:
aws-zone-type: public # or private
aws-batch-change-size: 4000
domainFilters:
- example.com
txtOwnerId: "eks-cluster-name"
AWS IAM 策略要求
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["route53:ChangeResourceRecordSets"],
"Resource": ["arn:aws:route53:::hostedzone/*"]
},
{
"Effect": "Allow",
"Action": ["route53:ListHostedZones", "route53:ListResourceRecordSets"],
"Resource": ["*"]
}
]
}
Google Cloud DNS 配置
provider:
name: google
env:
- name: GOOGLE_PROJECT
value: "<GCP_PROJECT_ID>"
serviceAccount:
annotations:
iam.gke.io/gcp-service-account: "external-dns@<PROJECT_ID>.iam.gserviceaccount.com"
domainFilters:
- example.com
txtOwnerId: "gke-cluster-name"
Kubernetes 资源注解
基本用法
# On Service or Ingress
metadata:
annotations:
external-dns.alpha.kubernetes.io/hostname: "app.example.com"
external-dns.alpha.kubernetes.io/ttl: "300"
多个主机名
metadata:
annotations:
external-dns.alpha.kubernetes.io/hostname: "app1.example.com,app2.example.com"
提供商专属注解
# Cloudflare - disable proxy for specific record
external-dns.alpha.kubernetes.io/cloudflare-proxied: "false"
# AWS Route53 - create ALIAS record
external-dns.alpha.kubernetes.io/alias: "true"
# Custom TTL
external-dns.alpha.kubernetes.io/ttl: "60"
特定环境的最佳实践
开发环境
policy: sync # Auto-delete orphaned records
interval: "1m" # Fast sync for rapid iteration
logLevel: info
resources:
requests:
memory: "50Mi"
cpu: "10m"
limits:
memory: "50Mi"
生产环境
policy: upsert-only # NEVER auto-delete
interval: "10m" # Conservative to reduce API load
logLevel: error # Minimal logging
# High Availability
replicaCount: 2
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values: [external-dns]
topologyKey: kubernetes.io/hostname
podDisruptionBudget:
enabled: true
minAvailable: 1
priorityClassName: high-priority
常用命令
# Check external-dns pods
kubectl get pods -n external-dns
# View logs
kubectl logs -n external-dns deployment/external-dns --tail=100 -f
# Check configuration
kubectl get deployment external-dns -n external-dns -o yaml | grep -A20 args
# Verify DNS records (Cloudflare)
dig @1.1.1.1 app.example.com
# Verify DNS records (Azure)
az network dns record-set list -g <RESOURCE_GROUP> -z example.com -o table
# Check TXT ownership records
dig TXT _externaldns.app.example.com
# Force restart
kubectl rollout restart deployment external-dns -n external-dns
# Dry-run mode (add to extraArgs)
extraArgs:
dry-run: true
关键指标
# Total endpoints managed
external_dns_registry_endpoints_total
# Sync errors
external_dns_controller_sync_errors_total
# Last sync timestamp
external_dns_controller_last_sync_timestamp_seconds
# DNS records by type
external_dns_registry_a_records
external_dns_registry_aaaa_records
external_dns_registry_cname_records
ArgoCD ApplicationSet 模式
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: external-dns
namespace: argocd
spec:
generators:
- list:
elements:
- cluster: dev
branch: main
- cluster: prd
branch: main
template:
metadata:
name: 'external-dns-{{cluster}}'
spec:
project: infrastructure
sources:
- chart: external-dns
repoURL: https://kubernetes-sigs.github.io/external-dns/
targetRevision: "1.18.0"
helm:
releaseName: external-dns
valueFiles:
- $values/argo-cd-helm-values/kube-addons/external-dns/{{cluster}}/values.yaml
- repoURL: https://your-repo.git
targetRevision: "{{branch}}"
ref: values
destination:
server: '{{url}}'
namespace: external-dns
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
安全检查清单
- 使用 Workload Identity/IRSA,而非静态凭据
- 为 DNS 区域授予最小权限
- 设置
runAsNonRoot: true和readOnlyRootFilesystem: true - 为每个集群使用唯一的
txtOwnerId - 将
domainFilters限制为必要的域名 - 将 API Token 存储在 Kubernetes Secrets 中
- 启用 Pod Security Standards(restricted)
- 在生产环境中使用
policy: upsert-only
参考资料
references/azure-dns.md- Azure DNS 完整配置指南references/cloudflare.md- Cloudflare 完整配置指南references/troubleshooting.md- 常见问题及解决方案- 官方文档:https://kubernetes-sigs.github.io/external-dns/
- Helm chart:https://artifacthub.io/packages/helm/external-dns/external-dns
常见陷阱
- 不同集群的
txtOwnerId冲突会静默破坏 DNS: 两个集群使用相同的所有者 ID 时,会在协调过程中互相覆盖对方的记录,直至记录全部消失。务必使用“集群名称 + 环境”(例如aks-example-app-prd),并使用dig TXT _externaldns.<host>验证。 - 当记录名称符合匹配模式时,
policy: sync会删除并非由 External-DNS 创建的记录: 如果手动创建的 A 记录与受管理的主机名匹配,它将在下一次协调时被删除。生产环境必须使用upsert-only;只有开发集群才使用sync。 - K8s 侧的 RBAC 和 DNS 提供商凭据缺一不可: External-DNS 既需要读取 Ingress/Service 对象,也需要写入 DNS 区域的权限。只读 DNS 凭据会导致操作静默无效,并且被监视的资源不会生成任何事件——只有 Pod 日志会显示身份验证错误。
- Workload Identity 需要三项配置,而不是一项: ServiceAccount 注解 + Pod 标签 + 托管身份上的联合凭据。缺少联合凭据时会出现
ManagedIdentityCredential: 400错误,看起来像是令牌问题,实际却是身份绑定问题。 domainFilters使用前缀匹配,而非精确匹配: 如果恶意 Ingress 声明了该主机名,domainFilters: [example.com]也会管理evil-example.com。在多租户集群上使用--exclude-domains或更严格的过滤规则。- Azure Private DNS Zone 需要不同于公共 DNS 区域的角色: “DNS Zone Contributor” 仅适用于公共 DNS 区域;私有 DNS 区域需要“Private DNS Zone Contributor”。如果分配了错误的角色,只有在首次触发记录同步时才会返回 403,而不是在部署时。
评论
支持 Markdown