Windows 서버와 BT Panel(보탑 패널) 환경에서 Python 스크립트를 실행할 때 환경 설정 문제로 어려움을 겪는 경우가 많습니다. 이러한 문제를 우회하고 안정적인 DDNS(동적 DNS) 환경을 구축하기 위해, Python 스크립트를 단일 EXE 파일로 패키징하여 BT Panel의 예약 작업으로 관리하는 방식을 추천합니다.
이 솔루션의 주요 특징은 다음과 같습니다:
- IPv4 및 IPv6 동시 지원 및 독립적인 레코드 관리
- API 호출 최적화: IP 주소가 실제로 변경된 경우에만 DNS 레코드를 업데이트하여 불필요한 API 요청 방지
- 상세한 로깅 기능: 오류 발생 시 정확한 원인 파악을 위한 로그 기록
- 예약 작업 실행 주기는 5분 간격을 권장합니다.
필수 라이브러리 설치
스크립트 실행 및 패키징에 필요한 라이브러리를 설치합니다.
pip install aliyun-python-sdk-core aliyun-python-sdk-alidns requests pyinstaller
핵심 소스 코드
알리바바 클라우드 DNS API를 사용하여 IPv4와 IPv6 주소를 자동으로 동기화하는 최적화된 코드입니다. 기존 코드의 중복 로직을 제거하고 가독성과 유지보수성을 높였습니다.
#!/usr/bin/env python3
import json
import logging
import os
import sys
import requests
from aliyunsdkcore.client import AcsClient
from aliyunsdkalidns.request.v20150109.DescribeSubDomainRecordsRequest import DescribeSubDomainRecordsRequest
from aliyunsdkalidns.request.v20150109.AddDomainRecordRequest import AddDomainRecordRequest
from aliyunsdkalidns.request.v20150109.UpdateDomainRecordRequest import UpdateDomainRecordRequest
def configure_logging():
if getattr(sys, 'frozen', False):
base_path = os.path.dirname(sys.executable)
else:
base_path = os.path.dirname(os.path.abspath(__file__))
log_path = os.path.join(base_path, 'ddns_sync.log')
logging.basicConfig(
filename=log_path,
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s'
)
class AliyunDnsManager:
def __init__(self):
# 알리바바 클라우드 API 인증 정보 및 도메인 설정
self.ak_id = "YOUR_ACCESS_KEY_ID"
self.ak_secret = "YOUR_ACCESS_KEY_SECRET"
self.region_id = "cn-hangzhou"
self.root_domain = "example.com"
# 서브도메인 설정
self.ipv4_hosts = ["www", "api"]
self.ipv6_hosts = ["www", "api"]
self.client = AcsClient(self.ak_id, self.ak_secret, self.region_id)
def fetch_public_ip(self, version=4):
try:
if version == 4:
res = requests.get('http://ip-api.com/json/?fields=query', timeout=10)
return res.json().get('query')
else:
res = requests.get('https://api6.ipify.org?format=json', timeout=10)
return res.json().get('ip')
except Exception as e:
logging.error(f"Failed to fetch public IPv{version}: {e}")
return None
def get_existing_records(self, subdomain):
req = DescribeSubDomainRecordsRequest()
req.set_accept_format('json')
req.set_SubDomain(f"{subdomain}.{self.root_domain}")
res = self.client.do_action_with_exception(req)
return json.loads(res)
def add_record(self, subdomain, record_type, value):
req = AddDomainRecordRequest()
req.set_accept_format('json')
req.set_DomainName(self.root_domain)
req.set_RR(subdomain)
req.set_Type(record_type)
req.set_Value(value)
self.client.do_action_with_exception(req)
logging.info(f"Added {record_type} record for {subdomain}.{self.root_domain} -> {value}")
def update_record(self, record_id, subdomain, record_type, value):
req = UpdateDomainRecordRequest()
req.set_accept_format('json')
req.set_RecordId(record_id)
req.set_RR(subdomain)
req.set_Type(record_type)
req.set_Value(value)
self.client.do_action_with_exception(req)
logging.info(f"Updated {record_type} record for {subdomain}.{self.root_domain} -> {value}")
def sync_records(self, target_ip, record_type, hosts):
if not target_ip:
return
for host in hosts:
try:
data = self.get_existing_records(host)
records = data.get('DomainRecords', {}).get('Record', [])
# 해당 타입의 기존 레코드 필터링
type_records = [r for r in records if r['Type'] == record_type]
if not type_records:
# 레코드가 없으면 새로 추가
self.add_record(host, record_type, target_ip)
else:
# 기존 레코드 값 비교 후 필요시 업데이트
current_value = type_records[0]['Value']
if current_value != target_ip:
self.update_record(type_records[0]['RecordId'], host, record_type, target_ip)
else:
logging.info(f"No change for {host}.{self.root_domain} ({record_type}). Current: {current_value}")
except Exception as e:
logging.error(f"Error syncing {host} ({record_type}): {e}")
if __name__ == "__main__":
configure_logging()
manager = AliyunDnsManager()
logging.info("Starting DDNS sync process...")
ipv4 = manager.fetch_public_ip(4)
manager.sync_records(ipv4, "A", manager.ipv4_hosts)
ipv6 = manager.fetch_public_ip(6)
manager.sync_records(ipv6, "AAAA", manager.ipv6_hosts)
logging.info("DDNS sync process completed.")
EXE 파일 패키징 및 배포
PyInstaller를 사용하여 스크립트를 단일 실행 파일로 변환합니다. 명령 프롬프트(cmd)를 열고 스크립트가 위치한 디렉토리로 이동한 후 다음 명령어를 실행합니다.
pyinstaller --onefile --hidden-import=queue ddns_updater.py
패키징이 완료되면 dist 폴더 내에 생성된 EXE 파일을 BT Panel의 예약 작업(Cron Job)에 등록하여 주기적으로 실행되도록 설정합니다.