맥북 서버 탈출기

[맥북 서버 탈출기 5편] 포트 하나 안 열고 서비스하기: Cloudflare Tunnel로 도메인 연결

TerryDev 2026. 10. 15. 19:15

이 글 요약

  • Cloudflare Tunnel은 서버가 Cloudflare로 먼저 연결을 맺는 방식이다. 그래서 서버에 웹 포트를 열지 않아도 된다
  • 대시보드에서 터널 만들기 → 서버에 cloudflared 설치 → 경로(호스트명) 추가 → DNS 전환까지 캡처 8장으로 정리
  • 맥북 터널은 설정 파일 없이 메모리로 버티고 있었다. 새 서버는 설정을 서버에 두지 않는 방식으로 바꿨다

3~4편에서 오라클에 서버를 만들었다. 이제 이 서버를 powerup.example.com 같은 도메인으로 접속할 수 있게 해야 한다.

보통은 서버의 80, 443 포트를 열고, 도메인을 서버 IP에 연결하고, HTTPS 인증서를 설치한다. 이번에는 셋 다 하지 않았다. 포트는 SSH(22) 하나만 열려 있고, 도메인은 IP를 모르고, 인증서는 서버에 없다. 그래도 앱은 HTTPS로 잘 돈다. Cloudflare Tunnel 덕분이다.


Cloudflare Tunnel은 무엇인가

보통의 방식: 손님이 서버 문을 두드린다

[사용자] → 도메인 → 서버 공인 IP:443 (포트 열림) → 웹 서버(nginx) → 앱
  • 서버가 인터넷에 문(포트)을 열어 두고 손님을 기다린다.
  • 문이 열려 있으니 손님만 오는 게 아니다. 포트 스캔, 무작위 로그인 시도 같은 공격도 같은 문으로 온다.
  • 도메인이 서버 IP를 직접 가리키니 IP가 바뀌면 DNS도 고쳐야 한다.
  • HTTPS 인증서를 서버에서 발급하고 갱신해야 한다.
IP가 바뀌면 왜 DNS를 고쳐야 할까

DNS: powerup.example.com 같은 도메인 이름을 서버의 IP 주소로 바꿔 주는 인터넷의 주소록이다.

전화번호부를 떠올리면 쉽다. powerup.example.com이라는 이름 옆에 서버 주소(IP)가 적혀 있고, 앱은 이 번호부를 보고 서버를 찾아간다. 서버가 이사해서 주소가 바뀌면 번호부도 고쳐야 한다. 고치기 전까지 손님은 옛날 주소로 찾아가 헛걸음을 한다.

터널 방식에서는 DNS가 IP가 아니라 터널을 가리킨다. 서버 IP가 바뀌어도 번호부는 그대로다.

HTTPS 인증서는 왜 발급하고 갱신해야 할까

인증서는 "이 사이트가 진짜 powerup.example.com이 맞다"는 신분증이다. 브라우저 주소창의 자물쇠와 암호화가 이 신분증으로 동작한다. 신분증에는 유효기간이 있다. 만료되면 브라우저는 "안전하지 않은 사이트" 경고를 띄우고, 앱의 API 요청은 그냥 실패한다.

터널 방식에서는 Cloudflare가 자기 쪽에서 인증서를 발급하고 갱신한다. 서버에서 따로 할 일이 없다.

터널 방식: 서버가 먼저 전화를 건다

[사용자] → 도메인 → Cloudflare ⇠⇠(터널)⇠⇠ 서버의 cloudflared → localhost:8080 앱
  • 서버에서 도는 cloudflared라는 프로그램이 Cloudflare 쪽으로 먼저 연결을 맺어 둔다. 서버에서 바깥으로 나가는(아웃바운드) 연결이다.
  • 사용자가 도메인으로 접속하면 Cloudflare가 이미 열려 있는 그 연결로 요청을 넘겨준다.
  • 서버는 문을 열 필요가 없다. 비유하면, 가게 문을 잠가 두고 단골 배달 기사(Cloudflare)와만 전화선을 연결해 둔 셈이다.

이 방식으로 얻는 것

서버에 여는 포트80, 443없음 (SSH 22만)
도메인이 가리키는 곳서버 공인 IPCloudflare 터널
서버 IP가 바뀌면DNS 수정 필요상관없음
HTTPS 인증서서버에서 발급·갱신Cloudflare가 처리
비용-무료

3편에서 고정 IP 대신 임시(Ephemeral) IP를 고른 이유가 이것이다. 도메인이 IP를 가리키지 않으니 IP가 바뀌어도 앱에는 영향이 없다. 공인 IP는 내가 SSH로 들어갈 때만 쓴다.
준비물: 도메인이 Cloudflare에 등록돼 있어야 한다(네임서버가 Cloudflare). 맥북 시절부터 쓰던 도메인이라 이미 돼 있었다.


