Skip to content
Published on

HAProxy Ingress Controller — 검증된 LB의 쿠버네티스 진입

Authors

들어가며

쿠버네티스 인그레스 컨트롤러를 고른다는 것은 사실상 "우리 클러스터의 정문(north-south 트래픽)을 무엇에 맡길 것인가"를 결정하는 일입니다. 오랫동안 이 자리의 사실상 표준은 ingress-nginx였지만, 2026년 현재 상황은 꽤 달라졌습니다.

첫째, 쿠버네티스 Ingress API 자체가 동결(frozen)되었습니다. 더 이상 새로운 기능이 들어오지 않고, 모든 신규 개발은 Gateway API로 옮겨가고 있습니다. 둘째, 오랫동안 표준이었던 ingress-nginx는 유지보수 모드에 가깝게 전환되었고, 그 사이 어노테이션 기반 설정 주입에서 비롯된 보안 이슈(CVE)들이 반복적으로 보고되면서 운영자들의 신뢰가 흔들렸습니다.

이 흐름 속에서 다시 주목받는 선택지가 HAProxy입니다. HAProxy는 쿠버네티스가 등장하기 훨씬 전부터 대규모 트래픽 환경에서 검증된 L4/L7 로드밸런서입니다. "이미 20년 넘게 프로덕션에서 굴러온 데이터플레인을 그대로 인그레스에 쓴다"는 것이 핵심 매력입니다.

다만 함정이 하나 있습니다. "HAProxy 인그레스 컨트롤러"라고 부르는 프로젝트가 사실 두 개라는 점입니다. 하나는 HAProxy Technologies가 직접 만드는 공식 프로젝트이고, 다른 하나는 Joao Morais가 오래 유지해 온 커뮤니티 프로젝트입니다. 이 둘은 이름이 비슷하지만 설정 모델과 어노테이션 prefix가 완전히 다릅니다. 이 글에서는 두 프로젝트를 명확히 구분하면서, 아키텍처와 동적 reload, TCP/TLS passthrough, rate limit, Helm 배포, Gateway API 전환까지 코드 중심으로 깊게 다룹니다.

인그레스 컨트롤러 지형 — 2026년의 현실

먼저 큰 그림을 잡아 봅시다. 인그레스 컨트롤러는 데이터플레인(실제 트래픽을 처리하는 프록시)과 컨트롤플레인(쿠버네티스 API를 watch해서 설정을 생성하는 부분)으로 나뉩니다. 어떤 프록시 엔진을 쓰느냐가 성능과 기능의 성격을 결정합니다.

컨트롤러데이터플레인 엔진2026년 상태강점주의점
ingress-nginxNGINX + Lua유지보수 모드, CVE 이슈 누적압도적 사용자 기반, 자료 풍부신규 기능 정체, 보안 패치 중심
HAProxy (haproxytech 공식)HAProxy + Data Plane API활발히 개발검증된 안정성, hitless reload, 공식 지원어노테이션 prefix 별도, 학습 필요
HAProxy (jcmoraisjr 커뮤니티)HAProxy활발히 개발풍부한 기능, embedded 모드단일 메인테이너 의존성
Traefik자체(Go)활발Gateway API 선도, 동적 설정초고부하에서 HAProxy/NGINX 대비 약함
Envoy 기반 (Contour, EG)Envoy활발, Gateway API 중심gRPC/HTTP2 강점, xDS리소스 사용량, 복잡도
CiliumEnvoy + eBPF활발커널 레벨, Gateway APICNI 종속, 학습 곡선

여기서 HAProxy 계열의 위치를 한 문장으로 요약하면 이렇습니다. "Traefik이나 Envoy처럼 새 표준(Gateway API)을 향해 가면서도, NGINX처럼 검증된 고성능 데이터플레인을 보장하고 싶을 때 선택하는 절충안"입니다.

두 개의 HAProxy 인그레스 프로젝트 — 절대 헷갈리면 안 되는 구분

이 글에서 가장 먼저 못 박고 가야 하는 부분입니다. "HAProxy Ingress"를 검색하면 나오는 두 프로젝트는 서로 다른 소프트웨어입니다.

