728x90

1. 지금 로그에서 실제로 벌어지는 일

Hacker News에 올라온 "A year of fighting scrapers on my 1.5 million-page website" 글의 제목은 사실 부제가 더 정확하다. 내 사이트 트래픽의 99%가 봇. 처음 이 문장을 보면 과장 같지만, 페이지 수가 많고 SEO로 롱테일 유입이 잡히는 사이트를 운영해본 사람은 대부분 고개를 끄덕인다.

내가 겪은 패턴은 이랬다. DB 사이즈 대비 페이지가 많은(카테고리 × 필터 × 페이지네이션 조합으로 URL이 기하급수로 늘어나는) 서비스에서, 어느 날부터 origin CPU가 평소의 3배로 붙어 있었다. APM에는 특정 느린 쿼리가 하나 튀는 것도 아니고, 그냥 전체적으로 골고루 요청이 늘어났다. 캐시 히트율은 오히려 떨어졌다. 이게 핵심 신호다.

사람 트래픽은 인기 페이지에 몰린다. 상위 1% URL이 트래픽의 대부분을 먹는다. 그래서 CDN 캐시가 먹힌다. 반면 스크래퍼는 사이트맵을 따라 전 페이지를 균등하게 순회한다. 150만 페이지를 다 긁으면 캐시 히트율이 바닥을 치고, 모든 요청이 origin으로 내려간다. "트래픽은 2배인데 DB 부하는 8배"가 되는 이유가 여기 있다.

가장 먼저 할 일은 남 얘기 그만 듣고 자기 로그를 세는 것이다.

# Nginx access log에서 UA별 요청 수 Top 10
awk -F'"' '{print $6}' /var/log/nginx/access.log \
  | sed 's/.*\(GPTBot\|ClaudeBot\|Bytespider\|Amazonbot\|bingbot\|Googlebot\).*/\1/' \
  | sort | uniq -c | sort -rn | head

  482119 Bytespider
  203847 GPTBot
   91204 ClaudeBot
   77650 Amazonbot
   41220 bingbot
   38904 Googlebot

여기서 이름을 대놓고 밝히는 놈들은 그래도 양심적인 편이다. 진짜 문제는 아래 명령에서 드러난다. UA가 평범한 Chrome인데 한 IP에서 수만 건이 나오는 경우.

# /24 단위로 묶어서 요청 수 집계 (분산 스크래퍼 탐지)
awk '{print $1}' /var/log/nginx/access.log \
  | cut -d. -f1-3 | sort | uniq -c | sort -rn | head -5

   58210 47.128.23
   41773 8.219.104
   19022 143.198.77
    9871 172.176.9
    3204 121.134.55

앞의 두 개는 클라우드 사업자 대역이었고, UA는 전부 최신 Chrome이었다. 사람이 클라우드 IP에서 크롬으로 5만 페이지를 볼 일은 없다.

2. robots.txt와 User-Agent 차단이 무너지는 지점

robots.txt는 규약이지 강제가 아니다. 이건 원래 그렇게 설계된 것이다. RFC 9309(Robots Exclusion Protocol)도 준수를 "권고"한다. 즉 robots.txt는 "예의를 아는 크롤러에게 부하를 줄여달라고 부탁하는 문서"이고, 예의 없는 쪽에는 어디를 긁으면 알찬지 알려주는 지도가 된다.

그래도 넣어라. 공개적으로 선언된 AI 크롤러 중 일부는 실제로 준수한다고 명시하고 있다(OpenAI GPTBot, Anthropic ClaudeBot 등이 공식 문서에서 robots.txt 준수를 밝힌다 — 정확한 UA 문자열과 IP 대역은 각 사 공식 문서 확인 필요). 법적/커뮤니케이션 측면에서도 "명시적으로 거부했다"는 기록이 남는다.

# robots.txt — 선언은 하되, 이걸로 끝났다고 믿지 말 것
User-agent: GPTBot
Disallow: /

User-agent: ClaudeBot
Disallow: /

User-agent: Bytespider
Disallow: /