터널 관리 방식 두 가지, 그리고 맥북의 사고

터널을 만드는 방법은 두 가지다. 이 차이가 이번 편의 핵심이다.

구분 로컬 관리 터널 대시보드 관리 터널
만드는 법 내 컴퓨터에서 cloudflared tunnel create Cloudflare 대시보드에서 클릭
설정(어떤 주소를 어디로) 내 컴퓨터의 config.yml Cloudflare 대시보드
인증 내 컴퓨터의 인증 파일(.json) 토큰 하나
컴퓨터가 날아가면 설정과 인증 파일도 같이 날아감 토큰만 있으면 어디서든 다시 연결

맥북은 로컬 관리 터널이었다. 각 터널을 cloudflared tunnel --config ~/.cloudflared/powerup.yml run으로 실행하고 있었다.

이사 준비를 하며 그 설정 파일을 열어 보려 했다. 없었다. ~/.cloudflared/ 폴더에는 로그 파일만 있었고, 설정 파일(powerup.yml)도 인증 파일도 보이지 않았다. 홈 폴더 전체와 휴지통까지 찾아봤지만 없었다.

그런데 앱은 정상으로 돌고 있었다. cloudflared가 실행될 때 읽어 둔 설정을 메모리에 들고 계속 버티고 있었던 것이다. 맥북이 재부팅되거나 cloudflared가 한 번이라도 재시작되면 설정 파일을 다시 읽으려다 실패하고, 앱은 그대로 끊길 상황이었다. 언제 지워졌는지는 끝내 알 수 없었다.
그래서 새 서버는 대시보드 관리 터널로 만들었다. 설정은 Cloudflare에 있고, 서버에는 토큰 하나만 있다. 서버가 통째로 사라져도 새 서버에 토큰으로 cloudflared를 설치하면 같은 터널이 다시 연결된다.


1단계. 대시보드에서 터널 만들기

Cloudflare 대시보드에서 네트워크 → 터널로 간다. (메뉴 구성이 자주 바뀐다. 예전에는 Zero Trust → Networks → Tunnels에 있었다.)

터널 목록과 터널 생성 버튼

기존 터널이 4개 보인다. gymtracker, powerup은 앱 이름과 도메인이 바뀔 때마다 새로 만들고 지우지 않은 맥북 시절의 터널들이다. 이 이야기는 뒤에서 한다.

터널 생성을 누르고 연결 방식에서 Cloudflared를 고른다. 이름을 입력한다.

터널 이름 입력과 운영 체제 선택

  • 터널 이름: 어느 서버의 터널인지 알 수 있게 짓는다. 나는 powerup-oracle로 지었다. 나중에 러브투데이까지 이 터널에 올리면서 이름이 안 맞게 됐다. 처음부터 서버 기준으로 oracle-server처럼 짓는 편이 낫다.
  • 터널 ID: 터널마다 붙는 고유 ID다. 뒤의 DNS 단계에서 쓴다.
  • 운영 체제: 설치 명령어를 보여 주기 위한 선택이다. 서버가 Ubuntu라서 Debian, 64-bit를 골랐다.

2단계. 서버에 cloudflared 설치하고 토큰으로 연결

아래로 내리면 설치 명령어가 나온다.

설치 명령어와 연결 성공 표시

sudo cloudflared service install eyJ...에서 eyJ로 시작하는 긴 문자열이 터널 토큰이다.

토큰은 비밀번호다. 이 토큰이 있으면 누구든 내 터널에 자기 서버를 연결할 수 있다. 화면에도 "이 토큰은 계정에 대한 접근 권한을 부여합니다"라고 경고가 나온다. 블로그, 깃허브, 스크린샷에 올리지 말자.

서버에 SSH로 접속해서 설치한다. 오라클 A1은 ARM 서버라 arm64용 패키지를 받는다.

# cloudflared 설치 (ARM 서버용)
curl -fsSL -o cloudflared.deb \
  https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64.deb
sudo dpkg -i cloudflared.deb
cloudflared --version
  • 화면의 "Install cloudflared" 명령은 Cloudflare 패키지 저장소를 등록하는 방식이다. 그 방법도 된다. 나는 파일 하나로 끝나는 .deb 방식을 썼다.
  • x86 서버라면 파일 이름의 arm64를 amd64로 바꾼다.

이어서 화면의 Install as service 명령을 그대로 실행한다.

sudo cloudflared service install <터널 토큰>

이 명령 하나가 세 가지를 한다.

  1. cloudflared를 systemd 서비스로 등록한다. systemd는 리눅스에서 프로그램을 상시 실행하고, 죽으면 다시 살리고, 부팅할 때 자동으로 켜 주는 관리자다. 맥의 launchd와 같은 역할이다.
  2. 토큰을 서비스 설정에 저장한다.
  3. 바로 실행해서 Cloudflare에 연결한다.