구분haproxytech/kubernetes-ingressjcmoraisjr/haproxy-ingress
주체HAProxy Technologies (회사 공식)Joao Morais (커뮤니티)
저장소github.com/haproxytech/kubernetes-ingressgithub.com/jcmoraisjr/haproxy-ingress
문서 사이트haproxy.com/documentation/kubernetes-ingresshaproxy-ingress.github.io
어노테이션 prefixhaproxy.orghaproxy-ingress.github.io
설정 갱신 방식Data Plane API + Runtime API 중심템플릿 기반 + Runtime API
상용 지원HAProxy Enterprise Kubernetes 제품 연계커뮤니티 베이스
특징회사가 직접 관리, 릴리스 주기 명확기능 풍부, 오래된 검증 이력

두 프로젝트 모두 데이터플레인은 동일한 HAProxy 엔진이지만, 컨트롤플레인 구현과 사용자 인터페이스(어노테이션, ConfigMap 키)가 다릅니다. 예를 들어 같은 "요청 타임아웃 설정"이라도 어노테이션 키가 다릅니다.

공식(haproxytech):   haproxy.org/timeout-client: "30s"
커뮤니티(jcmoraisjr): haproxy-ingress.github.io/timeout-client-fin: "30s"

따라서 인터넷에서 찾은 예제를 그대로 붙여넣기 전에, 반드시 "내가 쓰는 게 어느 프로젝트인가"를 먼저 확인해야 합니다. 이 글의 본문은 별도 표기가 없으면 공식 프로젝트(haproxytech/kubernetes-ingress)를 기준으로 하고, 커뮤니티 프로젝트의 차이는 그때그때 짚겠습니다.

선택 가이드를 요약하면 다음과 같습니다.

  • 회사 차원의 공식 지원과 명확한 릴리스 거버넌스, HAProxy Enterprise로의 업그레이드 경로가 필요하다면 공식 프로젝트(haproxytech)가 자연스럽습니다.
  • 오랜 기간 축적된 세밀한 기능(글로벌 설정 스니펫, 다양한 인증 옵션 등)과 커뮤니티 검증 이력을 중시한다면 jcmoraisjr 프로젝트도 강력합니다.
  • 신규 도입이고 회사 정책상 벤더 지원을 받을 수 있다면, 장기적으로는 공식 프로젝트를 권장합니다.

왜 HAProxy인가 — 데이터플레인의 강점

HAProxy가 인그레스 데이터플레인으로 매력적인 이유는 결국 "엔진 자체의 성숙도"입니다.

첫째, 성능과 안정성입니다. HAProxy는 단일 프로세스 이벤트 기반(multi-threaded event loop) 아키텍처로, 수십만 동시 연결을 낮은 CPU/메모리로 처리하도록 오랫동안 최적화되어 왔습니다. 커넥션 풀링, keep-alive 재사용, 백엔드 health check 같은 기본기가 견고합니다.

둘째, 풍부한 L7 기능입니다. ACL(Access Control List) 기반의 정교한 라우팅, 헤더 조작, rate limiting, 스틱 테이블(stick-table) 기반 상태 추적, 회로 차단 유사 동작(백엔드 차단) 등을 설정만으로 구현할 수 있습니다.

셋째, 관측성입니다. HAProxy의 stats 페이지와 Prometheus exporter는 백엔드별 응답 시간, 큐 길이, 연결 상태를 매우 상세하게 제공합니다.

넷째, 동적 reload입니다. 뒤에서 자세히 다루겠지만, HAProxy는 Runtime API와 hitless reload(seamless reload)를 통해 설정 변경 시 기존 연결을 끊지 않고 반영할 수 있습니다. 인그레스처럼 백엔드(Pod)가 수시로 바뀌는 환경에서 이것은 결정적인 장점입니다.

아키텍처 — 컨트롤러가 HAProxy를 어떻게 운전하는가

공식 컨트롤러의 내부 구조는 다음과 같은 흐름으로 동작합니다.

