본문 바로가기
DevOps

[ExternalDNS] Kubernetes + AWS Route53, Bind9 연동으로 자동 DNS 레코드 생성하기

by interlude-3 2025. 12. 9.

✔ 테스트 환경 

Kubernetes 환경

  • OS: Windows 10
  • WSL2 (Ubuntu 24.04)
  • Docker Desktop (WSL2 backend 사용)
  • kind 클러스터

네트워크 / LoadBalancer 환경

  • kind는 LoadBalancer 타입 Service를 기본 제공하지 않음
  • Metallb를 설치하여 LoadBalancer 기능 대체

 

✔ DNS Provider 및 ExternalDNS 정보

 

DNS Provider: AWS Route53

  • Public Hosted Zone 사용
  • Public Domain : lake-devops.click
  • ExternalDNS가 Route53 API를 호출하여 DNS 레코드 자동 생성

ExternalDNS 인증 방식

  • IAM AccessKey 기반 인증
  • Credentials 파일을 Kubernetes Secret으로 저장

ExternalDNS 설치 정보

  • Helm chart: kubernetes-sigs/external-dns
  • Provider: AWS
  • Domain Filter: lake-devops.click
  • Registry: TXT (충돌 방지용 OwnerID 관리)

 

✔  테스트 실행 순서

  1. ExternalDNS, Ingress Controller, Metallb 설치
  2. Sample Service or Ingress 배포
  3. ExternalDNS 로그에서 DNS 레코드 생성 확인
  4. AWS Route53에서 A 레코드 자동 생성 확인

 


첫번째 실습 (ExternalDNS + DNS Provider : AWS Route53)

[AWS]

 └─  Route53 Authoritative DNS Server

[WSL Ubuntu]
 └─ kind cluster
      ├─ ExternalDNS (provider: aws)

      ├─ ingress-nginx controller
      └─ test ingress & service
 

1. AWS에서 퍼블릭 도메인 구매

- Route 53 ->  등록된 도메인 -> 도메인 등록  (요청 후 10분정도 소요)

 

2. AWS Route53 호스팅 Zone 자동 생성 확인

 

3. AWS IAM 생성

- 생성 시 "AWS 외부에서 실행되는 애플리케이션" 에 체크

- 만들어진 Access Key & Secret Key 따로 저장

 

4. Host 서버에 credential 파일 생성

# mkdir /etc/aws/credentials

# cd /etc/aws/credentials
# cat <<EOF > credentials
[default]
aws_access_key_id=AKIAxxxxxxxxxxxxxxx
aws_secret_access_key=xxxxxxxxxxxxxxxxx
EOF

 

5. 클러스터에 ExternalDNS 설치

# helm repo add external-dns https://kubernetes-sigs.github.io/external-dns/

# helm repo update

# helm install externaldns external-dns/external-dns -n kube-system -f values.yaml

NAME: externaldns
LAST DEPLOYED: Tue Dec  9 12:05:16 2025
NAMESPACE: kube-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
***********************************************************************
* External DNS                                                        *
***********************************************************************
  Chart version: 1.19.0
  App version:   0.19.0
  Image tag:     registry.k8s.io/external-dns/external-dns:v0.19.0
***********************************************************************

provider:
  name: aws
  zoneType: public

env:
  - name: AWS_SHARED_CREDENTIALS_FILE
    value: /etc/aws/credentials/credentials
  - name: AWS_REGION
    value: ap-northeast-2

extraVolumes:
  - name: aws-creds
    secret:
      secretName: external-dns-aws

extraVolumeMounts:
  - name: aws-creds
    mountPath: /etc/aws/credentials
    readOnly: true

sources:
  - service
  - ingress

domainFilters:
  - lake-devops.click

policy: sync
registry: txt
txtOwnerId: "externaldns-test"

