EKS 환경에서 Ingress를 통해 애플리케이션을 노출하면 ALB(Application Load Balancer)가 자동으로 생성된다. 하지만 생성된 ALB를 Route 53에 연결하려면 매번 수동으로 DNS 레코드를 등록해야 하는 불편함이 있다.
이를 자동화할 수 있는 방법을 찾던 중, ExternalDNS라는 오픈소스를 활용하면 Ingress 또는 Service 생성과 동시에 ALB, NLB를 Route 53에 자동으로 등록할 수 있다는 점을 알게 되었다. 이번 글에서는 ExternalDNS를 EKS 환경에 적용하는 과정과, 최근 추가된 EKS Pod Identity 기능을 함께 활용하는 방법을 정리하였다.
쿠버네티스 클러스터 내부에서 실행되는 컨트롤러(Controller)이며, Ingress, Service 등의 리소스를 감지하여 외부 DNS 서비스(Route 53, Cloudflare, Google DNS 등)에 자동으로 레코드를 생성/삭제/업데이트 해주는 오픈소스이다.
Ingress나 Service가 생성되면 ExternalDNS가 해당 리소스를 감지하고, Load Balancer의 DNS 이름을 가져와 지정된 DNS Provider(Route 53 등)에 도메인을 자동으로 등록한다. 리소스가 삭제되면 DNS 레코드도 정리되도록 설정할 수 있어 운영 자동화 측면에서 유용하다.
이를 통해 쿠버네티스 리소스와 DNS 관리 간의 수동 작업을 줄이고 운영 효율성을 크게 향상시킬 수 있다.
EKS에서는 ExternalDNS를 Add-On 기능을 제공하며, Kubernetes 리소스를 통해 Route53 DNS 레코드를 관리할 수 있다.
--aws-assume-role 같은 Cross-Account 옵션을 제공하지 않는다.--aws-assume-role을 args에 추가하고, cross-account trust 설정을 통해 다른 계정 Role을 AssumeRole 하도록 구성한다.아래 아키텍처를 기준으로 구성을 진행하면서 정리한 제약 사항은 아래와 같다.

동작 순서
1. EKS 클러스터의 ExternalDNS 는 Ingress 및 Service 객체를 계속 감시한다.
2. 도메인 이름이 포함된 ExternalDNS 주석을 포함하여 객체 중 하나가 생성되면 ExternalDNS는 IP 또는 ALB DNS or NLB DNS에 대한 세부 정보를 수집한다.
3. Route 53에 DNS 레코드를 생성하거나 업데이트한다 .
4. ExternalDNS가 Route 53에서 변경 작업을 수행할 수 있도록 하기 위해 Pod Identity Agent 플러그인은 할당된 IAM 역할에서 필요한 권한을 사용한다.
Account1에서는 EKS Cluster가 위치한 계정이며, Pod Identity를 추가 된 상태여야 한다.
EKS > 추가 기능 > 추가 기능가져오기
EKS 대시보드에서 Add-ons에서 추가 기능을 가져와서 필요한 기능을 사용할 수 있다.


Account2의 Route53을 이용해야 하기에, 아래와 같은 정책 및 신뢰 관계를 추가한다.
# Policy
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"sts:AssumeRole"
],
"Resource": [
"arn:aws:iam::<ACCOUNT2>:role/<ROLE_NAME>"
]
}
]
}
# Trust RelationShip
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "pods.eks.amazonaws.com"
},
"Action": [
"sts:AssumeRole",
"sts:TagSession"
]
}
]
}
Pod Identity 기능 추가 후 아래와 같이 Pod Identity 구성에서 위와 같이 구성한 IAM Role을 추가하며, 네임스페이스 및 서비스 계정은 external-dns로 하였다.

