RSS

Cloudflare Tunnel로 Vercel을 연결하면 안 되는 이유

Vercel 배포는 성공했습니다. 프로젝트명.vercel.app 으로 접속하면 잘 뜹니다.
그런데 제 도메인 shop.example.co.kr 을 연결했더니 이런 화면이 나왔습니다.

404: NOT_FOUND
Code: DEPLOYMENT_NOT_FOUND
ID: icn1::g2bbd-1785287214941-916f5febf79b

"배포를 찾을 수 없다"는데, 배포는 분명히 살아 있었습니다.

먼저 응답 헤더를 봤습니다

에러 화면만 보고 추측하는 대신 헤더를 확인했습니다. 여기에 답이 있는 경우가 많습니다.

$ curl -sI https://shop.example.co.kr

HTTP/2 404
server: cloudflare
x-vercel-error: DEPLOYMENT_NOT_FOUND
x-vercel-id: icn1::lsjc2-1785287295622-a97d4479f9a9
cf-ray: a2283b3b5c79a102-LAX

이 두 줄이 핵심이었습니다.

  • x-vercel-id: icn1::... → 요청이 Vercel 서울 리전까지 도달했습니다
  • x-vercel-error: DEPLOYMENT_NOT_FOUND → 도달했는데 매칭에 실패했습니다

즉 DNS도 정상이고 네트워크 경로도 정상입니다. Vercel이 요청을 받긴 받았는데,
"이 요청을 어느 프로젝트에 줘야 할지 모르겠다"고 답한 상황이었습니다.

Vercel은 오직 Host 헤더로 프로젝트를 찾습니다. 여기서부터 원인이 좁혀졌습니다.

원인: Cloudflare Tunnel이 Host를 그대로 넘깁니다

제가 한 설정은 이랬습니다. 공개망 밖의 개발·검증 환경을 연결할 때 쓰던 Cloudflare Tunnel에
public hostname을 하나 더 추가하고, origin service를 https://프로젝트명.vercel.app 으로 지정한 것입니다.

익숙한 도구라 무심코 같은 방식을 썼는데, 트래픽이 이렇게 흘렀습니다.

브라우저 → Cloudflare → 비공개 환경의 터널 커넥터 → Vercel
                                  Host: shop.example.co.kr  ← 그대로 전달

Cloudflare Tunnel은 origin으로 요청을 넘길 때 원래 Host 헤더를 보존합니다.
그래서 Vercel은 Host: shop.example.co.kr 을 받았고, 그 호스트명을 등록한 프로젝트가 없으니
DEPLOYMENT_NOT_FOUND 를 반환한 것입니다.

사실 더 근본적인 문제가 있었습니다

Host 헤더는 표면적인 원인이고, 진짜 문제는 도구를 잘못 골랐다는 것이었습니다.

Cloudflare Tunnel은 공개망에서 접근할 수 없는 것을 노출하기 위한 도구입니다.
접근이 제한된 개발·검증 서버나 로컬 개발 서버처럼요. 고정 IP가 없거나 포트를 열 수 없는
환경에서 커넥터가 바깥으로 연결을 맺어 우회하는 방식입니다.

그런데 Vercel은 이미 공개 인터넷에 있습니다. 노출할 필요가 없습니다.

제 설정대로라면 트래픽이 이렇게 돕니다.

사용자 → Cloudflare 엣지 → 터널 커넥터 → 다시 인터넷 → Vercel 엣지

불필요한 우회가 두 번 들어가고, 중간 터널 커넥터가 중단되면 Vercel에 올린 사이트가 죽습니다.
전 세계 엣지에 배포한 의미가 사라지는 구조였습니다.

터널의 HTTP Host Header 옵션을 프로젝트명.vercel.app 으로 덮어쓰면 404 자체는 사라집니다.
하지만 그건 증상만 가리는 것이고, 위의 우회 경로는 그대로 남습니다.

올바른 방법

1. 터널 라우트를 삭제합니다

Cloudflare Zero Trust에서 해당 public hostname 라우트를 지웁니다.
터널이 자동 생성한 DNS 레코드도 함께 정리합니다.

2. 평범한 CNAME을 만듭니다

Type Name Target Proxy status
CNAME shop cname.vercel-dns.com DNS only (회색 구름)

