OrangePi Zero3 2대로 구성하는 HAProxy + Keepalived 고가용성 Reverse Proxy
홈 쿠버네티스 클러스터의 Ingress 앞단에 TLS Offload 및 장애 자동복구가 가능한 reverse proxy 레이어를 구축함. 저전력 SBC인 OrangePi Zero3 2대를 사용해 HAProxy + Keepalived 기반의 Active-Standby 구성을 만듦.
구성 목표
- 9노드 온프레미스 쿠버네티스 클러스터(마스터 3, 워커 3, Ceph 스토리지 3)의 Ingress 앞단에 별도 reverse proxy 레이어 배치
- HTTPS TLS Offload를 reverse proxy 단에서 처리해 백엔드 부하 경감
- 한 대가 죽어도 VIP(Virtual IP)가 자동으로 살아있는 노드로 이전되는 무중단 구조
- 저전력 ARM SBC(OrangePi Zero3, A53 쿼드코어, 2GB RAM)로도 충분한지 검증
하드웨어 구성
| 항목 | 사양 |
|---|---|
| 보드 | OrangePi Zero3 × 2 |
| CPU | Allwinner H618, Cortex-A53 4코어 @ 1.5GHz |
| RAM | 2GB |
| 네트워크 | 1Gbps Ethernet |
| 역할 | h0proxy1 (MASTER), h0proxy2 (BACKUP) |
A53 코어가 AES-NI에 해당하는 aes pmull sha1 sha2 명령어 셋을 지원해서, TLS 핸드셰이크 비용을 하드웨어 가속으로 일부 보완할 수 있음.
네트워크 구성
VIP: 10.1.0.99
│
┌─────────┴─────────┐
│ │
h0proxy1 h0proxy2
10.1.0.1 10.1.0.2
(MASTER) (BACKUP)
priority 110 priority 100
│ │
└─────────┬──────────┘
│
k8s Ingress
(worker1/2/3 :80)
사전 성능 검토
저전력 SBC에 부하를 맡기기 전에 대략적인 처리 한계를 먼저 가늠해봄.
| 시나리오 | 예상 처리량 |
|---|---|
| TCP L4 Passthrough | 30,000 ~ 50,000 RPS |
| HTTP L7 Proxy | 8,000 ~ 15,000 RPS |
| HTTPS TLS Offload | 1,500 ~ 3,500 TPS |
1Gbps NIC 환경에서는 네트워크 대역폭보다 A53 쿼드코어의 연산 능력이 먼저 한계에 도달함. 특히 TLS 핸드셰이크 비용이 가장 큰 병목이라, SSL 세션 캐시 튜닝이 핵심 최적화 포인트였음. 홈랩 Ingress 앞단 용도로는 이 정도 스펙으로도 충분히 여유 있음.
1. OS 레벨 튜닝
패키지 설치
apt update && apt upgrade -y
apt install -y haproxy keepalived ipvsadm net-tools curl htop
커널 파라미터
cat > /etc/sysctl.d/99-haproxy.conf << 'EOF'
# 소켓 버퍼
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
net.ipv4.tcp_rmem = 4096 87380 134217728
net.ipv4.tcp_wmem = 4096 65536 134217728
# 커넥션 백로그
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.core.netdev_max_backlog = 65535
# TIME_WAIT 처리
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 15
# 포트 범위
net.ipv4.ip_local_port_range = 1024 65535
# 파일 디스크립터
fs.file-max = 1000000
# IP 포워딩
net.ipv4.ip_forward = 1
# VIP 바인딩 허용 (keepalived 필수)
net.ipv4.ip_nonlocal_bind = 1
EOF
sysctl -p /etc/sysctl.d/99-haproxy.conf
net.ipv4.ip_nonlocal_bind = 1을 빠뜨리면 failover 시 BACKUP 노드가 VIP에 bind하지 못하는 문제가 생기므로 반드시 설정해야 함.
파일 디스크립터 한도
cat > /etc/security/limits.d/haproxy.conf << 'EOF'
haproxy soft nofile 100000
haproxy hard nofile 100000
EOF
mkdir -p /etc/systemd/system/haproxy.service.d
cat > /etc/systemd/system/haproxy.service.d/limits.conf << 'EOF'
[Service]
LimitNOFILE=100000
EOF
systemctl daemon-reload
인터페이스명 확인
OrangePi Zero3는 eth0이 아니라 end0을 사용함. 이 부분을 놓치면 keepalived가 시작조차 안 되므로 사전에 반드시 확인해야 함.
ip -c a
2: end0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP
inet 10.1.0.1/8 brd 10.255.255.255 scope global dynamic noprefixroute end0
2. HAProxy 설정
인증서 준비
mkdir -p /etc/haproxy/certs
apt install -y certbot
certbot certonly --standalone -d yourdomain.com
cat /etc/letsencrypt/live/yourdomain.com/fullchain.pem \
/etc/letsencrypt/live/yourdomain.com/privkey.pem \
> /etc/haproxy/certs/yourdomain.com.pem
chmod 600 /etc/haproxy/certs/*.pem
haproxy.cfg
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
stats timeout 30s
user haproxy
group haproxy
daemon
# 4코어 풀 활용
nbthread 4
cpu-map auto:1/1-4 0-3
maxconn 50000
# TLS 전역 설정
ssl-default-bind-ciphers TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
# SSL 세션 캐시 (핸드셰이크 비용 절감 핵심)
tune.ssl.cachesize 100000
tune.ssl.lifetime 600
tune.ssl.maxrecord 1460
tune.bufsize 32768
tune.ssl.default-dh-param 2048
defaults
log global
mode http
option httplog
option dontlognull
option forwardfor
option http-server-close
option redispatch
retries 3
timeout connect 5s
timeout client 30s
timeout server 30s
timeout tunnel 1h
timeout queue 10s
timeout http-request 10s
timeout http-keep-alive 10s
frontend stats
bind *:8404
stats enable
stats uri /stats
stats refresh 10s
stats auth admin:changeme
frontend http_in
bind *:80
http-request redirect scheme https code 301
frontend https_in
bind *:443 ssl crt /etc/haproxy/certs/ alpn h2,http/1.1 ssl-min-ver TLSv1.2 no-tls-tickets
http-response set-header Strict-Transport-Security "max-age=31536000; includeSubDomains"
http-request set-header X-Forwarded-Proto https
http-request set-header X-Real-IP %[src]
default_backend k8s_ingress
backend k8s_ingress
balance leastconn
option httpchk GET /healthz
http-check expect status 200
server worker1 192.168.1.101:80 check inter 5s rise 2 fall 3
server worker2 192.168.1.102:80 check inter 5s rise 2 fall 3
server worker3 192.168.1.103:80 check inter 5s rise 2 fall 3
설정 적용 중 겪은 버전 호환성 이슈를 공유함. prefer-server-ciphers는 Ubuntu 24.04 기본 패키지인 HAProxy 2.8.x에서 bind 옵션으로 지원 안 해서 다음 에러가 발생함.
[ALERT] config : 'bind *:443' in section 'frontend': unknown keyword 'prefer-server-ciphers'.
global의 ssl-default-bind-ciphersuites 순서로 cipher 우선순위가 결정되므로 제거해도 보안상 문제는 없음. 또한 bind 라인을 백슬래시로 여러 줄에 나눠 작성하면 HAProxy 설정 파서가 이를 지원하지 않아 파싱 에러가 발생하므로, 반드시 한 줄로 작성해야 함.
검증 및 기동
haproxy -c -f /etc/haproxy/haproxy.cfg
systemctl enable --now haproxy
systemctl status haproxy
인증서 자동 갱신 훅
cat > /etc/letsencrypt/renewal-hooks/deploy/haproxy-reload.sh << 'EOF'
#!/bin/bash
DOMAIN="yourdomain.com"
cat /etc/letsencrypt/live/${DOMAIN}/fullchain.pem \
/etc/letsencrypt/live/${DOMAIN}/privkey.pem \
> /etc/haproxy/certs/${DOMAIN}.pem
chmod 600 /etc/haproxy/certs/${DOMAIN}.pem
systemctl reload haproxy
EOF
chmod +x /etc/letsencrypt/renewal-hooks/deploy/haproxy-reload.sh
3. Keepalived 장애복구 설정
MASTER 설정 (h0proxy1)
cat > /etc/keepalived/keepalived.conf << 'EOF'
global_defs {
router_id h0proxy1
script_user root
enable_script_security
}
vrrp_script chk_haproxy {
script "/etc/keepalived/check_haproxy.sh"
interval 2
weight 0
fall 3
rise 2
}
vrrp_instance VI_1 {
state MASTER
interface end0
virtual_router_id 51
priority 110
advert_int 1
preempt
authentication {
auth_type PASS
auth_pass ha2024
}
virtual_ipaddress {
10.1.0.99/8
}
track_script {
chk_haproxy
}
notify_master "/etc/keepalived/notify.sh MASTER"
notify_backup "/etc/keepalived/notify.sh BACKUP"
notify_fault "/etc/keepalived/notify.sh FAULT"
}
EOF
BACKUP 설정 (h0proxy2)
cat > /etc/keepalived/keepalived.conf << 'EOF'
global_defs {
router_id h0proxy2
script_user root
enable_script_security
}
vrrp_script chk_haproxy {
script "/etc/keepalived/check_haproxy.sh"
interval 2
weight 0
fall 3
rise 2
}
vrrp_instance VI_1 {
state BACKUP
interface end0
virtual_router_id 51
priority 100
advert_int 1
nopreempt
authentication {
auth_type PASS
auth_pass ha2024
}
virtual_ipaddress {
10.1.0.99/8
}
track_script {
chk_haproxy
}
notify_master "/etc/keepalived/notify.sh MASTER"
notify_backup "/etc/keepalived/notify.sh BACKUP"
notify_fault "/etc/keepalived/notify.sh FAULT"
}
EOF
auth_pass는 8자를 초과하면 keepalived가 자동으로 잘라버리고 경고 로그를 남기므로, 처음부터 8자 이하로 설정하는 게 나음.
HAProxy 상태 점검 스크립트
초기에는 HAProxy 다운 시 weight 값을 깎아 우선순위만 낮추는 방식으로 시도했는데, BACKUP 노드가 MASTER로 승격해도 기존 MASTER의 keepalived 프로세스 자체는 살아서 VRRP Advertisement를 계속 송출하는 문제가 있었음. 이로 인해 약 4초 뒤 우선순위가 원래대로 복귀하면서 BACKUP이 다시 강등되는 핑퐁 현상이 발생함.
이를 해결하기 위해 HAProxy 다운이 감지되면 keepalived 프로세스 자체를 종료시켜 VRRP 광고를 완전히 끊는 방식으로 변경함.
cat > /etc/keepalived/check_haproxy.sh << 'EOF'
#!/bin/bash
if ! systemctl is-active --quiet haproxy; then
logger -t keepalived "haproxy down - stopping keepalived via systemd"
systemctl kill keepalived
exit 1
fi
exit 0
EOF
chmod +x /etc/keepalived/check_haproxy.sh
systemctl stop keepalived를 keepalived 자식 프로세스 내부에서 호출하면 자기 자신을 멈추는 구조상 데드락이 걸려 실행이 안 됨. systemctl kill을 사용해 systemd가 직접 시그널을 보내도록 우회해서 해결함.
알림 스크립트
cat > /etc/keepalived/notify.sh << 'EOF'
#!/bin/bash
TYPE=$1
HOSTNAME=$(hostname)
DATE=$(date '+%Y-%m-%d %H:%M:%S')
logger -t keepalived "[${DATE}] ${HOSTNAME} → ${TYPE}"
case $TYPE in
MASTER)
systemctl enable keepalived
systemctl start haproxy
sleep 2
if systemctl is-active --quiet haproxy; then
logger -t keepalived "${HOSTNAME} became MASTER, haproxy OK"
else
logger -t keepalived "${HOSTNAME} haproxy failed, stopping keepalived"
systemctl stop keepalived
fi
;;
BACKUP)
logger -t keepalived "${HOSTNAME} became BACKUP"
;;
FAULT)
systemctl stop haproxy
logger -t keepalived "${HOSTNAME} FAULT, haproxy stopped"
;;
esac
EOF
chmod +x /etc/keepalived/notify.sh
이 스크립트는 양쪽 노드에 동일하게 배치함. 처음 배치할 때 실행 권한을 빠뜨려서 MASTER 전환 로그는 찍히는데 정작 HAProxy가 기동 안 되는 문제를 겪음. chmod +x 꼭 확인해야 함.
systemctl enable --now keepalived
ip addr show end0 | grep 10.1.0.99
4. 동작 테스트
기본 연결 확인
curl -sk https://10.1.0.99/healthz
echo | openssl s_client -connect 10.1.0.99:443 2>/dev/null | openssl x509 -noout -dates
Failover 시나리오별 테스트
세 가지 장애 유형을 구분해서 테스트함.
HAProxy 프로세스만 다운된 경우
# h0proxy1에서
systemctl stop haproxy
check_haproxy.sh가 2초 간격으로 3회 실패를 감지(약 6초)한 뒤 keepalived 자체를 종료시키고, h0proxy2가 VRRP 타임아웃을 거쳐 MASTER로 승격함.
keepalived 프로세스가 죽거나 네트워크가 끊긴 경우
# h0proxy1에서
ip link set end0 down
VRRP Advertisement 자체가 끊기므로 h0proxy2가 advert_int(1초) × 3회 무응답을 감지해서 약 3초 내에 자동으로 MASTER 승격함. 별도 스크립트 없이 keepalived 기본 동작만으로 처리됨.
서버 자체가 다운된 경우
네트워크 단절과 동일한 매커니즘으로, VRRP 패킷 송출 자체가 멈추므로 약 3초 내 자동 전환됨.
전환 시간 측정
while true; do
CODE=$(curl -sk -o /dev/null -w "%{http_code}" --connect-timeout 2 https://10.1.0.99/)
echo "$(date '+%H:%M:%S') → $CODE"
sleep 0.5
done
14:55:01 → 200
14:55:02 → 000 ← 장애 발생
14:55:03 → 000
14:55:04 → 200 ← 전환 완료 (약 2~4초)
최종 정리된 장애 유형별 전환 시간
| 장애 유형 | 감지 방식 | 전환 시간 |
|---|---|---|
| HAProxy 프로세스 다운 | check_haproxy.sh (fall 3 × interval 2) | 약 6초 |
| keepalived 프로세스 다운 | VRRP 타임아웃 | 약 3초 |
| 네트워크 단절 | VRRP 타임아웃 | 약 3초 |
| 서버 자체 다운 | VRRP 타임아웃 | 약 3초 |
운영 환경에서의 확장 고려사항
이번 구성은 Active-Standby 2대로 했는데, 실제 트래픽이 늘어나면 다음 방향으로 확장 가능함.
- Active-Active: DNS 라운드로빈으로 두 노드 모두 트래픽을 처리하도록 변경. 단, DNS 캐시 때문에 즉각적인 failover는 어려움
- L4 계층 추가: 현재는 keepalived가 L4(VIP)와 L7(TLS/라우팅)을 동시에 담당하는 구조. 트래픽이 더 커지면 LVS 같은 별도 L4 레이어를 앞단에 두고 HAProxy를 수평 확장하는 구조도 고려 가능
- Anycast: BGP 기반 글로벌 분산 기술로, ASN과 공인 IP 블록, ISP 피어링이 필요해서 홈랩/중소 규모에서는 사실상 해당 사항 없음
홈랩 규모의 쿠버네티스 Ingress 앞단이라면 현재 구성으로 충분하고, 저전력 SBC 2대만으로도 안정적인 HA reverse proxy 레이어를 구축할 수 있었음.