logLevel: debug
(⎈|kind-myk8s:kube-system) root@DESKTOP-HUU6SC7:~/ria/externaldns# k logs -f externaldns-external-dns-58689db4d-rflxq
time="2025-12-09T03:05:17Z" level=info msg="config: {APIServerURL: KubeConfig: RequestTimeout:30s DefaultTargets:[] GlooNamespaces:[gloo-system] SkipperRouteGroupVersion:zalando.org/v1 Sources:[service ingress] Namespace: AnnotationFilter: LabelFilter: IngressClassNames:[] FQDNTemplate: CombineFQDNAndAnnotation:false IgnoreHostnameAnnotation:false IgnoreNonHostNetworkPods:false IgnoreIngressTLSSpec:false IgnoreIngressRulesSpec:false ListenEndpointEvents:false ExposeInternalIPV6:false GatewayName: GatewayNamespace: GatewayLabelFilter: Compatibility: PodSourceDomain: PublishInternal:false PublishHostIP:false AlwaysPublishNotReadyAddresses:false ConnectorSourceServer:localhost:8080 Provider:aws ProviderCacheTime:0s GoogleProject: GoogleBatchChangeSize:1000 GoogleBatchChangeInterval:1s GoogleZoneVisibility: DomainFilter:[lake-devops.click] ExcludeDomains:[] RegexDomainFilter: RegexDomainExclusion: ZoneNameFilter:[] ZoneIDFilter:[] TargetNetFilter:[] ExcludeTargetNets:[] AlibabaCloudConfigFile:/etc/kubernetes/alibaba-cloud.json AlibabaCloudZoneType: AWSZoneType: AWSZoneTagFilter:[] AWSAssumeRole: AWSProfiles:[] AWSAssumeRoleExternalID: AWSBatchChangeSize:1000 AWSBatchChangeSizeBytes:32000 AWSBatchChangeSizeValues:1000 AWSBatchChangeInterval:1s AWSEvaluateTargetHealth:true AWSAPIRetries:3 AWSPreferCNAME:false AWSZoneCacheDuration:0s AWSSDServiceCleanup:false AWSSDCreateTag:map[] AWSZoneMatchParent:false AWSDynamoDBRegion: AWSDynamoDBTable:external-dns AzureConfigFile:/etc/kubernetes/azure.json AzureResourceGroup: AzureSubscriptionID: AzureUserAssignedIdentityClientID: AzureActiveDirectoryAuthorityHost: AzureZonesCacheDuration:0s AzureMaxRetriesCount:3 CloudflareProxied:false CloudflareCustomHostnames:false CloudflareDNSRecordsPerPage:100 CloudflareDNSRecordsComment: CloudflareCustomHostnamesMinTLSVersion:1.0 
CloudflareCustomHostnamesCertificateAuthority:none CloudflareRegionalServices:false CloudflareRegionKey: CoreDNSPrefix:/skydns/ AkamaiServiceConsumerDomain: AkamaiClientToken: AkamaiClientSecret: AkamaiAccessToken: AkamaiEdgercPath: AkamaiEdgercSection: OCIConfigFile:/etc/kubernetes/oci.yaml OCICompartmentOCID: OCIAuthInstancePrincipal:false OCIZoneScope:GLOBAL OCIZoneCacheDuration:0s InMemoryZones:[] OVHEndpoint:ovh-eu OVHApiRateLimit:20 OVHEnableCNAMERelative:false PDNSServer:http://localhost:8081 PDNSServerID:localhost PDNSAPIKey: PDNSSkipTLSVerify:false TLSCA: TLSClientCert: TLSClientCertKey: Policy:sync Registry:txt TXTOwnerID:externaldns-test TXTPrefix: TXTSuffix: TXTEncryptEnabled:false TXTEncryptAESKey: Interval:1m0s MinEventSyncInterval:5s Once:false DryRun:false UpdateEvents:false LogFormat:text MetricsAddress::7979 LogLevel:debug TXTCacheInterval:0s TXTWildcardReplacement: ExoscaleEndpoint: ExoscaleAPIKey: ExoscaleAPISecret: ExoscaleAPIEnvironment:api ExoscaleAPIZone:ch-gva-2 CRDSourceAPIVersion:externaldns.k8s.io/v1alpha1 CRDSourceKind:DNSEndpoint ServiceTypeFilter:[] CFAPIEndpoint: CFUsername: CFPassword: ResolveServiceLoadBalancerHostname:false RFC2136Host:[] RFC2136Port:0 RFC2136Zone:[] RFC2136Insecure:false RFC2136GSSTSIG:false RFC2136CreatePTR:false RFC2136KerberosRealm: RFC2136KerberosUsername: RFC2136KerberosPassword: RFC2136TSIGKeyName: RFC2136TSIGSecret: RFC2136TSIGSecretAlg: RFC2136TAXFR:false RFC2136MinTTL:0s RFC2136LoadBalancingStrategy:disabled RFC2136BatchChangeSize:50 RFC2136UseTLS:false RFC2136SkipTLSVerify:false NS1Endpoint: NS1IgnoreSSL:false NS1MinTTLSeconds:0 TransIPAccountName: TransIPPrivateKeyFile: DigitalOceanAPIPageSize:50 ManagedDNSRecordTypes:[A AAAA CNAME] ExcludeDNSRecordTypes:[] GoDaddyAPIKey: GoDaddySecretKey: GoDaddyTTL:0 GoDaddyOTE:false OCPRouterName: PiholeServer: PiholePassword: PiholeTLSInsecureSkipVerify:false PiholeApiVersion:5 PluralCluster: PluralProvider: WebhookProviderURL:http://localhost:8888 WebhookProviderReadTimeout:5s WebhookProviderWriteTimeout:10s WebhookServer:false TraefikEnableLegacy:false TraefikDisableNew:false NAT64Networks:[] ExcludeUnschedulable:true EmitEvents:[] ForceDefaultTargets:false sourceWrappers:map[]}"
time="2025-12-09T03:05:17Z" level=info msg="GitCommitShort=unknown, GoVersion=go1.24.6, Platform=linux/amd64, UserAgent=ExternalDNS/v20250902-v0.19.0"
time="2025-12-09T03:05:17Z" level=info msg="Instantiating new Kubernetes client"
time="2025-12-09T03:05:17Z" level=debug msg="apiServerURL: "
time="2025-12-09T03:05:17Z" level=debug msg="kubeConfig: "
time="2025-12-09T03:05:17Z" level=debug msg="serving 'healthz' on ':7979/healthz'"
time="2025-12-09T03:05:17Z" level=debug msg="serving 'metrics' on ':7979/metrics'"
time="2025-12-09T03:05:17Z" level=debug msg="registered '21' metrics"
time="2025-12-09T03:05:17Z" level=info msg="Using inCluster-config based on serviceaccount-token"
time="2025-12-09T03:05:17Z" level=info msg="Created Kubernetes client https://10.96.0.1:443"
time="2025-12-09T03:05:17Z" level=debug msg="Refreshing zones list cache"
time="2025-12-09T03:05:18Z" level=debug msg="Considering zone: /hostedzone/Z03737533SGX7MJ3SQBQV (domain: lake-devops.click.)"
time="2025-12-09T03:05:19Z" level=debug msg="nat64Source: collecting endpoints and processing NAT64 translation"
time="2025-12-09T03:05:19Z" level=debug msg="dedupSource: collecting endpoints and removing duplicates"
time="2025-12-09T03:05:19Z" level=debug msg="multiSource: collecting endpoints from 2 child sources and removing duplicates"
time="2025-12-09T03:05:19Z" level=debug msg="No endpoints could be generated from service default/kubernetes"
time="2025-12-09T03:05:19Z" level=debug msg="No endpoints could be generated from service ingress-nginx/ingress-nginx-controller"
time="2025-12-09T03:05:19Z" level=debug msg="No endpoints could be generated from service ingress-nginx/ingress-nginx-controller-admission"
time="2025-12-09T03:05:19Z" level=debug msg="No endpoints could be generated from service kube-system/externaldns-external-dns"
time="2025-12-09T03:05:19Z" level=debug msg="No endpoints could be generated from service kube-system/kube-dns"
time="2025-12-09T03:05:19Z" level=debug msg="Refreshing zones list cache"
time="2025-12-09T03:05:19Z" level=debug msg="Considering zone: /hostedzone/Z03737533SGX7MJ3SQBQV (domain: lake-devops.click.)"
time="2025-12-09T03:05:19Z" level=info msg="Applying provider record filter for domains: [lake-devops.click. .lake-devops.click.]"
time="2025-12-09T03:05:19Z" level=info msg="All records are already up to date"

 

