RSS듀오랩스
웹 인프라

Cloudflare 존 이전과 R2: 커스텀 도메인이 따라오지 않는 이유

작성자
듀오랩스 대표·8분 읽기

이미지가 하나도 안 나온다는 말을 듣고 열어 봤습니다. 이미지 원본을 서빙하는 호스트 세 개가 DNS 에서 통째로 사라져 있었습니다. dig 가 아무것도 돌려주지 않았습니다.

이틀 전에 도메인 존을 다른 Cloudflare 계정으로 옮긴 작업이 있었습니다. 그 작업 문서에는 이런 경고까지 적혀 있었습니다. "존을 추가하면 Cloudflare 가 옛 레코드를 스캔해 가져오는데, 프록시된 이름은 오리진이 아니라 엣지 IP 가 잡힌다. 지우고 R2 쪽에서 커스텀 도메인을 다시 연결해야 한다." 경고를 적은 사람도 저였고, 지우기만 하고 다시 연결하지 않은 사람도 저였습니다.

코드가 초록인 고장의 특징

이틀 동안 아무도 몰랐습니다. 이유는 단순합니다. 끊어진 곳이 DNS 한 군데였고, 나머지가 전부 멀쩡했기 때문입니다.

빌드가 성공했습니다. 타입 체크가 통과했습니다. 배포가 초록이었습니다. 앱은 정상적으로 이미지 URL 을 만들어 냈고, 그 URL 이 가리키는 주소에 아무것도 없었을 뿐입니다. 서버 로그에도 남지 않습니다. 이미지를 가져가는 것은 사용자의 브라우저이지 우리 서버가 아니니까요.

이런 고장은 "실패"로 보고되지 않습니다. 그냥 조용히 없어집니다.

커스텀 도메인이 존이 아니라 계정에 묶인다는 것

레코드만 다시 붙이면 될 줄 알았습니다. 시작할 때 저는 버킷과 존이 같은 계정에 있다고 보고 있었습니다. 아니었습니다.

새로 발급한 API 토큰으로 존이 속한 계정의 R2 를 조회하니 이렇게 답했습니다.

10042 Please enable R2 through the Cloudflare Dashboard.

그 계정에는 R2 가 켜져 있지도 않았습니다. 버킷은 전부 옛 계정에 남아 있었고, 새 토큰으로는 Authentication error 였습니다. 존만 이사를 갔고 스토리지는 그대로였던 겁니다.

여기서 R2 커스텀 도메인의 제약이 문제가 됩니다. 버킷과 존이 같은 계정 안에 있어야 붙습니다. 존을 옮기는 순간 그 존의 이름을 쓰던 R2 커스텀 도메인은 되살릴 방법이 없어집니다. DNS 레코드가 지워진 것이 문제가 아니라, 다시 만들 자격이 사라진 것이 문제였습니다.

버킷을 새로 만드는 쪽이 싼 이유

선택지는 둘뿐이었습니다.

존을 옛 계정으로 되돌리는 쪽은 네임서버 재위임부터 다시 해야 하고, 그 위에 얹어 둔 Vercel 도메인과 터널 설정을 통째로 무르는 일입니다. 이틀 전 작업을 되감는 셈입니다.

버킷을 새 계정에 다시 만드는 쪽은 객체를 옮기면 됩니다. 세어 보니 두 버킷을 합쳐 객체가 2개였습니다. 출시 전이라 실제 사용자 업로드가 없었고, 남아 있던 것은 테스트로 올린 PNG 두 장이었습니다.

데이터가 없다는 사실이 결정을 바꿨습니다. 이전 비용이 0이면 "옮긴다"와 "새로 만든다"는 같은 말이 됩니다. 그래서 새 계정에 버킷을 만들고 커스텀 도메인을 붙였습니다. PNG 두 장은 버렸습니다.

재구축하다 드러난, 원래 있던 세 가지 구멍

새로 만드는 김에 티어를 갈랐습니다. 개발, 스테이징, 운영이 각각 공개 버킷과 비공개 버킷을 하나씩 갖고, 토큰도 티어마다 따로 발급해 자기 버킷 두 개로만 스코프를 좁혔습니다.

그 과정에서 DNS 와 무관한 고장 세 개가 같이 나왔습니다. 전부 원래부터 있던 것들입니다.

첫째, 개발 키로 운영 버킷이 열렸습니다. 한 벌의 키가 계정의 모든 버킷을 볼 수 있었습니다. 개발 환경에서 운영 버킷의 객체 목록이 그대로 조회됐습니다. 검증 스크립트에 "남의 버킷은 막혀야 한다"를 넣고 나서야 눈에 들어왔습니다.

둘째, 운영 배포가 자기 이미지 호스트를 거절하고 있었습니다. Next 의 이미지 최적화기는 remotePatterns 에 없는 호스트를 400 으로 막습니다. 그런데 이 목록은 빌드 시점에 굳습니다. 운영 빌드는 18일 전 것이었고, 그때 박힌 호스트가 지금 쓰는 호스트가 아니었습니다. DNS 가 죽기 전부터 운영은 이미 이미지를 못 띄우고 있었던 겁니다.