User-agent: *
Crawl-delay: 10
Disallow: /search
Disallow: /*?sort=
Disallow: /*?page=

UA 차단이 무너지는 과정은 거의 정해진 각본이다.

  1. Bytespider UA를 차단한다 → 트래픽 뚝 떨어진다 → 뿌듯하다.
  2. 2주 뒤 같은 ASN에서 UA가 Mozilla/5.0 ... Chrome/121.0.0.0 Safari/537.36으로 바뀐 트래픽이 같은 패턴으로 들어온다.
  3. UA로는 이제 구분이 안 된다. 정상 유저와 문자열이 완전히 동일하다.
  4. IP를 차단한다 → 다음 주엔 주거용 프록시(residential proxy) 대역에서 IP 하나당 20~30건씩 쪼개서 들어온다.

이 지점에서 발상을 바꿔야 한다. "누구냐"로 막는 게임에서 "어떻게 행동하냐"로 막는 게임으로. UA는 클라이언트가 자기 입으로 하는 자기소개다. 거짓말하는 데 비용이 0원이다. 반면 행동 패턴은 스크래퍼의 목적(전 페이지 수집) 자체에서 나오기 때문에 위조하려면 수집 효율을 포기해야 한다. 방어자는 이 비대칭을 이용해야 한다.

3. 탐지·완화 계층 설계와 각 방식의 성격

탐지 신호를 겹쳐 쌓기

단일 신호로 판정하면 무조건 오탐이 난다. 점수를 합산하는 구조로 가야 한다. 내가 쓰는 신호는 대략 이렇다.

  • ASN / IP 대역: 클라우드 사업자(AWS, GCP, Azure, Alibaba, Hetzner, DigitalOcean 등) 대역에서 오는 브라우저 UA 트래픽은 강한 의심 신호. 단, 국내 서비스라면 기업 프록시·모바일 캐리어 NAT·앱 내 웹뷰 크롤링 때문에 무조건 차단은 위험하다.
  • 요청 분포의 균등성: 세션 내 URL이 서로 링크로 연결되지 않은 곳을 무작위로 훑고 있으면 봇. 사람은 목록 → 상세 → 뒤로 → 상세 패턴을 그린다.
  • 부수 리소스 미요청: HTML만 받고 CSS/JS/이미지/폰트를 전혀 안 받는 클라이언트. 브라우저를 자칭하면서 이러면 거짓말이다.
  • 헤더 지문: Accept-Language 부재, Accept*/*, Sec-Fetch-* 헤더 없음, HTTP/1.1 고정(최신 크롬은 h2/h3). TLS JA3/JA4 지문이 UA와 불일치하는 경우도 강한 신호다.
  • 요청 간격의 규칙성: 표준편차가 지나치게 작은 요청 간격. 사람은 이렇게 못 한다.

완화 수단 비교

수단막는 대상비용한계
Rate Limit (IP/세션)단일 IP 고속 크롤러매우 낮음분산 프록시에 무력
캐시 계층 강화부하만 (트래픽은 그대로)낮음, 효과 확실롱테일 페이지 캐시 히트 어려움
PoW 챌린지 (Anubis류)JS 미실행 대량 스크래퍼중간 (UX·SEO 영향)헤드리스 브라우저는 통과
타르핏 / 지연 응답수집 효율 자체커넥션 점유 주의내 리소스도 붙잡힘
CDN Bot Management광범위블랙박스, 오탐 시 디버깅 난이도

여기서 PoW 챌린지가 최근 오픈소스 진영에서 많이 언급된다. Anubis가 대표적이고, GNOME·kernel.org 같은 곳에서 도입했다는 이야기가 커뮤니티에 널리 돌았다(도입 현황과 현재 버전은 프로젝트 공식 문서 확인 필요). 원리는 단순하다.

요청이 오면 HTML 대신 작은 JS를 내려준다. 클라이언트는 조건을 만족하는 해시를 찾을 때까지 브루트포스를 돌린다. 답을 맞히면 쿠키를 발급하고 통과시킨다.

비유하면 입장 전에 팔굽혀펴기 20회다. 사람 한 명에겐 몇백 밀리초 짜증이지만, 150만 페이지를 긁으려는 쪽에는 150만 번의 팔굽혀펴기다. 스크래퍼의 경제성을 깨는 게 목적이지, 완벽히 막는 게 목적이 아니다. 실제로 헤드리스 크롬을 붙인 상대는 PoW를 그냥 푼다. 다만 헤드리스 브라우저는 curl보다 수십 배 비싸므로, 그 자체가 방어다.

