0. 들어가며
안녕하세요~ 시스템컨설턴트그룹 25기 인프라 담당 권민성입니다.
오늘은 DNS 관리 도구를 만들다가, 결국 유사 DNS 서버를 만들게 된 이야기를 해보려고 합니다.
DNS란?
우선 DNS(Domain Name System)은 우리가 사용하는 도메인 이름을 실제 서버의 IP 주소로 변환해 주는 시스템입니다.
예를 들어, 우리가 브라우저에 google.com을 입력하면, 컴퓨터는 해당 도메인의 IP 주소를 알아내기 위해 DNS 서버에 질의를 보내고, 그 결과로 얻은 IP로 실제 서버와 통신을 하게 됩니다.
이때 도메인에 대한 실제 정보를 가지고 있고, 요청에 대해 응답을 해주는 서버를 DNS 서버라고 합니다. 대표적으로 많이 사용되는 DNS 서버로는 ISC에서 개발한 BIND9이 있습니다.

실제로 nslookup이나 dig 명령어를 통해 DNS 서버에 IP 주소를 묻는 질문을 보낼 수 있습니다.
(dig가 nslookup 보다 최신 도구이고 더 많은 정보를 제공해 줍니다)


1. 배경
시스템컨설턴트그룹은 그동안 학교로부터 skku.ac.kr의 서브 도메인인 scg.skku.ac.kr이라는 도메인을 위임받아 BIND9 기반의 자체 DNS 서버를 운영해 왔습니다. 하지만 오랜 기간 운영을 해오면서 아래와 같은 문제점들을 마주하게 되었습니다.
- BIND9이 기본적으로 파일 기반의 저장 방식이다.
- 레코드를 수정할 때마다 직접 해당 서버에 접속해서 파일을 수정하고 적용해야 한다.
- 파일 기반의 저장 방식으로 인해 이중화에 제약이 많다(쿠버네티스에 여러 레플리카로 띄우기 어려움).
- 레코드가 많아지면 파일의 가독성이 떨어지고, 유지보수가 매우 어려워진다.
- DNS 레코드가 너무 많아짐에 따라 관리가 잘 되지 않고, DNS 레코드 관련 API가 없다.
- 학교에서 80번 포트의 사용을 제한함에 따라 기존의 80번 포트를 사용하는 인증서 갱신 방식을 사용할 수 없게 되었다(따로 다룰 예정)
위의 문제점들을 살펴봅시다.
파일 기반의 저장 방식?
BIND9은 사실 굉장히 오래된 프로그램입니다. 오랜 기간 동안 DNS 서버의 표준으로 사용되어 왔고, 지금도 많은 환경에서 안정적으로 사용되고 있습니다. 하지만 그만큼 설계 자체도 매우 전통적인 방식에 기반하고 있습니다. 그 대표적인 예가 DNS 레코드 같은 중요한 데이터를 데이터베이스에 보관하는 게 아니라 zone 파일이라는 텍스트 파일의 형태로 저장한다는 점입니다.

일반적으로 DNS 서버는 시작 시 모든 zone 파일을 메모리에 로드한 뒤, 들어오는 DNS 질의에 응답합니다. 사진에 보이는 A 레코드는 도메인 이름을 실제 서버의 IP 주소로 매핑해 주는 역할을 합니다. 레코드가 몇십 개 수준일 때는 파일로도 충분히 관리가 가능하지만, 레코드가 수 백, 수 천이 되는 순간 파일로는 관리가 사실상 불가능해집니다.
사실 BIND9에는 Dynamic Loadable Zones(DLZ)라고 하여 직접 데이터베이스와 연동하여 레코드를 관리할 수 있는 방법이 존재합니다. BIND9이 오픈소스이기 때문에 관련 라이브러리를 함께 빌드하여 사용하는 방식입니다. 하지만 이 방법 또한 아래와 같은 한계가 존재했습니다.
- 제대로 된 BIND9 전용 데이터베이스 라이브러리 코드가 없다(C로 된 BIND9과 연동되도록 데이터베이스 드라이버를 직접 작성해야 함...).
- BIND9의 소스 코드에 의존적이므로 버전업에 따라 스펙이 바뀌면 새로 작성해야 한다.
- 결국 구현한다고 하더라도 API의 형태로 DNS를 조회하려면 별도의 로직을 구현해야 한다.
또한 이중화의 관점에서 파일 기반의 저장 방식은 제약이 많아, 쿠버네티스 환경에서 여러 레플리카로 구성하기 어렵다는 문제가 있습니다. 모든 컨테이너가 zone 파일을 공유하려면 결국 volume 형태로 구성해야 하는데, 이 경우 파일 동기화와 일관성 문제로 인해 운영이 복잡해집니다.
목표
결국 저희는 아래와 같은 조건을 모두 만족하는 시스템을 만들고자 하였습니다.
- 데이터베이스를 단일 원천(Source of Truth)으로 사용하여 DNS 레코드를 관리할 수 있을 것
- BIND9은 그대로 사용하되, 소스 코드를 직접 수정하지 않을 것
- REST API 형태로 DNS 레코드를 조회하고 관리할 수 있을 것
- BIND9를 여러 레플리카로 구성하여 이중화를 손쉽게 할 수 있을 것
- DNS 기반 인증서 갱신 방식(DNS-01)에 대응할 수 있을 것
2. 첫 번째 해결 과정
이에 대한 첫 번째 해결 과정으로 아래와 같은 구조를 생각해 내었습니다.
데이터베이스(단일 원천)를 두고 각 DNS 서버의 zone 파일을 직접 수정하는 방식은 어떨까?

