AWS EKS Pod Identity 적용을 통한 External-DNS 사용

신동수·2025년 9월 18일

K8S

목록 보기
19/20

EKS 환경에서 Ingress를 통해 애플리케이션을 노출하면 ALB(Application Load Balancer)가 자동으로 생성된다. 하지만 생성된 ALB를 Route 53에 연결하려면 매번 수동으로 DNS 레코드를 등록해야 하는 불편함이 있다.

이를 자동화할 수 있는 방법을 찾던 중, ExternalDNS라는 오픈소스를 활용하면 Ingress 또는 Service 생성과 동시에 ALB, NLB를 Route 53에 자동으로 등록할 수 있다는 점을 알게 되었다. 이번 글에서는 ExternalDNS를 EKS 환경에 적용하는 과정과, 최근 추가된 EKS Pod Identity 기능을 함께 활용하는 방법을 정리하였다.

ExternalDNS란?

쿠버네티스 클러스터 내부에서 실행되는 컨트롤러(Controller)이며, Ingress, Service 등의 리소스를 감지하여 외부 DNS 서비스(Route 53, Cloudflare, Google DNS 등)에 자동으로 레코드를 생성/삭제/업데이트 해주는 오픈소스이다.

Ingress나 Service가 생성되면 ExternalDNS가 해당 리소스를 감지하고, Load Balancer의 DNS 이름을 가져와 지정된 DNS Provider(Route 53 등)에 도메인을 자동으로 등록한다. 리소스가 삭제되면 DNS 레코드도 정리되도록 설정할 수 있어 운영 자동화 측면에서 유용하다.

이를 통해 쿠버네티스 리소스와 DNS 관리 간의 수동 작업을 줄이고 운영 효율성을 크게 향상시킬 수 있다.

EKS Add-on: ExternalDNS

EKS에서는 ExternalDNS를 Add-On 기능을 제공하며, Kubernetes 리소스를 통해 Route53 DNS 레코드를 관리할 수 있다.

  • AWS가 EKS Add-on으로 제공하는 ExternalDNS는 Pod Identity 또는 IRSA로 동작한다.
  • 작성자는 Pod Identity 방식으로 클러스터에서 생성한 ServiceAccount ↔ Pod Identity ↔ IAM Role(다른 계정) 매핑 구조를 고려하였다.
  • 25년 9월 기준 ExternalDNS Add-on 에서는 --aws-assume-role 같은 Cross-Account 옵션을 제공하지 않는다.

제약 사항

  • ExternalDNS Add-on 기능은 동일 계정 내 Route53 Hosted Zone만 접근 가능하다.
  • 다른 계정의 Route53 Hosted Zone에 접근은 Add-on 의 구성 값을 넣어 적용해야한다.
    • Add-on은 Pod Identity에서 cross-account AssumeRole 설정을 arg 에 추가해야한다.

해결 방법

  • 동일 계정에서 Route53 관리 하는 경우
    - EKS Add-on ExternalDNS 그대로 사용 가능하다.
  • 다른 계정에서 Route53을 관리 하는 경우
    • Helm 차트나 Manifest 방식으로 ExternalDNS를 직접 설치를 해야한다.
    • 이때 --aws-assume-role을 args에 추가하고, cross-account trust 설정을 통해 다른 계정 Role을 AssumeRole 하도록 구성한다.

제한 사항

아래 아키텍처를 기준으로 구성을 진행하면서 정리한 제약 사항은 아래와 같다.

  • EKS Add-On 에서 제공하는 ExternalDNS는 사용X (25년 9월 기준 assumerole 미지원)
  • AWS LoadBalancer Controller 설치 필요 (Ingress에서 Annotations 사용)
  • Helm이 아닌 Manifest 방식으로 진행 (Helm 가능 - Helm 설치 참고 링크)

Architecture


동작 순서
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 - 클러스터 구성 계정

Account1에서는 EKS Cluster가 위치한 계정이며, Pod Identity를 추가 된 상태여야 한다.

EKS > 추가 기능 > 추가 기능가져오기
EKS 대시보드에서 Add-ons에서 추가 기능을 가져와서 필요한 기능을 사용할 수 있다.

IAM Role

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 기능 추가 후 아래와 같이 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 관리 계정

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 구성

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

ExternalDNS 예제

구성이 완료되었다면, 아래 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 파라미터를 추가하면 된다.

Public Zone & Public ALB


Private Zone & Private ALB


마무리

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/

profile
조금씩 성장하는 DevOps 엔지니어가 되겠습니다. 😄

0개의 댓글