필터#Slack×

[DR] DR 아키텍처, 왜 이렇게 설계했나.. (DR 테스트 글 후속편)

지난 글(서울 리전 장애 유발로 도쿄 리전 활성화 하기)에서는 실제로 서울 리전에 장애를 일으켜보고 도쿄로 전환되는 과정을 지켜본 페일오버 테스트 결과를 정리했다. 이번 글은 그 결과물이 "무엇으로 이루어져 있는가"가 아니라 "왜 하필 이런 구성이었는가"를 다룬다. 돌아보면 이 DR 구성의 서비스 하나하나는 따로따로 고른 게 아니었다. 앞 단계에서 생긴 문제를 풀다 보니 다음 서비스가 자연스럽게 필요해지는 흐름이었다. 서울이 통째로 죽을 수 있다는 문제가 도쿄에 사본을 두자는 결론으로 이어지고, 사본이 생기니 트래픽을 어떻게 그쪽으로 보낼지가 문제가 되고, 자동 전환을 만들다 보니 딱 한 곳(DB 승격)만 사람 손을 거치게 남겨야 했고, 그 수동 판단이 실제 테스트에서 발목을 잡는 걸 보고서야 "판단을 도와주는 자동화"가 필요해졌다. 이 글은 그 흐름을 5단계로 나눠 살펴본다. 1단계 — "다른 리전에 사본이 있어야 한다" 문제: 서울(apnortheast2) 리전이 AZ 장애 확산이나 네트워크 단절 같은 이유로 통째로 마비될 수 있다. 단일 리전 구조에서는 이 순간 서비스가 전면 중단된다. 결론: 물리적으로 격리된 다른 리전에 실행 가능한 사본을 둬야 한다 — 도쿄(apnortheast1)리전을 선택했다. 여기서부터 "서울에 있는 구성요소 하나하나를 도쿄에 어떻게 복제할까"라는 질문이 이어진다. 그리고 데이터의 성격(상태 데이터인가, 언제든 다시 만들 수 있는 파생 데이터인가)에 따라 복제 방식이 갈린다는 게 이 단계의 핵심 판단이다. 서울 구성요소 복제가 필요한 이유 선택된 서비스 왜 이 서비스인가 앱 실행 환경 서울과 동일한 방식으로 돌아가야 배포 파이프라인을 이원화하지 않는다 EKS 서울도 EKS이므로 같은 Helm 차트·같은 매니페스트 구조를 그대로 재사용 가능 트래픽 진입점 사용자 요청을 받아 파드로 연결 ALB HTTPS 종료 + K8s Target Group과 자연스럽게 연동 데이터베이스 DB는 "상태"라서 단순 스냅샷이 아니라 계속 최신을 따라가야 하고, 필요 시 독립 DB로 전환 가능해야 함 RDS CrossRegion Read Replica 지속적 복제 + 승격(promote) 한 번으로 독립 primary 전환이 가능한 구조가 요구에 정확히 부합 이미지·정적 파일 마찬가지로 상태 데이터 S3 리전 간 복제(CRR) S3가 리전 간 복제를 자체 기능으로 이미 제공 — 별도 구축 불필요 컨테이너 이미지 도쿄에서도 이미지를 pull 할 수 있어야 파드가 뜸 ECR 리전 간 복제 위와 동일한 논리 — 레지스트리 자체 복제 기능 활용 캐시 캐시는 상태가 아니라 언제든 재생성 가능한 파생 데이터 ElastiCache — 의도적으로 복제 안 함 상시 복제 비용을 낼 가치가 없다는 비용 대비 효익 판단 이 단계의 원칙을 한 줄로 줄이면 이렇다. 상태를 가진 것은 전부 복제하고, 언제든 다시 만들 수 있는 것(캐시)은 복제하지 않는다. 이 구분이 이후 모든 복제 전략의 비용 판단 기준이 된다. 2단계 — "사본은 있는데, 사용자를 어떻게 그리로 보내나" 문제: 도쿄에 동일한 인프라가 있어도, 사용자 요청이 자동으로 그쪽으로 흐르지 않으면 무용지물이다. 결론: DNS 레벨에서 "서울 응답 없음 → 도쿄로" 자동 전환이 필요하다. 구성요소 역할 Route 53 Failover Routing 도메인의 Primary는 서울, Secondary는 도쿄로 설정. 헬스체크 실패 시 자동으로 응답을 도쿄 ALB로 전환 Route 53 헬스체크 서울 ALB가 살아있는지 주기적으로 확인 CloudWatch 알람 + 알림 헬스체크 실패를 사람에게도 알림 — 자동 전환은 일어나지만, 그 사실을 사람이 인지해야 다음 판단(DB 승격 등)을 시작할 수 있음 왜 이 구간만 "완전 자동화"로 설계됐는가? 3단계에서 다룰 DB 승격과 달리, 이 구간은 유일하게 사람 개입 없이 전부 자동으로 설계된 부분이다. 이유는 명확하다: DNS 응답을 바꾸는 행위는 되돌리기 쉽고(가역적), 데이터를 전혀 건드리지 않는다. 잘못 전환돼도 다시 정상으로 돌아가면 그만이라 리스크가 낮다 — 그래서 자동화의 비용 대비 이득이 확실한 구간이다. 3단계 — DB 승격만 수동으로 남긴 이유, 그리고 그것이 낳은 문제 초기 설계는 DB 승격(promote)을 명시적으로 "수동, 운영자 판단 후 실행" 으로 못박았다. 인프라와 트래픽은 자동화하면서 이 한 지점만 수동으로 남긴 이유는, 2단계와 정반대의 리스크 구조 때문이다. Route53 트래픽 전환 (자동) DB 승격 (수동으로 시작) 되돌릴 수 있는가 가역적 — DNS 응답만 바뀜 비가역 — 한 번 승격하면 되돌릴 수 없음 잘못되면 일시적 오탐, 다시 정상 응답하면 자동 복구 splitbrain — 서울·도쿄가 동시에 쓰기 가능한 상태가 되어 데이터 정합성이 깨짐 설계 원칙 "빠른 게 최우선" "틀리지 않는 게 최우선" 그런데 "수동"이 새로운 문제를 낳았다 "수동"이라는 말은 곧 내가 항상 판단할 준비가 되어 있어야 한다는 뜻이었다. 이건 막연한 걱정이 아니라 실제 페일오버 테스트를 해보면서 몸으로 느낀 문제였다. 트래픽이 도쿄로 넘어간 시점과 내가 실제로 DB 승격 명령을 누른 시점 사이에 약 2분의 공백이 있었는데, 그 사이에 들어온 쓰기 요청은 도쿄 RDS가 아직 read replica라 실패했을 수 있겠다 싶었다. "판단은 도와주되, 거기 걸리는 시간은 줄이고 싶다" — 이 생각이 다음 단계에서 다룰 승인 자동화 워크플로를 만들게 된 직접적인 계기였다. 테스트가 끝나자마자 바로 이 워크플로를 만들기 시작했다. 4단계 — "판단은 돕되, 방아쇠는 사람이 당긴다" 3단계의 요구를 그대로 풀어보면 이렇다. "사람이 판단할 재료(위험도 해석)는 자동으로 마련해주되, 최종 실행(승격)의 방아쇠는 여전히 사람이 당기게 한다." 이 요구 하나에서 아래 서비스 선택이 전부 파생된다. ① 서버리스(Lambda) — 왜 상시 서버가 아닌가.. 이 파이프라인은 평소엔 1분에 한 번 상태를 확인하는 것 외엔 거의 동작하지 않는다. 장애라는 이벤트가 터질 때만 깨어나면 되는 워크로드라, 상시 서버를 띄워두면 대부분의 시간을 낭비하게 된다. 기능을 진단·탐지·차단·승격·전환·검증·검수 등 여러 개의 작은 단위로 쪼갠 이유도 있다 — 각 단위에 필요한 최소 권한만 부여하기 위해서다. 예를 들어 "차단" 역할은 DB 접근 제어 권한만, "승격" 역할은 DB 승격 권한만 갖는다 — 하나가 뚫려도 피해가 그 역할의 권한 범위 안으로 제한된다. ② 워크플로 오케스트레이션(Step Functions) — 왜 함수끼리 직접 호출하지 않았나.. 문제 1 — 긴 대기: 사람 승인을 최대 15분까지 기다려야 하는데, 서버리스 함수는 실행 시간 제한이 있고 대기 상태를 함수 자신이 붙들고 있으면 그 시간만큼 과금된다. 해결: 워크플로가 토큰을 발급하고 실행을 "일시정지" 상태로 두면, 외부 이벤트(Slack 버튼 클릭)가 토큰을 반납할 때까지 과금 없이 대기할 수 있는 패턴을 활용했다. 문제 2 — 회고 데이터 확보: "복구까지 몇 분 걸렸나"를 측정하려면 각 단계 소요시간을 어딘가에 기록해야 한다. 해결: 워크플로 오케스트레이션 서비스는 상태 전환마다 자동으로 타임스탬프를 실행 기록에 남긴다. 별도 계측 코드 없이 이 기록을 읽기만 하면 되므로, 사후 회고 보고서가 거의 공짜로 구현될 수 있었다. ③ 별도 상태 저장소(DynamoDB) — 왜 RDS가 아니라 별도 저장소인가.. 문제: "지금 진짜 주인(primary)이 어디냐"를 기록할 저장소가 필요하다. 그런데 이걸 RDS(서울 혹은 도쿄)에 기록하면, 장애의 당사자가 될 수도 있는 그 DB에, 장애 상태를 기록하는 시스템이 의존하는 순환 논리가 생긴다. 해결: RDS와 완전히 독립된, 별도로 가용성이 높은 저장소가 필요했다. 선택된 서비스는 서버리스·고가용성이면서 조건부 쓰기(동시에 같은 값을 쓰려는 시도 중 하나만 성공시키는 방식)를 기본 지원한다. 이게 "동시에 두 곳에서 같은 승격을 시도하지 못하게 막는" 잠금 장치를 구현하는 데 정확히 필요한 기능이었다. 부가적으로, Slack 버튼에는 담을 수 있는 글자 수 제한이 있어서 워크플로의 긴 대기 토큰을 직접 넣을 수 없었다. 그래서 짧은 임시 키로 바꿔 이 저장소에 보관하고, 버튼 클릭 시 그 키로 원래 토큰을 조회하는 우회 구조를 썼다. ④ 생성형 AI(Bedrock) — 왜 AI를 끼워 넣었나, 그리고 왜 판단까지는 맡기지 않았나.. 문제: 복제 지연(초)이나 처리 대기량(byte) 같은 원시 수치는 새벽에 깨어난 당직자가 즉시 위험도를 판단하기 어렵다. 해결 — 그러나 역할은 제한적으로: 생성형 AI가 숫자를 사람이 읽을 수 있는 문장(위험도·예상 손실·권고 절차)으로 번역해주는 "설명 계층"으로만 쓰였다. 다만 "AI가 실수하면 시스템이 잘못 움직인다" 는 반대 리스크가 있으므로, 실제 상태 분기(승인 이후 무엇을 할지)는 AI가 아니라 정해진 규칙의 코드가 전담하도록 역할을 분리했다. 검수(사후 점검) 단계에서는 도구를 스스로 골라 쓰는 자율 루프 방식도 썼는데, 검수는 미리 정해둔 체크리스트만으로 다 커버할 수 없는 "이상 탐지형" 조사이기 때문이다. 상황에 따라 "서울을 다시 찔러볼지, DB 상태를 볼지"가 달라져야 하므로, 고정된 절차보다 필요에 따라 확인 방법을 스스로 고르는 방식이 이 요구에 맞았다. ⑤ Slack — 왜 새 UI를 안 만들었나.. 조직이 이미 상시 사용하는 협업 도구를 승인 인터페이스로 재사용했다. 새로운 대시보드나 앱을 따로 만들 필요 없이, 담당자가 휴대폰 알림만으로 즉시 반응(승인/거부)할 수 있다는 실용적인 이유다. API Gateway — 설계 의도가 아니라 제약으로 인한 대체 처음에는 Slack에서 호출할 공개 HTTPS 엔드포인트 하나만 있으면 충분하다고 보고 더 가벼운 방식으로 구현했다. 그런데 조직의 보안 정책이 인증 없는 공개 엔드포인트를 막고 있었고, 그 바람에 Slack에서 오는 요청이 막혀 승인 버튼이 동작하지 않는 문제가 있었다. API 게이트웨이로 앞단을 교체해 해결했는데, 뒤에 붙는 함수가 받는 요청 형식이 동일했던 덕분에 함수 코드는 한 줄도 바꾸지 않고 앞단 배선만 바꿔서 해결할 수 있었다. "공개 HTTPS 엔드포인트가 필요하다"는 원래 설계 의도는 그대로였고, 구현 수단만 조직 제약에 맞춰 바뀐 사례다. 5단계 — "실제 테스트 진행" 실제로 장애를 주입해보는 실험 코드 리뷰나 유닛 테스트로는 "리전 네트워크가 실제로 끊기면 무슨 일이 벌어지는지" 검증할 수 없다. Route53 헬스체크가 정말 반응하는지, 이상 탐지가 정말 작동하는지는 실제로 끊어봐야 안다. 그래서 서울 프라이빗 서브넷의 네트워크를 인위적으로 격리하는 장애 주입 실험(AWS FIS)을 진행했다. 이 실험에 앞서 관측 도구(CloudWatch Container Insights)부터 먼저 갖춰뒀다. 이유는 명확하다 — "지금 도쿄 클러스터가 버티고 있는지 눈으로 볼 수 없으면, 장애를 인위로 만드는 실험 자체가 의미가 없다." 관측 인프라가 먼저 갖춰져야 검증 도구를 쓸 자격이 생기는 순서다. 실제로 걸려 넘어졌던 함정 하나 Route 53은 특정 리전에 속하지 않는 글로벌 서비스다. 그런데 헬스체크를 어느 리전에서 만들었든 그 결과 지표는 항상 미국 동부(버지니아) 리전의 CloudWatch에만 게시된다 — AWS 플랫폼 자체의 고정된 동작이라 설계로 바꿀 수 있는 부분이 아니다. 이 함정에 나도 실제로 걸려 넘어진 적이 있다. 지난 테스트 때 알람을 서울 리전에 만들었다가 지표가 없다는 이유로 Slack 알림이 안 온 적이 있었고, 그 뒤로는 헬스체크 지표를 항상 미국 동부 리전 기준으로 고정해서 확인하도록 코드를 고쳤다. 이 경험을 겪으면서, 내가 설계를 "처음부터 완벽하게 해서 그대로 구현"한 게 아니라 "설계 → 구현 → 실전에서 실패 → 원인 파악 → 교훈을 반영해 재발 방지"를 반복하면서 다듬어왔다는 걸 새삼 느꼈다. 종합 — 하나의 흐름으로 지금까지의 다섯 단계를 하나로 이으면 다음과 같은 단일한 이야기가 된다. 서울이 통째로 죽을 수 있다 → 도쿄에 사본을 둔다(앱·DB·이미지·컨테이너 이미지 복제) → 트래픽은 자동으로 보낸다(DNS 전환, 가역적이라 안전) → 단, DB 승격은 사람이 결정(비가역·정합성 리스크) → 수동 결정이 느려지는 게 실측으로 드러남(공백 발생, RPO 목표 미달) → 판단을 돕는 자동화를 만들되 방아쇠는 사람이 당기게 → 이 자동화가 쓸 독립 저장소를 따로 둔다(순환 의존 방지) → 전체가 진짜 작동하는지 실제로 장애를 주입해본다 핵심 설계 철학 3가지 1. 되돌릴 수 있는지에 따라 자동화 수준을 달리한다 — 되돌리기 쉬운 것(DNS 전환)은 완전 자동, 되돌릴 수 없는 것(DB 승격)은 사람 승인을 반드시 거친다. 2. 판단을 돕는 것과 판단을 대신하는 것을 분리한다 — AI는 설명·조사·권고까지만 하고, 실제 상태 분기는 항상 정해진 규칙의 코드가 담당한다. 3. 설계는 한 번에 완성되지 않는다 — 리전 함정 사례처럼, 실전 테스트에서 발견된 실패를 다음 버전 설계에 그대로 반영하는 과정을 계속 반복했다.