셋째, 개발 티어는 그 문제가 더 근본적이었습니다. 도커 빌드가 클라이언트 번들용 변수만 빌드 인자로 받고, 이미지 호스트 값은 런타임에만 주입하고 있었습니다. 빌드 때는 그 값이 비어 있으니 코드의 폴백인 운영 호스트가 박힙니다. 그래서 개발 환경이 자기가 만든 URL 을 자기가 거절했습니다.

dev 앱 ← dev 이미지 호스트   "url" parameter is not allowed
dev 앱 ← 운영 이미지 호스트  404 (허용됨)

두 번째 줄이 진단이었습니다. 운영 호스트가 허용된다는 것은 폴백이 박혔다는 뜻이니까요. 온디맨드 이미지 변환으로 구조를 바꾼 시점부터 줄곧 이 상태였습니다.

검사가 세 번 다 통과했는데 값이 전부 같았던 일

이 작업에서 가장 부끄러운 대목입니다.

티어별로 왕복을 검사하는 스크립트를 먼저 만들었습니다. 자격증명을 주입받아 실제로 올리고, 공개 도메인으로 내려받아 바이트 해시를 대조하고, 지웁니다. 세 티어에 대해 돌렸고 세 번 다 "이상 없다"가 나왔습니다.

그런데 출력을 다시 보니 세 번 모두 같은 버킷 이름이 찍혀 있었습니다. 비밀 저장소의 스테이징과 운영 환경에 개발 값이 그대로 들어가 있었던 겁니다. 스크립트는 주입된 값이 스스로 일관적인지만 봤고, 그 값이 이 티어의 것인지는 묻지 않았습니다.

그대로 배포처에 반영했다면 운영 앱이 개발 버킷에 사용자 사진을 쓰게 됩니다. 되는 것처럼 보이고, 한참 뒤에야 알게 되는 종류입니다.

고친 방법은 스크립트에 티어를 선언하게 하는 것이었습니다.

npm run check:r2 -- prod

주입된 버킷 이름과 공개 호스트가 그 티어와 맞지 않으면 검사를 시작하기 전에 멈춥니다. 사람이 출력을 눈으로 훑어서 잡은 것을 스크립트가 잡게 만든 것뿐이지만, 눈은 세 번째쯤부터 놓칩니다.

그리고 하나 더 놓쳤습니다. 새로 만든 버킷에 CORS 규칙이 없었습니다. 업로드는 브라우저가 presigned URL 로 R2 에 직접 하는 구조라, 규칙이 없으면 업로드가 통째로 막힙니다. 서버 로그에는 아무것도 안 남고 브라우저 콘솔에만 뜹니다. 스테이징에서 사진을 붙여 보다 발견했습니다. 옛 버킷의 규칙은 누가 언제 대시보드에서 넣었는지 아무도 몰랐습니다.

코드 대신 검사를 남긴 이유

고친 것을 세어 보면 다섯 개인데, 그중 넷은 아무도 모르고 있던 것이었습니다.

고장 언제부터 어떻게 발견했나
이미지 호스트 DNS 소멸 존 이전 시점 사용자가 "이미지가 안 나온다"
버킷과 존이 다른 계정 존 이전 시점 커스텀 도메인을 붙이려다
개발 키가 운영 버킷을 읽음 처음부터 검증 스크립트의 스코프 검사
빌드에 박힌 이미지 호스트 불일치 온디맨드 변환 전환 이후 HTTP 프로브
새 버킷에 CORS 없음 재구축 당일 스테이징에서 업로드하다

공통점이 하나 있습니다. 전부 코드는 정상이고 동작만 죽는 종류입니다. 코드 리뷰로 잡히지 않고, 타입 체커가 모르고, 빌드가 통과하고, 배포가 초록입니다.

그래서 이번에 리포에 남긴 것은 기능이 아니라 검사 두 개입니다. 하나는 티어별 왕복 검사이고, 다른 하나는 CORS 규칙의 선언을 코드에 두고 실제 버킷과 대조하는 스크립트입니다. 대시보드에만 사는 설정은 재생성할 때 반드시 유실됩니다. 이번에 그것을 직접 겪었으니까요.

마지막으로 남는 교훈은 순서에 관한 것입니다. 존을 옮기기 전에 세어야 할 것은 DNS 레코드가 아니라 그 이름을 쓰는 서비스였습니다. 레코드는 옮기면 따라오지만, R2 커스텀 도메인처럼 계정에 묶인 것은 존만 옮겨서는 따라오지 않습니다. 그 차이를 이사 전에 알았다면 이틀은 벌었을 겁니다.

마지막 수정:

공유하실 때는 출처(Duolabs)와 원문 주소를 표시해 주세요.