6. Nginx Ingress Controller 설치

kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml

 

7. 테스트용 Ingress 배포

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: test-externaldns
spec:
  rules:
  - host: user.lake-devops.click
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: nginx-service
            port:
              number: 80

 

8. DNS 자동 등록 확인

time="2025-12-09T03:19:52Z" level=debug msg="Considering zone: /hostedzone/Z03737533SGX7MJ3SQBQV (domain: lake-devops.click.)"
time="2025-12-09T03:19:52Z" level=info msg="Applying provider record filter for domains: [lake-devops.click. .lake-devops.click.]"
time="2025-12-09T03:19:52Z" level=debug msg="Refreshing zones list cache"
time="2025-12-09T03:19:52Z" level=debug msg="Considering zone: /hostedzone/Z03737533SGX7MJ3SQBQV (domain: lake-devops.click.)"
time="2025-12-09T03:19:52Z" level=debug msg="Adding user.lake-devops.click. to zone lake-devops.click. [Id: /hostedzone/Z03737533SGX7MJ3SQBQV]"
time="2025-12-09T03:19:52Z" level=debug msg="Adding cname-user.lake-devops.click. to zone lake-devops.click. [Id: /hostedzone/Z03737533SGX7MJ3SQBQV]"
time="2025-12-09T03:19:52Z" level=info msg="Desired change: CREATE cname-user.lake-devops.click TXT" profile=default zoneID=/hostedzone/Z03737533SGX7MJ3SQBQV zoneName=lake-devops.click.
time="2025-12-09T03:19:52Z" level=info msg="Desired change: CREATE user.lake-devops.click CNAME" profile=default zoneID=/hostedzone/Z03737533SGX7MJ3SQBQV zoneName=lake-devops.click. 
time="2025-12-09T03:19:52Z" level=info msg="2 record(s) were successfully updated" profile=default zoneID=/hostedzone/Z03737533SGX7MJ3SQBQV zoneName=lake-devops.click.

 

9. ingress 삭제 후 도메인 업데이트 확인


time="2025-12-09T03:26:34Z" level=debug msg="Considering zone: /hostedzone/Z03737533SGX7MJ3SQBQV (domain: lake-devops.click.)"
time="2025-12-09T03:26:34Z" level=info msg="Applying provider record filter for domains: [lake-devops.click. .lake-devops.click.]"
time="2025-12-09T03:26:34Z" level=debug msg="Refreshing zones list cache"
time="2025-12-09T03:26:34Z" level=debug msg="Considering zone: /hostedzone/Z03737533SGX7MJ3SQBQV (domain: lake-devops.click.)"
time="2025-12-09T03:26:34Z" level=debug msg="Adding user.lake-devops.click. to zone lake-devops.click. [Id: /hostedzone/Z03737533SGX7MJ3SQBQV]"
time="2025-12-09T03:26:34Z" level=debug msg="Adding cname-user.lake-devops.click. to zone lake-devops.click. [Id: /hostedzone/Z03737533SGX7MJ3SQBQV]"
time="2025-12-09T03:26:34Z" level=info msg="Desired change: DELETE cname-user.lake-devops.click TXT" profile=default zoneID=/hostedzone/Z03737533SGX7MJ3SQBQV zoneName=lake-devops.click.     
time="2025-12-09T03:26:34Z" level=info msg="Desired change: DELETE user.lake-devops.click CNAME" profile=default zoneID=/hostedzone/Z03737533SGX7MJ3SQBQV zoneName=lake-devops.click.
time="2025-12-09T03:26:34Z" level=info msg="2 record(s) were successfully updated" profile=default zoneID=/hostedzone/Z03737533SGX7MJ3SQBQV zoneName=lake-devops.click.

 

추가 테스트 (Service)

1. LoadBalancer External-IP 생성을 위한 MetalLB 설치

kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.10/config/manifests/metallb-native.yaml

 

2. IP 풀 생성

cat <<EOF | kubectl apply -f -
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: my-ip-pool
  namespace: metallb-system
spec:
  addresses:
  - 172.18.255.1-172.18.255.250
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: my-l2adv
  namespace: metallb-system
spec:
  ipAddressPools:
  - my-ip-pool
EOF

 

3. 테트리스 샘플 pod, service 배포

cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tetris
spec:
  replicas: 1
  selector:
    matchLabels:
      app: tetris
  template:
    metadata:
      labels:
        app: tetris
    spec:
      containers:
      - name: tetris
        image: bsord/tetris
---
apiVersion: v1
kind: Service
metadata:
  name: tetris
  annotations:
    external-dns.alpha.kubernetes.io/hostname: "tetris.lake-devops.click"