간단하게 말해서 BIND9과 같은 서버에 플러그인의 형태로 프로그램을 설치하고, 해당 프로그램이 BIND9의 zone 파일들을 직접 수정하고 반영하는 구조를 생각했습니다. 따라서 사용자의 레코드 변경 및 생성 요청에 대한 구체적인 흐름은 다음과 같습니다.
- REST API를 통해 DNS 레코드 변경 및 생성 요청을 받습니다.
- 요청된 내용을 데이터베이스에 반영합니다.
- 데이터베이스 상태를 기준으로 BIND9의 zone 파일을 갱신(덮어쓰기)합니다.
- RNDC 프로토콜을 통해 BIND9에 reload를 요청하여 변경 사항을 반영합니다.
BIND9은 단순히 zone 파일을 수정한다고 해서 바로 변경 사항이 반영되지 않습니다. 따라서 BIND9 서비스를 재시작하거나, RNDC 명령어를 통해 zone 파일을 reload 하도록 해야 합니다.
각 과정을 천천히 살펴봅시다.
API 서버 구현: Axum 기반 라우팅 구조
Axum은 Rust 기반의 웹 프레임워크로, 익숙한 Node.js 계열의 Express.js와 유사하게 라우팅, 요청 추출, 미들웨어 등의 기능을 제공합니다. Bindizr에서는 기존 웹 생태계에서 널리 사용되는 MVC(Model-View-Controller) 구조를 반영하여 zone과 record를 관리하는 API 서버를 구성하였습니다.

왼쪽 코드는 전체 API 라우터를 구성하는 부분입니다. ZoneApi, RecordApi처럼 도메인별 라우터를 merge()를 통해 하나의 메인 라우터로 합치고 CorsLayer::permissive()를 통해 CORS 설정한 코드가 있습니다.
오른쪽 코드는 실제 Zone API의 실제 구현입니다. /zones, /zones/{name}와 같은 REST API 경로를 정의하고, 각 HTTP Method를 서비스 계층의 로직과 연결합니다. get_zones 핸들러는 ZoneService::get_zones()와 같은 Service 계층을 호출하고 GetZoneResponse와 같은 DTO로 직렬화하여 StatusCode::OK 같은 상태 코드와 함께 응답을 마무리합니다.
SQLx 기반 멀티 데이터베이스 지원
SQLx는 Rust에서 많이 사용되는 비동기 SQL 라이브러리로, ORM보다는 직접 SQL을 작성하는 방식에 가깝습니다. 또한 컴파일 타임 쿼리 검증 기능을 제공하고, 다양한 데이터베이스를 지원합니다.
사실 Rust에는 Diesel과 같은 ORM 라이브러리도 존재하지만, Bindizr에서는 복잡한 ORM 기능이 크게 필요하지 않았습니다. 대부분 zone과 record에 대한 단순한 CRUD 작업이 중심이었고, 동시에 여러 데이터베이스를 지원해야 했기 때문에 직접 SQL을 작성하는 방식이 더 단순하다고 판단했습니다.
기본적인 사용 예시는 아래와 같습니다. JDBC를 사용해 보신 분들이라면 SQL 문을 작성하고, ? 또는 placeholder 위치에 들어갈 값을 직접 바인딩하는 방식이 익숙하실 것입니다.