참고로 세션 태그 활성화를 한다면 아래와 같은 에러가 external-dns Pod에서 확인되며, sts:TagSession권한을 추가해도 권한이 없다는 에러가 발생하기에 세션 태그 비활성화를 해야한다.
kubectl logs external-dns-private-6477444cd4-cjbm7 -n external-dns
time="2025-09-16T14:04:18Z" level=error msg="Failed to do run once: soft error\nrecords retrieval failed: soft error\nfailed to list hosted zones: operation error Route 53: ListHostedZones, get identity: get credentials: failed to refresh cached credentials, operation error STS: AssumeRole, https response error StatusCode: 403, RequestID: f5aabe55-fcac-431c-9c9f-e8ae392646f8, api error AccessDenied: User: arn:aws:sts::<ACCOUNT1>:assumed-role/<ACCOUNT1_ROLE_NAME>/eks-test-eks-external-d-97c131ca-c46c-4a7b-81e5-9e319714ff45 is not authorized to perform: sts:TagSession on resource: arn:aws:iam::<ACCOUNT2>:role/<ACCOUNT2_ROLE_NAME> (consecutive soft errors: 1)"
Account2에서는 Route53을 관리하는 계정이며, Account1에서 AssumeRole을 통해 Route53에 접근할 수 있는 권한과 신뢰 관계를 구성해야 한다.
참고한 최소 권한은 Github - ExternalDNS 링크를 참고했다.
# Policy
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"route53:ChangeResourceRecordSets"
],
"Resource": [
"arn:aws:route53:::hostedzone/*"
]
},
{
"Effect": "Allow",
"Action": [
"route53:ListHostedZones",
"route53:ListResourceRecordSets",
"route53:ListTagsForResources"
],
"Resource": [
"*"
]
}
]
}
# Trust RelationShip
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Statement1",
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::<ACCOUNT1>:role/<ACCOUNT1_ROLE_NAME>"
]
},
"Action": [
"sts:AssumeRole",
"sts:TagSession"
]
}
]
}
ExternalDNS는 Helm 또는 Manifest 방식이 있는데, 작성자는 Manifest 방식을 통해 진행했다.
Helm을 사용하여 확인을 하고 싶다면 ExternalDNS-on-Amazon-EKS 링크를 참고하자.
구성에 필요한 yaml은 아래와 같이 구성을 했다.
external-dns.yaml파일에서 Deployment가 각 각 존재하는 것을 볼 수 있는데, 이것은 Public 및 Private 영역을 구분하기 위해서 분리를 했다. → Public 도메인은 Public Hosted Zone에만 등록, Private 도메인은 Private Hosted Zone에만 등록
또한, 각 파라미터는 아래의 링크를 참고하며 수정했다.
Helm Package
Github ExternalDNS
ExternalDNS Docs
# sa.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: external-dns
namespace: external-dns
# ns.yaml
apiVersion: v1
kind: Namespace
metadata:
name: external-dns
# external-dns.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: external-dns
labels:
app.kubernetes.io/name: external-dns
rules:
- apiGroups: [""]
resources: ["services","endpoints","nodes","pods"]
verbs: ["get","list","watch"]
- apiGroups: ["extensions","networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get","list","watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: external-dns-viewer
labels:
app.kubernetes.io/name: external-dns
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: external-dns
subjects:
- kind: ServiceAccount
name: external-dns
namespace: external-dns
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns-public
namespace: external-dns
spec:
replicas: 1
selector:
matchLabels:
app: external-dns-public
template:
metadata:
labels:
app: external-dns-public
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.k8s.io/external-dns/external-dns:v0.18.0
args:
- --source=ingress
- --provider=aws
- --policy=upsert-only # 레코드 생성,업데이트 (삭제 불가능)
- --registry=noop # txt 유형의 레코드 등록 X
- --txt-owner-id=external-dns
- --aws-assume-role=arn:aws:iam::<ACCOUNT2>:role/<ACCOUNT2_ROLE_NAME>
- --aws-zone-type=public
- --label-filter=external-dns-zone=public # 라벨 기반 필터링 (Ingress 에 라벨 추가 필요)
- --domain-filter=<DOMAIN> # 도메인이 2개 이상이면 파라미터 제거
- --exclude-record-types=AAAA # AAAA 레코드는 등록 X
# - --aws-prefer-cname # CNAME 으로 등록 필요 시
env:
- name: AWS_DEFAULT_REGION
value: ap-northeast-2
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: external-dns-private
namespace: external-dns
spec:
replicas: 1
selector:
matchLabels:
app: external-dns-private
template:
metadata:
labels:
app: external-dns-private
spec:
serviceAccountName: external-dns
containers:
- name: external-dns
image: registry.k8s.io/external-dns/external-dns:v0.18.0
args:
- --source=ingress
- --provider=aws
- --policy=upsert-only # 레코드 생성,업데이트 (삭제 불가능)
- --registry=noop # txt 유형의 레코드 등록 X
- --txt-owner-id=external-dns
- --aws-assume-role=arn:aws:iam::<ACCOUNT2>:role/<ACCOUNT2_ROLE_NAME>
- --aws-zone-type=private
- --label-filter=external-dns-zone=private # 라벨 기반 필터링 (Ingress 에 라벨 추가 필요)
- --domain-filter=<DOMAIN> # 도메인이 2개 이상이면 파라미터 제거
- --exclude-record-types=AAAA # AAAA 레코드는 등록 X
# - --aws-prefer-cname # CNAME 으로 등록 필요 시
env:
- name: AWS_DEFAULT_REGION
value: ap-northeast-2
구성이 완료되었다면, 아래 yaml을 배포하여 Route53에 등록여부를 확인해보자.
ingress-public, ingress-priavate가 따로 있는 것은 external-dns-zone: public 라벨에 따라 Public or Private 영역에 등록 여부가 나뉜다.
#deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx
name: nginx
ports:
- containerPort: 80
name: http
# svc.yaml
apiVersion: v1
kind: Service
metadata:
name: nginx
spec:
type: ClusterIP
selector:
app: nginx
ports:
- name: http
port: 80
targetPort: 80
# ingress-public.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-ingress-public
namespace: default
labels:
external-dns-zone: public
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
# External-DNS
external-dns.alpha.kubernetes.io/hostname: <DOMAIN>
# ALB Subnet
alb.ingress.kubernetes.io/subnets: <PUBLIC_SUBNET1,PUBLIC_SUBNET2>
# ALB Security Group
alb.ingress.kubernetes.io/security-groups: <SECURITY_GROUP_ID>
# ALB Listener 설정
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP":80}]'
spec:
rules:
- host: <DOMAIN>
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx
port:
number: 80
# ingress-private.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: nginx-ingress-private
namespace: default
labels:
external-dns-zone: private
annotations:
kubernetes.io/ingress.class: alb
alb.ingress.kubernetes.io/scheme: internal
alb.ingress.kubernetes.io/target-type: ip
# External-DNS
external-dns.alpha.kubernetes.io/hostname: <DOMAIN>
# ALB Subnet
alb.ingress.kubernetes.io/subnets: <PRIVATE_SUBNET1,PRIVATE_SUBNET2>
# ALB Security Group
alb.ingress.kubernetes.io/security-groups: <SECURITY_GROUP_ID>
# ALB Listener 설정
alb.ingress.kubernetes.io/listen-ports: '[{"HTTP":80}]'
spec:
rules:
- host: <DOMAIN>
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: nginx
port:
number: 80
external-dns Pod의 로그 확인 시 아래와 같이 확인할 수 있다.
time="2025-09-16T14:51:49Z" level=info msg="Desired change: CREATE <DOMAIN> A" profile=default zoneID=/hostedzone/<HOSTEDZONE_ID> zoneName=<ZONE_NAME>.
time="2025-09-16T14:51:49Z" level=info msg="1 record(s) were successfully updated" profile=default zoneID=/hostedzone/HOSTEDZONE_ID zoneName=<ZONE_NAME>.
time="2025-09-16T14:52:49Z" level=info msg="Applying provider record filter for domains: [<ZONE_NAME>. .<ZONE_NAME>.]"
아래 결과 확인 시 지정한 도메인으로 Route53에 A레코드로 등록이 된 것을 확인 할 수 있었다. 만약, CNAME으로 등록을 하고 싶다면 위 external-dns.yaml 파일에서 --aws-prefer-cname 파라미터를 추가하면 된다.




ExternalDNS를 활용해 Route 53 DNS 레코드를 자동화하는 방법과, Pod Identity를 이용한 cross-account 구성까지 정리했다.
ExternalDNS를 적용하면 DNS 등록/삭제 작업을 수동으로 할 필요가 없어지고, 운영 효율성을 확보할 수 있다고 생각했다. 특히, 기업에서는 멀티 계정 환경으로 사용하고 있을텐데 Route53을 관리하는 경우에는 --aws-assume-role 옵션과 Pod Identity를 적절히 조합해 활용하면 좋을 것 같다.
EKS를 운영하면서 DNS 관련 수동 작업을 줄이고 싶다면, ExternalDNS는 좋은 선택지라고 생각한다.
참고
https://devopscube.com/setup-externaldns-on-eks/#best-practices
https://repost.aws/knowledge-center/eks-set-up-externaldns
https://artifacthub.io/packages/helm/bitnami/external-dns
https://github.com/kubernetes-sigs/external-dns/blob/master/docs/tutorials/aws.md
https://kubernetes-sigs.github.io/external-dns/latest/