본문 바로가기

DevOps

[AWS EKS] (6) EKS 스터디 2주차 ( DNS )

CloudNet@팀의 EKS 스터디 AEWS 2기에 작성된 자료를 토대로 작성합니다.

 

이번 포스팅에서는  CoreDNS 사용시 이슈사항을 소개해보겠습니다.

운영환경의 경험을 기반으로 DNS에 대한 이야기를 풀어보는 시간을 가지도록 하겠습니다.

 

✅  환경 준비

☑️ 편의를 위해 ExternalDNS 설정까지 같이 진행하겠습니다.

(BINARY-N/A:N/A) [root@operator-host ~]# MyDomain=cubicle.com
(BINARY-N/A:N/A) [root@operator-host ~]# aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." | jq

{
  "HostedZones": [
    {
      "ResourceRecordSetCount": 7,
      "CallerReference": "RISWorkflow-RD:c9f2e4c5-4a7b-4ba4-a1f1-acba36eb2a65",
      "Config": {
        "Comment": "HostedZone created by Route53 Registrar",
        "PrivateZone": false
      },
      "Id": "/hostedzone/Z05493571IQ5M12FEKOC1",
      "Name": "cubicle.zone."
    }
  ],
  "DNSName": "cubicle.com.",
  "IsTruncated": false,
  "MaxItems": "100"
}
(BINARY-N/A:N/A) [root@operator-host ~]#
(BINARY-N/A:N/A) [root@operator-host ~]# MyDnzHostedZoneId=`aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." --query "HostedZones[0].Id" --output text`

(BINARY-N/A:N/A) [root@operator-host ~]# echo $MyDnzHostedZoneId
/hostedzone/Z05493571IQ5M12FEKOC1



# (옵션) NS 레코드 타입 첫번째 조회
aws route53 list-resource-record-sets --output json --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'NS']" | jq -r '.[0].ResourceRecords[].Value'
# (옵션) A 레코드 타입 모두 조회
aws route53 list-resource-record-sets --output json --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A']"

# A 레코드 타입 조회
aws route53 list-resource-record-sets --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A']" | jq
aws route53 list-resource-record-sets --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A'].Name" | jq
aws route53 list-resource-record-sets --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A'].Name" --output text

# A 레코드 값 반복 조회
while true; do aws route53 list-resource-record-sets --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A']" | jq ; date ; echo ; sleep 1; done

☑️ ExternalDNS 설치 

# EKS 배포 시 Node IAM Role 설정되어 있음
# eksctl create cluster ... --external-dns-access ...

# 
MyDomain=<자신의 도메인>
MyDomain=cubicle.com

# 자신의 Route 53 도메인 ID 조회 및 변수 지정
MyDnzHostedZoneId=$(aws route53 list-hosted-zones-by-name --dns-name "${MyDomain}." --query "HostedZones[0].Id" --output text)

# 변수 확인
echo $MyDomain, $MyDnzHostedZoneId

# ExternalDNS 배포
curl -s -O https://raw.githubusercontent.com/gasida/PKOS/main/aews/externaldns.yaml
cat externaldns.yaml
MyDomain=$MyDomain MyDnzHostedZoneId=$MyDnzHostedZoneId envsubst < externaldns.yaml | kubectl apply -f -

# 확인 및 로그 모니터링
kubectl get pod -l app.kubernetes.io/name=external-dns -n kube-system
kubectl logs deploy/external-dns -n kube-system -f

☑️  NLB + SVC 배포 

# 터미널1 (모니터링)
watch -d 'kubectl get pod,svc'
kubectl logs deploy/external-dns -n kube-system -f
혹은
kubectl stern -l app.kubernetes.io/name=external-dns -n kube-system

# 테트리스 디플로이먼트 배포
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tetris
  labels:
    app: 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:
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: ip
    service.beta.kubernetes.io/aws-load-balancer-scheme: internet-facing
    service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: "true"
    service.beta.kubernetes.io/aws-load-balancer-backend-protocol: "http"
    #service.beta.kubernetes.io/aws-load-balancer-healthcheck-port: "80"
spec:
  selector:
    app: tetris
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  type: LoadBalancer
  loadBalancerClass: service.k8s.aws/nlb
EOF

# 배포 확인
kubectl get deploy,svc,ep tetris