Bindizr에서는 데이터베이스 접근 로직과 도메인 비즈니스 로직을 분리하기 위해 Repository 패턴을 사용했습니다. Repository 계층은 데이터베이스 연결 관리와 DBMS별 SQL 실행을 담당하고, Service 계층은 zone 생성, record 수정, 검증과 같은 도메인 로직을 담당합니다.
즉, API나 NSUPDATE 모듈은 직접 SQL을 실행하지 않고 ZoneService::get_zone(name)과 같은 서비스 메서드를 호출합니다. Service 계층 역시 특정 데이터베이스의 connection pool이나 SQL 문을 직접 다루지 않고, RepositoryService::get_zone_by_name(name)과 같은 Repository 메서드를 통해 데이터에 접근합니다.
이렇게 계층을 분리하면 MySQL, PostgreSQL, SQLite처럼 데이터베이스별 구현이 달라지더라도, 상위의 Service/API/NSUPDATE 로직은 크게 변경하지 않고 유지할 수 있습니다.

SQLx는 트랜잭션 기능 또한 지원합니다. 트랜잭션 또한 여러 데이터베이스에서 동일한 방식으로 사용하기 위해, 데이터베이스별 트랜잭션 생성 로직을 분리하고 이를 감싸는 래퍼 함수를 만들어 사용했습니다.

위 코드는 데이터베이스 타입에 따라 서로 다른 SQLx transaction 객체를 생성한 뒤, 이를 RepositoryTx라는 공통 타입으로 감싸는 과정입니다.
MySQL, PostgreSQL, SQLite는 각각 반환하는 transaction 타입이 다르기 때문에 서비스 계층에서 이를 직접 처리하면 데이터베이스별 분기 로직이 계속 등장하게 됩니다. 이를 방지하기 위해 Repository 계층에서 begin_transaction() 함수를 통해 DB별 transaction 생성 로직을 숨기고, Service 계층에서는 begin_tx() 래퍼 함수만 호출하도록 구성했습니다.
덕분에 Service 계층에서는 현재 어떤 데이터베이스를 사용하는지 신경 쓰지 않고, 동일한 방식으로 트랜잭션을 시작하고 사용할 수 있게 되었습니다.
RNDC 프로토콜을 이용한 변경 사항 반영
RNDC(Remote Name Daemon Control) 명령어는 원래 BIND9을 원격에서 제어하기 위한 유틸리티 프로그램입니다. RNDC 명령어를 통해 BIND9 서버를 끄거나 상태를 조회할 수 있고, zone 관련 작업 등을 할 수 있습니다. 저희는 RNDC를 사용하여 zone 파일을 reload 하는 데 사용하였습니다.
rndc reload # BIND 설정과 zone 파일을 다시 로드
RNDC는 기본적으로 BIND9을 설치하면 함께 설치되는 프로그램이지만, 궁극적으로는 본 프로그램이 BIND9이 설치되지 않는 독립적인 환경에서도 동작하는 것을 목표로 하였습니다. 따라서 본 프로그램에서는 RNDC 명령어를 CLI를 통해 날리는 게 아니라, RNDC 프로그램이 BIND9에 직접 보내는 프로토콜을 프로그램에 녹여야 했습니다.
하지만 RNDC 프로토콜의 Rust 구현체가 없어 고민하던 와중, 고맙게도 ISC에서 RNDC 프로토콜을 Node.js로 포팅해둔 레포지토리를 발견하였고, 이를 그대로 Rust로 포팅하여 사용하였습니다.

이외에도 본 프로그램을 만들면서 고민했던 부분들을 하나씩 소개해보려 합니다.
CLI 구현과 UNIX 소켓
CLI를 구현하면서 최대한 MySQL이나 Docker와 같은 널리 사용되는 CLI 도구들의 동작 방식을 참고하고자 하였습니다. CLI를 구현하면서 고민했던 부분은 다음과 같습니다.
- CLI가 네트워크 통신(TCP/UDP 소켓)을 사용하지 않아야 한다.
- CLI를 통해 실행 중인 Bindizr 프로그램의 상태를 조회할 수 있어야 한다.
- Bindizr를 foreground로 실행할 수 있어야 하며, 다른 터미널에서도 동일한 바이너리를 통해 실행 중인 프로세스에 요청을 보낼 수 있어야 한다.
UNIX 소켓을 통한 CLI 구현은 이러한 조건들을 만족함과 동시에 실제로 많은 CLI 도구들에서 사용되는 방식이었습니다. UNIX 소켓은 TCP/UDP 소켓 통신과 유사하게 소켓이라는 파일을 프로세스 간 쓰고 읽으면서 통신하는 로컬 통신 방식입니다.

