Published on

EKS에서 ExternalDNS가 Route53 레코드 생성 실패할 때

Authors

서버리스/컨테이너 환경에서 DNS 자동화는 운영 난이도를 크게 낮춰주지만, 그만큼 권한(Identity)네트워크(Reachability) 가 조금만 어긋나도 “아무 레코드도 안 만들어짐” 같은 애매한 증상으로 나타납니다. 특히 EKS에서 ExternalDNS를 Route53에 붙이면, 설치는 잘 되는데 레코드가 생성되지 않거나, 생성되었다가 롤백되거나, TXT 레코드만 남는 식의 문제가 자주 발생합니다.

이 글은 ExternalDNS가 Route53 레코드를 못 만들 때를 전제로, 원인을 빠르게 분류하고 재현/검증할 수 있는 실전 디버깅 절차를 정리합니다.

문제를 "증상"으로 분류하기

ExternalDNS 장애는 크게 4가지로 나뉩니다.

  1. AWS API 호출 자체가 실패: STS AssumeRole 실패, AccessDenied, Throttling 등
  2. 대상 Hosted Zone을 못 찾음: 도메인 필터/존 타입(퍼블릭/프라이빗)/권한 범위 문제
  3. 레코드 생성은 시도하지만 충돌: TXT registry ownership 충돌, 이미 존재하는 레코드와 타입 충돌
  4. 레코드가 생성되었는데 기대한 엔드포인트가 아님: Service/Ingress annotation, source 설정, ALB/NLB 타입 차이

가장 먼저 해야 할 일은 ExternalDNS 로그를 “에러 단위”로 읽을 수 있게 만드는 것입니다.

1) ExternalDNS 로그부터 제대로 보기

Pod 로그 확인

kubectl -n external-dns logs deploy/external-dns -f --tail=200

자주 보는 로그 패턴:

  • AccessDenied / UnauthorizedOperation → IAM/IRSA
  • NoCredentialProviders → IRSA 미적용 또는 서비스어카운트 토큰 문제
  • failed to list hosted zones → Route53 ListHostedZones 권한 또는 네트워크
  • Skipping... / No endpoints could be generated → source/annotation/ingress class
  • TXT 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-dns
    • aud: 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 충돌로 스킵/실패

해결책은 보통 둘 중 하나입니다.

  1. 클러스터별로 --txt-owner-id 를 고유하게 주고, 레코드 네임스페이스를 분리(서브도메인 분리 권장)
  2. 동일 레코드를 여러 클러스터가 다루지 않도록 설계

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) 재현 가능한 "최소" 디버깅 시나리오

문제를 빨리 좁히려면, 아래 순서로 “가정”을 하나씩 제거하는 게 좋습니다.

  1. IRSA 확인: aws sts get-caller-identity 성공?
  2. Route53 조회 확인: aws route53 list-hosted-zones 성공?
  3. Hosted Zone 매칭 확인: domain-filter/zone-type 재검토
  4. K8s 엔드포인트 생성 확인: Service/Ingress에 hostname이 있고, LB/Address가 존재?
  5. 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 네트워크로 즉시 분리하라.