3. Vercel 프로젝트에 도메인을 등록합니다

프로젝트 → Settings → Domains → Add Existing → 도메인 입력 → Production 연결

DNS가 맞게 설정돼 있으면 몇 초 안에 Valid Configuration 으로 바뀌고,
인증서도 자동으로 발급됩니다.

저는 이렇게 바꾼 뒤 바로 정상 동작했습니다.

$ curl -sI https://shop.example.co.kr | grep -E 'HTTP|server|x-vercel-id'
HTTP/2 200
server: Vercel
x-vercel-id: icn1::iad1::7s9w9-...

servercloudflare 에서 Vercel 로 바뀐 것이 보이실 겁니다.
이제 Cloudflare를 거치지 않고 Vercel 엣지로 직접 갑니다.

"프록시(주황 구름)는 켜는 게 좋지 않나요?"

저도 처음엔 켜두는 게 안전하다고 생각했습니다. 하지만 이 경우는 끄는 쪽이 맞습니다.

Vercel 자체가 이미 CDN이기 때문입니다. 서울을 포함한 전 세계 엣지에서 캐싱과
TLS 종료를 처리합니다. 여기에 Cloudflare 프록시를 얹으면 CDN을 두 겹으로 쌓는 셈인데,
얻는 것보다 관리 포인트가 늘어납니다.

  • 인증서 갱신 실패 가능성 - Vercel이 도메인 소유를 주기적으로 재확인하는데
    프록시가 앞을 가리면 검증이 실패할 수 있습니다
  • SSL 모드 사고 - Cloudflare가 Flexible 이면 무한 리다이렉트에 빠집니다.
    Full (strict) 로 맞춰야 하는데, 굳이 신경 쓸 일을 만들 이유가 없습니다
  • 캐시 정책 충돌 - Vercel이 내려보내는 캐시 헤더를 Cloudflare가 한 번 더 해석합니다

WAF나 봇 차단이 필요하시면 Cloudflare 대신 Vercel Firewall 을 쓰시는 편이 낫습니다.
같은 층위에서 처리하니 이중 프록시 문제가 생기지 않습니다.

정리하면: 언제 무엇을 쓰나

상황 방식 이유
공개망 밖의 개발·검증 환경 Cloudflare Tunnel 외부에서 직접 접근할 수 없으므로 커넥터로 연결
자체 서버에 고정 IP가 있음 A 레코드 + 프록시 ON 오리진 IP를 가리고 WAF를 얹음
Vercel, Netlify 등 관리형 호스팅 CNAME + DNS only 이미 공개 CDN 위에 있음. 앞에 뭘 더 둘 필요 없음

기존 설정에는 공개망 밖의 개발·검증 환경을 연결하는 Tunnel + Proxied 레코드가 있었습니다.
그 레코드에는 목적에 맞는 구성이지만, Vercel에 배포된 shop 은 성격이 달랐습니다.
익숙한 방식을 그대로 적용하다 보니 놓쳤던 것입니다.

남는 교훈

두 가지를 얻었습니다.

첫째, 에러 헤더를 먼저 읽습니다.
x-vercel-errorx-vercel-id 두 줄이 "어디까지 갔고 무엇이 실패했는지"를
정확히 알려줬습니다. 화면의 404만 보고 DNS 설정을 뒤졌다면 한참 헤맸을 것입니다.
플랫폼이 제공하는 진단 헤더는 대부분 문서화돼 있으니 한 번 확인해두시면 좋습니다.

둘째, 익숙한 도구를 습관으로 쓰지 않습니다.
Cloudflare Tunnel은 좋은 도구입니다. 다만 "공개망에 없는 것을 노출한다"는
목적이 분명한 도구입니다. 이미 공개망에 있는 대상에 쓰면 목적과 어긋납니다.

동작하게 만드는 방법(Host 헤더 덮어쓰기)이 있다고 해서 그게 맞는 방법인 것은 아닙니다.
설정 한 줄로 404는 사라졌겠지만, 중간 터널 커넥터가 중단되면 사이트가 죽는 구조는 그대로였을 테니까요.


관련 글

온프레미스로 돌던 서비스를 Vercel + Supabase 로 옮기고 워커를 분리하기까지,
같은 작업에서 나온 기록입니다.