01 소개 02 경력 03 작업 04 블로그 05 이력서 PDF
← 블로그 목록
// HAProxy

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
CPUAllwinner H618, Cortex-A53 4코어 @ 1.5GHz
RAM2GB
네트워크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 Passthrough30,000 ~ 50,000 RPS
HTTP L7 Proxy8,000 ~ 15,000 RPS
HTTPS TLS Offload1,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 레이어를 구축할 수 있었음.


← 목록으로
이 글이 도움이 됐다면 메일로 알려주기 →