spec:
  type: LoadBalancer
  selector:
    app: tetris
  ports:
  - port: 80
    targetPort: 80
EOF

 

4. ExternalDNS 로그 확인

time="2025-12-09T05:09:47Z" level=debug msg="Considering zone: /hostedzone/Z03737533SGX7MJ3SQBQV (domain: lake-devops.click.)"
time="2025-12-09T05:09:47Z" level=info msg="Applying provider record filter for domains: [lake-devops.click. .lake-devops.click.]"
time="2025-12-09T05:09:47Z" level=debug msg="Refreshing zones list cache"
time="2025-12-09T05:09:47Z" level=debug msg="Considering zone: /hostedzone/Z03737533SGX7MJ3SQBQV (domain: lake-devops.click.)"
time="2025-12-09T05:09:47Z" level=debug msg="Adding tetris.lake-devops.click. to zone lake-devops.click. [Id: /hostedzone/Z03737533SGX7MJ3SQBQV]"
time="2025-12-09T05:09:47Z" level=debug msg="Adding a-tetris.lake-devops.click. to zone lake-devops.click. [Id: /hostedzone/Z03737533SGX7MJ3SQBQV]"
time="2025-12-09T05:09:47Z" level=info msg="Desired change: CREATE a-tetris.lake-devops.click TXT" profile=default zoneID=/hostedzone/Z03737533SGX7MJ3SQBQV zoneName=lake-devops.click.
time="2025-12-09T05:09:47Z" level=info msg="Desired change: CREATE tetris.lake-devops.click A" profile=default zoneID=/hostedzone/Z03737533SGX7MJ3SQBQV zoneName=lake-devops.click.
time="2025-12-09T05:09:47Z" level=info msg="2 record(s) were successfully updated" profile=default zoneID=/hostedzone/Z03737533SGX7MJ3SQBQV zoneName=lake-devops.click.

 

 

# kubectl get svc tetris
NAME     TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)            AGE

tetris   LoadBalancer   10.96.27.131     172.18.255.2       80:30370/TCP   2m5s

 


두번째 실습 (ExternalDNS + DNS Provider : Linux Bind9)

[WSL Ubuntu]
 ├─ bind9 DNS Server
 └─ kind cluster
       ├─ ExternalDNS (provider: rfc2136)

       ├─ ingress-nginx controller
       └─ test service

1. bind9 설치

sudo apt install bind9 bind9utils bind9-doc -y

# cat /etc/bind/named.conf.options
options {
        directory "/var/cache/bind";

        // If there is a firewall between you and nameservers you want
        // to talk to, you may need to fix the firewall to allow multiple
        // ports to talk.  See http://www.kb.cert.org/vuls/id/800113

        // If your ISP provided one or more IP addresses for stable
        // nameservers, you probably want to use them as forwarders.
        // Uncomment the following block, and insert the addresses replacing
        // the all-0's placeholder.

        // forwarders {
        //      0.0.0.0;
        // };

        //========================================================================
        // If BIND logs error messages about the root key being expired,
        // you will need to update your keys.  See https://www.isc.org/bind-keys
$TTL 86400
        //========================================================================
        dnssec-validation auto;

        listen-on-v6 { any; };
};

2. bind9 기본 설정 변경  (/etc/bind/named.conf.local 파일 수정)

tsig-keygen -a hmac-sha256 externaldns

# cat /etc/bind/named.conf.local
//
// Do any local configuration here
//

// Consider adding the 1918 zones here, if they are not used in your
// organization
//include "/etc/bind/zones.rfc1918";

key "externaldns" {
    algorithm hmac-sha256;
    secret "88OM9SE9wUp91peYxNUH+JbAhsXl1n4iCw+OFmYjY3o=";
};

zone "lake-devops.com" IN {
  type master;
  file "lake-devops.com.zone";
  allow-transfer {
      key "externaldns";
  };
  update-policy {
      grant externaldns zonesub ANY;
  };
};

3. Zone 파일 작성 (/var/cache/bind/lake-devops.com.zone 파일 수정)

$TTL 86400
@   IN SOA ns1.lake-devops.com. admin.lake-devops.com. (
        1
        3600
        900
        604800
        86400
)

@       IN NS    ns1.lake-devops.com.
ns1     IN A     172.24.148.155

 

4. bind9 설정 검사

# named-checkzone  lake-devops.com  /var/cache/bind/lake-devops.com.zone
zone lake-devops.com/IN: loaded serial 1
OK

5. bind9 재시작

#sudo systemctl restart bind9

6. /etc/resolv.conf 설정 변경

# cat /etc/resolv.conf 
# This file was automatically generated by WSL. To stop automatic generation of this file, add the following entry to /etc/wsl.conf:
# [network]
# generateResolvConf = false
nameserver 172.24.144.1
nameserver 127.0.0.1

 

7. ExternalDNS 설정 변경

helm uninstall externaldns -n kube-system

# cat values_bind9.yaml 
provider:
  name: rfc2136

extraArgs:
  - --rfc2136-host=172.24.148.155
  - --rfc2136-port=53
  - --rfc2136-zone=lake-devops.com
  - --rfc2136-tsig-keyname=externaldns
  - --rfc2136-tsig-secret=88OM9SE9wUp91peYxNUH+JbAhsXl1n4iCw+OFmYjY3o=
  - --rfc2136-tsig-secret-alg=hmac-sha256

sources:
  - service
  - ingress

domainFilters:
  - lake-devops.com

policy: sync
registry: txt
txtOwnerId: "externaldns-test"

logLevel: debug

 

# helm install externaldns external-dns/external-dns   -n kube-system -f values_bind9.yaml