확인은 이렇게 한다.

systemctl is-active cloudflared      # active
sudo journalctl -u cloudflared -n 5  # Registered tunnel connection ... 이 보이면 연결 성공

로그에 Registered tunnel connection이 여러 줄 찍힌다. cloudflared는 Cloudflare 데이터센터 여러 곳(나는 서울 쪽 두 곳)에 동시에 연결을 맺어 둔다. 한 연결이 끊겨도 다른 연결로 요청이 들어온다.

대시보드로 돌아가면 연결 상태에 "터널이 성공적으로 연결되었습니다"가 뜬다. 이 시점에는 터널만 연결됐을 뿐, 어떤 도메인도 이 터널로 오지 않는다. 운영 중인 앱에는 아무 영향이 없다.


3단계. 경로 추가: 어떤 주소를 서버의 어디로 보낼지

터널 상세 화면의 경로 탭에서 경로 추가를 누르고 게시된 애플리케이션을 고른다.

경로 유형: 게시된 애플리케이션

  • 게시된 애플리케이션: 도메인으로 누구나 접속할 수 있는 웹 서비스. 앱 API는 이것이다.
  • 비공개 호스트명 / CIDR: 회사 내부망처럼 허가된 사람만 접속하게 할 때 쓴다. 지금은 필요 없다.

서브도메인, 도메인, 서비스 URL 입력

항목 입력값 뜻
서브도메인 powerup 앞에 붙는 이름
도메인 Cloudflare에 등록된 내 도메인 목록에서 고른다
서비스 URL http://localhost:8080 서버 안에서 앱이 듣고 있는 주소

서비스 URL이 http이고 localhost인 점이 중요하다.

  • localhost: cloudflared와 앱이 같은 서버에 있다. cloudflared가 서버 안에서 앱으로 요청을 넘겨주니, 앱은 바깥에 노출될 필요가 없다.
  • http: 사용자와 Cloudflare 사이는 HTTPS로 암호화되고, Cloudflare와 서버 사이는 터널 자체가 암호화된다. 서버 안의 마지막 구간만 http다. 그래서 서버에 인증서가 필요 없다.

경로 추가를 눌렀더니 이런 결과가 나왔다.

경로는 추가됐지만 DNS 레코드 자동 생성 실패

경로는 추가됐는데, 아래에 빨간 오류가 붙었다. "같은 이름의 A, AAAA, CNAME 레코드가 이미 있다." 그리고 "DNS 레코드를 자동으로 생성하지 못했으니 CNAME 레코드를 수동으로 설정하라."

당연했다. powerup 주소는 지금 맥북 터널을 가리키는 레코드가 이미 있다. 운영 중인 주소니까.

처음 쓰는 새 주소라면 이 단계에서 DNS까지 자동으로 만들어지고 끝난다. 이미 쓰던 주소를 옮기는 경우에만 이 오류가 난다. 그리고 이사하는 입장에서는 이 오류가 오히려 고마웠다. 경로는 미리 만들어 두고, 실제로 주소를 넘기는 순간은 내가 정할 수 있게 됐기 때문이다.


4단계. DNS 전환: 실제로 주소를 넘기는 순간

도메인의 DNS → 레코드에서 해당 이름을 찾아 편집을 누른다.

CNAME 대상을 새 터널로 바꾸기

항목 값
이름 powerup (그대로)
형식 CNAME (그대로)
대상 <새 터널 ID>.cfargotunnel.com 으로 변경
프록시 상태 프록싱됨(주황 구름) 유지
  • CNAME: "이 이름은 저 이름과 같다"는 별명 레코드다. 터널을 쓰는 주소는 <터널 ID>.cfargotunnel.com을 가리킨다. 그 앞의 ID만 바꾸면 다른 터널로 넘어간다.
  • 프록시 상태는 꼭 켜 둔다. 터널은 Cloudflare 프록시를 거쳐야만 동작한다. 꺼져 있으면 접속이 안 된다.

저장하면 콘텐츠 열이 새 터널 이름으로 바뀐다.

DNS 레코드가 새 터널을 가리킨다

바로 확인한다.

curl https://powerup.example.com/api/v1/health
# {"status":"ok", ...}

Cloudflare 내부에서 바뀌는 레코드라 반영이 빠르다. 나는 첫 요청부터 새 서버가 200으로 응답했다.

되돌리는 방법도 같은 자리에 있다

