- Published on
EKS에서 ExternalDNS가 Route53 레코드 생성 실패할 때
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버리스/컨테이너 환경에서 DNS 자동화는 운영 난이도를 크게 낮춰주지만, 그만큼 권한(Identity) 과 네트워크(Reachability) 가 조금만 어긋나도 “아무 레코드도 안 만들어짐” 같은 애매한 증상으로 나타납니다. 특히 EKS에서 ExternalDNS를 Route53에 붙이면, 설치는 잘 되는데 레코드가 생성되지 않거나, 생성되었다가 롤백되거나, TXT 레코드만 남는 식의 문제가 자주 발생합니다.
이 글은 ExternalDNS가 Route53 레코드를 못 만들 때를 전제로, 원인을 빠르게 분류하고 재현/검증할 수 있는 실전 디버깅 절차를 정리합니다.
문제를 "증상"으로 분류하기
ExternalDNS 장애는 크게 4가지로 나뉩니다.
- AWS API 호출 자체가 실패: STS AssumeRole 실패, AccessDenied, Throttling 등
- 대상 Hosted Zone을 못 찾음: 도메인 필터/존 타입(퍼블릭/프라이빗)/권한 범위 문제
- 레코드 생성은 시도하지만 충돌: TXT registry ownership 충돌, 이미 존재하는 레코드와 타입 충돌
- 레코드가 생성되었는데 기대한 엔드포인트가 아님: Service/Ingress annotation, source 설정, ALB/NLB 타입 차이
가장 먼저 해야 할 일은 ExternalDNS 로그를 “에러 단위”로 읽을 수 있게 만드는 것입니다.
1) ExternalDNS 로그부터 제대로 보기
Pod 로그 확인
kubectl -n external-dns logs deploy/external-dns -f --tail=200
자주 보는 로그 패턴:
AccessDenied/UnauthorizedOperation→ IAM/IRSANoCredentialProviders→ IRSA 미적용 또는 서비스어카운트 토큰 문제failed to list hosted zones→ Route53 ListHostedZones 권한 또는 네트워크Skipping.../No endpoints could be generated→ source/annotation/ingress classTXT registry관련 메시지 → ownership 충돌/레지스트리 설정
실행 인자(Args) 확인
kubectl -n external-dns get deploy external-dns -o jsonpath='{.spec.template.spec.containers[0].args}' | jq
문제 재현 시, 아래 플래그들이 특히 중요합니다.
--provider=aws--sources=service,ingress(어떤 리소스에서 DNS를 만들지)--domain-filter=example.com(너무 좁으면 아무것도 못 만듦)--zone-type=public|private--registry=txt/--txt-owner-id=...--policy=sync|upsert-only
2) IRSA(AssumeRole) 문제: 가장 흔한 1순위
EKS에서 ExternalDNS는 보통 IRSA(IAM Roles for Service Accounts) 로 Route53 권한을 얻습니다. 여기서 하나라도 어긋나면 레코드를 못 만듭니다.
(A) 서비스어카운트에 role-arn 어노테이션이 있는지
kubectl -n external-dns get sa external-dns -o yaml
기대값 예시:
metadata:
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/eks-externaldns
(B) OIDC 신뢰 정책(Trust Policy) 점검
Role의 Trust Relationship에 아래 조건이 정확해야 합니다.
- Federated:
arn:aws:iam::<ACCOUNT_ID>:oidc-provider/<OIDC_URL> - Condition:
sub:system:serviceaccount:external-dns:external-dnsaud:sts.amazonaws.com
sub가 namespace/sa 이름과 다르면 AssumeRoleWithWebIdentity 가 실패합니다.
(C) STS 접근 자체가 타임아웃/네트워크 문제일 수도
Private Subnet + NAT 미구성, 또는 STS/Route53 엔드포인트 접근 경로가 깨지면 AssumeRole이 타임아웃으로 실패합니다. 이 경우는 권한이 아니라 네트워크 이슈로 보입니다.
(D) Pod에서 실제로 어떤 IAM으로 호출되는지 빠르게 확인
ExternalDNS 컨테이너에 awscli가 없다면, 임시 디버그 Pod를 띄워 확인합니다.
kubectl -n external-dns run aws-debug --rm -it \
--image=public.ecr.aws/aws-cli/aws-cli:2 \
--serviceaccount=external-dns \
--command -- sh
쉘에서:
aws sts get-caller-identity
aws route53 list-hosted-zones --max-items 5
get-caller-identity가 실패하면 IRSA/STS/네트워크list-hosted-zones가 AccessDenied면 IAM policy
3) IAM Policy: "레코드 생성"에 필요한 최소 권한
ExternalDNS는 Hosted Zone을 찾고(List), 레코드를 변경(ChangeResourceRecordSets)하며, TXT registry를 쓰면 TXT 레코드도 관리합니다.
아래는 실무에서 자주 쓰는 최소 정책 예시(HostedZone ARN 제한 포함). Hosted Zone ID는 환경에 맞게 교체하세요.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"route53:ListHostedZones",
"route53:ListResourceRecordSets",
"route53:ListHostedZonesByName"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"route53:ChangeResourceRecordSets"
],
"Resource": "arn:aws:route53:::hostedzone/Z123456ABCDEFG"
}
]
}
추가로, ExternalDNS가 AWS API 호출에서 태그 기반 필터를 쓰거나(예: zone-id-filter), 다른 리소스 접근이 필요하면 권한이 더 필요할 수 있습니다.
권한 에러가 403 형태로 보일 때의 원인 분해는 아래 글의 체크리스트도 도움이 됩니다.
4) Hosted Zone을 "못 찾는" 케이스: domain-filter/zone-type 함정
ExternalDNS는 기본적으로 “내가 관리해야 할 존”을 찾은 뒤 레코드를 업데이트합니다. 여기서 필터가 잘못되면 에러 없이 스킵되기도 합니다.
(A) --domain-filter가 너무 좁거나, 실제 레코드 FQDN과 불일치
예를 들어 --domain-filter=example.com 인데 실제로 만들고 싶은 레코드가 app.dev.example.com 이면 괜찮습니다.
하지만 반대로 --domain-filter=dev.example.com 으로 해두고, Ingress hostname이 app.example.com 으로 나오면 ExternalDNS는 해당 엔드포인트를 무시합니다.
(B) Public/Private Hosted Zone 혼재
VPC에 연결된 Private Hosted Zone이 있고, 동일한 도메인에 Public Hosted Zone도 있는 경우가 있습니다. 이때 --zone-type=public 또는 --zone-type=private 를 명시하지 않으면 의도치 않은 존을 잡거나, 권한/조회가 꼬일 수 있습니다.
점검:
aws route53 list-hosted-zones-by-name --dns-name example.com
5) TXT Registry 충돌: "이미 누가 관리 중" 문제
ExternalDNS는 기본 registry로 TXT 레코드를 사용해 “이 레코드를 누가 관리하는지”를 기록합니다. 멀티 클러스터/멀티 환경에서 같은 도메인을 공유하면 아래 상황이 흔합니다.
- A 클러스터가
app.example.com을 만들고 TXT owner를cluster-a로 기록 - B 클러스터가 같은 이름을 만들려다 ownership 충돌로 스킵/실패
해결책은 보통 둘 중 하나입니다.
- 클러스터별로
--txt-owner-id를 고유하게 주고, 레코드 네임스페이스를 분리(서브도메인 분리 권장) - 동일 레코드를 여러 클러스터가 다루지 않도록 설계
Helm values 예시:
extraArgs:
- --registry=txt
- --txt-owner-id=eks-prod-01
- --txt-prefix=_externaldns.
이미 꼬여버린 경우, Route53에서 해당 레코드의 TXT를 확인하고 “정말로 내가 소유권을 가져와도 되는지” 판단한 뒤 정리해야 합니다.
6) "레코드가 안 만들어지는" 것처럼 보이지만, 사실 엔드포인트가 생성되지 않는 경우
ExternalDNS는 --source 로 지정한 Kubernetes 리소스에서 엔드포인트를 뽑아냅니다. 즉, Route53 이전에 Kubernetes 오브젝트 설정이 틀리면 아무것도 안 합니다.
(A) Service 타입/어노테이션 확인
대표적으로 Service에 아래 어노테이션이 필요합니다.
apiVersion: v1
kind: Service
metadata:
name: my-svc
annotations:
external-dns.alpha.kubernetes.io/hostname: app.example.com
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 8080
selector:
app: my-app
type: LoadBalancer가 아니면 외부 IP/호스트가 없어 레코드 생성이 안 될 수 있습니다(설계에 따라 다름).- Ingress를 source로 쓴다면 Ingress에 hostname annotation 또는 ingress spec host가 필요합니다.
(B) Ingress Controller와 함께 쓸 때: 실제 Address가 생겼는지
Ingress가 생성되어도 ALB/NLB 주소가 아직 할당되지 않으면 ExternalDNS가 보류할 수 있습니다.
kubectl get ingress -A -o wide
kubectl describe ingress <name>
7) 네트워크/DNS 이슈: Route53 API는 되는데 내부 DNS가 꼬인 경우도 있음
ExternalDNS가 AWS API를 호출하려면 다음이 안정적이어야 합니다.
- Pod → CoreDNS → 외부 DNS 해석
- Pod → STS/Route53 엔드포인트로 egress
Private Subnet에서 NAT 없이 돌리거나, VPC DNS 설정/프라이빗 링크 구성이 어긋나면 i/o timeout, no such host 류 에러가 납니다.
이 경우는 ExternalDNS 자체보다 클러스터 네트워킹/노드 상태가 원인일 수도 있습니다. CNI 초기화 문제로 노드가 불안정하면 egress가 간헐적으로 끊깁니다.
8) 재현 가능한 "최소" 디버깅 시나리오
문제를 빨리 좁히려면, 아래 순서로 “가정”을 하나씩 제거하는 게 좋습니다.
- IRSA 확인:
aws sts get-caller-identity성공? - Route53 조회 확인:
aws route53 list-hosted-zones성공? - Hosted Zone 매칭 확인: domain-filter/zone-type 재검토
- K8s 엔드포인트 생성 확인: Service/Ingress에 hostname이 있고, LB/Address가 존재?
- TXT ownership 확인: 동일 레코드를 다른 ExternalDNS가 잡고 있지 않나?
이 5단계를 통과하면, 대부분은 해결되거나 “어디가 문제인지”가 명확해집니다.
9) Helm 설치 예시(권장 베이스라인)
아래 values는 운영에서 자주 쓰는 보수적인 설정입니다.
provider: aws
policy: upsert-only
registry: txt
txtOwnerId: eks-prod-01
sources:
- service
- ingress
extraArgs:
- --domain-filter=example.com
- --zone-type=public
serviceAccount:
create: true
name: external-dns
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/eks-externaldns
적용 후에는 반드시 로그에서 다음을 확인하세요.
- Hosted Zone을 인식했는지
- Endpoints를 생성했는지
- Change batch가 성공했는지
마무리: "권한"과 "필터"가 80%, "레지스트리"가 15%, 나머지가 5%
EKS에서 ExternalDNS가 Route53 레코드를 못 만들 때, 체감상 대부분은 IRSA/권한 또는 Hosted Zone 필터링(domain-filter, zone-type) 에서 발생합니다. 그 다음이 TXT ownership 충돌이고, 마지막이 네트워크/DNS 같은 인프라 이슈입니다.
운영 관점에서의 팁은 두 가지입니다.
- ExternalDNS는 반드시
--registry=txt와 고유한--txt-owner-id를 써서 “누가 소유자인지”를 남겨라. - 문제가 생기면 ExternalDNS 로그만 보지 말고, 동일 서비스어카운트로
aws sts,aws route53를 직접 호출해 원인을 IAM vs ExternalDNS 설정 vs 네트워크로 즉시 분리하라.