# k logs -f externaldns-external-dns-766ff7958b-xkr5d

time="2025-12-09T07:25:36Z" level=info msg="config: {APIServerURL: KubeConfig: RequestTimeout:30s DefaultTargets:[] GlooNamespaces:[gloo-system] SkipperRouteGroupVersion:zalando.org/v1 Sources:[service ingress] Namespace: AnnotationFilter: LabelFilter: IngressClassNames:[] FQDNTemplate: CombineFQDNAndAnnotation:false IgnoreHostnameAnnotation:false IgnoreNonHostNetworkPods:false IgnoreIngressTLSSpec:false IgnoreIngressRulesSpec:false ListenEndpointEvents:false ExposeInternalIPV6:false GatewayName: GatewayNamespace: GatewayLabelFilter: Compatibility: PodSourceDomain: PublishInternal:false PublishHostIP:false AlwaysPublishNotReadyAddresses:false ConnectorSourceServer:localhost:8080 Provider:rfc2136 ProviderCacheTime:0s GoogleProject: GoogleBatchChangeSize:1000 GoogleBatchChangeInterval:1s GoogleZoneVisibility: DomainFilter:[lake-devops.com] ExcludeDomains:[] RegexDomainFilter: RegexDomainExclusion: ZoneNameFilter:[] ZoneIDFilter:[] TargetNetFilter:[] ExcludeTargetNets:[] AlibabaCloudConfigFile:/etc/kubernetes/alibaba-cloud.json AlibabaCloudZoneType: AWSZoneType: AWSZoneTagFilter:[] AWSAssumeRole: AWSProfiles:[] AWSAssumeRoleExternalID: AWSBatchChangeSize:1000 AWSBatchChangeSizeBytes:32000 AWSBatchChangeSizeValues:1000 AWSBatchChangeInterval:1s AWSEvaluateTargetHealth:true AWSAPIRetries:3 AWSPreferCNAME:false AWSZoneCacheDuration:0s AWSSDServiceCleanup:false 
AWSSDCreateTag:map[] AWSZoneMatchParent:false AWSDynamoDBRegion: AWSDynamoDBTable:external-dns AzureConfigFile:/etc/kubernetes/azure.json AzureResourceGroup: AzureSubscriptionID: AzureUserAssignedIdentityClientID: AzureActiveDirectoryAuthorityHost: AzureZonesCacheDuration:0s AzureMaxRetriesCount:3 CloudflareProxied:false CloudflareCustomHostnames:false CloudflareDNSRecordsPerPage:100 CloudflareDNSRecordsComment: CloudflareCustomHostnamesMinTLSVersion:1.0 
CloudflareCustomHostnamesCertificateAuthority:none CloudflareRegionalServices:false CloudflareRegionKey: CoreDNSPrefix:/skydns/ AkamaiServiceConsumerDomain: AkamaiClientToken: AkamaiClientSecret: AkamaiAccessToken: AkamaiEdgercPath: AkamaiEdgercSection: OCIConfigFile:/etc/kubernetes/oci.yaml OCICompartmentOCID: OCIAuthInstancePrincipal:false OCIZoneScope:GLOBAL OCIZoneCacheDuration:0s InMemoryZones:[] OVHEndpoint:ovh-eu OVHApiRateLimit:20 OVHEnableCNAMERelative:false PDNSServer:http://localhost:8081 PDNSServerID:localhost PDNSAPIKey: PDNSSkipTLSVerify:false TLSCA: TLSClientCert: TLSClientCertKey: Policy:sync Registry:txt TXTOwnerID:externaldns-test TXTPrefix: TXTSuffix: TXTEncryptEnabled:false TXTEncryptAESKey: Interval:1m0s MinEventSyncInterval:5s Once:false DryRun:false UpdateEvents:false LogFormat:text MetricsAddress::7979 LogLevel:debug TXTCacheInterval:0s TXTWildcardReplacement: ExoscaleEndpoint: ExoscaleAPIKey: ExoscaleAPISecret: ExoscaleAPIEnvironment:api ExoscaleAPIZone:ch-gva-2 CRDSourceAPIVersion:externaldns.k8s.io/v1alpha1 CRDSourceKind:DNSEndpoint ServiceTypeFilter:[] CFAPIEndpoint: CFUsername: CFPassword: ResolveServiceLoadBalancerHostname:false RFC2136Host:[172.24.148.155] RFC2136Port:53 RFC2136Zone:[lake-devops.com] RFC2136Insecure:false RFC2136GSSTSIG:false RFC2136CreatePTR:false RFC2136KerberosRealm: RFC2136KerberosUsername: RFC2136KerberosPassword: RFC2136TSIGKeyName:externaldns RFC2136TSIGSecret:****** RFC2136TSIGSecretAlg:hmac-sha256 RFC2136TAXFR:false RFC2136MinTTL:0s RFC2136LoadBalancingStrategy:disabled RFC2136BatchChangeSize:50 RFC2136UseTLS:false RFC2136SkipTLSVerify:false NS1Endpoint: NS1IgnoreSSL:false NS1MinTTLSeconds:0 TransIPAccountName: TransIPPrivateKeyFile: DigitalOceanAPIPageSize:50 ManagedDNSRecordTypes:[A AAAA CNAME] ExcludeDNSRecordTypes:[] GoDaddyAPIKey: GoDaddySecretKey: GoDaddyTTL:0 GoDaddyOTE:false OCPRouterName: PiholeServer: PiholePassword: PiholeTLSInsecureSkipVerify:false PiholeApiVersion:5 PluralCluster: PluralProvider: WebhookProviderURL:http://localhost:8888 WebhookProviderReadTimeout:5s WebhookProviderWriteTimeout:10s WebhookServer:false TraefikEnableLegacy:false TraefikDisableNew:false NAT64Networks:[] ExcludeUnschedulable:true EmitEvents:[] ForceDefaultTargets:false sourceWrappers:map[]}"
time="2025-12-09T07:25:36Z" level=info msg="GitCommitShort=unknown, GoVersion=go1.24.6, Platform=linux/amd64, UserAgent=ExternalDNS/v20250902-v0.19.0"   
time="2025-12-09T07:25:36Z" level=info msg="Instantiating new Kubernetes client"
time="2025-12-09T07:25:36Z" level=debug msg="serving 'healthz' on ':7979/healthz'"
time="2025-12-09T07:25:36Z" level=debug msg="serving 'metrics' on ':7979/metrics'"
time="2025-12-09T07:25:36Z" level=debug msg="registered '21' metrics"
time="2025-12-09T07:25:36Z" level=debug msg="apiServerURL: "
time="2025-12-09T07:25:36Z" level=debug msg="kubeConfig: "
time="2025-12-09T07:25:36Z" level=info msg="Using inCluster-config based on serviceaccount-token"
time="2025-12-09T07:25:36Z" level=info msg="Created Kubernetes client https://10.96.0.1:443"
time="2025-12-09T07:25:36Z" level=info msg="Configured RFC2136 with zones '[lake-devops.com]' and nameservers '[172.24.148.155]'"
time="2025-12-09T07:25:36Z" level=debug msg="axfr is disabled"
time="2025-12-09T07:25:36Z" level=debug msg="nat64Source: collecting endpoints and processing NAT64 translation"
time="2025-12-09T07:25:36Z" level=debug msg="dedupSource: collecting endpoints and removing duplicates"
time="2025-12-09T07:25:36Z" level=debug msg="multiSource: collecting endpoints from 2 child sources and removing duplicates"
time="2025-12-09T07:25:36Z" level=debug msg="No endpoints could be generated from service metallb-system/webhook-service"
time="2025-12-09T07:25:36Z" level=debug msg="No endpoints could be generated from service default/kubernetes"
time="2025-12-09T07:25:36Z" level=debug msg="No endpoints could be generated from service ingress-nginx/ingress-nginx-controller"
time="2025-12-09T07:25:36Z" level=debug msg="No endpoints could be generated from service ingress-nginx/ingress-nginx-controller-admission"
time="2025-12-09T07:25:36Z" level=debug msg="No endpoints could be generated from service kube-system/externaldns-external-dns"
time="2025-12-09T07:25:36Z" level=debug msg="No endpoints could be generated from service kube-system/kube-dns"
time="2025-12-09T07:25:36Z" level=info msg="All records are already up to date"

 

