MeetBack 2026.08.31 6 min read

Cloudflare Tunnel을 기본 경로로, NPM을 예비 경로로 둔 이유

공인 IP를 항상 쓸 수 없는 환경에서 Tunnel을 기본 경로로 선택했다. NPM과 DNS-01 인증서를 준비하고 당시 검증한 범위와 보류한 외부 접속 시험을 정리했다.

프로젝트 기간: 2026-08-18 ~ 2026-09-02
글 범위: Cloudflare Tunnel 기본 경로와 NPM 예비 경로

외부 접속 경로를 만들 때 공인 IP와 포트 포워딩을 항상 사용할 수 없는 조건이 있었다. App은 Docker Swarm의 여러 복제본으로 배포할 예정이었다.

기본 접속은 Cloudflare Tunnel로 정했다. NGINX Proxy Manager는 공인 NAT를 사용할 수 있을 때 연결할 예비 경로로 따로 구성했다.

기본 경로
Client
  → Cloudflare Edge
  → Cloudflare Tunnel
  → cloudflared connectors
  → Swarm Service DNS/VIP
  → Application replica

예비 경로
Client
  → Cloudflare Edge
  → Public IP / NAT
  → NGINX Proxy Manager
  → Swarm Service DNS/VIP
  → Application replica

왜 Tunnel이 NPM을 거치지 않게 했는가

처음에는 Tunnel에서 NPM을 거쳐 App VIP로 연결하는 방법도 검토했다. 프록시 규칙을 중앙에서 관리하기는 편하지만 NPM이 멈추면 기본 경로까지 막힌다. 거쳐야 할 단계와 인증서를 확인할 위치도 늘어났다.

Tunnel이 Swarm 서비스 이름과 VIP에 직접 연결할 수 있어 기본 경로에 NPM을 넣지 않았다. NPM은 외부 인바운드 접속이 가능한 때 사용할 예비 경로로 남겼다.

cloudflared를 Global Service로 배포했다

Swarm 노드 5대에 cloudflared 커넥터가 하나씩 올라가도록 global 서비스를 사용했다. 한 노드가 중단돼도 나머지 커넥터가 Cloudflare와 연결을 유지할 수 있게 구성했다.

Tunnel 토큰은 Docker Secret으로 전달했다. 커넥터가 외부로 QUIC 연결을 만들기 때문에 라우터에서 App용 인바운드 포트를 계속 열어 둘 필요가 없었다.

서비스 전체 상태는 Manager 노드에서 확인했다.

docker service ls
docker service ps <cloudflared-service>
docker service logs <cloudflared-service>

Worker에서 같은 명령을 실행하니 “This node is not a swarm manager” 오류가 나왔다. 클러스터 상태 조회에는 Manager 권한이 필요했다. Worker에서는 로컬 컨테이너를 확인하고 전체 서비스 상태는 Manager에서 확인했다.

이 단계에서는 커넥터 태스크 5개의 연결과 오버레이 접근을 확인했다. 아직 App 서비스를 배포하기 전이라 최종 호스트 이름을 통한 사용자 동작은 검증하지 못했다.

NPM은 상태가 있는 단일 Service로 뒀다

NPM은 인증서와 프록시 호스트, 사용자 설정을 저장한다. 특정 Manager에 복제본 하나를 배치하고 로컬 영구 저장 디렉터리를 연결했다.

다른 노드로 옮기면 같은 로컬 데이터가 없을 수 있어 배치 위치를 고정했다. 당시에는 예비 경로를 단순하게 유지하려고 선택한 구성이다. NPM 자체를 이중화하려면 공유 DB와 인증서 저장소, active/standby 전환 방식을 별도로 준비해야 한다.

Public port 없이 DNS-01 인증서를 발급했다

HTTP-01 검증은 공개 DNS와 80번 포트를 통해 원본 서버에 요청이 도달해야 한다. 당시에는 공인 NAT를 사용할 수 없어 이 방법으로 검증하기 어려웠다.