+-----------------------------------------------------------------------+
|  HAProxy Ingress Controller Pod                                       |
|                                                                       |
|   +-------------------------+        +----------------------------+   |
|   |  Controller (Go)        |        |  HAProxy 프로세스            |   |
|   |                         |        |                            |   |
|   |  - K8s API watch        | -----> |  - frontend (80/443)       |   |
|   |    Ingress / Service /  |  Data  |  - backend (Pod IP:port)   |   |
|   |    Endpoints / Secret / |  Plane |  - ACL / map / stick-table |   |
|   |    ConfigMap            |  API   |  - stats / runtime socket  |   |
|   |  - 설정 생성/검증        | <----- |                            |   |
|   |  - Runtime API 호출      |  state |                            |   |
|   +-------------------------+        +----------------------------+   |
+-----------------------------------------------------------------------+
            ^                                       |
            | watch (informer)                      | forward
            |                                       v
   +------------------+                  +-------------------------+
   | Kubernetes API   |                  |  백엔드 Pods (Service)   |
   |  Server          |                  |  app-a / app-b / ...    |
   +------------------+                  +-------------------------+

        클라이언트 ──HTTP/HTTPS/TCP──> [frontend] ──ACL 라우팅──> [backend] ──> Pod

핵심은 컨트롤러(Go 프로세스)와 데이터플레인(HAProxy 프로세스)이 같은 Pod 안에서 분리되어 있다는 점입니다. 컨트롤러는 쿠버네티스 API를 informer로 watch하다가, Ingress/Service/Endpoints/Secret/ConfigMap 변화를 감지하면 다음 둘 중 하나로 HAProxy에 반영합니다.

  • 엔드포인트(Pod IP) 같은 빈번한 변화는 Runtime API를 통해 reload 없이 즉시 서버를 추가/제거합니다.
  • frontend 추가나 ACL 구조 변경 같은 구조적 변화는 설정을 재생성하고 hitless reload를 트리거합니다.

이 "reload가 필요한 변경"과 "reload가 필요 없는 변경"을 구분하는 것이 HAProxy 인그레스를 잘 운영하는 핵심 개념입니다.

설정의 세 가지 레이어 — Global / Defaults / Backend

HAProxy 인그레스의 설정은 적용 범위에 따라 세 레이어로 나뉩니다. 이 구조를 이해하면 "이 설정을 ConfigMap에 넣어야 하나, 어노테이션에 넣어야 하나"가 명확해집니다.

레이어적용 대상설정 위치예시
GlobalHAProxy 프로세스 전체ConfigMap (컨트롤러 전역)maxconn, ssl 기본값, nbthread
Defaults모든 frontend/backend 기본값ConfigMap기본 타임아웃, 로그 포맷
Ingress/Service개별 라우팅 단위Ingress 또는 Service 어노테이션path 라우팅, rate limit, 백엔드 프로토콜

이 우선순위 때문에, 전역으로 깔되 특정 서비스만 다르게 가져가는 패턴이 자연스럽게 구현됩니다. 예를 들어 기본 타임아웃은 ConfigMap에서 30초로 잡되, 업로드 API만 어노테이션으로 300초를 줄 수 있습니다.

배포 실습 — Helm으로 공식 컨트롤러 설치하기

이제 실제로 설치해 봅시다. 공식 프로젝트는 Helm 차트를 제공합니다. 기준 환경은 쿠버네티스 v1.32 계열입니다.

먼저 레포를 추가합니다.

helm repo add haproxytech https://haproxytech.github.io/helm-charts
helm repo update
helm search repo haproxytech/kubernetes-ingress

가장 단순한 설치는 다음과 같습니다.

kubectl create namespace haproxy-controller

helm install haproxy-ingress haproxytech/kubernetes-ingress \
  --namespace haproxy-controller \
  --set controller.kind=Deployment \
  --set controller.replicaCount=2

프로덕션에서는 값 파일로 명시적으로 관리하는 것이 좋습니다. 다음은 실무에서 자주 쓰는 values 예시입니다.