8. 테스트 서비스 배포

apiVersion: v1
kind: Service
metadata:
  name: my-service
  annotations:
    external-dns.alpha.kubernetes.io/hostname: app.lake-devops.com
spec:
  type: LoadBalancer
  selector:
    app: my-app
  ports:
  - port: 80
    targetPort: 8080

9. ExternalDNS 로그 확인

time="2025-12-09T07:31:05Z" level=debug msg="AddRecord.ep=app.lake-devops.com 0 IN A  172.18.255.29 []"
time="2025-12-09T07:31:05Z" level=info msg="Adding RR: app.lake-devops.com 0 A 172.18.255.29"
time="2025-12-09T07:31:05Z" level=debug msg="AddRecord.ep=a-app.lake-devops.com 0 IN TXT  \"heritage=external-dns,external-dns/owner=externaldns-test,external-dns/resource=service/kube-system/my-service\" []"
time="2025-12-09T07:31:05Z" level=info msg="Adding RR: a-app.lake-devops.com 0 TXT \"heritage=external-dns,external-dns/owner=externaldns-test,external-dns/resource=service/kube-system/my-service\""
time="2025-12-09T07:31:05Z" level=debug msg=SendMessage
time="2025-12-09T07:31:05Z" level=debug msg="Sending message to nameserver: 172.24.148.155:53"
time="2025-12-09T07:31:05Z" level=debug msg=SendMessage.success
(⎈|kind-exdns-k8s:N/A) root@DESKTOP-HUU6SC7:/etc/bind# cat /var/cache/bind/lake-devops.com.zone
$ORIGIN .
$TTL 86400      ; 1 day
lake-devops.com         IN SOA  ns1.lake-devops.com. admin.lake-devops.com. (
                                6          ; serial
                                3600       ; refresh (1 hour)
                                900        ; retry (15 minutes)
                                604800     ; expire (1 week)
                                86400      ; minimum (1 day)
                                )
                        NS      ns1.lake-devops.com.
$ORIGIN lake-devops.com.
$TTL 0  ; 0 seconds
a-app                   TXT     "heritage=external-dns,external-dns/owner=externaldns-test,external-dns/resource=service/kube-system/my-service"
a-tetris                TXT     "heritage=external-dns,external-dns/owner=externaldns-test,external-dns/resource=service/default/tetris"
app                     A       172.18.255.29
cname-tetris            TXT     "heritage=external-dns,external-dns/owner=externaldns-test,external-dns/resource=ingress/default/tetris-ingress"
$TTL 86400      ; 1 day
ns1                     A       172.24.148.155
$TTL 0  ; 0 seconds
tetris                  CNAME   localhost.

 

추가 테스트 (웹 접속 확인)

[WSL Ubuntu]
 ├─ bind9 DNS Server
 ├─ kind cluster
 │     ├─ ExternalDNS (provider: rfc2136)
 │     ├─ ingress-nginx controller
 │     │   (listens on containerPort 80/443)
 │     └─ tetris service,ingress
 └─ extraPortMappings
       (hostPort 80  → kind control-plane :80)
       (hostPort 443 → kind control-plane :443)
 

