도메인 하나 옮기면서 배운 것들: 이관, 메일, 그리고 터널 이중화의 착각
새로 산 도메인을 세팅할 일이 생겼다. 이왕 하는 김에 계정부터 완전히 새로 만들어서 처음부터 끝까지 직접 해보기로 했다. 기존 설정을 복사해오는 대신 하나씩 이해하면서 가는 쪽으로.
세 덩어리였다.
- 도메인을 새 Cloudflare 계정으로 이관
- 새 Zoho 계정으로 메일 붙이기
- 집에 있는 서버를 터널로 공개
셋 다 흔한 작업이고 검색하면 자료도 많다. 그런데 하다 보니 자료에 잘 안 나오는 지점에서 한 번씩 걸렸다. 그 부분만 모아서 적어둔다.
특히 마지막 터널 이야기는, 내가 몇 달째 이중화라고 믿고 있던 구성이 사실 이중화가 아니었다는 발견이라 이 글에서 제일 하고 싶은 이야기다.
0. 먼저: DNS 레코드는 각각 다른 질문에 답한다
작업을 시작하기 전에 이 정리가 필요했다. 도메인 하나는 여러 역할을 동시에 갖는데, 레코드 타입이 곧 역할이다.
| 타입 | 답하는 질문 |
|---|---|
| NS | 이 도메인의 DNS는 누가 관리하나? |
| A / AAAA | 이 이름의 IP는? |
| CNAME | 이 이름은 다른 어떤 이름인가? |
| MX | 이 도메인으로 온 메일은 어디로 배달하나? |
| TXT | 임의의 문자열 (소유 검증·SPF·DKIM·DMARC가 전부 여기) |
여기서 제일 자주 헷갈리는 것: 웹(A/CNAME)과 메일(MX)은 완전히 별개다. 웹사이트를 Cloudflare로 프록시하든 말든 메일 배달에는 아무 영향이 없고, 반대도 마찬가지다. 같은 도메인 이름을 쓸 뿐 서로 다른 경로다.
이걸 확실히 해두면 나중에 "터널을 켰는데 메일이 안 되나요?" 같은 걱정을 안 하게 된다. (안 된다. 아무 상관 없다.)
1. 계정 간 도메인 이관: 등록만 옮겨간다
Cloudflare에서 산 도메인을 다른 Cloudflare 계정으로 옮기는 건, 다른 등록기관으로 나갔다 오는 게 아니라 계정 간 등록 이동이라는 별도 기능을 쓴다.
시작조차 안 되게 막는 조건들
신청 버튼을 누르기 전에 이게 다 맞아야 한다.
- 받을 계정에 그 도메인이 사이트로 먼저 추가되어 있을 것 (Free 플랜이면 충분)
- 도메인이 잠금 상태가 아닐 것
- 등록자 이메일이 검증 완료 상태일 것
- DNSSEC이 꺼져 있을 것
마지막 항목에서 한 번 막혔다. DNSSEC은 켜둔 걸 잊고 지내기 쉬운데, 켜져 있으면 이동 신청 자체가 진행되지 않는다. 끄고 나서 다시 시도했다. (이동이 끝나면 새 계정에서 다시 켜면 된다.)
진짜 중요한 것: 설정은 따라오지 않는다
문서에 적혀 있긴 한데 무심코 지나치기 쉬운 문장이 있다.
이동하면 원래 계정의 모든 설정이 사라진다.
옮겨가는 건 도메인 등록(registration)뿐이다. DNS 레코드, 페이지 룰, 리다이렉트, 방화벽 규칙, 캐시 설정, 하나도 따라오지 않는다. 운영 중인 도메인을 이렇게 옮기면 이동이 끝난 순간 사이트가 통째로 죽는다.
그래서 순서가 이렇게 되어야 한다.
- 옮기기 전에 기존 계정에서 DNS 레코드를 Export 해서 백업
- 이동 진행
- 새 계정에서 레코드를 다시 넣고
- 그 다음에 나머지 설정 복원
나는 이번에 아직 아무것도 안 붙인 새 도메인이라 잃을 게 없었다. 운이 좋았던 거고, 운영 중인 도메인이었으면 그냥 사고였을 것이다.
절차 자체는 간단하다
보내는 계정에서 Manage Domain → Configuration 탭에서 신청하고, 받는 계정으로 승인 메일이 온다. 받는 계정에서 Manage Domains → View Actions로 들어가 승인하면 끝.
- 5일 안에 승인하지 않으면 신청이 만료된다
- 이동 후 30일간 transfer-lock이 걸린다
- 갱신 비용 책임이 받는 계정으로 넘어간다
2. 메일: MX만 넣으면 절반만 된다
Zoho Mail을 붙였다. 여기서 배운 건 하나로 요약된다.
수신(MX)과 발신 인증(SPF/DKIM/DMARC)은 완전히 다른 일이다.
MX만 넣으면 메일이 오긴 온다. 그래서 "됐네" 하고 넘어가기 쉽다. 그런데 그 상태로 메일을 보내면 상대방 스팸함으로 간다. 받는 쪽 서버 입장에서는 "이 도메인 이름으로 보냈다는데 진짜인지 확인할 방법이 없는" 메일이기 때문이다.
SPF: 누가 내 이름으로 보낼 수 있나
TXT @ v=spf1 include:zoho.com include:zohomail.com ~allinclude:는 저 도메인이 공표한 허용 IP 목록을 그대로 가져다 쓴다는 뜻이다. ~all은 목록에 없는 서버는 softfail(일단 받되 의심), -all은 hardfail.
함정: 도메인당 SPF TXT는 딱 한 줄만 존재해야 한다.
두 줄이면 규격 위반이라 둘 다 무효가 된다. 절반만 적용되는 게 아니라 통째로 날아간다. 나중에 메일 발송 서비스를 하나 더 붙일 때, 새 TXT 레코드를 추가하는 게 아니라 기존 줄에 include:를 덧붙여야 한다. 이걸 모르고 줄을 추가했다가 멀쩡하던 메일이 스팸으로 가기 시작하는 경우가 흔하다.
DKIM: 서명이 진짜인지 검증할 공개키
TXT zmail._domainkey v=DKIM1; k=rsa; p=MIGfMA0GCS...발신 서버가 메일에 개인키로 서명하고, 받는 쪽이 이 TXT에 있는 공개키로 검증한다.
함정: 이 키는 도메인 전용이다.
이미 잘 돌아가는 다른 도메인이 있으면 설정을 복사해오고 싶어진다. MX와 SPF는 그래도 되는데 DKIM은 안 된다. 도메인마다 키 쌍이 따로 생성되므로, 새 도메인은 Zoho 관리 콘솔에서 새로 발급받아야 한다. 복사해 넣으면 검증이 실패한다.
DMARC: 실패하면 어떻게 할까
TXT _dmarc v=DMARC1; p=none; rua=mailto:[email protected]p=none(관측만) → p=quarantine(스팸함) → p=reject(거부) 순으로 단계가 있다. 처음엔 무조건 p=none으로 시작하는 게 맞다. 설정이 덜 된 상태에서 reject를 걸면 내 메일이 내 손으로 차단된다. rua=로 리포트를 받아보면서 문제가 없는 걸 확인한 뒤에 올린다.
Cloudflare 쪽 주의
- 메일 관련 A/CNAME은 회색 구름(DNS only). 오렌지 구름(프록시)은 HTTP(S)만 처리해서 SMTP/IMAP 포트가 통과하지 못한다. MX 레코드 자체는 프록시 대상이 아니라 늘 안전하다.
- 같은 도메인에서 Cloudflare Email Routing을 켜면 안 된다. 자기 MX를 강제로 심어서 충돌한다. (반대로 "받아서 다른 주소로 전달만" 하면 되는 도메인이라면 Email Routing이 무료라 더 나은 선택이다.)
확인은 명령어로
대시보드 화면 말고 실제 전파된 값을 본다.
dig +short MX example.dev
dig +short TXT example.dev
dig +short TXT zmail._domainkey.example.dev
dig +short TXT _dmarc.example.dev발신 인증이 제대로 되는지는 [email protected]으로 메일을 한 통 보내면 SPF/DKIM/DMARC 판정 결과를 회신해준다. 이게 제일 확실하다.
3. 터널: 그리고 이중화라고 믿었던 것
여기가 이 글의 본론이다.
배경
집에 NAS가 두 대 있다. 한 대에서 Cloudflare Tunnel로 내부 서비스 몇 개를 밖에 노출하고 있었고, 다른 한 대에는 같은 터널의 백업 컨테이너를 만들어 뒀다. 주 NAS가 죽으면 저쪽이 받아주겠거니 하고.
NAS-A : cf-tunnel-main (상시 가동)
NAS-B : cf-tunnel-standby (평소 정지)이번에 새 계정용 터널을 추가하려고 구성을 들여다보다가, 백업 컨테이너 상태를 봤다.
cf-tunnel-standby cloudflare/cloudflared:latest Exited (0) 3 days ago정지 상태. 그리고 이게 문제라는 걸 그때 알았다.
Cloudflare에는 primary/backup이 없다
터널의 고가용성은 이렇게 동작한다.
같은 토큰으로 떠 있는 커넥터는 전부 동등하다. Cloudflare가 등록된 커넥터들 사이로 트래픽을 분산하고, 하나가 죽으면 나머지로 보낸다.
즉 액티브-액티브다. main이니 standby니 하는 건 내가 컨테이너 이름에 붙인 관례일 뿐, Cloudflare는 그런 구분을 모른다.
여기서 첫 번째 결론이 나온다.
정지된 백업은 백업이 아니다. 커넥터가 꺼져 있으면 Cloudflare 입장에선 애초에 존재하지 않는다. 주 터널이 죽어도 자동으로 넘어가지 않고, 사람이 알아채고 직접 켜야 한다. 나는 몇 달 동안 "이중화 되어 있음"이라고 생각하고 있었는데, 실제로는 수동 복구 절차를 미리 준비해둔 것에 불과했다.
그런데 그냥 켜면 되는 게 아니다
"그럼 켜두면 되겠네"가 자연스러운 결론인데, 여기서 두 번째이자 더 고약한 함정이 있다.
액티브-액티브라는 건 요청이 어느 커넥터로 갈지 예측할 수 없다는 뜻이다. 그렇다면:
떠 있는 모든 커넥터가 동일한 오리진에 도달할 수 있어야 한다.
한쪽 커넥터만 오리진에 닿는 상태에서 두 번째 커넥터를 켜면 어떻게 될까. 전면 장애가 나면 차라리 낫다. 실제로는 요청의 절반쯤이 간헐적으로 502가 된다. 새로고침하면 되기도 하고 안 되기도 하는, 원인 추적이 제일 어려운 종류의 장애다.
그리고 이건 백업을 켜는 순간 없던 장애가 생기는 형태다. 안정화하려고 한 행동이 반대로 작용한다.
가장 흔한 원인: localhost
Public Hostname 설정에서 오리진 주소를 이렇게 적어두기 쉽다.
URL: http://localhost:3000커넥터가 한 대일 땐 잘 동작한다. 그런데 커넥터를 하나 더 띄우면 그 localhost는 두 번째 기계의 자기 자신을 가리킨다. 거기엔 그 서비스가 없다. 그래서 절반이 죽는다.
URL: http://10.0.0.10:3000 ← LAN IP로커넥터를 여러 대 둘 생각이라면 처음부터 localhost를 쓰지 않는 게 맞다.
그래서 어떻게 해야 하나
커넥터를 추가하기 전에 도달성부터 확인한다.
# 새로 커넥터를 띄울 기계에서, 현재 노출 중인 오리진들에 실제로 닿는지
curl -s -o /dev/null -w "%{http_code}\n" http://10.0.0.10:3000전부 응답하면 그때 상시 가동으로 바꾼다.
services:
cloudflared:
image: cloudflare/cloudflared:latest
restart: unless-stopped # ← 이게 있어야 이중화다
command: tunnel --no-autoupdate run
environment:
- TUNNEL_TOKEN=${TUNNEL_TOKEN}한 대에만 존재하는 서비스(그 기계의 로컬 디스크를 쓴다든가)를 노출 중이라면, 그 터널은 애초에 커넥터를 늘리면 안 된다. 이중화하고 싶다면 서비스 쪽을 먼저 양쪽에서 접근 가능하게 만들어야 한다. 터널 이중화는 오리진 이중화가 선행되어야 성립한다.
4. 덤: 계정이 여러 개면 토큰 방식을 쓴다
터널을 설정하는 방법이 두 가지 있다.
| 토큰 방식 | config.yml 방식 | |
|---|---|---|
| 터널 생성 | 대시보드 | cloudflared tunnel create |
| 자격증명 | 토큰 문자열 하나 | cert.pem + <UUID>.json |
| 라우팅 설정 | 대시보드 | 로컬 config.yml |
| 계정 여러 개 | 충돌 없음 | 머신당 cert.pem 하나 |
cloudflared tunnel login을 하면 ~/.cloudflared/cert.pem이 생기는데, 이게 머신당 하나다. 계정을 두 개 쓰면 로그인할 때마다 서로 덮어쓰게 되어 골치가 아프다.
반면 토큰 방식은 컨테이너가 자기 토큰만 들고 있어서, 같은 기계에 서로 다른 계정의 터널을 나란히 띄워도 서로를 모른다. cert.pem이 아예 필요 없다. 계정을 나눠 쓸 생각이라면 처음부터 토큰 방식으로 가는 게 맞다.
한 가지만 조심하면 된다. 컨테이너가 여러 개가 되면 어느 토큰이 어느 계정 것인지 헷갈리기 시작한다. 폴더명과 컨테이너명으로 확실히 구분해두는 게 좋다. 토큰을 잘못 넣으면 엉뚱한 계정의 터널에 커넥터가 붙는다.
그리고 토큰은 그 터널의 전체 권한을 가진 비밀값이다. compose 파일에 직접 쓰지 말고 .env로 빼고, Git에는 올리지 않는다. 유출되면 대시보드에서 터널을 지우고 다시 만드는 게 가장 빠르다.
정리
세 덩어리에서 하나씩 건진 것.
- 이관: 계정 간 이동은 등록만 옮긴다. DNS 레코드는 전부 사라지므로 옮기기 전에 Export 해둔다.
- 메일: MX는 받는 것, SPF/DKIM/DMARC는 보내는 것. SPF는 한 줄만, DKIM은 도메인마다 새로 발급.
- 터널: primary/backup은 없다. 전부 액티브-액티브다. 꺼둔 백업은 백업이 아니고, 켜기 전에 모든 커넥터가 모든 오리진에 닿는지 확인해야 한다.
마지막 것이 제일 값졌다. 동작하고 있는 시스템은 점검할 이유가 잘 안 생기는데, 이번엔 다른 작업을 하다가 우연히 들여다본 덕에 알았다. "이중화 해뒀음"이라고 적어둔 것들 중에, 실제로 페일오버를 테스트해본 게 몇 개나 되는지 한 번쯤 세어볼 만하다.
함께 읽기
- DNSSEC을 끄라고 해서 알아봤다: DNS에 서명을 붙인다는 것도메인을 다른 계정으로 옮기려는데 조건 목록에 이런 게 있었다.
- Cloudflare Tunnel로 Vercel을 연결하면 안 되는 이유Vercel 배포는 성공했습니다. 프로젝트명.vercel.app 으로 접속하면 잘 뜹니다.
- 배포하면 CSS가 404: stale-while-revalidate 함정"모바일에서 사이트가 이상해요."
- 캐시 퍼지 순서: 오리진을 먼저 비워야 하는 이유엣지 캐시를 켜고 나니 대가가 하나 생겼습니다. 글을 고쳐 발행해도 최대 5분 동안 옛 내용이 나갔습니다. 캐시가 없던 때는 즉시 반영되던 일이라 후퇴처럼 느껴졌습니다.
- Cloudflare가 캐시하지 않는 3가지 이유: Vary, RSC, 언어 감지사이트를 Cloudflare 뒤에 두고 캐시 헤더를 제대로 붙였는데도 CF-Cache-Status가 계속 DYNAMIC으로 나오는 일을 겪었습니다. 한 번이 아니라 세 번이었고, 매번 다른 문제처럼 보였습니다.