본문 바로가기

DNS 네임서버 구축 및 API를 통한 자동화 관리 올인원

설계목표

- 네임서버를 구축한다.
- 네임서버를 API를 통해 편하게 관리한다.
- API를 통해 DNS 레코드 및 Redirection을 관리한다.

참고사항

- 네임서버는 API를 제공하지 않는다.
- 네임서버는 Redirection을 지원하지 않는다.

- AI도 할 줄 모른다... 이 글 학습하면 앞으로 가능할듯

목차

1. DNS 조회 원리

2. DNS 서버 설정 (bind9)

3. DNS  TLD에 공개

4. NginX 기반 리다이렉트 진행  

5. DNS 세팅 API 자동화 - Bind9 자동화  

6. DNS 세팅 API 자동화 - Nginx 자동화  

7. DNS 세팅 API 자동화 - API를 통해  DNS, Nginx 관리 자동화  

8. 마치며

DNS 조회 원리

다음 이미지로 대체한다.

중요한 것은 우리 네임서버(klr.kr)를 TLD에 등록하여, 쿼리 가능하도록 해야한다는 점이다.

DNS 서버 설정(Bind9)

목표 : DNS 서버를 구축한다.

bind9 설치

sudo apt install bind9

/etc/bind/named.conf.local 수정

내 네임서버를 정의하고, 레코드 설정을 기록할 경로를 입력한다.

Zone 파일은 밑에서 설명하겠다.

zone "klr.kr" {
    type master;
    file "/etc/bind/zones/db.klr.kr"; # Zone 파일 경로
};

 

Zone 설정

1. /etc/bind/zones 폴더 생성

2. /etc/bind/zones/db.klr.kr 생성

이후 편리한 설정을 위해 db.local 을 복사한다.

sudo cp db.local ./zones/db.klr.kr

해당 파일을 열어 아래와 같이 도메인(klr.kr) 및 네임서버 ip를 일치시킨다.

;
; BIND data file for local loopback interface
;
$TTL    604800
@       IN      SOA     ns1.klr.kr. admin.klr.kr. (
                              2         ; Serial
                         604800         ; Refresh
                          86400         ; Retry
                        2419200         ; Expire
                         604800 )       ; Negative Cache TTL
; Name Server Info
@       3600    IN      NS      ns1.klr.kr.
; Name Server A record
ns1     IN      A       203.253.76.163

@       IN      A       203.253.76.163
a       IN      CNAME   google.com.
b       IN      A       158.180.67.189

문법을 상세히 알고싶다면? 아래 더보기

더보기

설정 내용 설명

위 문법이 뭔지도 모르고 그냥 따라하면 찝찝하다. 각각을 명확히 알아보자.

$TTL 604800 : DNS 레코드가 캐시될 시간을 초단위로 지정 (1주일로 지정됨)

 

상기된 레코드는 다음과 같이 분류된다.

A, CNAME은 다들 알텐데, SOA와 NS는 생소할 수 있다.

SOA NS A CNAME
도메인 기본 설정 네임서버 정보 IP 주소 안내 도메인 주소 별칭

차례로 분석해보면 다음과 같다

@ IN SOA ns1.klr.kr. admin.klr.kr.
현재 영역 이름
(klr.kr
인터넷 클래스 Start of Authority
시작 권한 레코드
주 네임서버 도메인 관리자 메일 주소
(실제로 @는 .으로 대체됨)
Serial 영역 파일 버전 일련번호
DNS 설정 변경시마다 증가해야 함
Refresh 보조 네임서버가 주 네임서버에
변경사항 확인하는 주기
Retry 주 네임서버 연결 실패시
재시도할 시간 간격
Expire 보조 네임저버가 주 네임서버와
연결하지 못할 때 정보유지 기간
Negative Cache TTL 존재하지 않는 레코드에 대한
응답을 캐시하는 시간

(선택) 정의되지 않은 DNS 매핑

특정 PC/서버에서 DNS를 이 네임서버로만 가리키고 싶은 경우,

현재 서버에는 다른 DNS 정보가 없기 때문에 구글,네이버 등의 외부 사이트 쿼리에 실패한다.

일반적으로 네임서버 여기 하나만 쓸 일은 없을테니 안해도 된다.

 

/etc/bind/named.conf.options 파일에서 다음을 추가한다.

forwarders {
    8.8.8.8;
    8.8.4.4;
};
allow-query { any; };

수정사항 반영

# 방법 1(추천) - 변경사항만 반영
rndc reload 

# 방법 2 - bind9 재시작
sudo systemctl restart bind9

DNS 테스트

dig a.klr.kr @localhost
dig b.klr.kr @localhost
#또는 nslookup b.klr.kr

오류가 발생한 경우? 아래 더보기

더보기

오류 발생 시

다음을 입력해 오류 발생 지점 확인 가능

named-checkzone [영역 이름] [영역 파일 경로]

예)

