필터#FIS×

[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 테스트 후기 — 서울 리전 장애 유발로 도쿄 리전 활성화 하기