controller:
  kind: Deployment
  replicaCount: 2

  # 노드의 호스트 네트워크 대신 LoadBalancer 타입 Service 사용
  service:
    type: LoadBalancer
    externalTrafficPolicy: Local   # 클라이언트 소스 IP 보존

  # IngressClass 설정 — 여러 컨트롤러 공존 시 필수
  ingressClass: haproxy
  ingressClassResource:
    enabled: true
    default: false

  # HAProxy 전역/기본 설정을 주입하는 ConfigMap 값
  config:
    timeout-client: "30s"
    timeout-server: "30s"
    timeout-connect: "5s"
    maxconn: "100000"
    ssl-redirect: "true"

  # 리소스 요청/제한
  resources:
    requests:
      cpu: 500m
      memory: 256Mi
    limits:
      cpu: "2"
      memory: 512Mi

  # Prometheus 메트릭 노출
  serviceMonitor:
    enabled: true

  # Pod 분산 배치
  podDisruptionBudget:
    enable: true
    minAvailable: 1
helm upgrade --install haproxy-ingress haproxytech/kubernetes-ingress \
  --namespace haproxy-controller \
  -f values-prod.yaml

설치 후 확인은 다음과 같습니다.

kubectl -n haproxy-controller get pods
kubectl -n haproxy-controller get svc
kubectl get ingressclass

여기서 externalTrafficPolicy를 Local로 두는 이유를 한 번 짚고 갑시다. 기본값인 Cluster는 노드 간 SNAT를 거치면서 클라이언트의 진짜 IP를 잃어버립니다. Local로 두면 트래픽이 받은 노드에서만 처리되어 소스 IP가 보존됩니다. rate limit이나 IP 기반 ACL을 쓰려면 이 IP 보존이 사실상 전제 조건입니다.

기본 라우팅 — Ingress 리소스 작성

가장 기본적인 호스트/경로 기반 라우팅 예시입니다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-ingress
  namespace: demo
  annotations:
    haproxy.org/load-balance: "roundrobin"
spec:
  ingressClassName: haproxy
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web-frontend
                port:
                  number: 80
          - path: /api
            pathType: Prefix
            backend:
              service:
                name: api-backend
                port:
                  number: 8080

ingressClassName을 반드시 명시해야 한다는 점에 주의하세요. 여러 인그레스 컨트롤러가 공존하는 클러스터에서 이것을 빼면 어느 컨트롤러도 처리하지 않거나, 의도하지 않은 컨트롤러가 잡아갑니다.

어노테이션 — 실무에서 자주 쓰는 것들

공식 프로젝트의 어노테이션 prefix는 haproxy.org입니다. 자주 쓰는 것들을 정리하면 다음과 같습니다.

어노테이션의미예시 값
haproxy.org/load-balance로드밸런싱 알고리즘roundrobin, leastconn
haproxy.org/timeout-client클라이언트 타임아웃30s
haproxy.org/timeout-server백엔드 응답 타임아웃60s
haproxy.org/ssl-redirectHTTP를 HTTPS로 리다이렉트"true"
haproxy.org/server-proto백엔드 프로토콜h2 (HTTP/2)
haproxy.org/path-rewrite경로 재작성/api/(.*) /v1/(원본)
haproxy.org/rate-limit-requests요청 rate limit"100"
haproxy.org/whitelist허용 IP 대역10.0.0.0/8

예를 들어 백엔드와 HTTP/2로 통신하고 응답 타임아웃을 늘리려면 다음과 같이 작성합니다.

metadata:
  annotations:
    haproxy.org/server-proto: "h2"
    haproxy.org/timeout-server: "120s"
    haproxy.org/check: "enabled"

여기서 커뮤니티 프로젝트(jcmoraisjr)와의 차이를 다시 강조합니다. 같은 의도라도 prefix와 키 이름이 다릅니다.

공식(haproxytech):   haproxy.org/server-proto: "h2"
커뮤니티(jcmoraisjr): haproxy-ingress.github.io/backend-protocol: "h2"

동적 reload — Runtime API와 hitless reload