# NLB에 ExternanDNS 로 도메인 연결
kubectl annotate service tetris "external-dns.alpha.kubernetes.io/hostname=tetris.$MyDomain"
while true; do aws route53 list-resource-record-sets --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A']" | jq ; date ; echo ; sleep 1; done

# Route53에 A레코드 확인
aws route53 list-resource-record-sets --hosted-zone-id "${MyDnzHostedZoneId}" --query "ResourceRecordSets[?Type == 'A']" | jq

# 확인
dig +short tetris.$MyDomain @8.8.8.8
dig +short tetris.$MyDomain

# 도메인 체크
echo -e "My Domain Checker Site1 = https://www.whatsmydns.net/#A/tetris.$MyDomain"
echo -e "My Domain Checker Site2 = https://dnschecker.org/#A/tetris.$MyDomain"

# 웹 접속 주소 확인 및 접속
echo -e "Tetris Game URL = http://tetris.$MyDomain"

 

✅  DNS 이해하기

ndots / search domain / coredns에 대한 개념은 아래 포스팅 참고 

https://themapisto.tistory.com/240

 

Core DNS 성능개선 [ nodelocaldns / ndots & FQDN ]

먼저 CoreDNS 성능 개선을 위해 여러가지 테스트를 하기전에 CoreDNS와 NodeLocalDNS가 무엇인지, 그리고 그 동작과정에 대해 먼저 알아보겠습니다.이를 이해하기 위해서는 ndots과 search 도메인 , 그리고

themapisto.tistory.com

목표:

Known 이슈사항 정리

DNS lookup timeouts due to races in conntrack # 3287

https://github.com/weaveworks/weave/issues/3287

 

DNS lookup timeouts due to races in conntrack · Issue #3287 · weaveworks/weave

What happened? We are experiencing random 5 second DNS timeouts in our kubernetes cluster. How to reproduce it? It is reproducible by requesting just about any in-cluster service, and observing tha...

github.com

 

conntrack 테이블에 트래픽이 과도하게 발생 시 UDP Race condition을 일으킴 

  • Race condition이란? 

같은 소켓을 통해 동시에 두 개의 레이싱 UDP 패킷이 전송될 때 발생하는 UDP 패킷이 드랍되는 현상 예를 들어,  DNS 조회의 경우 UDP 통신 입니다. glibc와 musl libc는 모두 A와 AAAA DNS 조회를 병렬로 수행합니다. 그 결과, UDP 패킷 중 하나가 커널에 의해 삭제되고, 그러면 클라이언트가 기본적으로 보통 5초인 시간 초과 후에 다시 시도하게 됩니다.

 

conntrack -S 명령어 입력시 insert_failed 항목을 아래 유심히 봅시다. 

 

어플리케이션 로그에서 DNS 타임아웃이 많이 보이나요? 

UDP Race condition을 의심해볼수 있습니다.

 

다행히도 경쟁 조건 중 두 가지는 2018년 커널 코드 내에서 완화되었습니다.

하지만 netfilter 커널에 대한 이러한 패치는 DNS 서버의 단일 인스턴스를 실행할 때만 문제를 해결했고(다른 조건의 영향만 줄였음) 문제를 완전히 해결하지는 못했습니다.

 

[root@ip-192-168-1-61 ~]# conntrack -S
cpu=0   	found=58 invalid=2121 insert=0 insert_failed=19 drop=19 early_drop=0 error=38 search_restart=0
cpu=1   	found=67 invalid=2120 insert=0 insert_failed=16 drop=16 early_drop=0 error=29 search_restart=0
  • insert_failed란? 
  • 아래 그림을 보고 이해해 봅시다. 

임시 조치

# nf_table conntrack max 설정

✅  해결 방안 

# nodelocal dns 설치

# 어플리케이션 API 게이트웨이 FQDN 적용 

✅  DNS 이해하기 

  • 모든 파드에는 다음과 같은 설정값이 있다. 각각의 값은 dns 서버의 IP와 searchdomain 그리고 ndots option이 있다. 아래와 같이 dns config를 직접 적어주지 않는 일반적인 경우에는 CoreDNS의 Corefile의 설정을 따른다.
apiVersion: v1
kind: Pod
metadata:
  namespace: default
  name: dns-example
spec:
  containers:
    - name: test
      image: nginx
  dnsPolicy: "None"
  dnsConfig:
    nameservers:
      - 1.2.3.4
    searches:
      - ns1.svc.cluster-domain.example
      - my.dns.search.suffix
    options:
      - name: ndots
        value: "2"
      - name: edns0
 