이 방식은 UNIX 소켓 서버가 실행 중이어야만 응답을 받을 수 있다는 단점이 있습니다. 하지만 zone 정보를 조회하는 CLI가 직접 DB에 연결하고 데이터를 파싱해 출력하는 구조보다는, 실행 중인 Bindizr 프로세스에 요청을 보내고 결과만 받아오는 방식이 역할 분리 측면에서 더 적절하다고 판단했습니다.


데몬화와 systemd 기반 서비스 관리
데몬(Daemon)이란 메모장이나 CLI 프로그램과 같이 사용자가 직접 실행해서 조작하는 프로그램이 아니라, 백그라운드에서 계속 실행되면서 특정 작업(task)을 수행하는 프로그램을 의미합니다. 대표적으로 HTTP 요청을 수신하고 설정한 경로로 요청을 넘겨 (프록시)주는 NGINX, 사용자의 ssh 요청을 기다리고 모든 ssh 연결을 관리하는 sshd, MySQL 쿼리 요청을 수신하고 처리하는 mysqld 모두 데몬 프로그램입니다. HTTP 요청을 수신하고 들어온 요청에 따라 DNS를 제어하는 Bindizr의 경우 또한 데몬 프로그램이라고 할 수 있습니다.
학교에서 OS 수업을 들으신 분들이라면, 프로세스를 실행할 때 fork를 통해 현재 프로세스와 동일한 자식 프로세스를 생성하고, exec를 통해 실행하려는 프로그램을 해당 자식 프로세스에 덮어씌우는 흐름을 접해보셨을 것입니다.

Bindizr의 초기 구현에서는 데몬 프로그램처럼 동작하도록 하기 위해, Bindizr의 자식 프로세스를 생성한 뒤 부모 프로세스를 종료시키는 방식을 사용했습니다. 이를 통해 터미널에서 실행한 부모 프로세스가 종료되더라도, 자식 프로세스는 백그라운드에서 계속 실행되도록 만들었습니다.

초기 버전에서는 Windows 구현체도 함께 지원하려고 했기 때문에, Rust의 조건부 컴파일 매크로를 활용하여 플랫폼별로 다른 코드가 빌드되도록 구성했습니다. 현재는 Windows 지원은 종료되었지만, 당시에는 Linux와 Windows에서 각각 다른 방식으로 데몬화 로직을 처리해야 했습니다.