이 절이 HAProxy 인그레스의 진짜 차별점입니다. 인그레스 환경에서는 Pod가 스케일 인/아웃되고, 롤링 업데이트로 IP가 계속 바뀝니다. 이 변화를 데이터플레인에 반영할 때마다 프록시를 완전히 재시작하면 연결이 끊깁니다.

HAProxy는 두 가지 메커니즘으로 이 문제를 해결합니다.

첫째, Runtime API입니다. HAProxy는 유닉스 소켓을 통한 런타임 명령을 받습니다. 컨트롤러는 백엔드 서버 추가/제거/가중치 변경 같은 동작을 reload 없이 즉시 적용합니다.

[Pod 스케일 아웃 발생]
   Endpoints 변경 감지
   컨트롤러가 Runtime API로 명령 전송:
     set server backend_app/srv3 addr 10.244.2.17 port 8080
     set server backend_app/srv3 state ready
   HAProxy가 reload 없이 즉시 신규 서버 활성화
   (기존 연결 영향 없음)

둘째, hitless reload(seamless reload)입니다. frontend 추가나 ACL 구조 변경처럼 설정 파일 자체를 바꿔야 할 때는 reload가 불가피한데, HAProxy는 이 reload를 무중단으로 수행합니다. 새 설정으로 새 프로세스를 띄우되, 기존 프로세스는 진행 중인 연결을 마칠 때까지 유지하고, 리스닝 소켓은 두 프로세스가 공유합니다. SO_REUSEPORT와 소켓 핸드오버 메커니즘 덕분에 reload 순간에도 새 연결이 거절되지 않습니다.

운영 관점에서 중요한 함의는 이것입니다. "엔드포인트 변경은 reload를 일으키지 않으므로 안심해도 되지만, 잦은 ConfigMap/구조 변경은 reload를 유발할 수 있다"는 점입니다. reload 빈도가 너무 높으면 진행 중 연결을 마치지 못한 구 프로세스가 누적될 수 있으니, hard-stop-after 같은 값으로 구 프로세스의 최대 생존 시간을 제어합니다.

controller:
  config:
    hard-stop-after: "30m"   # reload 후 구 프로세스 최대 생존 시간

TCP 모드와 TLS passthrough

HTTP만 처리한다면 인그레스로 충분하지만, 데이터베이스나 메시지 브로커처럼 TCP를 그대로 노출해야 하는 경우가 있습니다. HAProxy 인그레스는 이를 두 가지로 지원합니다.

첫째, TCP 서비스입니다. 공식 컨트롤러는 별도 ConfigMap으로 임의 포트의 TCP를 백엔드 서비스로 연결합니다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: haproxy-kubernetes-ingress-tcp
  namespace: haproxy-controller
data:
  # 형식: "프론트포트": "네임스페이스/서비스:포트"
  "5432": "demo/postgresql:5432"
  "6379": "demo/redis:6379"

이렇게 하면 컨트롤러가 5432, 6379 포트에 TCP frontend를 만들어 각 서비스로 전달합니다. 단, LoadBalancer Service나 호스트 포트로 이 포트들이 외부에 열려 있어야 합니다.

둘째, TLS passthrough입니다. 일반적인 인그레스는 컨트롤러에서 TLS를 종단(termination)하지만, 백엔드가 직접 TLS를 처리해야 하는 경우(예: mTLS를 백엔드까지 끝까지 유지)에는 컨트롤러가 복호화하지 않고 그대로 흘려보내야 합니다. 이때는 SNI(Server Name Indication)를 보고 라우팅합니다.

TLS termination (기본):
  클라이언트 ──TLS──> [HAProxy: 복호화] ──평문/재암호화──> 백엔드

TLS passthrough:
  클라이언트 ──TLS──> [HAProxy: SNI만 읽고 복호화 안 함] ──TLS 그대로──> 백엔드
                            (인증서는 백엔드에 있음)

passthrough는 TCP 모드의 일종으로 동작합니다. 컨트롤러가 SSL 핸드셰이크의 SNI 필드를 ACL로 검사해 호스트별로 다른 백엔드로 보냅니다. 다만 이 경우 컨트롤러는 HTTP 헤더를 볼 수 없으므로 path 기반 라우팅이나 헤더 조작은 불가능합니다. 호스트(SNI) 단위 라우팅만 가능하다는 한계를 기억해야 합니다.