Nginx에서 당장 적용 가능한 형태

http {
  # 봇성 신호에 따라 존을 분리
  map $http_user_agent $bot_ua {
    default            0;
    ~*(GPTBot|ClaudeBot|Bytespider|Amazonbot|CCBot)  1;
  }
  map $bot_ua $limit_key {
    1  $binary_remote_addr;   # 자칭 봇: 강하게
    0  "";                    # 나머지는 아래 존에서 처리
  }

  limit_req_zone $limit_key            zone=botzone:10m rate=6r/m;
  limit_req_zone $binary_remote_addr   zone=perip:10m   rate=20r/s;

  server {
    location / {
      limit_req zone=botzone burst=3 nodelay;
      limit_req zone=perip  burst=40;
      limit_req_status 429;
      proxy_pass http://app;
    }
  }
}

중요한 건 limit_req_status 429다. 기본값은 503인데, 503은 "서버 장애"로 해석돼서 정상 검색엔진 크롤러가 색인을 빼버릴 수 있다. 429는 "속도만 줄여라"라는 뜻이라 훨씬 안전하다. 그리고 CDN 뒤에 있다면 $binary_remote_addr는 CDN IP이므로 real_ip_headerset_real_ip_from 설정이 먼저다. 이거 안 하면 전 세계 트래픽을 CDN 엣지 IP 몇 개로 묶어서 레이트리밋하게 되고, 서비스가 통째로 429를 뱉는다.

4. 실무 관점: 트레이드오프와 흔한 함정

함정 1: real_ip 설정 누락으로 전체 429

가장 자주 보는 사고다. 로그에 이런 게 도배된다.

2024/03/11 14:22:07 [error] 1274#1274: *88213 limiting requests,
excess: 40.812 by zone "perip", client: 172.68.34.9,
server: example.com, request: "GET /product/8812 HTTP/2.0",
host: "example.com"

client:가 실제 사용자 IP가 아니라 CDN 엣지 IP(위 예시는 Cloudflare 대역 형태)로 찍히면 100% 이 문제다. 해결은 아래처럼.

set_real_ip_from 173.245.48.0/20;   # CDN 대역 전체를 넣어야 함
# ... (사업자 공식 IP 목록을 스크립트로 갱신)
real_ip_header CF-Connecting-IP;    # 또는 X-Forwarded-For
real_ip_recursive on;

CDN IP 목록은 바뀌므로 하드코딩하고 방치하면 안 된다. 사업자별 공식 IP 목록 엔드포인트를 크론으로 받아 include 파일로 떨구는 게 정석이다.

함정 2: PoW 챌린지가 SEO와 접근성을 때린다

Anubis류를 전 경로에 걸면 Googlebot·네이버 Yeti도 챌린지를 만난다. JS를 실행하는 크롤러도 있지만 렌더 예산이 있어서 색인 속도가 떨어질 수 있고, 실행하지 못하는 쪽은 그냥 페이지를 못 본다. 실무 처방은:

  • 검증된 검색엔진은 역방향 DNS + 정방향 확인으로 화이트리스트. UA만 믿으면 안 된다.
  • 챌린지는 비싼 경로에만. 검색·필터·정렬 파라미터가 붙은 URL, API 엔드포인트, 페이지네이션 깊은 곳.
  • 정적 상세 페이지는 캐시로 버티고 챌린지 없이 둔다.
  • JS 미지원 클라이언트(스크린리더 환경, 구형 브라우저)를 위한 우회 경로를 반드시 남긴다.
# 자칭 Googlebot 검증 (rDNS 왕복 확인)
$ dig +short -x 66.249.66.1
crawl-66-249-66-1.googlebot.com.
$ dig +short crawl-66-249-66-1.googlebot.com
66.249.66.1        # 일치하면 진짜

$ dig +short -x 47.128.23.77
ec2-47-128-23-77.ap-southeast-1.compute.amazonaws.com.
# UA는 Googlebot인데 EC2 → 위조 확정

함정 3: ASN 통째 차단이 정상 사용자를 자른다