sudo named-checkzone klr.kr /etc/bind/zones/db.klr.kr

당장 내 컴퓨터에서 네임서버에 접근 가능한지 보고 싶다면?

더보기

(개별 클라이언트) DNS 연동

별거 없다. 그냥 클라이언트 네임서버 주소 변경이다.

 

아직 TLD에 네임서버를 등록하지 않아서 외부에서는 네임서버를 알아볼 수 없다.

이 방식은 임의로 클라이언트에서 네임서버를 현재 서버로 지정하여 접속 가능하게 한다.

이 설정은 개별 클라이언트에 적용되므로, 범용적이지 않다.

 

방법 1

neplan에서 nameserver를 공인 ip로 설정한다.

그냥 보면 알 수 있다. 별도 설명은 하지 않겠다.

방법 2

일시 변경 : sudo vi /etc/resolv.conf 에 다음을 추가한다.

nameserver [ip주소]

 영구 변경 : sudo vi /etc/resolvconf/resolv.conf.d/head 에 다음을 추가하고 재부팅한다.

nameserver [ip주소]

 

이로써 매우 간단하게 네임서버 구축을 완료하였다.

DNS TLD에 공개

목표 : DNS 서버를 외부에 공개하여, 모든 클라이언트에서 DNS 쿼리를 가능하게 한다.

사설 보안망 구축이 아닌 이상, 외부에서 쿼리도 못하는 네임서버는 반쪽짜리 네임서버다.

호스트 등록

네임서버를 TLD 수준에 등록한다.

네임서버 변경

등록한 네임서버로 도메인을 연동한다.

 

  • .com.net : 3~4 시간 (실시간 루트 네임서버 변경)
  • .kr (한국인터넷진흥원) : 약 1일 소요
    • 전일 18:00 ~ 금일 08:00 변경 건 : 금일 08:20 업데이트
    • 금일 08:00 ~ 금일 12:00 변경 건 : 금일 12:20 업데이트
    • 금일 12:00 ~ 금일 18:00 변경 건 : 금일 18:20 업데이트
  • .us .cn .jp .in : 약 1일 소요
  • .biz .info .org : 3~4시간 (실시간 루트 네임서버 변경)

변경된 네임서버 확인

https://www.whatsmydns.net/

 

DNS Propagation Checker - Global DNS Checker Tool

Instant DNS Propagation Check. Global DNS Propagation Checker - Check DNS records around the world.

www.whatsmydns.net

Nginx 기반 리다이렉트 진행

목표 : 내 DNS 서버가 Redirection까지 지원하도록 설계한다.

내가 만든 사이트를 내 도메인을 활용해 단축 URL을 만들고 싶지 않은가?

안타깝게도 이 방법을 서술한 블로그는 전혀 보이지 않는다.

 

Nginx 를 활용해 응용해보자. 필자는 다음과 같은 리디렉션 방식을 설계했다.

1. 사용자가 특정 서브도메인(a.klr.kr)에 대한 리디렉션(naver.com) 추가를 요청한다.

2. 서브도메인(a.klr.kr)은 네임서버(1.2.3.4)를 가리킨다.

3. 네임서버의 80 포트는 Nginx이므로, Nginx에서 해당 리디렉션을 안내한다.

 

원하는 서비도메인은 Bing9에서 직접 네임서버를 가리키도록 추가하고,

이후 Nginx가 신경써야할 부분은 위 3번이다.

NginX 설치

sudo apt update
sudo apt install nginx

conf 파일 수정

DNS 서버가 단독으로 공인 ip를 지닌 경우

map $host $redirect_destination {
        hostnames;
        입력주소 http://연결대상주소;
        입력주소2 http://연결대상주소2;
                ...
}

server {
        listen 80;
        server_name _;

        location / {
                if ($redirect_destination) {
                        return 301 $redirect_destination$request_uri;
                }
        }
}

 

내 네임서버가 다른 VM(인스턴스)와 공인 ip를 공유하나요?

= HyperVisor 환경에서 "네임서버"와 "웹 서버"를 격리된 인스턴스로 운용중이라면?

아래 더보기

더보기

DNS 서버가 다른 VM(인스턴스)와 공인 ip를 공유하는 경우

필자와 같이 KVM 등으로 DNS VM을 별도로 구축한 경우, Redirect 대상이 아닌 모든 요청은 웹서버 VM으로 다시 빠져야한다.

안그러면 웹 서버에 접근이 안된다.

map $host $redirect_destination {
        hostnames;
        입력주소 http://연결대상주소;
        입력주소2 http://연결대상주소2;
                ...
}

server {
        listen 80;
        server_name _;

        location / {
                if ($redirect_destination) {
                        return 301 $redirect_destination$request_uri;
                }

                proxy_pass http://웹서버VM주소;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
        }
}