문제가 생기면 같은 레코드의 대상을 예전 터널 ID로 되돌리면 된다. 맥북의 터널과 앱은 지우지 않고 며칠 그대로 뒀다. 되돌릴 길을 남겨 두고 넘어가는 것, 그게 이번 전환에서 가장 신경 쓴 부분이다.
실제 전환 순서는 이랬다. 앱 데이터를 옮기는 부분은 6편에서 자세히 다룬다.

  1. 새 서버에 앱 설치, 빈 DB로 기동 테스트 (중단 없음)
  2. 새 터널 만들고 서버에 연결 (중단 없음)
  3. 새 터널에 경로 추가 (중단 없음, DNS는 아직 맥북)
  4. 맥북 앱 중지 → DB 옮기기 → 서버 앱 시작 (여기서부터 중단)
  5. DNS 대상을 새 터널로 변경 (여기서 복구)

러브투데이도 같은 방법으로 옮겼다. 새 터널을 만들지 않고 같은 터널에 경로만 하나 더 추가했다(lovetoday → http://localhost:8083). 터널 하나에 경로를 여러 개 둘 수 있다.


서버 방화벽은 그대로

터널을 쓰면 서버 쪽에서 할 일이 없다. 열지 않으면 된다.

  • 1겹, 오라클 VCN 보안 규칙(Security List): 기본값 그대로 22번(SSH)만 열려 있다.
  • 2겹, 서버 안 방화벽(iptables): 오라클의 Ubuntu 이미지는 기본으로 22번 말고는 전부 거절하게 설정돼 있다. 이것도 그대로 뒀다.
  • MySQL(3306): 아예 127.0.0.1에서만 듣게 띄웠다. 서버 안에서만 접속할 수 있다.

앱은 8080, 8083 포트에서 돌지만, 바깥에서 서버 공인 IP:8080으로 접속하면 두 겹의 방화벽에서 막힌다. cloudflared는 서버 안에서 localhost로 접속하니 방화벽과 상관없다.

인터넷에 오라클 무료 서버 글을 찾아보면 "Security List에 80, 443 추가하고 iptables도 열어야 한다"는 단계가 꼭 나온다. 터널을 쓰면 그 단계 전체를 건너뛴다. 설정할 게 없으니 설정 실수도 없다.


남은 정리: 이름만 남은 터널들

1단계 터널 목록에 있던 gymtracker는 PowerUp의 옛 이름이다. 옛 도메인 시절에 만든 터널로, 주소 9개가 몰려 있었다. 확인해 보니 그 주소들은 전부 응답이 없었고 앱 코드도 쓰지 않았다. 대시보드에서 지웠다.

그런데 맥북에는 이 터널의 커넥터가 관리자 권한 서비스로 아직 돌고 있었다. 5월에 등록된 서비스였다. 터널이 지워진 뒤에도 연결을 계속 재시도하며 로그를 쌓았다. 관리자 권한이 필요해서 이건 내가 직접 껐다.

sudo launchctl bootout system/com.cloudflare.cloudflared
sudo rm /Library/LaunchDaemons/com.cloudflare.cloudflared.plist

앱 이름이 바뀔 때마다 터널을 새로 만들고, 예전 것은 지우지 않았다. 그렇게 터널 4개와 서비스 4개가 쌓였다. 이번에 운영 앱은 전부 터널 하나로 모였다.


에이전트와 한 부분

터널 작업은 Claude Code가 브라우저(Cloudflare 대시보드)와 서버 터미널을 오가며 진행했다.

  • 맥북의 설정 파일이 없다는 걸 찾아낸 것도 에이전트다. 이사 계획을 세우려고 기존 설정을 읽으려다 발견했다. 나는 몇 달 동안 몰랐다.
  • 토큰을 화면이나 로그에 남기지 않았다. 대시보드의 복사 버튼으로 토큰을 복사하고, 클립보드에서 바로 서버로 넘겨 설치했다. 로그에는 "토큰 길이 184"만 남았다. 나중에 이 글을 쓰며 캡처를 고를 때도 토큰과 터널 ID부터 가렸다.
  • 전환을 시작하기 직전에 멈추고 확인을 받았다. 앱이 실제로 멈추는 구간이 시작되는 지점이라서다. 되돌리는 방법을 먼저 설명하고 "지금 시작할까요?"라고 물었다.
  • 터널 삭제도 내가 고르게 했다. 세 터널의 정체를 표로 정리하고 "gymtracker는 삭제 추천, powerup은 며칠 운영 후 삭제 추천"이라고 제안만 했다. 삭제는 되돌릴 수 없는 작업이라 결정은 나한테 넘겼다.

나는 "삭제해줘"와 "진행해줘"를 담당했다. 터널 이름을 gymtracker로 지은 것도 나였다.


오늘의 교훈 설정 파일 없이 돌아가는 서비스는 돌아가는 게 아니다. 아직 안 멈춘 것이다.

다음 편에서는 운영 중인 앱의 데이터를 옮긴다. 앱을 멈춘 시간은 앱당 3~4분이었다. 그 사이에 DB를 덤프하고, 복원하고, 행 개수를 하나하나 맞춰 봤다.