- Published on
Ingress 레이트 리미팅과 DDoS 완화: ingress-nginx부터 Gateway API까지 실전 가이드
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 들어가며
- 레이트 리미팅의 기본 원리
- ingress-nginx 어노테이션 심화
- Traefik / Kong / APISIX 비교
- 분산 환경의 카운팅 문제
- 화이트리스트와 지역(geo) 제어
- L3/L4 DDoS와 L7 방어의 분리
- 봇 트래픽 대응
- 부하 시나리오로 보는 동작
- Gateway API와의 관계
- 운영과 튜닝
- 흔한 함정과 트러블슈팅
- 운영 체크리스트
- 마치며
- 참고 자료
들어가며
어느 날 새벽, 평소 초당 200건 정도를 처리하던 로그인 API에 갑자기 초당 4만 건의 요청이 쏟아졌습니다. 데이터베이스 커넥션 풀은 순식간에 고갈되었고, 정상 사용자들은 504 Gateway Timeout을 마주했습니다. 트래픽을 추적해 보니 수천 개의 IP에서 동일한 엔드포인트로 동일한 페이로드가 반복 전송되고 있었습니다. 전형적인 L7 애플리케이션 계층 공격이었습니다.
이런 상황에서 가장 먼저 떠오르는 방어선은 애플리케이션 코드입니다. 하지만 애플리케이션이 요청을 받았다는 것은 이미 커넥션이 수립되고, TLS 핸드셰이크가 끝나고, 워커 스레드가 점유되었다는 뜻입니다. 즉 비용은 이미 발생한 것입니다. 진짜 방어는 요청이 비싼 자원에 도달하기 전, 가능한 한 바깥쪽에서 이루어져야 합니다. Kubernetes 환경에서 그 바깥쪽 경계가 바로 Ingress 계층입니다.
이 글에서는 Ingress 계층에서 레이트 리미팅을 어떻게 구성하는지, 분산 환경에서 정확한 카운팅이 왜 어려운지, 그리고 L3/L4 볼류메트릭 공격과 L7 애플리케이션 공격을 어떻게 서로 다른 계층에서 분리해 방어하는지를 실전 코드와 함께 정리합니다. 2026년 현재 Ingress API는 사실상 동결(frozen)되었고 Gateway API가 후속 표준으로 자리 잡았기 때문에, 두 API의 관계와 마이그레이션 관점도 함께 짚어 보겠습니다.
레이트 리미팅의 기본 원리
왜 L7에서 제한하는가
네트워크 방어는 OSI 계층별로 역할이 다릅니다. 아래 표는 각 계층이 무엇을 보고 무엇을 막을 수 있는지 정리한 것입니다.
| 계층 | 보는 정보 | 막을 수 있는 것 | 막을 수 없는 것 |
|---|---|---|---|
| L3 (IP) | 출발지·목적지 IP | IP 단위 차단, geo 차단 | 정상 IP를 위장한 봇넷 |
| L4 (TCP/UDP) | 포트, 커넥션 상태 | SYN 플러드, 커넥션 수 제한 | HTTP 경로별 세밀한 제어 |
| L7 (HTTP) | 메서드, 경로, 헤더, 쿠키 | 엔드포인트별 RPS, 사용자별 쿼터 | 대용량 볼류메트릭 플러드 |
핵심은 각 계층이 자기가 가장 잘 막을 수 있는 것을 막아야 한다는 점입니다. 초당 수백 기가비트의 볼류메트릭 공격을 L7 Ingress에서 막으려 하면, 패킷이 클러스터 네트워크까지 들어와 대역폭을 잡아먹은 뒤에야 차단됩니다. 반대로 특정 로그인 엔드포인트에 초당 5회 제한 같은 세밀한 규칙은 L3/L4 장비로는 표현할 수 없습니다.
토큰 버킷과 리키 버킷
레이트 리미팅 알고리즘의 양대 축은 토큰 버킷(token bucket)과 리키 버킷(leaky bucket)입니다.
[토큰 버킷] [리키 버킷]
보충률 r ──> ( 버킷 용량 b ) 요청 ──> ( 큐 용량 b ) ──> 누출률 r ──> 처리
│ │
요청 도착 시 토큰 1개 소비 큐가 가득 차면 요청 폐기(drop)
토큰 없으면 거부(reject) 일정 속도로만 빠져나감
- 토큰 버킷: 평균 속도 r을 유지하되 버킷 용량 b만큼의 순간 버스트를 허용합니다. 짧은 스파이크를 자연스럽게 흡수합니다.
- 리키 버킷: 출력 속도를 엄격히 평탄화합니다. 버스트를 흡수하지 않고 일정 속도로만 흘려보냅니다.
ingress-nginx의 레이트 리미팅은 내부적으로 nginx의 limit_req(리키 버킷에 가까운 모델)와 limit_conn을 사용합니다. burst 파라미터로 약간의 버스트를 허용하는 방식입니다.
ingress-nginx 어노테이션 심화
ingress-nginx는 Ingress 리소스의 어노테이션으로 레이트 리미팅을 선언합니다. 가장 자주 쓰이는 네 가지를 정리합니다.
| 어노테이션 | 의미 | 단위 |
|---|---|---|
| limit-rps | 초당 허용 요청 수 | requests per second |
| limit-rpm | 분당 허용 요청 수 | requests per minute |
| limit-connections | 동시 커넥션 수 제한 | connections |
| limit-burst-multiplier | burst 허용 배수 (기본 5) | multiplier |
다음은 로그인 엔드포인트를 보호하는 Ingress 예시입니다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: auth-ingress
namespace: production
annotations:
nginx.ingress.kubernetes.io/limit-rps: "5"
nginx.ingress.kubernetes.io/limit-burst-multiplier: "2"
nginx.ingress.kubernetes.io/limit-connections: "10"
# 신뢰할 수 있는 내부 대역은 제한에서 제외
nginx.ingress.kubernetes.io/limit-whitelist: "10.0.0.0/8,192.168.0.0/16"
spec:
ingressClassName: nginx
rules:
- host: api.example.com
http:
paths:
- path: /auth/login
pathType: Prefix
backend:
service:
name: auth-service
port:
number: 8080
여기서 limit-rps: 5와 limit-burst-multiplier: 2를 함께 쓰면 실제 burst 용량은 초당 10건까지 허용됩니다. 즉 평균 5 RPS를 유지하되 순간적으로 10건까지는 통과시키고, 그 이상은 503 Service Temporarily Unavailable로 거부합니다.
키(key)는 무엇으로 잡히는가
ingress-nginx의 기본 레이트 리미팅 키는 클라이언트 IP입니다. 하지만 프록시·로드밸런서 뒤에 있다면 실제 클라이언트 IP를 정확히 식별해야 합니다. 이때 핵심이 되는 설정이 ConfigMap의 신뢰 설정입니다.
apiVersion: v1
kind: ConfigMap
metadata:
name: ingress-nginx-controller
namespace: ingress-nginx
data:
# 신뢰할 프록시 대역(예: 클라우드 LB CIDR)
proxy-real-ip-cidr: "130.211.0.0/22,35.191.0.0/16"
use-forwarded-headers: "true"
compute-full-forwarded-for: "true"
use-forwarded-headers를 켜면 X-Forwarded-For 헤더의 IP를 클라이언트로 사용합니다. 단, 이 헤더는 클라이언트가 위조할 수 있으므로 반드시 신뢰 대역(proxy-real-ip-cidr)을 함께 지정해야 합니다. 그렇지 않으면 공격자가 매 요청마다 가짜 X-Forwarded-For 값을 넣어 카운터를 우회할 수 있습니다.
글로벌 한도와 커스텀 응답
전체 Ingress에 공통으로 적용할 기본 한도는 ConfigMap에서 설정합니다.
apiVersion: v1
kind: ConfigMap
metadata:
name: ingress-nginx-controller
namespace: ingress-nginx
data:
limit-rate: "0" # 응답 대역폭 제한(바이트/초), 0은 무제한
limit-req-status-code: "429" # 기본 503 대신 429 Too Many Requests 반환
limit-conn-status-code: "429"
레이트 리미팅 거부 시 기본 응답 코드는 503이지만, 클라이언트가 백오프(backoff)를 명확히 인지하도록 429 Too Many Requests로 바꾸는 것이 일반적입니다. 가능하면 Retry-After 헤더도 함께 내려 주는 것이 좋습니다.
Traefik / Kong / APISIX 비교
ingress-nginx가 가장 널리 쓰이지만, 운영 환경과 요구사항에 따라 다른 컨트롤러를 선택하기도 합니다. 레이트 리미팅 관점에서 비교하면 다음과 같습니다.
| 항목 | ingress-nginx | Traefik | Kong | APISIX |
|---|---|---|---|---|
| 설정 방식 | 어노테이션 | Middleware CRD | Plugin (KongPlugin) | Plugin / Route |
| 알고리즘 | leaky bucket 계열 | 슬라이딩 윈도우 평균 | 고정/슬라이딩 윈도우 | 토큰/리키 버킷 등 다양 |
| 분산 카운팅 | 기본 미지원 (per-pod) | 기본 미지원 | Redis 지원 | Redis 클러스터 지원 |
| 키 커스터마이즈 | 제한적 | 소스 기준 | consumer/IP/header | 변수 기반 유연 |
| 거부 응답 코드 | 설정 가능 | 설정 가능 | 429 기본 | 설정 가능 |
Traefik Middleware
Traefik은 Middleware CRD로 레이트 리미팅을 선언합니다.
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: api-ratelimit
namespace: production
spec:
rateLimit:
average: 100 # 평균 초당 100 요청
burst: 50 # 순간 50 버스트 허용
period: 1s
sourceCriterion:
ipStrategy:
depth: 1 # X-Forwarded-For에서 뒤에서 1번째 IP 사용
Kong Plugin
Kong은 KongPlugin 리소스로 선언하며, Redis를 백엔드로 지정하면 분산 카운팅이 가능합니다.
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
name: rate-limiting-redis
namespace: production
plugin: rate-limiting
config:
minute: 60
policy: redis
redis_host: redis.production.svc.cluster.local
redis_port: 6379
fault_tolerant: true
APISIX Route
APISIX는 limit-req, limit-conn, limit-count 플러그인을 제공하며, Redis 클러스터를 통한 전역 카운팅을 지원합니다.
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
name: api-route
namespace: production
spec:
http:
- name: limited
match:
hosts:
- api.example.com
paths:
- /api/*
backends:
- serviceName: api-service
servicePort: 8080
plugins:
- name: limit-count
enable: true
config:
count: 200
time_window: 60
rejected_code: 429
policy: redis
redis_host: redis.production.svc.cluster.local
redis_port: 6379
분산 환경의 카운팅 문제
여기서부터가 실전에서 가장 많이 발목을 잡는 지점입니다. Ingress 컨트롤러는 보통 여러 개의 파드(replica)로 떠 있습니다. 각 파드가 자기 메모리에 카운터를 들고 있다면, 전체 한도는 의도한 값의 배수가 되어 버립니다.
[ 클라이언트 트래픽 ]
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ ingress pod1 │ │ ingress pod2 │ │ ingress pod3 │
│ 메모리 카운터 │ │ 메모리 카운터 │ │ 메모리 카운터 │
│ 한도 100 │ │ 한도 100 │ │ 한도 100 │
└──────────────┘ └──────────────┘ └──────────────┘
의도한 한도: 100 RPS → 실제 허용: 최대 300 RPS (파드 수 x 100)
In-memory 방식의 특성을 정리하면 다음과 같습니다.
| 방식 | 정확도 | 레이턴시 | 장애 내성 | 운영 복잡도 |
|---|---|---|---|---|
| In-memory (per-pod) | 낮음 (파드 수만큼 곱) | 가장 빠름 | 높음 | 낮음 |
| Redis 중앙 카운터 | 높음 | 네트워크 1홉 추가 | Redis 장애에 취약 | 높음 |
ingress-nginx 기본 구현은 per-pod 메모리 방식이므로, 한도를 설정할 때 레플리카 수로 나눠서 잡아야 의도한 전역 한도에 근접합니다. 예를 들어 전역 300 RPS를 원하고 파드가 3개면 파드당 100 RPS로 설정하는 식입니다. 다만 HPA로 파드 수가 변하면 전역 한도도 따라 변한다는 점에 주의해야 합니다.
정확한 전역 카운팅이 필요하다면 Redis 같은 중앙 저장소를 백엔드로 쓰는 Kong, APISIX를 선택하거나, ingress-nginx 앞단에 별도의 레이트 리미팅 게이트웨이를 두는 구성을 검토합니다. Redis를 쓸 때는 다음 설정이 중요합니다.
- 원자적 카운팅: INCR + EXPIRE 또는 Lua 스크립트로 race condition 제거
- fault tolerant 모드: Redis 장애 시 요청을 막을지(fail-closed) 통과시킬지(fail-open) 결정
- 키 만료: 윈도우 단위로 키가 정확히 만료되도록 슬라이딩/고정 윈도우 선택
- 레이턴시 예산: Redis 왕복이 p99 레이턴시에 미치는 영향을 측정
fail-open과 fail-closed는 트레이드오프입니다. Redis가 죽었을 때 fail-open은 가용성을 지키되 공격에 노출되고, fail-closed는 안전하되 정상 트래픽까지 막습니다. 로그인·결제처럼 민감한 엔드포인트는 fail-closed, 일반 조회 API는 fail-open을 권장합니다.
화이트리스트와 지역(geo) 제어
모든 트래픽을 동일하게 제한하면 안 됩니다. 신뢰할 수 있는 파트너, 내부 모니터링, 헬스체크는 제외해야 합니다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: partner-api
namespace: production
annotations:
nginx.ingress.kubernetes.io/limit-rps: "20"
# 화이트리스트 대역은 레이트 리미팅 우회
nginx.ingress.kubernetes.io/limit-whitelist: "203.0.113.0/24,198.51.100.10/32"
spec:
ingressClassName: nginx
rules:
- host: partner.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: partner-service
port:
number: 80
지역 차단은 GeoIP 모듈로 구현합니다. ingress-nginx는 nginx의 GeoIP2 모듈을 통해 국가 코드 기반 제어를 지원합니다.
apiVersion: v1
kind: ConfigMap
metadata:
name: ingress-nginx-controller
namespace: ingress-nginx
data:
use-geoip2: "true"
# 특정 국가만 허용하거나 차단하는 정책은 server-snippet으로 작성
다만 GeoIP 차단은 VPN·프록시로 쉽게 우회되므로, 단독 방어가 아니라 보조 신호로 활용하는 것이 현실적입니다. 정상 사용자가 전혀 없는 지역에서 오는 공격성 트래픽을 1차로 걸러 내는 용도로 적합합니다.
L3/L4 DDoS와 L7 방어의 분리
가장 흔한 설계 실수는 모든 DDoS를 Ingress 한 곳에서 막으려는 것입니다. 볼류메트릭 공격(L3/L4)과 애플리케이션 공격(L7)은 방어 위치 자체가 달라야 합니다.
[인터넷]
│
▼
┌──────────────────────────────────────────────┐
│ 클라우드 엣지 (CDN / Anycast / Scrubbing) │ ← L3/L4 볼류메트릭 방어
│ - SYN 플러드, UDP 앰프, 대용량 패킷 흡수 │ (AWS Shield, Cloud Armor 등)
└──────────────────────────────────────────────┘
│ (정상에 가까운 트래픽만 통과)
▼
┌──────────────────────────────────────────────┐
│ Ingress 계층 (ingress-nginx / Gateway) │ ← L7 애플리케이션 방어
│ - 엔드포인트별 RPS, 커넥션 제한, WAF 룰 │ (레이트 리미팅, 봇 차단)
└──────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────┐
│ 애플리케이션 (서비스/파드) │ ← 비즈니스 로직 쿼터
│ - 사용자별 쿼터, 멱등성, 회로 차단기 │
└──────────────────────────────────────────────┘
각 계층의 역할 분담을 표로 정리하면 다음과 같습니다.
| 공격 유형 | 예시 | 방어 위치 | 도구 |
|---|---|---|---|
| 볼류메트릭 (L3/L4) | SYN 플러드, UDP 앰프 | 클라우드 엣지 | Shield, Cloud Armor, 스크러빙 |
| 프로토콜 (L4) | 커넥션 고갈, slowloris | 엣지 + Ingress | 커넥션 제한, 타임아웃 |
| 애플리케이션 (L7) | HTTP 플러드, 캐시 버스팅 | Ingress + 앱 | 레이트 리미팅, WAF |
클라우드 엣지에서 볼류메트릭을 흡수하면, Ingress까지 도달하는 트래픽은 이미 상당히 정제된 상태입니다. 이 단계에서 Ingress는 L7 규칙에만 집중할 수 있습니다. 반대로 엣지 보호 없이 Ingress 단독으로 수백 기가비트를 받으면, 노드의 NIC 대역폭과 conntrack 테이블이 먼저 무너집니다.
slowloris 같은 저속 공격은 별도 주의가 필요합니다. 적은 대역폭으로 커넥션만 오래 점유하는 공격이므로, 다음과 같이 타임아웃을 짧게 잡아 방어합니다.
apiVersion: v1
kind: ConfigMap
metadata:
name: ingress-nginx-controller
namespace: ingress-nginx
data:
client-header-timeout: "10"
client-body-timeout: "10"
keep-alive-requests: "100"
worker-shutdown-timeout: "30s"
봇 트래픽 대응
DDoS의 상당수는 봇넷이 일으킵니다. 봇과 정상 사용자를 구분하는 신호는 여러 가지가 있습니다.
[봇 식별 신호]
- User-Agent 누락 또는 비정상 패턴
- 쿠키/세션을 유지하지 않는 무상태 반복 요청
- JavaScript 챌린지를 통과하지 못함
- TLS 핑거프린트(JA3/JA4)가 알려진 봇 도구와 일치
- 비정상적으로 균일한 요청 간격(인간은 지터가 있음)
Ingress 계층에서는 단순 User-Agent 차단이나 헤더 검증 정도가 가능합니다. server-snippet으로 빈 User-Agent를 거부하는 예시입니다.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: bot-protect
namespace: production
annotations:
nginx.ingress.kubernetes.io/server-snippet: |
if ($http_user_agent = "") {
return 403;
}
if ($http_user_agent ~* "(curl|wget|python-requests|scrapy)") {
return 403;
}
spec:
ingressClassName: nginx
rules:
- host: www.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-service
port:
number: 80
단, User-Agent는 손쉽게 위조되므로 정교한 봇에는 효과가 제한적입니다. JavaScript 챌린지나 JA3/JA4 핑거프린팅 같은 고급 봇 관리 기능은 보통 CDN·WAF 계층(클라우드 엣지)에서 처리하고, Ingress는 1차 필터 역할을 맡는 것이 합리적입니다.
부하 시나리오로 보는 동작
설정값을 정할 때는 구체적인 시나리오로 검증해야 합니다. 아래는 평소 200 RPS, 한도 300 RPS(burst 포함 600)인 API에 세 가지 상황이 발생했을 때의 동작입니다.
| 시나리오 | 입력 트래픽 | 통과 | 거부(429) | 사용자 체감 |
|---|---|---|---|---|
| 정상 피크 | 280 RPS | 280 | 0 | 정상 |
| 마케팅 스파이크 | 550 RPS (10초) | 약 600(버스트 흡수 후 평탄화) | 일부 | 약간 지연 |
| L7 플러드 | 40000 RPS | 300 | 39700 | 정상 사용자 보호됨 |
세 번째 시나리오가 레이트 리미팅의 존재 이유입니다. 4만 RPS가 들어와도 백엔드에는 한도만큼만 전달되고 나머지는 Ingress에서 429로 잘려 나가므로, 데이터베이스와 애플리케이션은 보호됩니다. 거부된 요청 처리 비용은 백엔드 처리 비용에 비해 무시할 수 있을 만큼 작습니다.
부하 테스트는 실제 배포 전에 반드시 수행합니다. 다음은 간단한 검증 명령 예시입니다.
# hey로 동시 200, 총 20000 요청 발사
hey -z 30s -c 200 https://api.example.com/api/items
# 거부율 확인을 위해 응답 코드 분포 집계
hey -z 30s -c 500 https://api.example.com/auth/login \
| grep -A20 "Status code distribution"
Gateway API와의 관계
2026년 현재 Ingress API는 신규 기능 추가가 멈춘 동결 상태이고, 후속 표준은 Gateway API입니다. Gateway API는 역할 분리(인프라 운영자 / 클러스터 운영자 / 앱 개발자), 프로토콜 확장성, 표현력 있는 라우팅을 목표로 설계되었습니다.
| 관점 | Ingress | Gateway API |
|---|---|---|
| 표준 상태 | 동결 (frozen) | 활발히 발전 중 |
| 리소스 | Ingress 단일 | GatewayClass / Gateway / HTTPRoute 등 |
| 역할 분리 | 약함 | 명시적 (RBAC 친화적) |
| 레이트 리미팅 | 구현체 어노테이션 | 정책 어태치먼트 + 구현체 확장 |
| 트래픽 분할 | 어노테이션 | 가중치 기반 표준 필드 |
다만 레이트 리미팅은 Gateway API 표준 스펙에 완전히 들어가 있지 않고, 구현체별 정책(policy attachment)이나 확장 필터로 제공되는 경우가 많습니다. 따라서 마이그레이션 시에는 사용 중인 구현체가 레이트 리미팅을 어떤 방식으로 노출하는지 확인해야 합니다.
ingress-nginx는 2025년을 기점으로 사실상 유지보수(maintenance) 모드에 들어갔고, 보안 취약점 대응 위주로 운영되는 추세입니다. 신규 클러스터를 설계한다면 Gateway API 기반 구현체(예: Envoy Gateway, Traefik의 Gateway 지원, Cilium Gateway 등)를 우선 검토하는 것이 장기적으로 안전합니다. 다음은 간단한 HTTPRoute 예시입니다.
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: api-route
namespace: production
spec:
parentRefs:
- name: prod-gateway
hostnames:
- api.example.com
rules:
- matches:
- path:
type: PathPrefix
value: /api
backendRefs:
- name: api-service
port: 8080
운영과 튜닝
관측 가능성 확보
레이트 리미팅은 켜 두는 것으로 끝나지 않습니다. 얼마나 많이 거부되는지, 거부된 트래픽이 공격인지 정상 사용자인지 끊임없이 관측해야 합니다.
[관측 지표]
- 429/503 응답 비율 (전체 대비)
- 거부된 요청의 IP/경로/User-Agent 분포
- 백엔드 p50/p95/p99 레이턴시 변화
- Ingress 파드의 CPU/메모리/커넥션 수
- upstream 5xx 비율 (한도가 효과적인지 판단)
Prometheus로 ingress-nginx 메트릭을 수집하면 nginx_ingress_controller_requests의 status 라벨로 429 비율을 추적할 수 있습니다. 거부율이 갑자기 치솟으면 공격이거나, 한도가 너무 빡빡한 것입니다.
단계적 적용
레이트 리미팅을 프로덕션에 바로 강하게 걸면 정상 사용자가 차단될 위험이 있습니다. 다음 순서를 권장합니다.
1. 관측 모드: 한도를 매우 높게 잡고 거부 없이 메트릭만 수집
2. 기준선 파악: 평소 피크 RPS, p99, 정상 버스트 패턴 분석
3. 보수적 적용: 평소 피크의 3~5배를 한도로 설정
4. 점진적 강화: 거부율과 사용자 영향을 보며 한도를 조정
5. 엔드포인트 차등화: 로그인/결제는 엄격, 정적 조회는 느슨
흔한 함정과 트러블슈팅
실무에서 반복적으로 마주치는 함정들을 정리합니다.
[함정 1] X-Forwarded-For 미설정
→ 모든 요청이 LB IP 하나로 카운팅되어 전체가 한꺼번에 차단됨
→ proxy-real-ip-cidr와 use-forwarded-headers를 반드시 함께 설정
[함정 2] per-pod 카운팅을 전역으로 오해
→ 한도 100인데 파드 3개라 실제 300까지 허용됨
→ 레플리카 수로 나눠 설정하거나 중앙 카운터(Redis) 도입
[함정 3] 헬스체크/프로브가 레이트 리밋에 걸림
→ kubelet 프로브나 모니터링이 429를 받아 파드가 죽음
→ 내부 대역을 whitelist에 추가
[함정 4] 거부 코드를 503으로 방치
→ 클라이언트가 서버 장애로 오인해 무한 재시도 → 악순환
→ 429 + Retry-After로 백오프 유도
[함정 5] L7에서 볼류메트릭을 막으려 함
→ 패킷이 노드까지 들어와 NIC/conntrack이 먼저 포화
→ 볼류메트릭은 클라우드 엣지에서 흡수
[함정 6] 한도를 너무 낮게 잡음
→ 정상 피크에도 429가 발생해 사용자 이탈
→ 관측 모드로 기준선부터 측정
디버깅 명령
문제가 의심될 때 빠르게 확인할 수 있는 명령들입니다.
# Ingress 컨트롤러 로그에서 limiting 관련 메시지 확인
kubectl logs -n ingress-nginx deploy/ingress-nginx-controller | grep -i limit
# 실제 적용된 nginx 설정 확인 (생성된 limit_req 존을 검증)
kubectl exec -n ingress-nginx deploy/ingress-nginx-controller -- \
cat /etc/nginx/nginx.conf | grep -A3 limit_req_zone
# 특정 IP에서 보이는 응답 코드 분포 빠른 점검
for i in $(seq 1 20); do
curl -s -o /dev/null -w "%{http_code}\n" https://api.example.com/auth/login
done | sort | uniq -c
운영 체크리스트
배포 전 최종 점검 목록입니다.
[ ] 엔드포인트별로 한도를 차등 설정했는가 (로그인 엄격, 조회 느슨)
[ ] X-Forwarded-For 신뢰 대역(proxy-real-ip-cidr)을 정확히 지정했는가
[ ] 거부 응답을 429 + Retry-After로 설정했는가
[ ] 헬스체크/모니터링/내부 대역을 whitelist에 넣었는가
[ ] per-pod vs Redis 카운팅 방식을 의도대로 선택했는가
[ ] 레플리카 수 변동(HPA)이 전역 한도에 미치는 영향을 고려했는가
[ ] L3/L4 볼류메트릭 방어를 클라우드 엣지에 배치했는가
[ ] slowloris 대비 타임아웃을 짧게 설정했는가
[ ] 429/503 비율과 백엔드 레이턴시를 대시보드로 관측 중인가
[ ] 부하 테스트로 한도 동작을 사전 검증했는가
[ ] Gateway API 마이그레이션 경로를 검토했는가
마치며
레이트 리미팅과 DDoS 완화의 핵심은 계층별로 적절한 위치에서 적절한 공격을 막는 것입니다. 볼류메트릭 공격은 클라우드 엣지에서, 애플리케이션 계층 남용은 Ingress에서, 비즈니스 규칙 위반은 애플리케이션에서 막아야 합니다. 어느 한 계층에 모든 책임을 몰면 반드시 무너집니다.
분산 환경의 카운팅 문제는 특히 조용히 사람을 속입니다. per-pod 메모리 카운팅을 전역 한도로 착각하면, 설정한 값의 몇 배까지 트래픽이 새어 나갑니다. 정확성이 중요하다면 중앙 카운터를 도입하되, Redis 장애 시의 fail-open/fail-closed 정책을 명확히 정해 두어야 합니다.
마지막으로, 2026년의 방향성은 분명합니다. Ingress API는 동결되었고 ingress-nginx는 유지보수 모드에 들어갔으며, Gateway API가 후속 표준으로 자리 잡고 있습니다. 지금 운영 중인 ingress-nginx 설정을 잘 다듬되, 신규 설계에서는 Gateway API 기반 구현체와 그 레이트 리미팅 모델을 적극적으로 검토하시길 권합니다.
참고 자료
- Kubernetes Ingress 개념: https://kubernetes.io/docs/concepts/services-networking/ingress/
- Gateway API 공식 문서: https://gateway-api.sigs.k8s.io/
- ingress-nginx 어노테이션 레퍼런스: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/annotations/
- ingress-nginx 레이트 리미팅 가이드: https://kubernetes.github.io/ingress-nginx/examples/customization/custom-configuration/
- Traefik Rate Limit 미들웨어: https://doc.traefik.io/traefik/middlewares/http/ratelimit/
- Kong Rate Limiting 플러그인: https://docs.konghq.com/hub/kong-inc/rate-limiting/
- Apache APISIX limit-count 플러그인: https://apisix.apache.org/docs/apisix/plugins/limit-count/
- AWS Shield 문서: https://docs.aws.amazon.com/waf/latest/developerguide/shield-chapter.html
- Google Cloud Armor 문서: https://cloud.google.com/armor/docs/security-policy-overview
- nginx limit_req 모듈: https://nginx.org/en/docs/http/ngx_http_limit_req_module.html