(Ted:N/A) [root@operator-host koo]# k exec dns-example -- cat /etc/resolv.conf
search ns1.svc.cluster-domain.example my.dns.search.suffix
nameserver 1.2.3.4
options ndots:2 edns0
(Ted:N/A) [root@operator-host koo]#
  • Corefile 확인 
(Ted:N/A) [root@operator-host koo]# k describe cm -n kube-system coredns
Name:         coredns
Namespace:    kube-system
Labels:       eks.amazonaws.com/component=coredns
              k8s-app=kube-dns
Annotations:  <none>

Data
====
Corefile:
----
.:53 {
    errors
    health {
        lameduck 5s
      }
    ready
    kubernetes cluster.local in-addr.arpa ip6.arpa {
      pods insecure
      fallthrough in-addr.arpa ip6.arpa
    }
    prometheus :9153
    forward . /etc/resolv.conf
    cache 30
    loop
    reload
    loadbalance
}



BinaryData
====

Events:  <none>
  • 일반적인 파드의 /etc/resolv.conf 설정
(Ted:N/A) [root@operator-host koo]# k exec tetris-6786cddbd5-h9wr2 -- cat /etc/resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local ap-northeast-2.compute.internal
nameserver 10.100.0.10
options ndots:5

✅  클러스터 내부 DNS 

☑️ tcpdump about internal DNS  

#  워커노드에 각각 1개씩 netshoot 파드를 배포해본다. 
#  테스트용 netshoot-pod 디플로이먼트 생성
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: netshoot-pod
spec:
  replicas: 3
  selector:
    matchLabels:
      app: netshoot-pod
  template:
    metadata:
      labels:
        app: netshoot-pod
    spec:
      containers:
      - name: netshoot-pod
        image: nicolaka/netshoot
        command: ["tail"]
        args: ["-f", "/dev/null"]
      terminationGracePeriodSeconds: 0
EOF

# 파드 이름 변수 지정
PODNAME1=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[0].metadata.name}')
PODNAME2=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[1].metadata.name}')
PODNAME3=$(kubectl get pod -l app=netshoot-pod -o jsonpath='{.items[2].metadata.name}')

# 파드 확인
kubectl get pod -o wide
kubectl get pod -o=custom-columns=NAME:.metadata.name,IP:.status.podIP

# 1번. tetris.svc.cluster.local로 질의시 5번만에 조회 ( 같은 네임스페이스 ) 
(Ted:N/A) [root@operator-host koo]# 
kubectl exec -it $PODNAME1 -- curl -v tetris.svc.cluster.local

# 2번. tetris로 질의시 1번만에 조회 ( 서치도메인 default.svc.cluster.local 제일 앞에 적용)
(Ted:N/A) [root@operator-host koo]# 
kubectl exec -it $PODNAME1 -- curl -v tetris

# 2번. tetris.default 로 질의시 2번만에 조회 ( 서치도메인 svc.cluster.local  2번째에 적용)
(Ted:N/A) [root@operator-host koo]# 
kubectl exec -it $PODNAME1 -- curl -v tetris.default


# 4번. tetris.default.svc.cluster.local. ( 풀FQDN으로 질의시 1번만에 조회)
(Ted:N/A) [root@operator-host koo]# 
kubectl exec -it $PODNAME1 -- curl -v tetris.default.svc.cluster.local.

✅  클러스터 외부 DNS 

☑️ tcpdump about internal DNS  (google.com)  

# 운영 서버 
(Ted:N/A) [root@operator-host ~]# kubectl exec -it $PODNAME1 -- curl -v google.com

# 노드 1번 
[ec2-user@ip-192-168-3-215 ~]$ sudo tcpdump -i any -nn udp and src host 192.168.3.113

 

☑️  서치 도메인 

search default.svc.cluster.local svc.cluster.local cluster.local ap-northeast-2.compute.internal
nameserver 10.100.0.10
options ndots:5

 

 

 

✅ coreDNS + nodeLocal DNS 통신 과정 알아보기

☑️  nodelocal dns 설치

wget https://github.com/kubernetes/kubernetes/raw/master/cluster/addons/dns/nodelocaldns/nodelocaldns.yaml