curl test (WSL/Windows) - ExternalDNS랑 별개로 접속 테스트용

curl http://tetris.example.com
                   ↓
      DNS → 127.0.0.1
                   ↓
           hostPort 80
                   ↓
   kind ingress controller
                   ↓
   tetris service (ClusterIP)
                   ↓
                 Pod

 

1. Cluster 설치

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
    extraPortMappings:
      - containerPort: 80
        hostPort: 80
        protocol: TCP
      - containerPort: 443        
        hostPort: 443
        protocol: TCP
  - role: worker
  - role: worker

2. ingress nginx, metallb 설치 (ip-pool 생성)

apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: pool1
  namespace: metallb-system
spec:
  addresses:
  - 172.18.255.100-172.18.255.110
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: l2advertisement1
  namespace: metallb-system
spec:
  ipAddressPools:
  - pool1

3. externaldns 설치

provider:
  name: rfc2136

extraArgs:
  - --rfc2136-host=172.24.148.155
  - --rfc2136-port=53
  - --rfc2136-zone=lake-devops.com
  - --rfc2136-tsig-keyname=externaldns
  - --rfc2136-tsig-secret=88OM9SE9wUp91peYxNUH+JbAhsXl1n4iCw+OFmYjY3o=
  - --rfc2136-tsig-secret-alg=hmac-sha256

sources:
  - service
  - ingress

domainFilters:
  - lake-devops.com

policy: sync
registry: txt
txtOwnerId: "externaldns-test"

logLevel: debug

4. pod, service, ingress 배포 후 확인

apiVersion: apps/v1
kind: Deployment
metadata:
  name: tetris
spec:
  replicas: 1
  selector:
    matchLabels:
      app: tetris
  template:
    metadata:
      labels:
        app: tetris
    spec:
      containers:
      - name: tetris
        image: bsord/tetris
---
apiVersion: v1
kind: Service
metadata:
  name: tetris
spec:
  type: ClusterIP
  selector:
    app: tetris
  ports:
  - port: 80
    targetPort: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: tetris-ingress
  annotations:
    external-dns.alpha.kubernetes.io/hostname: tetris.lake-devops.com
spec:
  ingressClassName: nginx
  rules:
  - host: tetris.lake-devops.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: tetris
            port:
              number: 80
time="2025-12-09T08:34:26Z" level=debug msg="axfr is disabled"
time="2025-12-09T08:34:26Z" level=debug msg="nat64Source: collecting endpoints and processing NAT64 translation"
time="2025-12-09T08:34:26Z" level=debug msg="dedupSource: collecting endpoints and removing duplicates"
time="2025-12-09T08:34:26Z" level=debug msg="multiSource: collecting endpoints from 2 child sources and removing duplicates"
time="2025-12-09T08:34:26Z" level=debug msg="No endpoints could be generated from service ingress-nginx/ingress-nginx-controller"
time="2025-12-09T08:34:26Z" level=debug msg="No endpoints could be generated from service ingress-nginx/ingress-nginx-controller-admission"
time="2025-12-09T08:34:26Z" level=debug msg="No endpoints could be generated from service kube-system/externaldns-external-dns"
time="2025-12-09T08:34:26Z" level=debug msg="No endpoints could be generated from service kube-system/kube-dns"
time="2025-12-09T08:34:26Z" level=debug msg="No endpoints could be generated from service metallb-system/webhook-service"
time="2025-12-09T08:34:26Z" level=debug msg="No endpoints could be generated from service default/tetris"
time="2025-12-09T08:34:26Z" level=debug msg="No endpoints could be generated from service default/kubernetes"
time="2025-12-09T08:34:26Z" level=debug msg="Endpoints generated from ingress: default/tetris-ingress: [tetris.lake-devops.com 0 IN CNAME  localho IN CNAME  localhost []]"
time="2025-12-09T08:34:26Z" level=debug msg="Removing duplicate endpoint tetris.lake-devops.com 0 IN CNAME  localhost []"
time="2025-12-09T08:34:26Z" level=debug msg="ApplyChanges (Create: 2, UpdateOld: 0, UpdateNew: 0, Delete: 0)"
time="2025-12-09T08:34:26Z" level=debug msg="Processing batch 0 of create changes"
time="2025-12-09T08:34:26Z" level=debug msg="AddRecord.ep=tetris.lake-devops.com 0 IN CNAME  localhost []"
time="2025-12-09T08:34:26Z" level=info msg="Adding RR: tetris.lake-devops.com 0 CNAME localhost"
time="2025-12-09T08:34:26Z" level=debug msg="AddRecord.ep=cname-tetris.lake-devops.com 0 IN TXT  \"heritage=external-dns,external-dns/owner=externce=ingress/default/tetris-ingress\" []"
time="2025-12-09T08:34:26Z" level=info msg="Adding RR: cname-tetris.lake-devops.com 0 TXT \"heritage=external-dns,external-dns/owner=externaldns-tess/default/tetris-ingress\""
time="2025-12-09T08:34:26Z" level=debug msg=SendMessage
time="2025-12-09T08:34:26Z" level=debug msg="Sending message to nameserver: 172.24.148.155:53"
time="2025-12-09T08:34:26Z" level=debug msg=SendMessage.success

5. 도메인 질의 확인

#  dig @172.24.148.155 tetris.lake-devops.com

; <<>> DiG 9.18.39-0ubuntu0.24.04.2-Ubuntu <<>> @172.24.148.155 tetris.lake-devops.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 13523
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; COOKIE: b7ed6e0cd541cca7010000006937ed9bc2b70f2bfe00e928 (good)
;; QUESTION SECTION:
;tetris.lake-devops.com.                IN      A