하지만, 전통적인 프로세스 생성 방식으로 직접 데몬 프로그램을 구현하는 데에는 여러 가지 문제가 있었습니다.
- Rust에서 fork 시스템 콜을 사용하려면 unsafe 구문이 필요합니다.
- 부모 프로세스와 자식 프로세스의 생명주기를 직접 관리해야 하므로 좀비 프로세스 문제가 발생할 수 있습니다.
- 데몬 프로세스의 PID를 관리하기 위한 PID 파일 문제도 있었습니다.
fork와 같은 시스템 콜은 현재 프로세스의 실행 상태를 그대로 복제하는 저수준의 시스템 콜이기 때문에, Rust의 안전성 보장을 벗어난 unsafe 구문을 사용해야 했습니다. 단순히 백그라운드 실행을 위해 unsafe 코드를 포함해야 한다는 점은 안정성을 중요하게 생각하는 Rust 프로젝트 입장에서는 부담이었습니다.
프로세스 생명주기 관리도 직접 처리해야 했습니다. 자식 프로세스를 생성한 뒤 부모 프로세스가 종료되더라도, 자식 프로세스의 종료 상태가 제대로 회수되지 않으면 시스템에 좀비 프로세스로 남을 수 있습니다. 즉, 단순히 프로세스를 하나 더 만드는 것으로 끝나는 것이 아니라, 생성된 프로세스가 언제 종료되는지, 종료 상태는 어떻게 처리할지까지 함께 고려해야 했습니다.
마지막으로 PID 파일 관리 문제도 있었습니다. 일반적으로 데몬은 /var/run 또는 /run 아래에 PID 파일을 생성해 현재 실행 중인 프로세스의 PID를 기록합니다. 하지만 PID 파일을 언제 생성하고 삭제할지, 비정상 종료로 인해 남아 있는 PID 파일을 어떻게 처리할지, 이미 실행 중인 프로세스를 어떻게 판별할지 등을 모두 직접 구현해야 했습니다.
결과적으로 전통적인 방식으로 데몬화를 직접 구현하면, Bindizr의 핵심 기능인 DNS 관리와는 거리가 있는 프로세스 관리 로직이 늘어나게 됩니다. 이는 코드 복잡도를 높이고, 유지보수성과 안정성 측면에서도 부담이 되었습니다.
이러한 문제를 해결하기 위해 자체적으로 데몬화를 수행하는 방식을 버리고, 프로그램은 포그라운드에서 실행하고 프로세스 관리는 Linux의 systemd에 맡기는 방식으로 전환했습니다.
systemd는 Linux에서 서비스의 실행과 생명주기를 관리하는 표준적인 서비스 관리자입니다. 서비스의 시작, 중지, 재시작뿐만 아니라 부팅 시 자동 실행, 비정상 종료 시 자동 복구, 로그 수집, 실행 사용자 및 작업 디렉터리 설정 등을 담당합니다.
따라서 프로그램이 직접 fork를 사용해 백그라운드 프로세스로 전환하거나, PID 파일을 관리할 필요가 없습니다. 프로그램은 일반적인 서버 프로그램처럼 포그라운드에서 실행되고, 이를 백그라운드 서비스처럼 관리하는 역할은 systemd가 맡습니다.
systemd 기반 서비스 관리 방식으로 전환하면서 얻을 수 있는 장점은 다음과 같습니다.
- 프로그램 내부에 fork를 사용한 데몬화 로직을 직접 작성하지 않아도 됩니다.
- PID 파일을 직접 생성, 삭제, 검증하는 로직이 필요하지 않습니다.
- systemd가 표준 출력과 표준 에러를 수집하므로, journalctl을 통해 로그를 확인할 수 있어, 로그를 저장하는 로직을 직접 작성하지 않아도 됩니다.
- 기본 실행 방식이 포그라운드이기 때문에 Docker와 같은 컨테이너 환경과도 잘 어울립니다.
- 서비스 시작, 중지, 재시작, 자동 복구와 같은 운영 기능을 표준적인 방식(systemctl, service)으로 처리할 수 있습니다.
Bindizr도 이 방식을 사용하여 자체 데몬화 로직을 제거하고, 서비스 실행과 프로세스 관리는 systemd에 위임했습니다. 이를 통해 코드 복잡도를 줄이고, Linux 운영 환경에서 더 표준적인 방식으로 서비스를 관리할 수 있게 되었습니다.
포그라운드로 동작하는 Bindizr 프로그램을 systemd 서비스로 등록하려면 아래와 같은 .service 파일을 작성해야 합니다. 문법은 ini 형식을 따릅니다.