"AWS 대역 전부 차단"은 시원하지만 위험하다. 국내 환경에서 실제로 걸리는 것들: 카카오톡·라인 링크 미리보기 봇, 슬랙 unfurl, 결제 PG사 서버 콜백, 파트너사 API 클라이언트, 기업 VPN, 그리고 자사 모니터링/합성 모니터링. Synthetic 체크가 죽으면서 "장애 알림이 안 왔다"가 아니라 "알림이 계속 온다"로 나타나기도 한다.

그래서 순서를 지켜야 한다. 탐지만 켜고 로그 기록(dry-run) → 며칠 관찰 → 챌린지/지연 → 마지막에 차단. 처음부터 403을 쏘는 건 자기 발등 찍기다.

함정 4: 타르핏은 상대만 붙잡는 게 아니다

일부러 응답을 30초 지연시키거나 무한 스트림을 내려주는 타르핏은 심리적 만족도가 높다. 하지만 그 커넥션은 내 워커/파일디스크립터도 점유한다. Nginx처럼 이벤트 기반이면 그나마 싸지만, 그 뒤에 스레드 기반 앱 서버가 있다면 자기 자신을 DoS 하게 된다. 타르핏은 반드시 엣지에서, 커넥션 상한을 걸고 쓴다.

실제 효과가 가장 컸던 것: 캐시와 URL 설계

솔직히 말하면, 화려한 챌린지보다 이게 효과가 컸다. 150만 페이지의 정체는 대개 파라미터 조합 폭발이다.

  • 정렬·필터 파라미터 조합 URL에 rel="canonical"을 걸고 robots에서 차단
  • 파라미터를 화이트리스트 방식으로 정규화 → 캐시 키 수를 한 자릿수로 줄임
  • 깊은 페이지네이션(예: page=500 이상)은 아예 404 또는 챌린지
  • 익명 사용자에겐 무조건 CDN 캐시 응답. 로그인 쿠키가 있을 때만 origin

스크래퍼가 여전히 오더라도 전부 엣지 캐시에서 끝나면 origin은 조용하다. 트래픽 비용은 남지만 DB는 산다. 트래픽 비용까지 잡으려면 그때 차단 단계로 간다.

5. 정리

한 줄 요약: UA와 robots.txt로 "누구냐"를 묻는 방어는 이미 끝났다. 행동 패턴으로 점수를 매기고, 캐시로 부하를 흡수하고, PoW·레이트리밋으로 상대의 수집 단가를 올리는 다층 방어로 가야 한다.

단계별로 이렇게 권한다.

  • 페이지 수천 개 이하, 부하 여유 있음 → robots.txt 선언 + 자칭 봇 UA 레이트리밋(429)까지. 그 이상은 과투자다.
  • 페이지 수만~수십만, origin CPU가 튀기 시작 → 캐시 키 정규화와 파라미터 통제가 1순위. 차단보다 먼저.
  • 페이지 수십만 이상 + 트래픽 비용이 아프다 → ASN 스코어링, JA3/JA4 지문, CDN Bot Management 검토. 여기서 오탐 관리 프로세스(dry-run → 챌린지 → 차단)를 반드시 문서화.
  • 커뮤니티/위키/문서 사이트, 익명 읽기 트래픽이 핵심 → Anubis류 PoW 챌린지가 가장 가성비 좋은 구간으로 보인다. 단 SEO 경로 예외 처리가 전제.

마지막으로 하나. 이건 끝나는 싸움이 아니다. 원문 제목이 "1년간의 싸움"인 이유가 그거다. 차단 규칙은 만드는 순간부터 낡기 시작하니, 규칙 자체보다 탐지 지표를 대시보드에 상시 띄워두는 것이 더 중요하다. 나는 "캐시 히트율", "origin 요청 중 부수 리소스 미요청 비율", "클라우드 ASN 비중" 세 개를 그래프로 보고 있다. 이 셋 중 하나가 꺾이면 새 스크래퍼가 들어온 것이다.

참고 자료

728x90
728x90

1. 왜 지금 이 얘기가 나오는가