이상으로 설정을 마치면 된다.

DNS 세팅 API 자동화 - Bind9 자동화

목표 : DNS 및 리디렉션을 자동화 하는 API 서버를 구축한다.
클라우드 서비스 (AWS EC2) OnPremise
우측 이미지에서
녹색 영역만 보면 된다.

 

위와 같은 형태로 제작하는 것을 목표로 한다.

개인적으로 창의적인 방법을 고안하여 생각한 방법이므로,

더 좋은 방법이 있는지 알아볼 필요가 있음

Bind9 설정

bind의 zone 영역은 include가 불가능하다. 따라서 이를 스크립트화 시킬 것이다.

change_record.sh

ZONES_DIR="/etc/bind/zones/list_dns"
ZONE_FILE="/etc/bind/zones/zone파일명"

cat > $ZONE_FILE <<EOF
;
; BIND data file for local loopback interface
;
\$TTL    300
@       IN      SOA     네임서버. 메일주소. (
                              2         ; Serial
                          86400         ; Refresh
                           7200         ; Retry
                        2419200         ; Expire
                          86400 )       ; Negative Cache TTL
; Name Server Info
@       3600    IN      NS      네임서버.
; Name Server A record
ns1     IN      A       네임서버ip
@       IN      A       네임서버ip

EOF

