Cloudflare 공식 포스트모템 헤더 — 출처: Cloudflare
2026년 2월 20일, 인터넷의 상당 부분을 떠받치는 회사가 자기 고객들의 IP 주소를 인터넷에서 지웠습니다. 공격도, 하드웨어 고장도, 케이블 절단도 아니었습니다. 원인은 API 호출 한 줄에 값이 빠진 것이었고요.
Cloudflare가 이 사건의 포스트모템을 공개했는데, 장애 회고 문서로 드물게 솔직하고 구체적입니다. 문제의 코드를 그대로 싣고, 스테이징에서 왜 못 걸렀는지, 왜 복구가 6시간이나 걸렸는지까지 적었습니다. 읽어 볼 가치가 있어서 정리했습니다.
BYOIP가 뭐길래
BYOIP(Bring Your Own IP)는 말 그대로 자기 IP 대역을 가져와 쓰는 서비스입니다. 보통 CDN을 쓰면 그 회사가 주는 IP를 쓰는데, 이미 자기 IP 대역을 보유한 기업(금융사·통신사·대기업)은 그 주소를 그대로 유지하고 싶어 합니다. 방화벽 화이트리스트, 파트너사 연동, 규제 신고 문서에 그 IP가 이미 박혀 있으니까요.
Cloudflare 공식 로고 — 출처: Cloudflare
그래서 고객이 IP 대역을 맡기면 Cloudflare가 대신 광고(advertise)해 줍니다. 여기서 광고는 마케팅이 아니라 BGP(Border Gateway Protocol)로 “이 IP 대역은 우리 쪽으로 보내라”고 전 세계 네트워크에 알리는 걸 말합니다. 인터넷의 길 안내 체계죠.
핵심은 이겁니다 — 광고를 멈추면 그 IP는 인터넷에서 사라집니다. 서버는 멀쩡히 돌고 있어도 아무도 찾아오는 길을 모르게 됩니다.
무슨 일이 있었나
Cloudflare는 BYOIP 프리픽스를 제거하는 수동 작업을 자동화하는 중이었습니다. 고객이 “이 대역 이제 안 씁니다” 하면 엔지니어가 손으로 처리하던 걸, 주기적으로 도는 정리 작업으로 바꾸는 일이었죠.
그 정리 작업이 API를 이렇게 호출했습니다.
장애를 일으킨 클라이언트 호출과 서버 구현 — 출처: Cloudflare
클라이언트는 삭제 대기 중인 프리픽스만 달라는 뜻으로 pending_delete 를 붙였습니다. 그런데 값을 주지 않았습니다. 서버 쪽 구현은 그 값을 꺼내 빈 문자열이 아닐 때만 “삭제 대기 목록”을 반환하도록 돼 있었고요.
값 없이 붙은 파라미터는 빈 문자열로 읽힙니다. 조건이 어긋나면서 서버는 그 분기로 들어가지 않았고, 삭제 대기분이 아니라 전체 BYOIP 프리픽스 목록을 돌려줬습니다. 그리고 정리 작업은 돌려받은 목록을 전부 “삭제 대상”으로 간주했습니다.
값 없는 파라미터와 아예 없는 파라미터를 구분하지 못한 것이 사고의 전부입니다
Go의 쿼리 파싱에서 ?pending_delete 와 ?pending_delete= 는 둘 다 빈 문자열을 돌려줍니다. 파라미터를 안 보낸 것과 비워서 보낸 것이 코드에서 똑같이 보인다는 뜻이고, 그래서 클라이언트가 의도한 “켜짐”이 서버에는 “꺼짐”으로 전달됐습니다.
정리 작업은 프리픽스만 지운 게 아니라 그에 딸린 서비스 바인딩까지 지워 나갔습니다. 나중에 복구가 길어진 이유가 바로 여기 있습니다.
6시간의 타임라인
결함 병합에서 복구 완료까지의 타임라인 — 출처: Cloudflare 공식 발표 기반 자가 렌더
가장 먼저 눈에 띄는 건 결함 코드가 15일 동안 잠복해 있었다는 점입니다. 2월 5일에 병합돼 2월 20일에야 배포됐습니다. 코드 리뷰와 병합 시점의 검증을 통과한 뒤 보름이 지나 터진 셈이라, 배포 당시엔 아무도 그 변경을 주목하고 있지 않았을 겁니다.
| 시각 (UTC) | 무슨 일이 |
|---|---|
| 02-05 21:53 | 결함이 있는 하위 작업 코드 병합 |
| 02-20 17:46 | 해당 코드를 포함한 Addressing API 릴리스 배포 완료 |
| 02-20 17:56 | 하위 작업 실행 시작 · 프리픽스 철회 개시 |
| 02-20 18:13 | 1.1.1.1 웹사이트 실패로 Cloudflare 인지 |
| 02-20 18:18 | 내부 인시던트 선언 |
| 02-20 18:21 | Addressing API 담당 팀 호출 |
| 02-20 18:46 | 원인 식별 · 하위 작업 강제 종료 |
| 02-20 19:19 | 고객 대상 대시보드 자가 복구 안내 |
| 02-20 20:30 | 바인딩이 남아 있던 프리픽스 복구 릴리스 완료 |
| 02-20 21:08 | 전역 머신 설정 롤아웃 시작 |
| 02-20 23:03 | 복구 완료 |
철회가 진행된 건 17:56부터 18:46까지 50분이고, 그 사이 프리픽스 1,100개가 사라졌습니다. 전체 BYOIP 프리픽스 4,306개의 25%입니다. 나머지가 무사했던 건 설정 변경이 한 번에 전체로 퍼지지 않고 점진적으로 적용되던 중이었기 때문입니다. 전부 동시에 적용됐다면 규모는 네 배가 됐을 겁니다.
(주: Cloudflare는 총 장애 시간을 6시간 7분으로 밝혔는데, 공개된 타임라인의 시작·종료 시각과는 한 시간가량 차이가 있습니다. 본문 시각은 타임라인 표를 따랐습니다.)
감지가 아니라 우연에 가까웠다
철회가 시작된 게 17:56, Cloudflare가 알아챈 게 18:13입니다. 17분이 걸렸고, 알아챈 경로가 고객 신고도 자체 경보도 아니었습니다. 자기네 공개 DNS 리졸버 사이트인 one.one.one.one이 죽으면서 알게 됐습니다.
여기엔 묘한 지점이 있습니다. 1.1.1.1의 DNS 해석 기능 자체는 멀쩡했습니다. 망가진 건 웹사이트 쪽이었고, 방문자는 403 Edge IP Restricted 오류를 봤습니다. 즉 자사 서비스가 BYOIP를 쓰고 있던 덕분에 우연히 카나리아 역할을 했습니다.
바꿔 말하면 대량 철회 자체를 감시하는 장치가 없었습니다. 프리픽스가 분당 수십 개씩 사라지는데 그걸 이상 신호로 잡는 경보가 없었던 겁니다.
고객이 겪은 증상 — BGP Path Hunting
경로가 사라지면 연결이 즉시 끊기지 않습니다. 대신 경로 사냥(Path Hunting)이 일어납니다.
목적지로 가는 경로가 사라진 네트워크 개념 씬 — 출처: 개념 컷 — agy 자가 생성
한 경로가 철회되면 라우터들이 아직 남아 있는 대체 경로를 차례로 시도합니다. 실제로는 어디에도 목적지가 없는데, 네트워크는 그걸 모른 채 이 경로 저 경로를 헤맵니다. 사용자 입장에서는 즉시 실패가 아니라 한참 멈춰 있다가 타임아웃입니다. 진단하기 가장 나쁜 형태의 장애죠.
영향을 받은 제품은 BYOIP로 광고되던 전부입니다.
| 제품 | 증상 |
|---|---|
| Core CDN·보안 | 트래픽이 Cloudflare로 유입되지 않아 접속 실패 |
| Spectrum | BYOIP 기반 앱의 프록시 실패 |
| Dedicated Egress | 지정 IP로 나가는 트래픽 송출 불가 |
| Magic Transit | 보호 대상 애플리케이션이 인터넷에 광고되지 않음 |
왜 복구에 4시간이 더 걸렸나
원인은 18:46에 잡혔습니다. 그런데 완전 복구는 23:03이었습니다. 원인을 알고도 4시간 17분이 더 걸린 겁니다.
이유는 피해가 한 가지 상태가 아니었기 때문입니다. 정리 작업이 프리픽스와 딸린 객체를 순서대로 지우던 중에 멈췄으니, 고객마다 어디까지 지워졌는지가 달랐습니다.
| 상태 | 무엇이 지워졌나 | 복구 방법 |
|---|---|---|
| ① | 광고만 철회 | 고객이 대시보드에서 토글하면 즉시 복구 |
| ② | 광고 철회 + 일부 바인딩 삭제 | 일부만 토글 가능, 나머지는 추가 조치 |
| ③ | 광고 철회 + 전체 바인딩 삭제 | 토글할 대상 자체가 없음 — 전역 설정 배포 필요 |
③번이 문제였습니다. 서비스 바인딩이 전부 지워진 고객은 대시보드에 켤 스위치가 남아 있지 않았습니다. 이들을 되살리려면 Cloudflare 엣지의 모든 머신에 설정을 다시 뿌려야 했고, 그게 21:08에 시작해 23:03에 끝난 작업입니다.
철회된 프리픽스의 복구 경로별 규모 — 출처: Cloudflare 공식 수치 기반 자가 렌더
숫자로 보면 1,100개 중 800개는 고객이 스스로 되살렸고, 300개는 엔지니어가 손으로 끌어올려야 했습니다. 자가 복구가 가능했던 800개조차 19:19에 안내가 나간 뒤의 이야기라, 고객이 상황을 파악하고 대시보드에 들어가는 시간은 별도로 들었습니다.
Cloudflare는 여기서 자기네 구조적 약점을 그대로 인정합니다 — 고객 설정을 담는 데이터베이스와 운영에 쓰는 데이터베이스가 같은 것이라, 잘못된 상태가 되면 롤백할 중간 계층이 없다는 것입니다.
스테이징은 왜 못 걸렀나
이 대목이 가장 뼈아프고, 동시에 가장 배울 게 많습니다. Cloudflare의 설명은 두 가지입니다.
첫째, 스테이징 데이터가 프로덕션을 충분히 닮지 않았습니다. 목(mock) 데이터로는 이 상황이 재현되지 않았습니다.
둘째가 진짜입니다.
테스트는 사람이 하는 경로만 검증했고, 작업 러너가 스스로 도는 경로는 검증하지 않았습니다
엔지니어들은 고객이 셀프서비스 API로 프리픽스를 지우는 흐름을 정확히 테스트했고 통과했습니다. 검증하지 않은 건 task-runner가 사람의 입력 없이 독자적으로 사용자 데이터를 바꾸는 시나리오였습니다. 자동화를 도입하면서 자동화가 스스로 도는 경로는 테스트 범위 밖에 있었던 겁니다.
수동 작업을 자동화할 때 흔히 빠지는 함정입니다. 기존 경로(사람이 누른다)는 잘 테스트하는데, 새로 생긴 경로(기계가 알아서 돈다)는 “같은 API를 쓰니까 같겠지” 하고 넘어갑니다. 다른 점은 사람은 결과를 보고 멈추지만 기계는 안 멈춘다는 것이고요.
가장 아픈 아이러니
이 변경이 무슨 작업의 일부였는지가 이 회고의 백미입니다.
Cloudflare는 Code Orange: Fail Small이라는 안정성 개선 프로그램을 돌리고 있었습니다. 큰 사고를 겪은 뒤 “실패하더라도 작게 실패하자”는 기조로 시작한 것이고, 세 갈래입니다.
| 갈래 | 내용 |
|---|---|
| 1 | 모든 설정 변경도 소프트웨어 릴리스처럼 단계적으로 배포한다 |
| 2 | 장애 중에도 시스템에 접근할 수 있도록 순환 의존성을 제거한다 |
| 3 | 예외 상황을 포함한 모든 조건에서 동작이 정의돼 있게 만든다 |
그리고 이번에 문제가 된 변경은 1번 갈래에 속하는 작업이었습니다. 위험한 수동 작업을 안전한 자동 워크플로로 바꾸려던 것이었죠.
안전하게 만들려던 변경이 장애를 냈습니다. Cloudflare도 이걸 피해 가지 않고, 개선 조치가 완전히 배포되기 전에 변경이 나갔다고 적었습니다. 그러면서 “이번 장애는 전역은 아니었지만 파급 범위가 용납할 수 없이 컸다”고 덧붙입니다.
무엇을 고치기로 했나
Cloudflare가 약속한 개선은 셋입니다. 남의 회고지만 우리 시스템에 그대로 대입해 볼 만한 항목들입니다.
| 개선 | 내용 | 우리 쪽에 옮기면 |
|---|---|---|
| API 스키마 표준화 | 플래그를 문자열로 해석해 값을 판단하기 어려웠던 구조를 고친다 | 불리언은 불리언 타입으로. 값 없는 플래그를 참으로 쓰지 않는다 |
| 운영 상태와 설정 상태 분리 | DB를 스냅샷으로 떠서 헬스 지표에 따라 배포하고, 문제 시 즉시 되돌린다 | 설정도 배포물이다 — 버전과 롤백 대상이 있어야 한다 |
| 대량 철회 차단 | 변경이 너무 빠르거나 넓게 일어나면 배포를 멈추는 서킷 브레이커 | “정상적으로 이만큼 바뀔 리 없다”는 상한을 코드에 박아 둔다 |
세 번째가 가장 값싸고 효과가 큽니다. 50분 동안 1,100개가 사라지는 동안 아무것도 막지 않았다는 게 이번 장애의 핵심이니까요. 정상 운영에서 프리픽스가 분당 20개씩 사라질 일이 없다면, 그 속도를 넘는 순간 멈추는 장치 하나로 피해를 몇십 개 선에서 끊을 수 있었습니다.
남는 교훈
남의 장애를 읽는 이유는 우리 시스템에서 같은 모양을 찾기 위해서입니다. 이 건에서 옮길 만한 건 넷입니다.
값 없는 플래그를 신뢰하지 않는다. 있음/없음/비어 있음이 코드에서 같아 보이는 순간이 사고 지점입니다. 불리언은 명시적으로 =true 를 받게 만듭니다.
자동화의 경로를 따로 테스트한다. 사람이 누르는 흐름을 테스트했다고 기계가 도는 흐름을 테스트한 게 아닙니다. 사람은 이상하면 멈추지만 스케줄러는 안 멈춥니다.
삭제에는 속도 제한을 건다. 만드는 건 몰라도 지우는 작업에는 “한 번에 이 이상은 안 됨” 상한이 있어야 합니다. 리뷰로는 못 막고 실행 시점에 막아야 합니다.
설정도 배포물로 다룬다. 코드는 단계 배포하고 롤백하면서 설정은 즉시 전역 반영하는 구조가 흔합니다. 이번 장애는 정확히 그 틈에서 났습니다.
마지막으로 하나 더. Cloudflare는 문제의 코드를 그대로 공개했습니다. 원인을 뭉뚱그리지 않고, 스테이징이 왜 부족했는지, 왜 원인을 알고도 4시간이 더 걸렸는지까지 적었습니다. 장애는 어디서나 나지만 이렇게 쓰인 회고는 드뭅니다. 그 자체가 참고할 만한 대목입니다.