두 리전에서 하나의 서비스처럼 동작하는 아키텍처 설계
Block 1: Multi-Region Foundation | 30분
총 150min 세션
GLOBAL 모드apg_write_forward.consistency_mode 기본값 SESSIONEVENTUAL 모드, S3 복제SERIALIZABLE 격리 미지원max_connections의 기본 25%까지만 허용10.x.0.0/24, .1.0/24, .2.0/2410.x.32.0/19, .64.0/19, .96.0/1910.x.128.0/21, .136.0/21, .144.0/21/16 → TGW, S3, DynamoDB → gateway endpoint# us-east-1 TGW route table (us-west-2는 대칭) 10.0.0.0/16 → vpc attachment (local VPC, propagation) 10.1.0.0/16 → peering attachment (static, us-west-2)
ecr.api, ecr.dkr, sts, eks-auth(Pod Identity), logs| 영역 | 결정 | 대가, 주의점 |
|---|---|---|
| 데이터 패턴 | Write-Primary / Read-Local, 데이터별 일관성 선택 | 서부 쓰기는 리전 간 RTT 부담 |
| 진입 | CloudFront → VPC origin → internal ALB | VPC에 IGW 필요, ALB SG는 CloudFront prefix list 또는 VPC origin SG만 허용 |
| 컴퓨트 | 리전별 독립 EKS, 같은 버전, GitOps 배포 | 흡수 용량을 평소에 확보 |
| 네트워크 | 겹치지 않는 /16, 3-tier, public 티어엔 NAT만 | Pod IP까지 포함해 CIDR 산정 |
| 리전 간 | TGW peering + static route | SG 참조, DNS 해석 불가. 리전 3개 이상이면 Cloud WAN 검토 |
| AWS API | Gateway endpoint 항상, EKS 필수 interface endpoint | 리전마다 생성, 손익분기 확인 |
| IAM | EKS Pod Identity | association은 클러스터마다 |
쓰기는 어디서 받고, 복제는 얼마나 늦으며, 장애 때 무엇을 잃는가
| 값 | 같은 세션의 쓰기 | 다른 세션, 리전의 커밋 | 대기 비용 |
|---|---|---|---|
SESSION (기본) | 즉시 보임 (복제 대기) | 기다리지 않음 | 중간 |
EVENTUAL | 복제된 뒤에야 보임 | 기다리지 않음 | 가장 낮음 |
GLOBAL | 즉시 보임 | 쿼리 시작 시점까지의 모든 커밋을 기다림 | 가장 높음 |
OFF | 해당 세션에서 write forwarding 끔 | ||
apg_write_forward.consistency_mode, 세션 단위로 바꿀 수 있고 재부팅 불필요. 격리 수준은 READ COMMITTED, REPEATABLE READ만 지원 (SERIALIZABLE 불가).# secondary 클러스터에서 켜기 (중단, 재부팅 없음, 기본은 꺼짐) aws rds modify-db-cluster --region us-west-2 \ --db-cluster-identifier orders-usw2 \ --enable-global-write-forwarding # 세션에서 일관성 모드 확인 SHOW apg_write_forward.consistency_mode;
GlobalWriteForwardingStatus로 확인합니다.# --region 은 현재 primary 리전 # USW2_ARN: us-west-2 secondary 클러스터 ARN aws rds switchover-global-cluster \ --region us-east-1 \ --global-cluster-identifier shop-global \ --target-db-cluster-identifier "$USW2_ARN"
rds:unplanned-global-failover-... 스냅샷 시도# --region 은 승격할 secondary 리전 aws rds failover-global-cluster \ --region us-west-2 \ --global-cluster-identifier shop-global \ --target-db-cluster-identifier "$USW2_ARN" \ --allow-data-loss
AuroraGlobalDBRPOLag (ms, secondary 기준)GlobalClusterReplicationLag (secondary)AllowDataLoss, 미복제분은 스냅샷으로 보존 시도| 키 패턴 | 자료형 | TTL (설계 예시) | 패턴 |
|---|---|---|---|
product:{id} | Hash | 1h | cache-aside + 이벤트 무효화 |
cart:{userId} | Hash | 7d (item별 field TTL 가능) | write-through |
session:{id} | Hash | 2h | write-through |
ratelimit:{userId} | String counter | 60s | INCR + EXPIRE |
stock:{productId} | String | 30s | 표시용 사본, 원본은 Aurora |
ReplicationLag 관측PREFIXED(기본) 또는 identical. identical이면 전환 시 앱 설정 변경 불필요LEGACY(기본, 단방향), ENHANCED(양방향 Replicator 필요)ReplicationLatency(ms), MessageLag, target 리전 AWS/KafkaReplicationLatency 평균, 최대로 최악의 lag 확인ReplicatedWriteConflictException, 재시도# MRSC: us-east-1, us-west-2 replica
# + us-east-2 witness
aws dynamodb put-item --region us-west-2 \
--table-name payment-idempotency \
--item '{"pk":{"S":"pay#ord-1001"}}' \
--condition-expression \
"attribute_not_exists(pk)"
| 저장소 | 복제 모델 | 평시 lag | 계획 전환 | 비계획 장애 시 손실 | 볼 메트릭 |
|---|---|---|---|---|---|
| Aurora PostgreSQL Global DB | 단일 writer, 비동기 | 보통 1초 미만 | switchover RPO 0 | lag만큼 (rds.global_db_rpo로 상한) | AuroraGlobalDBRPOLag |
| Aurora DSQL multi-Region | 동기, active-active | 커밋에 포함 | 불필요 | 0 | OCC 재시도율 |
| DocumentDB Global | 단일 writer, 비동기 | 보통 1초 미만 | switchover RPO 0 | lag만큼 | GlobalClusterReplicationLag |
| DynamoDB MREC | multi-writer, 비동기 | 보통 1초 이내, SLA 없음 | 불필요 | 미전파 쓰기는 해당 리전 복구 전까지 없음 | ReplicationLatency |
| DynamoDB MRSC | 동기, 3개 리전 | 쓰기에 포함 | 불필요 | 0 | 충돌 예외 수 |
| 저장소 | 복제 모델 | 평시 lag | 계획 전환 | 비계획 장애 시 손실 | 볼 메트릭 |
|---|---|---|---|---|---|
| Valkey Global Datastore | 단일 writer, 비동기 | 보통 1초 미만 | 수동 promote | lag만큼 (캐시는 재구성) | ReplicationLag |
| OpenSearch CCR | leader → follower, 비동기 pull | 측정 필요 | follower 승격(replication 중지) | 미복제 문서, 원천 DB에서 재색인 | replication 상태 API |
| MSK Replicator | 비동기 | 측정 필요 | producer 전환 | in-flight 유실, 재처리 중복 | ReplicationLatency, MessageLag |
| S3 CRR + RTC | 비동기 | 대부분 수초 | 해당 없음 | 99.99% 객체 15분 내 (SLA 99.9%) | RTC 복제 메트릭 |
공개 입구는 CloudFront 하나, 리전 선택은 Edge에서, 절체 스위치는 데이터 플레인에
CloudFront-Viewer-Country-Region)cf.createRequestOriginGroup()import cf from 'cloudfront';
const kvs = cf.kvs();
async function handler(event) {
const req = event.request;
const pin = req.cookies['mr-region']; // affinity
const region = pin ? pin.value
: (await kvs.get('routing', { format: 'json' })).active;
const ids = region === 'us-west-2'
? ['alb-usw2', 'alb-use1'] : ['alb-use1', 'alb-usw2'];
cf.createRequestOriginGroup({
originIds: ids.map(id => ({ originId: id })),
failoverCriteria: { statusCodes: [500, 502, 503, 504] }
});
return req;
}
동적 API: 캐시 끄고, 함수가 리전을 고른 origin group으로
버전이 붙은 정적 자산: 길게 캐시, S3는 OAC로만
SPA 라우팅: custom error response 대신 함수로 rewrite
CloudFront-VPCOrigins-Service-SG를 소스로. 내 distribution 트래픽으로만 제한되어 가장 엄격 (VPC origin 생성 후 가능, 이 SG는 편집 금지)com.amazonaws.global.cloudfront.origin-facing (dual-stack은 ipv6 목록도). 모든 CloudFront 고객의 origin-facing IP라 더 넓음| 항목 | VPC origin에서 | 대응 |
|---|---|---|
| gRPC | 미지원 | gRPC는 별도 경로 설계 (예외 심사) |
| Lambda@Edge origin request/response | 미지원 | origin 선택은 viewer request CloudFront Function으로 |
updateRequestOrigin() | 요청 실패 | selectRequestOriginById(), createRequestOriginGroup() |
| NACL | inbound 미평가, outbound는 평가 | 반환 경로용 ephemeral TCP 1024~65535 outbound 허용 |
internal. internet-facing LB는 생성 자체를 IaC 정책 검사에서 차단AWSManagedRulesAntiDDoSRuleSet, 상단 배치, scope-down 금지bot:verified label만 붙임. web bot auth label은 v4.0 이상SignalNonBrowserUserAgent는 기본 Block. 앱의 비브라우저 User-Agent가 막힘"ManagedRuleGroupStatement": {
"VendorName": "AWS",
"Name": "AWSManagedRulesBotControlRuleSet",
"ManagedRuleGroupConfigs": [
{ "AWSManagedRulesBotControlRuleSet": { "InspectionLevel": "COMMON" } }
],
"RuleActionOverrides": [
{ "Name": "SignalNonBrowserUserAgent", "ActionToUse": { "Count": {} } }
]
}
// 다음 규칙: label awswaf:managed:aws:bot-control:signal:non_browser_user_agent
// AND NOT (앱 User-Agent) → Block
| 항목 | ||
|---|---|---|
| 계층, 프로토콜 | L7 HTTP(S), WebSocket | L4 TCP, UDP |
| 입구 주소 | DNS 이름 (Anycast static IP 옵션 있음) | 고정 anycast IPv4 2개 |
| 캐시, WAF | Edge 캐시, WAF를 Edge에서 적용 | 캐시 없음, WAF는 뒤쪽 ALB에 붙임 |
| 리전 분배 | origin group, CloudFront Function | traffic dial, endpoint weight, 자동 failover |
| private origin | VPC origin (ALB, NLB, EC2) | internal ALB endpoint (client IP 보존) |
| 과금 | 요청 + 전송량, AWS origin fetch 무료 | $0.025/시간 + DT-Premium (미국→미국 $0.015/GB) + public IPv4 |
createRequestOriginGroup(), DNS latency는 내부 이름에전략 선택 → 장애 시나리오 → 자동화 파이프라인 → 훈련과 검증
| 서비스 | 데이터 스토어 (현재) | us-east-1 장애 시 | RPO 성격 | 전환 방식 |
|---|---|---|---|---|
| order, payment, inventory | Aurora PostgreSQL Global DB | us-west-2 읽기 유지, 쓰기 중단 | 계획 0, 비계획은 복제 lag만큼 | failover (allow-data-loss), global writer endpoint (앱 적용 필요) |
| catalog | DocumentDB global cluster | us-west-2 읽기 유지 | 초 단위 (비동기) | managed failover, 보통 1분 내 승격 |
| cart, session | ElastiCache (Valkey) Global Datastore | us-west-2 읽기 유지 | 복제 지연 보통 1초 미만 | 수동 promote, 보통 1분 미만 |
| 이벤트 | MSK, Replicator 미구성 | us-west-2에 이벤트 없음 | 미처리 이벤트가 us-east-1에 남음 | Replicator 구성 전에는 전환 불가 |
| 정적 자산 | S3 CRR + RTC | 복제본에서 읽기 | 99.99% 객체 15분 이내 설계 | CloudFront origin group |
| 컴퓨트 | 리전별 EKS 클러스터 | us-west-2 확장 | stateless | GitOps + 용량 확장 |
us-east-1 전체가 응답하지 않는 경우입니다.
| 계층 | 자동 | 사람이 결정 |
|---|---|---|
| 엣지 (CloudFront) | GET/HEAD/OPTIONS는 origin group으로 자동 재시도 | 쓰기 요청의 활성 리전 전환 |
| catalog (DocumentDB) | 없음 | failover-global-cluster 실행 |
| cart (ElastiCache) | 없음 | failover-global-replication-group 실행 |
| order, payment (Aurora Global DB) | 없음 | failover-global-cluster 실행 |
결론: 읽기는 살아남지만 주문, 결제는 DB 승격과 쓰기 리전 전환이 끝날 때까지 멈춥니다. 순서를 정한 Runbook이 필요합니다.
리전은 정상이고 한 데이터 스토어만 문제인 경우입니다.
| 상황 | 먼저 할 일 |
|---|---|
| catalog 장애 | 캐시된 상품 정보로 읽기 전용 모드 |
| cart 장애 | 세션 쿠키와 DB로 장바구니 재구성, 쓰기는 재시도 큐 |
| 데이터 손상 (잘못된 쓰기) | 복제본에도 전파되므로 PITR, 스냅샷으로 복구 |
| 리전 전환 판단 | "DB 하나 때문에 모든 서비스를 옮길 가치가 있는가" |
원칙: 리전 전체 전환보다 해당 서비스만 degrade하는 것이 먼저입니다.
노드, AZ 수준인지 클러스터 전체인지 먼저 구분합니다.
| 범위 | 대응 |
|---|---|
| 노드, AZ | Karpenter 노드 교체, ARC zonal shift로 해당 AZ 우회 |
| drain 중 | PodDisruptionBudget 없으면 replica가 동시에 빠질 수 있음 |
| 클러스터 전체 | GitOps로 같은 상태인 us-west-2 클러스터를 EKS resource scaling block으로 확장 |
| 항목 | 내용 |
|---|---|
| 파라미터 | rds.global_db_rpo (Aurora PostgreSQL, 초 단위, 최솟값 20초) |
| 동작 | 모든 secondary의 RPO lag이 값을 넘으면 primary 커밋을 멈추고, 하나라도 따라잡으면 재개 |
| 관측 | AuroraGlobalDBRPOLag (ms) 알람으로 상한에 다가가는 것을 먼저 감지 |
| 2리전 주의 | secondary 쪽 parameter group은 기본값 유지: failover 직후 커밋 정지 방지 |
| 결정 | 커밋 가용성과 손실 상한 중 무엇을 택할지 결제 담당 팀과 합의 |
| 단계 | 내용 |
|---|---|
| 스냅샷 | old primary 회복 시 rds:unplanned-global-failover-... 스냅샷 생성 시도 |
| 비교 | 스냅샷을 별도 클러스터로 복원해 새 primary에 없는 마지막 트랜잭션 확인 |
| 외부 원장 | PG 승인 내역과 대조해 주문 레코드 없는 승인 건 처리 |
| 재반영 | DynamoDB Global Tables의 idempotency key로 중복 없이 재처리 |
| 항목 | 내용 |
|---|---|
| 구성 | peered cluster 2개 (us-east-1, us-west-2) + witness 리전 (us-east-2) |
| 얻는 것 | 두 리전 모두 쓰기, 동기 커밋으로 RPO 0, failover 명령 불필요 |
| 대가 | 쓰기마다 리전 간 왕복, OCC 충돌 재시도, 트랜잭션당 3,000 rows, 10 MiB 한도 |
| 적용 범위 | 기존 Aurora 앱 일괄 이관이 아니라 RPO 0이 필수인 원장 테이블부터 검토 |
정상 상태의 global cluster에서 primary 리전을 옮깁니다. 훈련, 리전 순환, failover 후 원복에 씁니다.
모든 secondary가 동기화될 때까지 기다린 뒤 전환하므로 데이터 손실이 없습니다. 모든 리전의 engine version이 같아야 하고, 역할 전환은 수 분까지 걸릴 수 있습니다.
primary 리전 장애 시 secondary를 승격합니다. 명령은 승격할 리전에서 실행합니다.
실행 전 GlobalClusterReplicationLag(ms)가 가장 작은 secondary를 고릅니다. 보통 1분 안에 승격되며 복제되지 않은 쓰기는 유실될 수 있습니다. 이전 primary는 회복 후 자동으로 secondary로 재합류합니다.
| 점검 항목 | 내용 |
|---|---|
| writer endpoint | 바뀜: 앱이 새 endpoint를 쓰는지 확인 |
| parameter group, alarm | 상속되지 않음: 새 primary에 다시 구성 |
| 다른 secondary | 재구축에 수 분에서 수 시간 |
| 유실분 | rds:docdb-unplanned-global-failover-... snapshot을 수동 snapshot으로 복사 |
| 원복 | 원래 리전 회복 후 switchover로 수행 |
FIS scenario library의 시나리오로, 한 AZ의 전원 손실 증상을 주입합니다.
| 항목 | 내용 |
|---|---|
| 대상 | EC2 (EKS, ECS 노드 포함), EBS, ASG, 서브넷, ElastiCache, RDS |
| 용량 제약 재현 | 지정한 IAM role, ASG의 새 인스턴스 시작을 막아 "용량 확보 실패"까지 시험 |
| 관찰 지표 | Karpenter 노드 교체, zonal shift, DB writer 이동 시간 |
| 주기 (예시) | 월 1회 비운영, 분기 1회 운영: 조직 기준으로 조정 |
실험 리전에서 대상 리전으로 가는 트래픽을 막고 복제를 멈춥니다. 기본 지속 시간 3시간(disruptionDuration).
| Action | 효과 |
|---|---|
aws:network:route-table-disrupt-cross-region-connectivity | 서브넷의 대상 리전 경로 차단 |
aws:network:transit-gateway-disrupt-cross-region-connectivity | TGW 경유 트래픽 차단 |
aws:s3:bucket-pause-replication | S3 복제 일시 정지 |
aws:dynamodb:global-table-pause-replication | Global Table 복제 정지 |
AWS 관리형 서비스 트래픽은 막히지 않을 수 있습니다. DocumentDB, ElastiCache 복제는 이 시나리오로 끊기지 않으니 별도로 검증합니다.
| 단계 | 내용 |
|---|---|
| 실행 | 분기 1회 plan을 실제로 실행해 us-west-2로 전환, switchover로 원복 |
| 측정 | 감지까지, 승인까지, 전환 완료까지 시간과 유실 데이터 |
| 장애 중 전환 | FIS로 장애를 주입한 상태에서 plan 실행 |
| 회고 | PIR로 기록하고 Runbook과 plan을 함께 갱신 |
| 기능 | 내용 |
|---|---|
| Resiliency policy | Application, Infrastructure, Availability Zone, Region별 RTO/RPO 목표 |
| Assessment | 구성 요소를 분석해 목표 충족, 위반과 추정 RTO/RPO 표시 |
| Recommendations | 개선안별 예상 RTO, RPO, 비용과 alarm, FIS 실험, SOP 제안 |
| CI/CD 연동 | 구성 변경이 목표를 깨는지 배포 전에 확인 |
수집 → 샘플링 → 저장 → 리전 간 상관 → 비용 → 부하 테스트 → SLO → 출시 준비
노드 로컬에서 받아 trace ID 기준으로 Gateway에 분배합니다.
X-Ray OTLP endpoint는 Transaction Search 활성화가 전제입니다. us-west-2 Gateway는 region과 endpoint만 바꿉니다.
| 항목 | 2026-10-15 기준 |
|---|---|
| 수집 경로 | OTLP endpoint, SigV4 (service: xray) |
| 저장 위치 | CloudWatch Logs aws/spans log group |
| 검색 | 모든 span을 attribute로 검색, trace 최대 10,000 span |
| 인덱싱 | 수집 span의 1%는 trace summary로 무료 인덱싱 |
| 수집 요금 | $0.35/GB (첫 10 TB, us-east-1 list price) |
| 항목 | 내용 |
|---|---|
| 저장 | S3에 Parquet 블록 (storage.trace.backend: s3) |
| 쓰기 경로 | 3.0에서 ingester 제거, block-builder와 live-store |
| 배포 모드 | monolithic은 Kafka 불필요, microservices는 Kafka 호환 큐 |
| 질의 | TraceQL, 구조 연산자 >>, >, ~ |
| service graph | metrics-generator service_graphs, 기본 비활성 |
| 질문 | Transaction Search | Tempo |
|---|---|---|
| 운영 부담 | 관리형 | 클러스터 직접 운영 |
| 샘플링 | 100% 수집 후 일부 인덱싱 | tail sampling 결과만 저장 |
| AWS 연계 | Application Signals, SLO | Grafana 생태계 |
| 비용 성격 | GB당 수집 요금 | S3 + 컴퓨트 |
X-Ray SDK와 daemon은 2026-02-25부터 maintenance mode입니다. 보안 수정만 릴리스되며 X-Ray 서비스 자체는 계속 지원됩니다. 신규 계측은 OpenTelemetry(ADOT SDK 또는 upstream SDK)로 시작합니다.
RedisCluster 계열 client로 교체MaxConns 기본값은 max(4, NumCPU)입니다. 작은 Pod에서는 4개라 고부하에서 대기열이 생깁니다.pool_max_conns를 부하 테스트로 정하고 명시maxPoolSize 기본 100입니다. Pod 수 × 100이 DocumentDB 연결 한도를 넘지 않는지 계산합니다.DefaultMaxIdleConnsPerHost는 2입니다. 내부 API 호출이 많으면 연결을 계속 새로 맺습니다.MaxIdleConnsPerHost 상향vus: 300, duration: '4h'로 4시간 유지합니다. 지표 대신 추세를 봅니다.
| 관찰 대상 | 누수 신호 |
|---|---|
| Pod 메모리 | 시간에 비례해 증가 |
| DB 연결 수 | pool 한도에 수렴 후 대기 |
| goroutine, thread | 반환되지 않고 누적 |
| tail sampling 메모리 | num_traces 도달 후 drop |