# 환경변수 설정
kubedns=`kubectl get svc kube-dns -n kube-system -o jsonpath={.spec.clusterIP}`
domain=cluster.local
localdns=169.254.20.10

# __PILLAR__DNS__SERVER__ : kubectl get svc -n kube-system | grep kube-dns | awk '{ print $3 }'
# __PILLAR__DNS__DOMAIN__ : 클러스터 도메인을 나타내며 기본값은 cluster.local
# __PILLAR__LOCAL__DNS__ : NodeLocal DNSCache에 대해 선택된 로컬 수신 IP 주소 (169.254.20.10으로 고정입니다.)


# manifest 변수 수정 
sed -i "s/__PILLAR__LOCAL__DNS__/$localdns/g; s/__PILLAR__DNS__DOMAIN__/$domain/g; s/__PILLAR__DNS__SERVER__/$kubedns/g" nodelocaldns.yaml


### MAC 일경우
sed -i '' "s|__PILLAR__LOCAL__DNS__|$localdns|g; s|__PILLAR__DNS__DOMAIN__|$domain|g; s|__PILLAR__DNS__SERVER__|$kubedns|g" nodelocaldns.yaml
  • 배포 후 확인
kubectl create -f nodelocaldns.yaml

# kubectl get pods -n kube-system | grep node-local
node-local-dns-2fncz                       1/1     Running   0          98s
node-local-dns-cb422                       1/1     Running   0          98s
node-local-dns-f22bq                       1/1     Running   0          98s
  • 테스트 전 로그 설정
# kubectl -n kube-system edit configmap node-local-dns
...
apiVersion: v1
data:
Corefile: |
cluster.local:53 {
  log
  errors
  cache {
    success 9984 30
    denial 9984 5
  }
...

# nodelocaldns daemonset 배포
kubectl rollout restart daemonset -n kube-system node-local-dns


# 테스트 pod 배포
kubectl apply -f https://k8s.io/examples/admin/dns/dnsutils.yaml
  • 테스트 pod에 접속해서 nslookup 실행
# k exec -it dnsutils -- /bin/sh

# nslookup
> kubernetes.default
Server:		10.100.0.10
Address:	10.100.0.10#53

Name:	kubernetes.default.svc.cluster.local
Address: 10.100.0.1
> google.com
Server:		10.100.0.10
Address:	10.100.0.10#53

Non-authoritative answer:
Name:	google.com
Address: 142.250.206.238
  • nodelocaldns의 로그 확인
kubectl logs --namespace=kube-system -l k8s-app=node-local-dns -f

# 로그를 확인 해보면 클러스터 내부에 있는 서비스 예를 들면 default 네임스페이스의 kubernetes 같이
# DNS 쿼리를 외부로 보내거나 외부 DNS 서버에서 응답을 기다릴 필요가 없으므로 응답 시간이 훨씬 빠릅니다

[INFO] Added back nodelocaldns rule - {filter INPUT [-p tcp -d 10.200.1.10 --dport 53 -j ACCEPT -m comment --comment NodeLocal DNS Cache: allow DNS traffic]}
[INFO] Added back nodelocaldns rule - {filter INPUT [-p udp -d 10.200.1.10 --dport 53 -j ACCEPT -m comment --comment NodeLocal DNS Cache: allow DNS traffic]}
[INFO] Added back nodelocaldns rule - {raw OUTPUT [-p tcp -s 10.200.1.10 --sport 53 -j NOTRACK -m comment --comment NodeLocal DNS Cache: skip conntrack]}
[INFO] Added back nodelocaldns rule - {raw OUTPUT [-p udp -s 10.200.1.10 --sport 53 -j NOTRACK -m comment --comment NodeLocal DNS Cache: skip conntrack]}

# conntrack을 우회하도록 설정한 iptables 규칙이 추가되었음을 나타냅니다.
# conntrack의 비활성화로 성능을 최적화합니다.

# 1. Skipping iptables DNAT and connection tracking will help reduce conntrack races and avoid UDP DNS entries filling up conntrack table.
# 2. reducing the number of queries for the kube-dns service.

 

 

  • 왼쪽은 nodelocalDNS 에 조회된 로그 입니다.
  • 오른쪽은 coreDNS에 조회된 로그입니다. ( 기본적으로 30초간 캐쉬에 저장해놓고 그 동안에 요청이 들어올때는 캐쉬에서 처리 하기 때문에 요청이 들어오지 않는 모습)