Sep 18, 2026AWS
[DR] DR 아키텍처, 왜 이렇게 설계했나.. (DR 테스트 글 후속편)

[DR] DR 테스트 후기 — 서울 리전 장애 유발로 도쿄 리전 활성화 하기

서비스를 서울(apnortheast2) 단일 리전으로만 운영하고 있으면, 그 리전 전체에 장애가 났을 때(AZ 장애 확산, 네트워크 단절 등) 서비스가 통째로 멈춰버린다. 이걸 대비하기 위해 도쿄(apnortheast1)에 웜 스탠바이(Warm Standby) 인프라를 미리 구축해뒀는데, "구축해뒀다"는 것과 "실제로 장애가 나도 넘어간다"는 것 사이에는 꽤 큰 간극이 있다. 그래서 실제로 서울 리전에 장애를 일으켜보고, 트래픽이 도쿄로 넘어가서 서비스가 계속되는지 눈으로 확인하는 페일오버 테스트를 진행했다. 이 글은 그 테스트 과정과 결과를 정리한 기록이다. 테스트 개요 서울 리전에 장애가 났다고 "가정"하고, 트래픽이 도쿄 리전으로 넘어가 실제로 서비스가 돌아가는지 확인하는 테스트다. 오후 2시 10분에 시작해서 2시 30분에 종료했다. 장애는 AWS FIS(Fault Injection Service)로 서울 프라이빗 서브넷의 네트워크를 인위적으로 끊어서 만들었고, 그 뒤 Route53 헬스체크가 장애를 감지해 도쿄로 전환하는 전 과정을 Grafana(AMG) 대시보드로 지켜봤다. 확인하려던 질문은 여섯 가지였다. 서울 장애를 FIS로 유도할 수 있는가? Route53이 도쿄로 전환되는가? 도쿄 EKS의 애플리케이션이 정상 응답하는가? 도쿄 RDS read replica를 승격(promote)하면 쓰기가 되는가? RTO와 RPO를 어떤 기준으로 잴 것인가? DR 중 도쿄에 쌓인 데이터가 서울에 자동으로 반영되는가? 목표 기준선은 RTO 15분, RPO 60초로 잡았다. RTO는 장애가 시작된 시점부터 서비스가 다시 정상으로 돌아오기까지 걸린 시간이고, RPO는 장애 시점에 유실될 수 있는 데이터의 시간 폭이다. RPO가 60초라는 건 최악의 경우 마지막 60초어치 데이터까지는 잃을 수 있다고 본다는 뜻이다. 왜 웜 스탠바이인가? DR 전략은 크게 복구 속도와 평시 운영 비용의 트레이드오프다. 껐다가 필요할 때 다시 세우는 백업·복원은 저렴하지만 복구가 시간 단위로 걸리고, 반대로 두 리전을 항상 동시에 운영하는 액티브액티브는 거의 즉시 전환되지만 비용이 두 배로 들고 양방향 동기화까지 신경 써야 한다. Farmily는 24시간 서비스라 시간 단위 복구는 받아들이기 어려웠지만, 그렇다고 액티브액티브를 감당할 트래픽 규모도 아니었다. 그래서 축소된 규모로 도쿄를 항상 띄워 두고 분 단위로 복구하는 웜 스탠바이를 선택했다. 대상 리소스 서울이 평상시 트래픽을 받는 active 리전이고, 도쿄가 standby다. 서울에는 prodeks 클러스터, prodalb, prodrds가 있고, 도쿄에는 dreks 클러스터, dralb, 그리고 서울 prodrds의 crossregion read replica인 drrds가 있다. 사용자 트래픽의 진입점은 api.farmily.info 도메인이고, Route53 failover 라우팅으로 PRIMARY는 서울 ALB, SECONDARY는 도쿄 ALB를 가리킨다. 헬스체크는 api.farmily.info의 /api/v1/health 경로를 443 포트로 10초마다 찌르고, 3번 연속 실패하면 장애로 판정한다. 복구 절차 자동화 이번 테스트에는 장애 감지부터 결과 검증까지 이어지는 복구 절차 자동화를 함께 도입해서 돌렸다. EventBridge가 1분마다 서울 상태를 확인하다 이상을 감지하면(DynamoDB로 중복 실행은 막는다) Step Functions가 상태 진단 → (참고용) AI 위험도 분석 → 사람 승인 대기 → 승인 후 DB 승격·트래픽 전환 → 결과 검증까지 이어가는 흐름으로 움직인다. AI는 어디까지나 판단을 돕는 조언자일 뿐이고, 실제 전환은 사람이 Slack에서 승인 버튼을 눌러야 넘어간다. 테스트에 앞서 — 도쿄를 "서비스 가능한" 상태로 인프라(VPC, EKS, RDS Replica 등) 자체는 이미 구축돼 있었지만, 페일오버 테스트에 앞서 도쿄 dreks를 단순 인프라에서 실제로 앱이 도는 환경까지 끌어올려야 했다. 오토스케일링과 운영 애드온(clusterautoscaler, metricsserver 등)을 배치하고, IAM 정책과 Helm values, 네임스페이스 템플릿 같은 앱 배포 전제조건을 준비한 뒤, farmilyapp을 도쿄 dreks에 Helm으로 실제 배포했다. 배포 자체는 성공했지만 앱 파드가 곧바로 Ready 상태가 되지 않았다. CrashLoopBackOff였다. 원인은 앱 코드가 아니라 DR 시크릿의 DB 접속 정보 두 가지였다. 첫 번째는 DB URL 누락이었다. DR 시크릿에는 DBHOST, DBPORT, DBNAME, DBUSER, DBPASSWORD는 있었지만, Spring Boot가 요구하는 jdbc:postgresql:// 형식의 DBURL 키가 빠져 있었다. 'url' must start with "jdbc" 오류로 datasource 생성이 실패한 거였고, jdbc 형식의 URL을 추가해서 해결했다. 두 번째는 DB 사용자명이 어긋난 문제였다. FATAL: password authentication failed for user "farmily" 에러가 떴는데, 처음엔 비밀번호 문제로 보였지만 실제로는 DR RDS의 마스터 계정이 farmily가 아니라 farmilyadmin이었다. 계정명을 실제 RDS 마스터 계정에 맞춰 고치고 나서야 해결됐다. 두 가지를 고친 뒤 시크릿을 강제로 재동기화하고 앱을 재시작하자 정상으로 돌아왔다. DB 커넥션 풀이 drrds에 붙었고, 마이그레이션이 정상적으로 검증됐으며, Deployment가 2/2 Available로 안정됐다. 여기서 얻은 교훈은 세 가지다. 환경변수 키를 여러 군데에 중복으로 넣으면 어디서 병합된 결과인지 추적하기 어려우니 한쪽에만 둘 것, DB 계정명은 반드시 RDS 실제 master username과 맞출 것, 시크릿을 갱신해도 이미 떠 있는 파드의 환경변수는 자동으로 바뀌지 않으니 수정 뒤에는 앱을 재시작할 것. 이렇게 도쿄 앱을 2/2로 띄워 둔 상태가 이어지는 페일오버 테스트의 출발점이 됐다. 측정 결과 요약 지표 측정값 목표 판정 읽기/헬스 RTO 약 5분 29초 RTO 15분 충족 전체 서비스 RTO (쓰기 성공) 약 9분 14초 RTO 15분 충족 보수적 RTO (백업 완료) 약 13분 5초 RTO 15분 충족 RPO (승격 직전 복제 지연) 약 126초(2.10분) RPO 60초 미달 읽기/헬스 RTO는 Route53이 도쿄로 넘어가고 도쿄 ALB가 200을 처음 돌려준 시점까지의 시간이다. 쓰기까지 포함한 전체 서비스 RTO는 도쿄 RDS를 승격해 쓰기가 처음 성공한 시점 기준이며, 백업 완료까지 보수적으로 잡으면 약 13분 5초였다. RPO는 대시보드 카드로 2.10분(126초)이 찍혔다. 목표와 견주면 RTO는 9분 14초로 15분 안에 들어와 충족했지만, RPO는 126초로 60초 목표를 두 배 넘겼다. 복구 속도는 기준을 만족했지만 복제 지연이 목표보다 컸다는 게 이번 테스트의 가장 중요한 결과다. 장애 전환 타임라인 (KST) 시각 이벤트 14:11:55 FIS 네트워크 장애 시작 — 모든 RTO 기준점(T0) 14:12:00 Route53 헬스체크 첫 실패 14:1214:15 헬스체크 0 / 부분 실패 구간 14:16:00 헬스체크 다시 1로 표시 (서울 복구 아님 — 도쿄가 200 응답한 것) 14:17:24 api.farmily.info 도쿄 ALB로 200 응답 — 읽기/헬스 RTO 종료 (5분 29초) 14:19:20 RDS promotereadreplica 호출 (전환 후 약 2분 공백) 14:20:29 도쿄 drrds 독립 primary 승격 완료 (승격 약 1분 9초) 14:21:09 farmdiaries write 성공 — 전체 서비스 RTO 종료 (9분 14초) 14:25:00 RDS 백업 완료 — 보수적 RTO (13분 5초) 14:26:56 FIS 실험 종료 단계별 분석 ① 장애 감지 — 서울 리전 FIS로 서울 네트워크를 끊자 api.farmily.info 글로벌 헬스체크 상태값이 1에서 0으로 떨어졌다. 같은 시각 서울 ALB의 정상 타겟 수가 2에서 0으로 내려갔고 ALB 5xx가 1.4천 건 가까이 치솟았다. 세 패널이 같은 시점을 가리키는 걸 봐서 장애 주입은 의도대로 먹혔다. 한 가지 눈여겨볼 부분은 FIS의 scope를 vpc가 아니라 all로 줘야 한다는 점이다. 첫 시도에서 scope를 vpc로 했더니 VPC 내부의 ALB에서 Pod, RDS로 가는 통신이 살아있어서 헬스체크가 계속 성공했고 failover가 일어나지 않았다. ② 트래픽 전환 — 서울에서 도쿄로 서울 ALB 요청 수는 14시 11분께 3.5천까지 잠깐 튀었다가 곧바로 0으로 빠졌다. 같은 시간 도쿄 ALB 요청 수가 올라오기 시작해 100에서 130 사이를 오갔고 도쿄 ALB 정상 타겟 수는 2로 안정적으로 유지됐다. 트래픽이 서울에서 도쿄로 옮겨간 게 그래프에서 분명하게 보인다. 페일오버 타임라인 패널에서도 헬스체크가 약 14시 12분부터 16분까지 빨간 구간(장애)으로 표시됐다가 다시 초록으로 돌아왔다. ③ 데이터 복제 — RPO 도쿄 drrds의 복제 지연을 RPO 관점에서 본 패널이다. 60초 임계선을 그어 뒀는데, 실제 지연은 톱니 모양으로 출렁이며 최고 4분 17초까지 올라갔다. 승격 직전 기록한 값이 약 126초였고 이게 이번 테스트의 RPO가 됐다 — 60초 목표를 넘긴 셈이다. RDS 연결 수 패널을 보면 서울 prodrds 연결은 장애 중 잠깐 30까지 줄었다가 회복했고 도쿄 drrds 연결은 60 근처를 유지하다 14시 20분께 8까지 떨어진 구간이 있다. 승격 과정에서 연결이 끊기는 순간으로 보인다. 여기서 짚을 공백이 하나 있다. 트래픽은 14시 17분에 도쿄로 넘어갔지만 promote 호출은 14시 19분 20초에야 들어갔다. 2분 남짓한 이 사이에 도착한 쓰기는 도쿄 RDS가 아직 read replica라 readonly transaction 오류로 실패했을 수 있다. 다음 테스트에서는 도쿄 전환을 확인하는 즉시 ReplicaLag를 기록하고 promote를 실행해 이 틈을 좁혀야 한다. ④ 도쿄 리전 수용 도쿄가 트래픽을 받아낼 준비가 돼 있었는지를 본다. dreks 노드 수는 1로, 실행 중인 파드 수는 약 16으로 테스트 내내 일정했다. 도쿄 쪽 컴퓨트 용량이 흔들림 없이 떠 있었다. ⑤ 페일오버 후 서비스 상태 도쿄로 넘어간 뒤 서비스 품질을 본 구간이다. 도쿄 ALB 응답시간 p99는 14시 20분께 4초까지 튀는 등 몇 차례 스파이크가 있었다. 도쿄 ALB 5xx 비율은 14시 15분께 0.78%까지 올랐다가 5% 경계선 아래에서 가라앉았다. 노드 포화도는 서울 prodeks 기준 메모리가 50%에서 70% 사이, CPU는 간헐적으로 튀는 정도였다. 앱 시그널 지연은 0.7 근처에서 평탄했고 장애(Fault) 신호는 잡히지 않았다. 응답시간이 잠깐 튄 건 승격 직후 커넥션이 다시 맺히는 구간과 겹치는 것으로 보인다. 5xx가 1% 미만에 머문 만큼 도쿄가 트래픽을 받아내는 데는 무리가 없었다. ⑥ 복구·페일백 API 헬스체크는 14시 12분부터 15분까지 0으로 떨어졌다가 다시 1로 회복했다. 도쿄 RDS 복제 지연 소진 추이는 앞서 본 것과 같은 톱니 곡선을 그리며 14시 18분 무렵 데이터가 끊긴다. 옆에는 페일백 체크리스트가 함께 정리돼 있는데, 서울 Route53 헬스체크가 계속 UP인지 확인하고, 서울 RDS를 재기동한 뒤 도쿄에서 서울로 복제를 다시 세우고, ReplicaLag가 0에 가깝게 소진되면 행/체크섬으로 데이터 정합성을 검증한 다음, Route53 우선순위를 서울로 되돌리고, 도쿄 컴퓨트를 원상태 비율로 줄이는 순서다. 승격된 도쿄가 받은 쓰기를 서울로 역동기화하는 작업이 페일백의 핵심이다. 이번 테스트에서 도쿄에만 쓰인 데이터는 farmdiaries 한 건이었다. 같은 시간대 서울 RDS에는 이 행이 없었는데, promote로 복제가 끊긴 뒤 당연한 결과다. 한 건이라 행 단위 수동 이관으로 충분하다. 다만 실제 장애에서 도쿄에 쓰기가 많이 쌓였다면 행을 하나씩 옮기는 대신 도쿄를 새 기준(source of truth)으로 두고 서울을 도쿄에 맞춰 재구성하는 편이 낫다. 상시 운영 대시보드에서 함께 본 것들 페일오버 테스트와 같은 시간대를 상시 운영 대시보드(AMP)로도 떼어 봤다. FIS가 서울 네트워크를 끊었을 때 그 여파가 다른 컴포넌트에는 어떻게 나타났는지 함께 확인할 수 있었다. 클러스터 헬스 — 스크랩 타겟 24개가 모두 정상으로 잡혔고 노드는 3개였다. 노드 CPU는 한 노드만 14시 16분께 85%까지 올라갔고 나머지는 낮게 깔렸다(argocd가 올라간 노드의 동기화 작업으로 보인다). 노드 메모리는 클러스터 전체가 50% 안팎에서 평탄했다. 결국 테스트가 prod 클러스터의 기본 체력에는 부담을 주지 않았고, 한 노드의 CPU 스파이크도 메모리까지 번지지 않고 끝났다. Istio 메시 — farmilyapp 요청률이 12.5 req/s까지 올랐다가 14시 16분 이후 0으로 빠졌다. 서울 경로가 끊기면서 prod로 들어오던 트래픽이 사라진 구간이다. 응답시간 p99는 한때 1분 가까이 치솟았는데, 막힌 경로에서 타임아웃과 재시도가 쌓이며 생긴 꼬리 지연으로 보인다. 반면 5xx 에러율은 0.18%까지만 잠깐 올랐다가 0으로 떨어졌다. 메시가 장애를 그대로 드러내면서도 에러 폭증 없이 버텨냈다. KEDA — 스케일러 활성·메트릭 값·에러율이 모두 0 근처에서 평탄했다. 부하 테스트가 아니라 장애 전환 테스트라 큐 깊이 같은 스케일 트리거가 움직일 일이 없었다. 에러가 0인 걸 보면 KEDA는 발동만 안 했을 뿐 정상 대기 상태였다. Karpenter — 파드 상태는 Running 약 50개로 안정적이었고 NodePool CPU는 4코어 근처였다. 노드 생성·종료율과 프로비저닝 지연 p90 패널은 데이터가 없었다. 테스트 내내 새 노드를 띄울 일이 없어 이벤트 자체가 안 생긴 정상 상태다. 잠깐 보인 Pending은 롤아웃으로 파드가 갈려 나가는 순간이다. 이번 부하는 기존 용량으로 다 받아냈고 오토프로비저닝까지 갈 필요는 없었다. Argo Rollouts — 14시 13분께 farmilyapp 롤아웃이 Completed에서 Progressing으로 넘어갔다. desired가 2에서 4로 뛰고 available이 잠깐 0으로 떨어졌다가 2로 돌아왔다. 새 리비전이 뜨는 동안의 surge와 교체 과정이다. 시점이 장애 전환과 겹치는 걸 보면, FIS로 prod 파드가 흔들린 구간과 맞물려 available이 0까지 내려간 듯하다. 그래도 롤아웃은 멈추지 않고 끝까지 진행됐다. 핵심 교훈 이번 테스트로 분명해진 것들을 정리하면 이렇다. 헬스체크 하나로 서비스 전체 상태를 대표할 수 없다. /api/v1/health가 성공해도 내부 기능은 실패할 수 있다. 게다가 헬스체크 대상이 failover 도메인인 api.farmily.info 자체라서 도쿄가 200을 주기 시작하면 헬스체크가 다시 Healthy로 돌아온다. 실제로 서울이 여전히 장애인데도 14시 16분에 회복으로 표시됐다. Healthy가 곧 "서울 정상"을 뜻하지 않는다는 얘기다. 서울의 실제 생사를 보려면 서울 전용 엔드포인트가 따로 있어야 한다. RDS 승격은 앱 설정을 자동으로 바꿔주지 않는다. 사용자가 어느 앱과 DB를 쓰는지는 Route53이 정한다. 승격 이후에는 서울과 도쿄가 split 상태가 되어 자동으로 동기화되지 않는다. 무엇보다 쓰기 가능 RTO와 Route53 전환 RTO는 다른 값이라 반드시 나눠서 기록해야 한다. Route53이 도쿄로 넘어가도 도쿄 RDS가 아직 replica면 쓰기는 실패하기 때문이다. 한 줄로 줄이면 이렇다. DR failover 자체는 동작한다. 다만 쓰기까지 포함한 서비스 복구는 Route53 전환만으로 끝나지 않는다. RDS promote 타이밍과 데이터 복구 전략까지 runbook에 들어가야 진짜 복구다. 모니터링 체크리스트 다음 DR 테스트에서 무엇을 봐야 하는지 미리 추려 뒀다. 필수 세 가지는 최소한으로 챙길 지표고 나머지는 여유가 되면 함께 본다. 필수 지표 (최소 3가지) 지표 확인 내용 기준 1 Route53 헬스체크 서울 ALB가 Unhealthy로 전환되는지 30초 내 실패 감지 2 RDS Replica Lag 장애 시점 복제 지연 (RPO 측정) 60초 이내 3 트래픽 전환 서울 ALB 요청 수 0으로 감소 / 도쿄 증가 시작 전환 확인 추가 지표 (권장) 지표 확인 내용 기준 4 ALB Healthy Hosts 서울 Healthy 타겟 0 / 도쿄 유지 전환 확인 5 RDS Connections 서울 연결 감소 / 도쿄 증가 전환 확인 6 EKS 파드 개수 도쿄 파드 1개→증가 Auto Scaling 동작 7 API 응답 시간 도쿄 cold cache 응답 시간 증가 일시적 허용 8 에러율 5xx 에러 비율 5% 이내 시간대별 체크 (T = 장애 시작) 시점 기대 동작 T+0 장애 시작 — 서울 ALB 타겟 죽음 T+30초 헬스체크 실패 → Slack 알림 T+1분 도쿄로 트래픽 전환 T+2분 도쿄 파드 증가 시작 T+5분 RDS Lag 기록 (RPO) T+15분 도쿄 RDS 쓰기 가능 → 서비스 완전 복구 이번 테스트 실측은 트래픽 전환 확인이 T+5분 29초, 쓰기 복구가 T+9분 14초로 아래 기대치보다는 느렸지만 15분 목표 안에는 들어왔다. 헬스체크가 도쿄 200 응답에 다시 Healthy로 돌아오는 함정과 Slack 알림이 특정 리전에서만 뜨는 문제는 핵심 교훈·후속 조치 항목과 함께 보면 된다. 후속 조치 다음 테스트 전까지 손볼 것들이 있다. 도쿄 RDS에만 남은 데이터를 처리해야 하는데, 테스트 데이터면 폐기하고 보존이 필요하면 행 단위로 서울에 옮긴다. 도쿄 RDS는 서울 prodrds의 read replica로 다시 만들어 둬야 같은 방식으로 재테스트가 된다. 헬스체크는 서울 전용, 도쿄 전용, 글로벌로 나누고, CloudWatch 알람은 헬스체크 지표가 조회되는 리전에 맞춰 만들어야 한다. 지난 테스트에서 알람을 엉뚱한 리전에 만들었다가 INSUFFICIENTDATA가 떠 알림이 가지 않았다. 마지막으로 도쿄 앱이 지금 서울 ECR 이미지를 끌어다 쓰고 있으니 도쿄 ECR로 crossregion 복제를 걸고 이미지 경로를 도쿄 ECR로 바꿔야 진짜 리전 격리가 된다.

Sep 17, 2026AWS
[DR] DR 테스트 후기 — 서울 리전 장애 유발로 도쿄 리전 활성화 하기