for file in $ZONES_DIR/*; do
        cat $file >> $ZONE_FILE
        #echo "" >> $ZONE_FILE
done

add_record.sh

FILE_PATH="/etc/bind/zones/list_dns/$1"

if [ -f "$FILE_PATH" ]; then
            rm "$FILE_PATH"
fi

echo "$1        IN      $2      $3" > "$FILE_PATH"

sudo /etc/bind/zones/change_record.sh

sudo rndc reload

del_record.sh

FILE_PATH="/etc/bind/zones/list_dns/$1"

if [ -f "$FILE_PATH" ]; then
            rm "$FILE_PATH"
fi

sudo /etc/bind/zones/change_record.sh

sudo rndc reload

DNS 레코드 한번에 관리

이후 /etc/bind/zones/에 list_dns 폴더를 제작한다.

sudo mkdir list_reidrects
더보기

예)

/etc/bind/zones/list_dns/example1 의 내부 내용

example1        IN      A      1.2.3.4

/etc/bind/zones/list_dns/example2 의 내부 내용

example2        IN      A      5.6.7.8

 

위와 같이 모든 서브도메인을 파일로 관리한 뒤, 앞으로 api를 요청 받으면 해당 폴더에
[서브도메인] [레코드명] [대상주소]를 담은 

[서브도메인] 파일을 제작하기 위함이다.

 

이제 api를 요청 받으면 서버는 다음과 같이 파일을 실행하면 된다.

/etc/bind/zones/add_record.sh [서브도메인] [레코드명] [대상주소]

 

DNS 세팅 API 자동화 - Nginx 자동화

자동화를 위해 Nginx의 리디렉션 경로들도 위와 같이 파일로 관리한다.

파일로 관리하기 위해 Nginx의 conf 파일에서

"입력주소 http://연결대상주소" 형태의 목록을 include로 대체한다.

/etc/nginx/conf.d/redirect.conf

map $host $redirect_destination {
        hostnames;
        include /etc/nginx/conf.d/list_redirects/*; # 이 부분을 수정한다
}

server {
        listen 80;
        server_name _;

        location / {
                if ($redirect_destination) {
                        return 301 $redirect_destination$request_uri;
                }

                proxy_pass http://웹서버VM주소;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
        }
}

 

이후 /etc/nginx/conf.d/에 list_redirects 폴더를 제작한다. 

sudo mkdir list_reidrects
더보기

예) /etc/nginx/conf.d/list_redirects/example1 의 내부 내용

example1.test.com https://naver.com

이는 앞으로 api를 요청 받으면 해당 폴더에

[요청 서브도메인 주소] [리다이렉트 주소] 를 담은

[서브도메인] 파일을 제작하기 위함이다.

add_redirects.sh

DOMAIN_NAME=".도메인주소"
FILE_PATH="/etc/nginx/conf.d/list_redirects/$1"

if [ -f "$FILE_PATH" ]; then
            rm "$FILE_PATH"
fi

echo "$1$DOMAIN_NAME $2" > "$FILE_PATH"

sudo /etc/bind/zones/add_record.sh $1 A 네임서버ip

sudo nginx -s reload

del_redirects.sh

FILE_PATH="/etc/nginx/conf.d/list_redirects/$1"

if [ -f "$FILE_PATH" ]; then
            rm "$FILE_PATH"
fi

sudo /etc/bind/zones/del_record.sh $1

sudo nginx -s reload

 

이제 api를 요청 받으면 다음과 같이 실행한다.

/etc/nginx/conf.d/add_redirects.sh [서브도메인] [대상주소]

 

DNS 세팅 API 자동화 - API를 통해 DNS, Nginx 관리 자동화

목표 : API를 통해 "서브도메인과 레코드" 또는 "리디렉션 주소"를 정하면, 해당 세팅을 자동화하는 서버 제작

이 페이지에서는 DNS 서버의 SpringBoot영역을 구성하고 마무리한다.

DTO

요청 받을 정보들을 담을 DTO를 먼저 처리한다.

이 Application을 DB 저장을 하지 않고 process만 처리하므로, Entity는 생성하지 않는다.

DNSRecordDTO

@Data
public class DNSRecordDTO {
    // 양식
    // subdomain    A   1.2.3.4
    // subdomain    CNAME   example.com. (뒤에 온점)
    private String subdomain;
    private String record;
    private String targetAddress;
}

RedirectDTO

@Data
public class RedirectDTO {
    private String subdomain;
    private String targetAddress;
}

DeleteDomainDTO

@Data
public class DeleteDomainDTO {
    private String subdomain;
}

Service

DTO를 기반으로 ‘Record, Redirection 생성 및 삭제’을 위해 생성해두었던 ~.sh를 실행시킨다.

@Service
public class DnsService {

    private void executeScript(String scriptPath, String... args){
        try {
            List<String> command = new ArrayList<>();
            command.add("sudo");
            command.add(scriptPath);
            Collections.addAll(command, args);

            ProcessBuilder processBuilder = new ProcessBuilder(command);
            Process process = processBuilder.start();
            process.waitFor();
        } catch (Exception e) {
            throw new NotRunnableException(scriptPath);
        }
    }
    public void addDNSRecord(DNSRecordDTO dnsRecordDTO){
        String targetAddress = dnsRecordDTO.getTargetAddress();
        if(dnsRecordDTO.getRecord().equals("CNAME")){
            targetAddress = targetAddress+ ".";
        }
        executeScript("/etc/bind/zones/add_record.sh", dnsRecordDTO.getSubdomain(), dnsRecordDTO.getRecord(), targetAddress);
    }

    public void addRedirect(RedirectDTO redirectDTO){
        executeScript("/etc/nginx/conf.d/add_redirects.sh", redirectDTO.getSubdomain(), redirectDTO.getTargetAddress());
    }

    public void delDomain(String subDomain){
        executeScript("/etc/nginx/conf.d/del_redirects.sh", subDomain);
    }
}

Exception

Service에서 생성한 Exception을 정의한다

public class NotRunnableException extends RuntimeException{
    public NotRunnableException(String filepath){
        super("파일 실행에 실패함: "+filepath);
    }
}

Controller

일반 Controller

Service를 바탕으로, 이를 호출한다.

@RestController
@RequiredArgsConstructor
@RequestMapping("/v1")
public class DnsController {

    private final DnsService dnsService;

    @PostMapping("/records")
    public ResponseEntity<?> addDNSRecord(@RequestBody DNSRecordDTO dnsRecordDTO){
        dnsService.addDNSRecord(dnsRecordDTO);
        return ResponseEntity.status(HttpStatus.OK).build();
    }

    @PostMapping("/redirects")
    public ResponseEntity<?> addRedirects(@RequestBody RedirectDTO redirectDTO){
        dnsService.addRedirect(redirectDTO);
        return ResponseEntity.status(HttpStatus.OK).build();
    }

    @DeleteMapping("/subdomain/{subdomain}")
    public ResponseEntity<?> delDomain(@PathVariable String subdomain){
        dnsService.delDomain(subdomain);
        return ResponseEntity.status(HttpStatus.OK).build();
    }
}

예외 처리 Controller

발생시킨 Exception에 대한 Response를 정의한다.

@ControllerAdvice
public class ExceptionHandlerControllerAdvice {

    @ExceptionHandler({NotRunnableException.class})
    public ResponseEntity<?> handleNotRunnableException(NotRunnableException e){
        System.out.println(e.getMessage());
        return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

마치며

주변에서도 유사한 서비스를 구현하고 싶은데, 방법을 몰라 구축을 못한 사례들이 종종 있었다.

게시글도 없고, AI도 해결을 못해서 어려운게 아닐까 싶다.

 

이러한 서비스 구축을 희망하는 이들에게 도움이 되길 바란다.

 

참고 문헌

https://m.blog.naver.com/love_tolty/222690840923

https://dev.dwer.kr/2020/04/bind-9.html

아래는 DNS 갱신시간 관련 Webtizen 설명

https://www.webtizen.co.kr/support/faq/cs_faq_view?uid=2&vid=27&rm=50&this_page=6

NORMAL j/k: 이동 · Enter: 열기 · /: 검색 · ?: 도움말