;; ANSWER SECTION:
tetris.lake-devops.com. 0       IN      CNAME   localhost.
localhost.              604800  IN      A       127.0.0.1

;; Query time: 0 msec
;; SERVER: 172.24.148.155#53(172.24.148.155) (UDP)
;; WHEN: Tue Dec 09 18:36:27 KST 2025
;; MSG SIZE  rcvd: 118

 

[번외]

> tetris 서비스 접속 확인

# curl -v http://tetris.lake-devops.com
* Host tetris.lake-devops.com:80 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:80...
* connect to ::1 port 80 from ::1 port 57266 failed: Connection refused
*   Trying 127.0.0.1:80...
* Connected to tetris.lake-devops.com (127.0.0.1) port 80
> GET / HTTP/1.1
> Host: tetris.lake-devops.com
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Tue, 09 Dec 2025 09:36:06 GMT
< Content-Type: text/html
< Content-Length: 832
< Connection: keep-alive
< Last-Modified: Sun, 20 Mar 2022 03:31:50 GMT
< ETag: "6236a026-340"
< Accept-Ranges: bytes
<
<html>
    <head>
        <title>Tetris</title>
        <link href="https://fonts.googleapis.com/css?family=Russo+One&display=swap" rel="stylesheet">
        <style>
        .parent {
            display: flex;
            justify-content: center;
            align-items: center;
          }
        #score {
            font-size:2em
        }
        #tetris {
            height: 90vh;
            max-height: 900px
        }

        </style>

    </head>
    <body class="parent" style="background: #202025; font-family: 'Russo One', sans-serif;" >
        <div  style="color:white; background: #111; padding: 1em; border-radius: 5px">
            <canvas id="tetris" width="480" height="900" style="border-radius: 5px"></canvas>
            <script src="tetris.js"></script>
        </div>
    </body>
* Connection #0 to host tetris.lake-devops.com left intact

 

> window hosts 등록
127.0.0.1 tetris.lake-devops.com


추가 개념 정리

TSIG (보안)

  • Transaction SIGnature
  • DNS 패킷을 인증하기 위한 공유키 기반 HMAC 인증 방식
  • DNS 프로토콜(RFC2136, AXFR)를 이용해 zone을 관리하는 authoritative DNS 서버에서 사용됨
  • RFC2136 동적 업데이트 할때 또는 Zone Transfer (AXFR/IXFR) 보안을 위해 사용
  • key name + secret 기반
  • 인증, 무결성, 재전송 보호. DNS는 Plaintext라서 취약성 보완을 위해 사용

AXFR (데이터 복제)

  • Authoritative Zone TransFeR
  • DNS 영역 전체를 복제하는 프로토콜
  • BIND 계열 authoritative DNS 서버에서 사용
  • Primary DNS 서버에 있는 zone 파일 내용을 Secondary DNS 서버로 전부 복제하는 과정
  • 고가용성/백업을 위해 필수
  • Secondary가 Primary보다 빠르게 응답하도록 Local DNS를 구성할 때에도 사용

ExternalDNS 와 관계 (Delete 작업 - AXFR)

  • TSIG 인증은 선택적으로 사용가능
  • 의문점은 RFC2136 동적 업데이트 할 때 특히 DELETE 작업 시
    AXFR이 없어도 UPDATE하는 allow-update 권한이랑, TXT 조회하는 allow-query만 있으면 되지 않나
  • 하지만 실제 DELETE 작업시, AXFR을 활성화해야만 DELETE 작업이 수행됨

[해결]

external-dns/docs/tutorials/rfc2136.md at master · kubernetes-sigs/external-dns · GitHub

  • ExternalDNS는 RFC2136 UPDATE를 통해 레코드를 삭제하지만,
    RFC2136 provider의 구현상 삭제 대상 레코드를 식별하기 위해 zone transfer(AXFR) 사용.
    공식 문서에서도 DELETE 작업을 수행하려면 AXFR 활성화하라고 명시되어 있음
  • DELETE 동작 흐름
    1. external-dns가 AXFR을 통해 zone 전체 레코드를 읽음
    2. TXT registry 기반으로 내가 관리하는 레코드 식별
    3. Kubernetes desired state 와 비교
    4. 삭제 대상 결정
    5. RFC2136 UPDATE(delete) 실행     ->  이땐 AXFR 필요 없음

출처

external-dns/docs/tutorials/aws.md at master · kubernetes-sigs/external-dns · GitHub

 

external-dns/docs/tutorials/aws.md at master · kubernetes-sigs/external-dns

Configure external DNS servers dynamically from Kubernetes resources - kubernetes-sigs/external-dns

github.com

 

[AWS] 📚 Route53 개념 원리 & 사용 세팅 💯 정리

 

[AWS] 📚 Route53 개념 원리 & 사용 세팅 💯 정리

Route 53 Amazon Route 53 은 가용성과 확장성이 뛰어난 클라우드 Domain Name System (DNS) 웹 서비스이다. Route 53는 도메인 구입부터 네임서버 등록까지 dns에 필요한 모든 기능이 있고, aws 답게 추가로 모니

inpa.tistory.com

[Kubernetes] ingress - external dns - route53(cloud dns server)

 

[Kubernetes] ingress - external dns - route53(cloud dns server)

여기서 중요한 것은 ExternalDNS임 만약 public 이 필요 없다면 CoreDNS + Traefik 로 사용  정의external dns란 쿠버네티스 클러스터 내부 리소스를 외부 DNS 서비스와 연동하여 DNS 레코드를 자동으로 생성하

luv-n-interest.tistory.com