들어가며
Ingress 컨트롤러는 클러스터의 단일 진입점입니다. 진입점이 죽으면 그 뒤의 모든 서비스가 아무리 건강해도 외부에서는 전면 장애로 보입니다. 그래서 컨트롤러의 고가용성(HA)은 "있으면 좋은 것"이 아니라 운영의 전제 조건입니다.
HA는 단순히 "replica를 2개로 늘렸다"가 아닙니다. 컨트롤러가 reload될 때 진행 중인 커넥션이 끊기지 않아야 하고, Pod가 종료될 때 트래픽이 graceful하게 빠져야 하며, 노드나 AZ 하나가 통째로 사라져도 트래픽이 흘러야 합니다. 게다가 트래픽이 늘면 자동으로 확장되고, ingress 객체가 수천 개로 불어나도 컨트롤러가 버텨야 합니다.
이 글에서는 ingress-nginx를 기준으로 HA 토폴로지 선택, 노출 방식별 가용성, graceful drain과 무중단 배포, reload 영향 최소화, leader election, 멀티 AZ 분산, 대규모 ingress 환경의 성능, 리소스 사이징, 부하 테스트 방법, 장애 시나리오까지 운영 관점에서 다룹니다. 2026년 현재 Ingress API는 frozen이고 Gateway API가 후계 표준이므로, HA 설계가 Gateway API로 어떻게 이어지는지도 마지막에 짚습니다.
HA 토폴로지: DaemonSet vs Deployment + HPA
컨트롤러를 어떻게 배치할지가 첫 갈림길입니다. 두 가지 주류 패턴이 있습니다.
| 구분 | Deployment + HPA | DaemonSet |
| --- | --- | --- |
| 배치 | 임의 노드에 replica N개 | 모든(혹은 라벨된) 노드에 1개씩 |
| 스케일링 | HPA로 replica 자동 조절 | 노드 수에 종속 |
| 노출과 궁합 | cloud LoadBalancer Service | hostNetwork/hostPort |
| 적합 환경 | 대부분의 클라우드 | 베어메탈, 엣지, 노드=용량 |
| 자원 효율 | 부하에 맞춰 탄력적 | 노드마다 고정 비용 |
클라우드 환경의 기본 선택은 Deployment + HPA입니다. cloud LoadBalancer가 트래픽을 받아 노드로 분산하고, 컨트롤러 replica는 부하에 따라 늘었다 줄었다 합니다. 베어메탈이나 엣지처럼 노드 자체가 트래픽 진입점인 환경에서는 DaemonSet + hostNetwork가 흔합니다. 각 노드의 80/443을 컨트롤러가 직접 점유해 한 홉을 줄이는 방식입니다.
Deployment 기반의 기본 HA 구성 예시입니다.
controller:
kind: Deployment
replicaCount: 3
minAvailable: 2
podDisruptionBudget:
enabled: true
minAvailable: 2
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
여기서 핵심은 세 가지입니다. (1) PodDisruptionBudget으로 동시에 내려가는 Pod 수를 제한해 노드 유지보수 중에도 최소 가용성을 보장합니다. (2) podAntiAffinity로 replica를 서로 다른 노드에 분산합니다. (3) topologySpreadConstraints로 AZ 간에도 고르게 퍼뜨립니다.
노출 방식별 HA
컨트롤러가 외부 트래픽을 받는 방식에 따라 HA 특성이 달라집니다.
방식 A: cloud LoadBalancer Service
[인터넷] → [클라우드 LB] → [NodePort] → [컨트롤러 Pod]
- LB가 헬스체크로 죽은 노드 자동 제외
- externalTrafficPolicy: Local 시 클라이언트 IP 보존 + 추가 홉 제거
방식 B: hostNetwork (DaemonSet)
[인터넷] → [노드 IP:80/443] → [컨트롤러 (호스트 네트워크)]
- 한 홉 제거, 최저 지연
- 노드당 컨트롤러 1개 제약, 포트 충돌 주의
방식 C: NodePort + 외부 LB(직접 운영)
[인터넷] → [외부 LB] → [NodeIP:NodePort] → [컨트롤러]
- 베어메탈에서 MetalLB 등과 조합
방식 A의 externalTrafficPolicy는 중요한 선택입니다. `Cluster`(기본)는 모든 노드가 트래픽을 받아 내부에서 재분산하므로 분산은 고르지만 클라이언트 IP가 가려지고 한 홉이 더 듭니다. `Local`은 해당 노드의 Pod로만 보내 IP를 보존하고 홉을 줄이지만, Pod가 없는 노드는 LB 헬스체크에서 빠져야 하므로 분산이 노드별 Pod 수에 의존합니다. HA 관점에서는 Local일 때 노드별 컨트롤러 Pod 분포를 균형 있게 유지하는 것이 중요합니다.
graceful drain과 무중단 배포
컨트롤러 Pod가 종료될 때, 진행 중이던 요청이 잘리면 사용자는 502를 봅니다. 무중단의 핵심은 "엔드포인트에서 빠지는 것"과 "실제 종료" 사이에 충분한 유예를 두는 것입니다.
종료 시퀀스는 다음과 같이 흘러야 합니다.
1. Pod에 SIGTERM 도착 직전, kube가 Endpoints에서 Pod 제거 시작
2. preStop 훅: sleep로 LB/kube-proxy가 제거를 전파할 시간 확보
3. nginx가 graceful shutdown 시작 (새 커넥션 거부, 기존 커넥션 마무리)
4. terminationGracePeriodSeconds 내에 in-flight 요청 완료
5. 프로세스 종료
ingress-nginx에서의 설정 예시입니다.
controller:
terminationGracePeriodSeconds: 300
lifecycle:
preStop:
exec:
command:
- /bin/sh
- -c
- sleep 30; /wait-shutdown
preStop의 sleep이 필요한 이유는, Endpoints 제거가 모든 LB와 kube-proxy에 전파되기까지 시간이 걸리기 때문입니다. 이 유예 없이 곧장 종료하면, 아직 이 Pod로 트래픽을 보내는 경로가 남아 있어 커넥션이 잘립니다. terminationGracePeriodSeconds는 가장 긴 정상 요청(예: 대용량 업로드, 스트리밍)이 마무리될 만큼 넉넉해야 합니다.
배포 전략은 RollingUpdate에 maxSurge를 두어 항상 충분한 replica가 살아 있게 합니다.
controller:
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
`maxUnavailable: 0`은 새 Pod가 Ready가 되기 전에는 기존 Pod를 내리지 않게 하여, 배포 중 용량이 줄어드는 것을 막습니다.
reload 영향 최소화
ingress-nginx의 특이점은 Ingress 리소스가 바뀔 때마다 nginx 설정을 다시 로드한다는 점입니다. reload는 기본적으로 graceful하지만(기존 worker가 커넥션을 마무리하고 새 worker가 새 커넥션을 받음), 잦은 reload는 메모리 사용량 변동, long-lived 커넥션(WebSocket, gRPC 스트림)의 끊김, 순간 지연 스파이크를 유발할 수 있습니다.
reload 충격을 줄이는 방법들입니다.
ConfigMap
data:
동적 endpoint 변경은 reload 없이 Lua로 반영 (기본 활성)
worker shutdown 유예를 늘려 in-flight를 더 잘 보존
worker-shutdown-timeout: "240s"
reload 직후 부하를 완화하는 keepalive 튜닝
upstream-keepalive-connections: "320"
upstream-keepalive-timeout: "60"
핵심 통찰은, ingress-nginx가 **엔드포인트(Pod IP) 변경은 reload 없이 처리**한다는 것입니다. Pod가 스케일되거나 재시작되는 흔한 이벤트는 Lua 기반 동적 구성으로 흡수되므로 reload를 유발하지 않습니다. reload를 부르는 것은 주로 Ingress 스펙·annotation·인증서 변경입니다. 따라서 reload 빈도를 줄이려면 잦은 Ingress 변경(예: 자동화 도구가 매 배포마다 annotation을 토글)을 점검하는 것이 효과적입니다.
운영 중에는 reload 빈도를 메트릭(config_last_reload_success_timestamp의 changes)으로 감시하고, WebSocket/gRPC 같은 long-lived 트래픽이 많다면 worker-shutdown-timeout을 넉넉히 잡아 reload 시 끊김을 최소화합니다.
leader election
ingress-nginx의 여러 replica는 모두 트래픽을 처리하지만, 일부 작업(특히 Ingress의 status.loadBalancer 필드 업데이트)은 한 인스턴스만 수행해야 합니다. 이를 위해 leader election을 사용합니다.
리더는 Lease 객체를 통해 선출되며, 리더만 Ingress 객체의 상태를 갱신합니다. 데이터플레인(실제 트래픽 처리)은 모든 replica가 동등하게 담당하므로, 리더가 죽어도 트래픽은 끊기지 않고 새 리더가 선출될 뿐입니다. 운영자가 알아둘 점은, leader election이 정상 동작하려면 RBAC에 Lease 리소스에 대한 권한이 있어야 하고, election 관련 경고 로그가 반복되면 권한이나 API 서버 연결을 점검해야 한다는 것입니다.
[replica A] ──┐
[replica B] ──┼─→ 모두 데이터플레인(트래픽) 처리
[replica C] ──┘
│
└─ Lease 통해 1명만 리더 → Ingress status 갱신 담당
리더 사망 시 → 자동 재선출, 트래픽 영향 없음
멀티 AZ 분산
단일 AZ에 컨트롤러가 몰려 있으면 그 AZ 장애 시 전면 다운입니다. 앞서 본 topologySpreadConstraints가 AZ 분산의 핵심 도구입니다.
controller:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
zone 단위는 `DoNotSchedule`로 엄격히 분산하고, hostname 단위는 `ScheduleAnyway`로 가능한 한 분산하되 스케줄링을 막지 않게 하는 조합이 실용적입니다. 클라우드 LB는 보통 멀티 AZ를 인지해 살아 있는 AZ로만 트래픽을 보내므로, 컨트롤러가 모든 AZ에 골고루 있으면 한 AZ가 죽어도 나머지로 서비스가 지속됩니다. replica 수는 "AZ 하나가 통째로 빠져도 남은 AZ가 피크 트래픽을 감당"하도록 잡는 것이 원칙입니다(N+1 또는 그 이상).
대규모 ingress 객체 성능
ingress 객체가 수십 개일 때와 수천 개일 때는 컨트롤러의 동작 특성이 다릅니다. 객체가 많아지면 다음이 문제가 됩니다.
- 생성되는 nginx 설정 파일이 거대해져 reload 시간이 길어짐
- 컨트롤러의 메모리/CPU 사용량 증가
- API 서버 watch 부하 증가
- reload 한 번의 비용이 커져, 변경 빈도가 그대로 부담으로 직결
완화 전략은 다음과 같습니다. 첫째, 네임스페이스나 도메인 그룹별로 **컨트롤러를 샤딩**합니다. 여러 IngressClass와 컨트롤러 인스턴스를 두어, 각 컨트롤러가 일부 Ingress만 watch하게 하면 설정 크기와 reload 비용이 분산됩니다.
샤드 A 컨트롤러
controller:
ingressClassResource:
name: nginx-shard-a
controllerValue: "k8s.io/ingress-nginx-shard-a"
watchNamespaces: "team-a,team-b"
둘째, 불필요한 annotation 변경을 줄여 reload 빈도를 낮춥니다. 셋째, 메트릭으로 reload 소요 시간을 모니터링하다가 임계치를 넘으면 샤딩을 늘립니다. 대규모 환경에서 single 거대 컨트롤러보다 여러 작은 샤드가 운영적으로 안정적인 경우가 많습니다.
리소스 사이징과 부하 테스트
적절한 requests/limits 없이는 HA가 무너집니다. CPU 한계에 걸려 컨트롤러가 throttle되면, 헬스체크 실패 → 재시작 → 트래픽 출렁임의 악순환이 생깁니다.
사이징의 출발점은 측정입니다. 부하 테스트로 "요청당 CPU 비용"과 "동시 커넥션당 메모리"를 구한 뒤, 피크 트래픽에 헤드룸을 더해 산정합니다.
간단한 부하 발생 (vegeta)
echo "GET https://app.example.com/" | \
vegeta attack -rate=2000 -duration=300s | \
vegeta report
또는 k6로 단계적 부하 증가
k6 run --vus 500 --duration 5m loadtest.js
부하 테스트 중 관찰할 지표: 컨트롤러 CPU/메모리, active connections, p99 지연, 5xx 비율, 그리고 의도적으로 Pod를 죽였을 때(chaos)의 회복 시간입니다. 특히 reload를 부하 중에 일으켜 보고(예: Ingress를 반복 수정) long-lived 커넥션이 끊기는지, 지연 스파이크가 얼마나 큰지를 측정해야 실제 운영의 행동을 알 수 있습니다.
사이징 가이드라인은 환경마다 다르지만, requests를 너무 낮게 잡아 노드 과밀 시 컨트롤러가 자원을 못 받는 상황을 피하고, limits는 reload 시 순간 메모리 증가를 흡수할 만큼 여유를 두는 것이 안전합니다.
장애 시나리오와 대응
HA 설계는 "무엇이 죽을 수 있는가"를 미리 그려보는 것입니다.
| 장애 | 영향 | HA 설계가 막는 방법 |
| --- | --- | --- |
| 컨트롤러 Pod 1개 크래시 | 일부 용량 손실 | replica N개 + PDB로 잔여 용량 유지 |
| 노드 1개 다운 | 그 노드의 Pod 손실 | podAntiAffinity로 노드 분산 |
| AZ 1개 다운 | 해당 AZ 전체 손실 | topologySpread + N+1 사이징 |
| 트래픽 급증 | 포화, 지연 증가 | HPA 자동 스케일 + 헤드룸 |
| reload 폭주 | long-lived 끊김, 지연 | worker-shutdown-timeout, 변경 빈도 관리 |
| ingress 객체 폭증 | reload 지연 | 샤딩 |
| 인증서 만료 | TLS 전면 실패 | cert-manager 자동 갱신 + 만료 알림 |
각 시나리오에 대해 "탐지(메트릭/알림) → 자동 완화(HPA/재스케줄) → 수동 개입 필요 시점"을 미리 정해두면, 실제 장애 시 당황 없이 흘러갑니다. 특히 AZ 장애와 트래픽 급증은 자주 함께 오므로(한 AZ가 빠지면 남은 AZ에 부하 집중), N+1 용량과 HPA가 함께 동작하도록 검증해야 합니다.
Gateway API 시대의 HA
2026년 현재 Ingress API는 frozen이고 Gateway API가 후계 표준입니다. HA 원리는 대부분 이어지지만 몇 가지가 개선됩니다. Gateway API 구현체(Envoy 기반 Contour/Istio, Cilium, NGINX Gateway Fabric 등)는 컨트롤플레인과 데이터플레인이 더 명확히 분리되어, 설정 변경이 데이터플레인 reload로 직결되지 않는 경우가 많습니다. Envoy 기반은 xDS로 설정을 동적 푸시하므로 ingress-nginx식의 "파일 reload" 충격이 줄어듭니다.
또한 Gateway API의 3계층 모델(GatewayClass/Gateway/HTTPRoute)은 책임 분리가 명확해, 한 Gateway에 너무 많은 Route가 몰리는 문제를 여러 Gateway로 자연스럽게 나눌 수 있습니다. 이는 앞서 본 "샤딩"을 API 차원에서 더 우아하게 표현합니다. 지금 ingress-nginx에서 PDB·antiAffinity·topologySpread·graceful drain을 제대로 갖춰두면, Gateway API로 옮길 때 이 운영 패턴들이 거의 그대로 유효합니다.
마치며
Ingress 컨트롤러 HA의 핵심은 "단일 진입점을 단일 장애점으로 만들지 않는 것"입니다. replica 다중화와 PDB로 용량을 지키고, antiAffinity와 topologySpread로 노드·AZ에 분산하고, graceful drain과 무중단 배포로 변경 중에도 트래픽을 끊지 않으며, HPA로 부하에 적응하고, 샤딩으로 대규모를 감당합니다.
이 모든 설정은 한 번에 완성되지 않습니다. 부하 테스트와 의도적 장애 주입(chaos)으로 가정을 검증하고, 메트릭으로 실제 행동을 관찰하며 점진적으로 다듬어 가는 것이 HA 운영의 본질입니다. 진입점이 흔들리지 않을 때 비로소 그 뒤의 모든 서비스가 안심하고 돌아갈 수 있습니다.
참고 자료
- Kubernetes Ingress 개념: https://kubernetes.io/docs/concepts/services-networking/ingress/
- ingress-nginx 배포/HA 가이드: https://kubernetes.github.io/ingress-nginx/deploy/
- ingress-nginx ConfigMap 옵션: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/
- Kubernetes PodDisruptionBudget: https://kubernetes.io/docs/concepts/workloads/pods/disruptions/
- Kubernetes topologySpreadConstraints: https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/
- Kubernetes HorizontalPodAutoscaler: https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/
- Gateway API: https://gateway-api.sigs.k8s.io/
- Contour(Envoy 기반): https://projectcontour.io/docs/
- Traefik 고가용성 문서: https://doc.traefik.io/traefik/
- cert-manager: https://cert-manager.io/docs/
현재 단락 (1/151)
Ingress 컨트롤러는 클러스터의 단일 진입점입니다. 진입점이 죽으면 그 뒤의 모든 서비스가 아무리 건강해도 외부에서는 전면 장애로 보입니다. 그래서 컨트롤러의 고가용성(HA)은...