이 파일은 해당 서비스를 어떤 방식으로 실행하고, 언제 시작하며, 종료되었을 때 어떻게 처리할지를 systemd에 알려주는 설정 파일입니다. 여기서 사용된 주요 옵션을 간단히 살펴보면 다음과 같습니다.
- Description: 서비스에 대한 설명, 'systemctl status bindizr' 명령어를 실행했을 때 표시되는 이름
- After: 서비스의 시작 순서를 지정. 여기서는 network.target(네트워크 연결 보장)과 named.service(BIND 서비스 시작 보장) 이후에 Bindizr가 실행되도록 설정했습니다.
- ExecStart: 실제로 서비스를 시작할 때 실행할 명령어. '/usr/bin/bindizr start' 명령을 통해 Bindizr를 실행합니다.
- WorkingDirectory: 서비스가 실행될 작업 디렉터리. '/etc/bindizr'를 기준으로 Bindizr의 설정 파일 등을 다룹니다.
- StandardOutput / StandardError: 표준 출력과 표준 에러를 전달할 경로. journalctl로 전송하여 'journalctl -u bindizr' 명령어로 로그를 확인할 수 있습니다.
일반적으로 /etc/systemd/system 경로에 배치한 뒤, systemctl 명령어를 통해 서비스를 시작하거나 중지할 수 있습니다. 또한 등록된 서비스는 OS 부팅 시 systemd가 /etc/systemd/system 경로의 서비스 파일을 확인한 뒤, 설정에 따라 자동으로 실행합니다.
이제 Bindizr는 아래와 같은 표준 명령어들로 간편하게 제어, 로그 확인이 가능합니다.
sudo systemctl start bindizr # bindizr systemd 서비스 시작
sudo systemctl stop bindizr # bindizr systemd 서비스 중지
sudo systemctl restart bindizr # bindizr systemd 서비스 재시작
sudo systemctl status bindizr # bindizr systemd 서비스 상태 확인
journalctl -u bindizr # bindizr systemd 서비스 로그 확인
빌드와 패키징
이제 주요 개발이 어느 정도 완료되었다면, 프로젝트를 빌드하고 사용자가 쉽게 설치할 수 있는 형태로 패키징해야 합니다. 단순히 실행 파일 하나만 제공할 수도 있지만, 시스템 서비스로 동작하는 프로그램이라면 실행 파일뿐만 아니라 설정 파일, systemd 서비스 파일, 기본 디렉터리 구조까지 함께 배포하는 것이 좋습니다.
Linux 환경에서는 배포판 계열에 따라 주로 사용하는 패키지 형식이 다릅니다. Debian 계열에서는 deb 패키지를 사용하며, 이를 APT(Advanced Package Tool)를 통해 설치하고 관리합니다. Fedora 계열에서는 rpm 패키지를, 경량 리눅스로 유명한 Alpine Linux는 apk 패키지를 사용합니다.
각 패키지마다 빌드 방법이 다르지만 본 글에서는 Ubuntu(Debian 계열)를 기준으로 deb 패키지를 만드는 방법에 대해 알아보겠습니다.
우선 Debian 패키징에 필요한 도구를 설치해야 합니다.
sudo apt update
# build-essential: 컴파일에 필요한 기본 빌드 도구 모음(gcc, make 등 기본적인 빌드 도구 포함)
# devscripts: Debian 패키징 작업에 필요한 여러 보조 명령어를 제공
# debhelper: Debian 패키지를 만들 때 반복적으로 필요한 작업을 자동화해주는 도구
# dh-make: 기존 프로젝트에 Debian 패키징용 debian/ 디렉터리 구조를 생성해주는 도구
sudo apt install build-essential devscripts debhelper dh-make
도구 설치가 끝나면 프로젝트 루트에서 dh_make 명령어를 실행해 기본 패키징 구조를 생성할 수 있습니다.
dh_make --single --createorig
이 명령어를 실행하면 프로젝트 내부에 debian/ 디렉터리가 생성되고, Debian 패키징에 필요한 기본 파일들이 만들어집니다.
debian/
├── changelog
├── control
├── copyright
└── rules
debian/control에는 패키지 이름, 설명, 의존성 등의 메타데이터를 정의합니다. 아래는 Bindizr의 debian/controls 파일입니다.
Source: bindizr
Section: utility
Priority: optional
Maintainer: Minsung Kweon <kevin136583@gmail.com>
Build-Depends: debhelper-compat (= 13)
Standards-Version: 4.5.0
Homepage: https://github.com/kweonminsung/bindizr
Package: bindizr
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}
Description: DNS Synchronization Service for BIND9
DNS Synchronization Service for BIND9.
This service allows you to manage BIND9 DNS zones and records through a RESTful API.
debian/rules에는 프로젝트를 어떻게 빌드하고, 빌드 결과물을 패키지 내부의 어떤 경로에 배치할지 등을 작성합니다. 기본적으로 Makefile의 문법을 따르고, override_dh_auto_build에는 패키지 빌드 시 실행할 명령어를, override_dh_auto_install에는 빌드된 결과물과 실행에 필요한 설정 파일, 각종 문서를 패키지 내부에 복사하는 명령어를 작성합니다. override_dh_auto_install에 정의된 파일들은 실제 deb 설치 시 지정된 경로에 복사됩니다.
#!/usr/bin/make -f
export DH_VERBOSE=1
export RUST_BACKTRACE=1
%:
dh $@
override_dh_auto_build:
cargo build --release --locked --target x86_64-unknown-linux-musl
override_dh_auto_install:
install -d debian/bindizr/usr/bin
install -m 755 target/x86_64-unknown-linux-musl/release/bindizr debian/bindizr/usr/bin/
install -d debian/bindizr/etc/bindizr
install -m 644 bindizr.conf.toml debian/bindizr/etc/bindizr/bindizr.conf.toml
install -d debian/bindizr/usr/share/doc/bindizr
install -m 644 packaging/debian/README.md debian/bindizr/usr/share/doc/bindizr/README.md
install -d debian/bindizr/usr/share/licenses/bindizr
install -m 644 LICENSE debian/bindizr/usr/share/licenses/bindizr/LICENSE
install -d debian/bindizr/usr/lib/systemd/system
install -m 644 packaging/debian/bindizr.service debian/bindizr/usr/lib/systemd/system/
override_dh_auto_clean:
cargo clean
dh_auto_clean
설정이 끝나면 아래 명령어로 deb 패키지를 빌드할 수 있습니다.
dpkg-buildpackage -us -uc # bindizr_0.1.0_amd64.deb 생성
빌드된 Debian 패키지는 아래와 같이 apt나 dpkg를 사용하여 설치할 수 있습니다.
sudo apt install ./bindizr_0.1.0_amd64.deb # 필요한 의존성까지 알아서 설치
또는 dpkg를 사용해 직접 설치할 수도 있습니다.
sudo dpkg -i bindizr_0.1.0_amd64.deb # 해당 패키지만 설치
FPM을 사용한 패키지 빌드 간편화
앞서 Debian 패키지를 빌드하는 정석적인 방식에 대해 알아보았습니다. 하지만 느끼셨다시피 굉장히 번거롭고, 다른 Linux 배포판은 또 다른 방식을 사용해야 하므로 상당히 귀찮은 작업입니다.
fpm은 여러 패키지 형식을 간단한 명령어로 생성할 수 있게 해주는 패키징 도구입니다. 복잡한 debian/ 디렉터리 구조를 직접 작성하지 않아도, 실행 파일과 설정 파일, 서비스 파일의 설치 경로를 지정하는 방식으로 deb 또는 rpm 패키지를 만들 수 있습니다. 아래는 Bindizr의 Debian 패키지를 생성하는 fpm 명령어입니다.
fpm -s dir -t deb -n bindizr -v "$VERSION" --iteration "$RELEASE" \
-a x86_64 -m "Minsung Kweon <kevin136583@gmail.com>" \
--url "https://github.com/kweonminsung/bindizr" \
--license "Apache-2.0" \
--description "DNS Synchronization Service for BIND9" \
--config-files /etc/bindizr/bindizr.conf.toml \
--after-install scripts/postinstall.sh \
--after-remove scripts/postremove.sh \
-C "$TMP_DIR" \
usr/bin/bindizr usr/lib/systemd/system/bindizr.service etc/bindizr/bindizr.conf.toml usr/share/doc/bindizr/README.md usr/share/licenses/bindizr/LICENSE
위 명령어에서 rpm 패키지를 생성하고 싶으면 -t rpm 옵션을 주면 됩니다. -s는 패키징 할 폴더를 지정하는 옵션입니다. 패키징 전에 임시 폴더를 만들어 패키지에 넣을 파일들을 미리 넣어두어야 합니다. --after-install은 설치 이후에 실행할 스크립트를, --after-remove는 제거 이후에 실행할 스크립트를 지정하는 옵션입니다.
fpm을 활용하면 복잡한 패키징 설정을 단순화할 수 있을 뿐만 아니라, CI/CD 환경에서 패키지를 자동으로 빌드하고 릴리스하기도 훨씬 수월해집니다. GitHub Actions와 같은 CI 환경에서 바이너리 빌드 이후 동일한 명령어만 실행하면 deb와 rpm 패키지를 일관된 방식으로 생성할 수 있어, 릴리스 과정 전체를 자동화하기 매우 편리합니다.
다음 글에서는 RNDC 방식의 문제점과 이를 해결하기 위해 전체 아키텍처를 갈아엎은 이야기, 그리고 이 과정에서 공부했던 DNS의 더 깊숙한 이야기를 해보도록 하겠습니다. 긴 글 읽어 주셔서 감사합니다!!
P.S. Rust를 사용한 첫 프로젝트이다 보니 중간에 삽질이 정말 많았습니다.(강제로 DI 패턴을 적용하려 했던 등의...)

'Infra' 카테고리의 다른 글
| Postfix에서 메일을 Slack으로 포워딩하는 방법 (0) | 2026.05.10 |
|---|---|
| 웍실 네트워크 개편 (1) | 2026.04.03 |
| MinIO에 다량의 파일을 빠르게 업로드하는 방법 (1) | 2026.03.08 |