2026년 3월, Kubernetes SIG Network가 관리하던 ingress-nginx 컨트롤러가 공식 은퇴했다. 여기서 오해하면 안 되는 게 하나 있다. Ingress API 자체가 없어지는 게 아니다. Ingress 리소스(networking.k8s.io/v1)는 여전히 살아있고 잘 동작한다. 은퇴하는 건 커뮤니티가 유지보수하던 ingress-nginx 컨트롤러 구현체다.

이게 왜 실무에서 큰 문제냐면, 국내 대부분의 온프렘·EKS·GKE 클러스터가 사실상 이 컨트롤러 하나로 트래픽을 다 받고 있기 때문이다. Helm chart 한 번 깔고 nginx.ingress.kubernetes.io/* 어노테이션으로 리라이트, 타임아웃, 인증서 설정 다 해온 팀이 압도적으로 많다. 은퇴 이후 상황을 원문은 이렇게 정리한다:

  • CVE 미패치 — 새 취약점이 터져도 공식 패치가 안 나온다. 인그레스는 클러스터 진입점이라 이게 제일 무섭다.
  • 기능 업데이트 중단 — 새 Kubernetes 버전과의 호환성 검증도 더 이상 보장 안 된다.
  • 커뮤니티 지원 종료 — 이슈 올려도 답이 안 온다.

당장 클러스터가 죽는 건 아니다. 하지만 "동작하니까 놔둔다"는 시한폭탄이다. 특히 금융·공공처럼 보안 감사받는 환경이면 미패치 CVE가 감사 지적사항으로 바로 걸린다.

2. 먼저 우리 클러스터 영향 범위부터 파악하자

대안을 고르기 전에, 지금 뭘 얼마나 쓰고 있는지 진단부터 해야 한다. 마이그레이션 난이도는 순전히 proprietary 어노테이션을 얼마나 썼느냐에 달려있다.

먼저 클러스터에서 nginx 어노테이션을 쓰는 인그레스가 몇 개나 되는지 훑어보자.

kubectl get ingress -A -o json \
  | jq -r '.items[]
    | select(.metadata.annotations != null)
    | .metadata.namespace + "/" + .metadata.name + " -> " +
      (.metadata.annotations | keys | map(select(startswith("nginx.ingress.kubernetes.io"))) | join(", "))'

실제 출력은 이런 식으로 나온다:

default/web-app -> nginx.ingress.kubernetes.io/rewrite-target, nginx.ingress.kubernetes.io/ssl-redirect
payment/api-gw -> nginx.ingress.kubernetes.io/proxy-body-size, nginx.ingress.kubernetes.io/proxy-read-timeout, nginx.ingress.kubernetes.io/configuration-snippet
monitoring/grafana -> nginx.ingress.kubernetes.io/auth-type, nginx.ingress.kubernetes.io/auth-secret

여기서 configuration-snippet이나 server-snippet이 보이면 긴장해야 한다. 이건 raw nginx 설정을 그대로 박아넣은 거라서, 어떤 대안 컨트롤러로 가든 1:1 자동 변환이 안 된다. 손으로 다시 짜야 한다. 이게 마이그레이션에서 제일 시간 잡아먹는 부분이다.

어노테이션 사용 빈도를 카운트해서 우선순위를 매기면 계획 세우기 편하다:

kubectl get ingress -A -o json \
  | jq -r '.items[].metadata.annotations // {} | keys[]' \
  | grep '^nginx.ingress.kubernetes.io' \
  | sort | uniq -c | sort -rn
     14 nginx.ingress.kubernetes.io/ssl-redirect
     11 nginx.ingress.kubernetes.io/rewrite-target
      8 nginx.ingress.kubernetes.io/proxy-body-size
      3 nginx.ingress.kubernetes.io/configuration-snippet
      1 nginx.ingress.kubernetes.io/auth-url

이 표만 봐도 "snippet 3개, auth 1개만 손보면 나머지는 표준 매핑으로 넘어가겠구나"가 보인다.

3. 대안 컨트롤러 비교와 두 갈래 길

원문은 큰 틀에서 두 가지 경로를 제시한다. 여기에 실무에서 자주 후보로 오르는 Traefik, Nginx Ingress Operator까지 얹어서 정리한다.

Path A: Lift-and-Shift — Ingress API 유지하고 컨트롤러만 교체

기존 Ingress YAML은 그대로 두고, ingressClass만 다른 Envoy 기반 컨트롤러(원문에서는 Contour를 예로 듦)로 바꾸는 방식이다. 라우팅 정의를 갈아엎지 않으니 당장의 충격이 적다.

단, 원문이 명확히 짚는 함정이 있다. base Ingress 리소스는 그대로여도, nginx.ingress.kubernetes.io/* 어노테이션은 전부 무효가 된다. Contour는 이걸 자기 어노테이션이나 CRD(HTTPProxy)로 다시 만들어야 한다. Traefik으로 가도 마찬가지로 traefik.ingress.kubernetes.io/* 체계로 갈아타야 한다.

Path B: Gateway API로 아키텍처 현대화

Gateway API는 원문 표현으로 "Ingress의 upstream-backed 후계자"다. 핵심은 역할 분리(role-oriented design)다.

비유하자면 이렇다. 기존 Ingress는 인프라팀이 건물 전체 설계도(모놀리식 정의)를 혼자 들고 있는 구조였다. Gateway API는 이걸 나눈다:

  • 인프라팀Gateway 리소스로 "이 건물의 출입구(리스너, 포트, 인증서)"를 관리하고
  • 개발팀HTTPRoute로 "내 사무실로 가는 복도(경로 라우팅)"를 각자 관리한다.

원문에 따르면 Gateway API는 traffic splitting, 고급 header matching, 안전한 cross-namespace 라우팅을 코어 스펙에 내장하고 있다. ingress-nginx 시절 어노테이션 지옥으로 풀던 걸 표준 API로 처리한다는 게 가장 큰 차이다.

비교표 (원문 기준 + 실무 코멘트)

항목 Path A: Contour/Traefik (Ingress API) Path B: Gateway API
마이그레이션 공수 낮음~중간 (어노테이션 번역) 높음 (라우팅 매니페스트 전면 재작성)
운영 패러다임 단일 소유자 — Ops가 모놀리식 정의 관리 역할 기반 — Ops는 Gateway, Dev는 HTTPRoute
미래 대응성 낮음 — Ingress API는 feature-frozen 높음 — upstream 활발히 개발 중
기능 proprietary 어노테이션에 크게 의존 고급 기능이 코어 스펙에 내장

실무 코멘트: Nginx Ingress Operator나 NGINX가 상업적으로 관리하는 별도 컨트롤러 라인도 존재하는데, 이건 커뮤니티판 ingress-nginx와는 별개 프로젝트다. 다만 라이선스·지원 정책이 다를 수 있으니 도입 전 반드시 공식 문서 확인이 필요하다. 원문은 이 부분을 상세히 다루지 않으니 여기서 단정하지 않겠다.

4. 마이그레이션 아키텍처 설계 전략

원문이 제시한 마이그레이션 전략은 3단계다. 실무 관점 살을 붙였다.

① Audit (감사)

위 2번 섹션에서 뽑은 어노테이션 인벤토리가 곧 기술부채 목록이다. configuration-snippet, server-snippet, auth-url(외부 인증) 같은 건 자동 변환이 안 되니 별도 트랙으로 뺀다.

② Tooling — ingress2gateway

Gateway API로 갈 거라면 ingress2gateway로 기존 Ingress를 자동 변환할 수 있다. 완벽하진 않지만 표준 필드는 잘 옮겨준다.

ingress2gateway print --input-file=web-app-ingress.yaml
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: web-app-gateway
  namespace: default
spec:
  gatewayClassName: contour
  listeners:
  - name: http
    port: 80
    protocol: HTTP
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: web-app
  namespace: default
spec:
  parentRefs:
  - name: web-app-gateway
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /
    backendRefs:
    - name: web-app-svc
      port: 80

주의할 건, rewrite-target이나 커스텀 snippet은 이 도구가 변환 못 하고 조용히 빠뜨리는 경우가 있다. 변환 결과를 그대로 믿지 말고 반드시 diff 떠서 검증해야 한다.

③ Incremental Rollout (점진적 전환)

원문의 핵심 권고다. 새 컨트롤러를 기존 것과 병렬로 띄우고, 비핵심 워크로드부터 옮긴다. ingressClass를 분리하면 두 컨트롤러가 한 클러스터에 공존할 수 있다. 결제 API 같은 건 맨 마지막에 옮긴다.

5. 흔한 함정과 실제 에러들

함정 1: ingressClassName 안 걸어서 아무도 안 받음

새 컨트롤러 깔았는데 트래픽이 안 온다. Ingress 오브젝트에 ingressClassName이 비어있거나 옛날 값이면 새 컨트롤러가 무시한다. 병렬 운영 중엔 특히 자주 터진다. 확인:

kubectl get ingress -A -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,CLASS:.spec.ingressClassName

함정 2: snippet 어노테이션이 그냥 씹힌다

Contour나 Traefik으로 옮긴 뒤 로그를 보면 이런 게 뜬다:

Warning  IngressUpdate  ingress default/web-app: annotation "nginx.ingress.kubernetes.io/configuration-snippet" is not supported by this controller and will be ignored

에러가 아니라 warning이라 CI/CD에서 그냥 통과되고, 배포는 성공한 것처럼 보인다. 그런데 실제로는 rate limit이나 커스텀 헤더 설정이 통째로 날아간 상태다. 프로덕션에서 "왜 갑자기 헤더가 안 붙지?" 하고 새벽에 깨는 전형적인 케이스다.

함정 3: Gateway API CRD 안 깔고 HTTPRoute 적용

Gateway API는 CRD를 별도로 설치해야 한다. 안 깔고 매니페스트 적용하면:

error: resource mapping not found for name: "web-app" namespace: "default" from "httproute.yaml":
no matches for kind "HTTPRoute" in version "gateway.networking.k8s.io/v1"
ensure CRDs are installed first

CRD부터 설치해야 한다:

kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.0/standard-install.yaml

(버전 번호는 예시다. 실제 최신 버전은 Gateway API 릴리스 페이지에서 확인 필요.)

함정 4: cross-namespace 참조가 막혀있다

Gateway API의 장점인 cross-namespace 라우팅은 기본적으로 차단돼 있다. 다른 네임스페이스의 Gateway를 HTTPRoute가 참조하려면 ReferenceGrant를 명시적으로 만들어야 한다. 이걸 몰라서 "HTTPRoute는 Accepted인데 트래픽이 안 온다"고 헤매는 경우가 많다. 상태를 보면 힌트가 나온다:

kubectl describe httproute web-app -n default
Status:
  Parents:
    Conditions:
      Type:     ResolvedRefs
      Status:   False
      Reason:   RefNotPermitted
      Message:  Cross-namespace reference to Gateway is not allowed; create a ReferenceGrant

6. 정리: 누가 어느 길로 가야 하나

한 줄 요약: ingress-nginx는 은퇴했지만 Ingress API는 살아있다. 급하면 Contour/Traefik으로 옆걸음(Path A), 여유 있고 플랫폼 현대화 계획이 있으면 Gateway API로 갈아타라(Path B).

  • 시간 없고 리팩터 인력 부족 → Path A (Contour/Traefik). 원문 표현대로 "stopgap", 즉 임시방편이다. Ingress API는 feature-frozen이라 언젠가는 또 옮겨야 한다. 하지만 미패치 CVE 리스크를 당장 없애면서 계획 세울 시간을 번다.
  • 이미 플랫폼 현대화 중이거나 멀티팀 운영 → Path B (Gateway API). 공수는 크지만 역할 분리와 upstream 지원이라는 장기적 가치가 크다. 한 번 제대로 옮기면 어노테이션 지옥에서 벗어난다.

어느 쪽이든 공통 원칙은 같다. 감사 → 병렬 배포 → 비핵심부터 점진 전환. 결제·인증 게이트웨이는 절대 첫 타자로 옮기지 말 것. 그리고 전환 후엔 각 컨트롤러의 4xx/5xx 비율, 응답 지연을 옛날 nginx 지표와 나란히 놓고 최소 며칠은 비교 모니터링하자. snippet으로 걸어뒀던 숨은 설정이 빠졌는지는 트래픽을 태워봐야 드러난다.

참고 자료

※ 버전 번호와 명령어 출력은 예시이며, 실제 적용 전 각 프로젝트 공식 릴리스 문서에서 최신 버전과 호환성을 반드시 확인하세요.

728x90

+ Recent posts