Rate limiting과 ACL — 스틱 테이블의 힘

HAProxy의 진짜 강점 중 하나는 스틱 테이블(stick-table)을 이용한 상태 추적입니다. 스틱 테이블은 메모리 상의 키-값 저장소로, 소스 IP별 요청 횟수, 연결 수, 에러율 등을 추적할 수 있습니다. 이를 기반으로 rate limit과 abuse 차단을 구현합니다.

어노테이션 기반의 기본 rate limit은 다음과 같습니다.

metadata:
  annotations:
    haproxy.org/rate-limit-requests: "100"
    haproxy.org/rate-limit-period: "1m"
    haproxy.org/rate-limit-status-code: "429"

이 설정은 소스 IP당 1분에 100 요청을 초과하면 429를 반환합니다. 이때 앞서 강조한 externalTrafficPolicy: Local로 진짜 클라이언트 IP가 보존되어 있어야 의미가 있습니다.

ACL은 더 세밀한 제어를 가능하게 합니다. 특정 경로를 특정 IP 대역에만 허용하거나, 특정 헤더가 있는 요청만 통과시키는 식입니다. 공식 컨트롤러에서는 whitelist/blacklist 어노테이션과, 글로벌 설정 스니펫으로 raw HAProxy 설정을 주입할 수 있습니다.

metadata:
  annotations:
    # 관리자 경로는 사내 대역에서만 접근 허용
    haproxy.org/whitelist: "10.0.0.0/8,192.168.0.0/16"

내부적으로 생성되는 HAProxy 설정 조각을 개념적으로 보면 다음과 같습니다. (이것은 컨트롤러가 자동 생성하는 것이며, 직접 작성하는 것이 아닙니다.)

frontend https
    bind *:443 ssl crt /etc/haproxy/certs/

    # 스틱 테이블: 소스 IP별 요청 카운트 추적
    stick-table type ip size 100k expire 1m store http_req_rate(1m)
    http-request track-sc0 src

    # 분당 100 초과 시 429
    acl too_many sc_http_req_rate(0) gt 100
    http-request deny deny_status 429 if too_many

    # 사내망만 허용하는 ACL
    acl internal_net src 10.0.0.0/8 192.168.0.0/16
    acl admin_path path_beg /admin
    http-request deny if admin_path !internal_net

    use_backend api_backend if { path_beg /api }
    default_backend web_backend

이 정도 수준의 트래픽 제어를 어노테이션과 ConfigMap만으로 표현할 수 있다는 것이 HAProxy 데이터플레인의 힘입니다.

TLS와 cert-manager 연동

실무에서 인증서는 cert-manager로 자동 발급/갱신하는 것이 표준입니다. HAProxy 인그레스도 표준 Ingress TLS 스펙을 따르므로 cert-manager와 자연스럽게 연동됩니다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-ingress-tls
  namespace: demo
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    haproxy.org/ssl-redirect: "true"
spec:
  ingressClassName: haproxy
  tls:
    - hosts:
        - app.example.com
      secretName: app-example-com-tls
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web-frontend
                port:
                  number: 80