Cloudflare DNS-01로 전환했다. Cloudflare API로 필요한 TXT 레코드를 만들면 Let's Encrypt가 도메인 소유 여부를 확인하므로 인바운드 포트를 열지 않고도 발급할 수 있었다.

인증서에는 루트 도메인과 와일드카드 도메인을 함께 넣었다.

example.com
*.example.com

Cloudflare API 토큰은 대상 Zone의 DNS 수정·조회 권한만 부여했다. 토큰 값은 공개 문서나 소스에 남기지 않았다.

NPM 화면의 인증서는 Not Used로 표시됐다. 발급이 실패한 것은 아니고 프록시 호스트에 아직 연결하지 않은 상태였다. 발급 로그에서 fullchain과 개인 키 저장, 만료 시각을 확인했다.

왜 NPM 외부 E2E는 보류했는가

인증서 발급까지 확인했지만 외부 HTTPS 접속은 별도로 시험해야 했다. NPM 예비 경로의 전체 검증에는 아래 구성이 필요했다.

Public IP
  + TCP 80/443 port forwarding
  + Cloudflare DNS record 전환
  + NPM proxy host
  + Swarm application origin

이 글에서 다룬 구축 시점에는 공인 IP와 포트 포워딩을 사용할 수 없어 전체 경로 시험을 보류했다. 한시적으로 사용할 수 있게 되면 App 전용 호스트 이름으로 시험하고 포트 포워딩과 DNS 레코드를 원복할 계획이었다.

따라서 이 시점의 완료 범위는 인증서 발급과 NPM 준비까지였다. 외부 예비 경로의 검증 결과와는 구분해 기록했다.

자체 DNS를 Cloudflare 복제본으로 두지 않은 이유

Cloudflare Zone을 로컬 DNS에 복제해 보조 서버처럼 사용하는 방법도 검토했다. 권한 DNS를 함께 운영하려면 Zone 전송 지원, 위임, DNSSEC, 레코드 동기화와 전환 기준을 확인해야 했다.

BIND에 레코드를 복사하는 것만으로는 이 조건을 충족하기 어려웠다. 오래된 레코드가 남거나 권한 서버마다 응답이 달라질 수 있어 이번 프로젝트에서는 Cloudflare를 단일 권한 DNS로 유지했다.

WAF는 Edge에서 적용하는 방향

App 공개 후에는 Cloudflare WAF와 요청 수 제한을 적용하는 방향으로 계획했다. 원본 서버는 Tunnel 뒤에 두고 외부에서 들어오는 비정상 요청을 Cloudflare에서 먼저 거를 수 있게 하려는 목적이었다.

로그인 권한, 모임 접근과 업로드 제한은 App에서 계속 검증해야 한다. WAF로 이 검증을 대신할 수는 없다. NPM 예비 경로에도 Cloudflare 프록시를 유지해 같은 외부 접근 규칙을 적용할 계획이었다.

현재 구조의 정확한 상태

완료한 것은 다음이다.

  • Cloudflare Tunnel과 Swarm global 커넥터 구성
  • Tunnel 토큰을 Docker Secret으로 분리
  • 커넥터 연결과 오버레이 접근 확인
  • NPM 서비스와 영구 저장 데이터 구성
  • Cloudflare DNS-01 와일드카드 인증서 발급

이 단계에서는 App 원본 서버 연결, 공인 NAT를 이용한 NPM 외부 검증, 방화벽 적용이 남아 있었다.

Tunnel은 서비스 이름과 VIP로 바로 연결하고 NPM은 별도 예비 경로로 준비했다. 각 경로에서 확인한 항목과 당시 환경 때문에 시험하지 못한 항목을 나눠 남겼다.

SEARCH JOURNAL

기록 검색

글 제목, 요약, 태그에서 찾습니다.

검색을 준비하고 있어요.

검색어를 입력해 주세요.

    Ctrl/⌘ K 검색 Esc 닫기 ↑ ↓ 결과 이동