Python 3 기반으로 HAProxy 설정 파일을 프로그래밍 방식으로 조작하는 방법을 다룹니다. 주요 기능으로는 백엔드 섹션의 검색, 서버 노드 추가, 삭제, 그리고 설정 롤백이 포함됩니다.
대상 설정 파일 구조
HAProxy 설정은 크게 global, defaults, frontend, backend, listen 섹션으로 구성됩니다. 본 도구는 backend 블록을 중심으로 동작합니다.
global
log /dev/log local0
maxconn 4096
daemon
defaults
mode http
timeout connect 10s
timeout client 30s
timeout server 30s
frontend http-in
bind *:80
default_backend web_servers
backend web_servers
server node1 10.0.1.10:80 check weight 5
server node2 10.0.1.11:80 check weight 5
backend api_servers
server api1 10.0.2.20:8080 check
핵심 기능 구현
1. 백엔드 검색 기능
정 백엔드 이름으로 해당 섹션의 모든 서버 정의를 추출합니다. 섹션 경계를 backend 키워드로 식별합니다.
def extract_backend(config_path, target_name):
"""
지정된 백엔드 섹션의 서버 목록 반환
"""
collecting = False
servers = []
with open(config_path, 'r', encoding='utf-8') as cfg:
for raw_line in cfg:
stripped = raw_line.strip()
# 대상 백엔드 시작점 발견
if stripped == f'backend {target_name}':
collecting = True
continue
# 다음 백엔드 시작 시 종료
if collecting and stripped.startswith('backend'):
collecting = False
# 서버 라인 수집
if collecting and stripped and not stripped.startswith('#'):
servers.append(stripped)
return servers
2. 서버 노드 추가/수정
기존 백엔드에 서버를 추가하거나, 존재하지 않는 백엔드를 신규 생성합니다. 작업 전 자동 백업을 수행합니다.
def append_server(cfg_file, bname, host, port, prio, ceiling):
"""
백엔드에 서버 추가. 기존 항목이면 업데이트, 없으면 생성
"""
entry_line = f'backend {bname}'
srv_def = f'server {host} {host}:{port} weight {prio} maxconn {ceiling}'
# 현재 상태 조회
existing = extract_backend(cfg_file, bname)
if existing:
# 기존 백엔드 수정 모드
updated = list(existing)
found = False
# 중복 검사 및 갱신
for idx, line in enumerate(updated):
if line.startswith(f'server {host}'):
updated[idx] = srv_def
found = True
break
if not found:
updated.append(srv_def)
# 파일 재구성
_rewrite_section(cfg_file, bname, updated)
else:
# 신규 백엔드 추가 모드
_append_new_backend(cfg_file, bname, srv_def)
_create_backup(cfg_file)
def _rewrite_section(src, section_name, server_lines):
"""섹션 내용을 교체하여 새 파일 생성"""
marker = f'backend {section_name}'
inside = False
written = False
with open(src, 'r') as inp, open(f'{src}.tmp', 'w') as out:
for line in inp:
clean = line.strip()
if clean == marker:
inside = True
out.write(line)
continue
if inside and clean.startswith('backend'):
inside = False
if inside:
if not written:
for srv in server_lines:
out.write(f' {srv}\n')
written = True
else:
out.write(line)
_atomic_replace(src)
def _append_new_backend(src, name, srv):
"""파일 끝에 새 백엔드 추가"""
with open(src, 'a') as cfg:
cfg.write(f'\nbackend {name}\n')
cfg.write(f' {srv}\n')
def _atomic_replace(filepath):
"""원자적 파일 교환"""
import os
backup = f'{filepath}.prev'
if os.path.exists(backup):
os.remove(backup)
os.rename(filepath, backup)
os.rename(f'{filepath}.tmp', filepath)
3. 서버 노드 제거
def remove_server(cfg_file, bname, host):
"""
특정 백엔드에서 지정된 호스트의 서버 정의 제거
"""
current = extract_backend(cfg_file, bname)
if not current:
raise ValueError(f'백엔드 {bname} 존재하지 않음')
filtered = [ln for ln in current if not ln.startswith(f'server {host}')]
if len(filtered) == len(current):
raise ValueError(f'서버 {host} 미발견')
_rewrite_section(cfg_file, bname, filtered)
_create_backup(cfg_file)
4. 설정 롤백
def restore_previous(cfg_file):
"""
이전 백업으로 복원
"""
import os
backup = f'{cfg_file}.prev'
if not os.path.isfile(backup):
raise FileNotFoundError('복원 가능한 백업 없음')
# 현재를 임시로 보관
temp = f'{cfg_file}.corrupt'
os.rename(cfg_file, temp)
try:
os.rename(backup, cfg_file)
os.remove(temp)
except Exception:
# 롤백 실패 시 구
os.rename(temp, cfg_file)
raise
대화형 인터페이스
def interactive_manager(config_path='haproxy.cfg'):
"""
메뉴 기반 설정 관리 CLI
"""
actions = {
'1': ('백엔드 조회', _do_lookup),
'2': ('서버 등록', _do_register),
'3': ('서터 제거', _do_unregister),
'4': ('설정 복원', _do_rollback),
'5': ('종료', lambda _: exit(0))
}
while True:
print('\n' + '=' * 40)
for key, (desc, _) in actions.items():
print(f' {key}. {desc}')
choice = input('\n선택: ').strip()
if choice in actions:
try:
actions[choice][1](config_path)
except Exception as err:
print(f'오류: {err}')
else:
print('잘못된 입력')
def _do_lookup(path):
target = input('백엔드 이름: ')
result = extract_backend(path, target)
print('\n'.join(result) if result else '결과 없음')
def _do_register(path):
bk = input('백엔드: ')
ip = input('IP 주소: ')
wt = input('가중치: ')
mc = input('최대 연결: ')
append_server(path, bk, ip, 80, wt, mc)
def _do_unregister(path):
bk = input('백엔드: ')
ip = input('제거할 서버 IP: ')
remove_server(path, bk, ip)
def _do_rollback(path):
restore_previous(path)
print('복원 완료')
if __name__ == '__main__':
interactive_manager()
확장 고려사항
- 유효성 검증: 설정 문법 검사를 위해
haproxy -c -f호출 통합 - 동시성 제어: 다중 프로세스 환경을 위한 파일 잠금 메커니즘
- 템플릿 엔진: Jinja2 등을 활용한 동적 설정 생성
- API 노출: Flask/FastAPI 기반 REST 인터페이스 제공