cert-manager가 ACME(Let's Encrypt) 챌린지를 통과해 인증서를 Secret으로 만들면, 컨트롤러가 그 Secret을 watch하다가 HAProxy에 적용합니다. 인증서가 갱신될 때 Runtime API로 적용되므로 보통 reload 없이 반영됩니다.

Gateway API로의 흐름 — 인그레스 다음 표준

서두에서 언급했듯이, Ingress API는 동결되었고 후속 표준은 Gateway API입니다. Gateway API는 인그레스의 한계(어노테이션 난립, 역할 분리 부재, L4 빈약함)를 정면으로 해결하기 위해 설계되었습니다.

핵심 차이는 역할 기반 리소스 분리입니다.

Ingress 모델Gateway API 모델담당 역할
IngressClassGatewayClass인프라/플랫폼 팀
(해당 없음)Gateway (리스너, 포트, TLS)클러스터 운영자
Ingress 규칙HTTPRoute / TCPRoute / GRPCRoute앱 개발팀
[Gateway API 트래픽 흐름]

  GatewayClass (컨트롤러 = haproxy)
        │ 구현
  Gateway ──> 리스너 정의 (예: 443/HTTPS, TLS 인증서)
        │ 참조
  HTTPRoute ──> 호스트/경로 매칭 ──> backendRefs (Service)
   클라이언트 ──> [Gateway:HAProxy frontend] ──> [HTTPRoute 규칙] ──> Service

HAProxy 공식 컨트롤러도 Gateway API 지원을 점진적으로 추가해 왔으며, HTTPRoute와 TCPRoute 같은 핵심 리소스를 다룰 수 있습니다. 실무 권장 전략은 이렇습니다.

  • 기존에 잘 도는 Ingress 리소스는 당장 마이그레이션할 필요가 없습니다. Ingress API가 사라지는 것은 아니며, 다만 새 기능이 안 들어올 뿐입니다.
  • 신규 워크로드, 특히 L4(TCP/UDP) 라우팅이나 트래픽 분할(canary) 같은 고급 기능이 필요한 경우 Gateway API를 우선 검토합니다.
  • 컨트롤러의 Gateway API 지원 성숙도(어떤 Route 타입, 어떤 필터를 지원하는지)를 도입 전에 반드시 문서로 확인합니다.

간단한 HTTPRoute 예시는 다음과 같습니다.

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: haproxy-gateway
  namespace: demo
spec:
  gatewayClassName: haproxy
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      tls:
        mode: Terminate
        certificateRefs:
          - name: app-example-com-tls
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: web-route
  namespace: demo
spec:
  parentRefs:
    - name: haproxy-gateway
  hostnames:
    - "app.example.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /api
      backendRefs:
        - name: api-backend
          port: 8080
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - name: web-frontend
          port: 80

운영과 튜닝 — 관측성, HA, 성능

관측성

HAProxy의 stats와 Prometheus 메트릭은 트러블슈팅의 출발점입니다. ServiceMonitor를 켜면 백엔드별 응답 시간, 활성 연결 수, 큐 적체, reload 횟수 등을 수집할 수 있습니다. 특히 다음 지표를 대시보드에 올려 두는 것을 권장합니다.

  • 백엔드별 5xx 비율과 평균 응답 시간
  • 큐에 대기 중인 요청 수(백엔드 포화 신호)
  • reload 빈도(설정 변동이 너무 잦은지 진단)
  • 활성/유휴 연결 수와 maxconn 대비 사용률

고가용성 구성

컨트롤러는 최소 2 레플리카 이상으로 운영하고, PodDisruptionBudget으로 동시 종료를 제한합니다. 노드 장애에 대비해 topologySpreadConstraints나 anti-affinity로 서로 다른 노드/존에 분산합니다. 외부 진입은 LoadBalancer Service(클라우드) 또는 MetalLB(온프렘)로 노출하며, 그 앞단의 클라우드 LB가 컨트롤러 레플리카들에 트래픽을 분산합니다.

            외부 클라이언트
   [클라우드 LB / MetalLB VIP]
         │              │
         ▼              ▼
   [HAProxy Pod 1]  [HAProxy Pod 2]   (서로 다른 노드/존)
         │              │
         └──────┬───────┘
         백엔드 서비스 Pods

성능 튜닝 포인트

  • nbthread를 노드 코어 수에 맞춰 설정해 멀티코어를 활용합니다.
  • maxconn을 워크로드에 맞게 충분히 잡되, 메모리와 파일 디스크립터 한계를 함께 고려합니다.
  • keep-alive와 백엔드 커넥션 재사용을 켜서 핸드셰이크 오버헤드를 줄입니다.
  • 백엔드가 HTTP/2나 gRPC라면 server-proto로 명시해 프로토콜 다운그레이드를 방지합니다.
controller:
  config:
    nbthread: "4"
    maxconn: "100000"
    timeout-http-keep-alive: "60s"

함정과 트러블슈팅

실무에서 자주 마주치는 문제와 원인을 정리합니다.

첫째, 두 프로젝트의 어노테이션 혼용입니다. 인터넷 예제를 그대로 붙였는데 설정이 먹지 않는다면, 십중팔구 prefix가 다른 프로젝트의 것입니다. haproxy.org와 haproxy-ingress.github.io를 혼동하지 마세요.

둘째, ingressClassName 누락입니다. 여러 컨트롤러가 있는 클러스터에서 클래스를 지정하지 않으면 라우팅이 누락되거나 충돌합니다. Ingress마다 ingressClassName을 명시하는 것을 표준으로 삼으세요.

셋째, 클라이언트 IP 손실입니다. rate limit이나 IP ACL이 모든 트래픽을 한 IP(노드 IP)로 인식한다면, externalTrafficPolicy가 Cluster이거나 앞단 LB가 PROXY 프로토콜/X-Forwarded-For를 제대로 전달하지 않는 것입니다. 소스 IP 보존 경로를 끝까지 점검하세요.

넷째, reload 폭주입니다. 메트릭에서 reload 빈도가 비정상적으로 높다면, 설정이나 ConfigMap이 자동화에 의해 너무 자주 변경되고 있을 수 있습니다. 엔드포인트 변경은 reload를 일으키지 않아야 정상이므로, reload가 잦다면 구조적 변경의 원인을 추적합니다.

다섯째, TLS passthrough에서 path 라우팅 시도입니다. passthrough는 컨트롤러가 복호화를 하지 않으므로 HTTP path를 볼 수 없습니다. path 기반 라우팅이 필요하면 termination 모드를 써야 합니다.

여섯째, 백엔드 health check 미설정으로 인한 트래픽 블랙홀입니다. check를 켜지 않으면 죽은 Pod로도 트래픽이 갈 수 있습니다. 어노테이션으로 health check를 명시적으로 켜고, readinessProbe와의 일관성을 확인하세요.

진단의 출발점은 늘 컨트롤러 Pod 안의 HAProxy 설정과 로그입니다.

# 컨트롤러 로그 확인
kubectl -n haproxy-controller logs deploy/haproxy-ingress -c haproxy-ingress

# 컨트롤러 Pod에 들어가 생성된 설정 확인
kubectl -n haproxy-controller exec -it deploy/haproxy-ingress -- cat /etc/haproxy/haproxy.cfg

# Runtime 소켓으로 현재 서버 상태 조회 (Pod 내부)
echo "show servers state" | socat stdio /var/run/haproxy-runtime-api.sock

마치며

HAProxy 인그레스 컨트롤러의 가장 큰 가치는 "검증된 데이터플레인을 쿠버네티스 진입점에 그대로 가져온다"는 점에 있습니다. hitless reload와 Runtime API 덕분에 Pod가 수시로 바뀌는 환경에서도 연결을 끊지 않고 안정적으로 트래픽을 흘릴 수 있고, 스틱 테이블 기반 rate limit과 ACL은 별도 WAF 없이도 상당한 수준의 트래픽 제어를 제공합니다.

도입할 때 가장 먼저 정해야 할 것은 "공식(haproxytech) 프로젝트인가, 커뮤니티(jcmoraisjr) 프로젝트인가"입니다. 두 프로젝트는 어노테이션 모델이 다르므로, 이 선택을 명확히 한 뒤 일관되게 설정을 작성해야 합니다. 신규 도입이고 벤더 지원이 필요하다면 공식 프로젝트를, 오랜 커뮤니티 검증과 풍부한 기능을 원하면 jcmoraisjr를 고려하세요.

마지막으로 2026년의 큰 그림을 잊지 마세요. Ingress API는 동결되었고 미래는 Gateway API입니다. 지금 잘 도는 Ingress는 유지하되, 신규 워크로드와 고급 라우팅은 Gateway API로 설계하는 이중 전략이 현실적입니다. HAProxy는 이 전환기에 "안정성은 그대로 가져가면서 새 표준으로 넘어갈 수 있는" 든든한 선택지입니다.

참고 자료