Zero-Downtime 데이터베이스 마이그레이션 파이프라인 구축 (PostgreSQL to Aurora)
Source: Dev.to
번역을 진행하려면 번역하고자 하는 본문 텍스트를 제공해 주세요. 현재는 소스 링크만 포함되어 있어 번역할 내용이 없습니다. 텍스트를 알려주시면 바로 한국어로 번역해 드리겠습니다.
문제
작년에 저는 자체 관리형 PostgreSQL을 실행하고 있는 프로젝트를 인계받았습니다. 데이터베이스는 500 GB까지 성장했으며, 패치, 백업, 복제 문제 등 유지 관리에 너무 많은 시간을 소비하고 있었습니다. 관리형 운영, 더 나은 확장성, 그리고 AWS와의 네이티브 통합을 위해 Aurora PostgreSQL으로 이전하기로 결정했습니다.
문제는? 이 데이터베이스는 하루에 50,000명의 활성 사용자를 서비스하는 프로덕션 환경이었습니다. 다운타임이 발생하면 매출 손실과 고객 불만이 발생합니다. 비즈니스 측에서는 유지 보수 창을 0분으로 제시했습니다. 압박감이 심하네요.
내가 처음 시도한 방법 (그리고 왜 실패했는지)
내 초기 생각은 간단했습니다: 조용한 시간에 pg_dump와 pg_restore를 사용하는 것. 고전적인 접근법이죠?
# The naive approach
pg_dump -Fc production_db > backup.dump
pg_restore -d aurora_target backup.dump
500 GB 데이터베이스의 경우 네트워크와 인스턴스 규모에 따라 대략 4–6 시간이 걸립니다. 이는 사용자가 계속 쓰는 동안 소스에 4–6 시간 동안 오래된 데이터가 누적된다는 의미입니다. 받아들일 수 없습니다.
또한 PostgreSQL에 기본 제공되는 논리 복제도 살펴봤지만, 적절한 보안 제어를 갖춘 AWS 계정 간에 퍼블리케이션/구독을 설정하는 것이 거대한 작업으로 변했습니다. 게다가 그와 관련된 운영 도구는 사실상 직접 만들어야 했습니다.
그때 AWS DMS를 발견했습니다. 관리형 서비스에서 전체 로드와 변경 데이터 캡처를 모두 처리해 줍니다. 문제는 마이그레이션을 반복 가능하고 안전하게 만들기 위해 모든 것을 DMS 주변에 구축하는 것이었습니다.
The Solution
I built a complete migration framework with four major components:
- Terraform modules for all AWS infrastructure
- Python automation scripts for validation and cutover
- GitHub Actions workflows for CI/CD
- CloudWatch monitoring for full observability
Architecture Overview

The blue‑green strategy works like this: DMS performs a full load of all existing data, then switches to CDC mode to capture ongoing changes. Both databases stay in sync until we’re ready to cut over.
Terraform Infrastructure
I created modular Terraform for reproducibility across environments. Here’s how the DMS module looks:
module "dms" {
source = "./modules/dms"
project = "db-migration"
environment = "prod"
subnet_ids = var.private_subnet_ids
security_group_ids = [module.networking.dms_security_group_id]
replication_instance_class = "dms.r5.4xlarge"
multi_az = true
source_db_host = var.source_db_host
source_db_username = var.source_db_username
source_db_password = var.source_db_password
target_db_host = module.aurora.cluster_endpoint
target_db_username = var.aurora_master_username
target_db_password = var.aurora_master_password
}
The DMS module creates:
- Replication instance with appropriate sizing
- Source and target endpoints with proper SSL configuration
- Replication task with CDC enabled
- CloudWatch alarms for monitoring lag and errors
Validation Scripts
Before any cutover, you need confidence that the data matches. I wrote a Python validation tool that checks multiple dimensions:
# Quick validation (uses table statistics for fast estimates)
python validation.py --quick
# Full validation (exact counts, checksums, sequence values)
python validation.py --full
# Just check DMS status
python validation.py --dms
The full validation performs:
| Check | Description |
|---|---|
| Row Counts | Compare exact row counts between source and target |
| Checksums | MD5 hash of sample data from each table |
| Sequences | Verify sequence values are synchronized |
| Primary Keys | Ensure all tables have PKs (required for CDC) |
| DMS Status | Task running, replication lag below threshold |
Snippet from the checksum validation:
def calculate_checksum(self, table: str, columns: list, limit: int = 1000) -> str:
"""Calculate MD5 checksum of sample rows."""
cols = ", ".join(columns)
query = f"""
SELECT md5(string_agg(row_hash, '' ORDER BY row_hash))
FROM (
SELECT md5(ROW({cols})::text) as row_hash
FROM {table}
ORDER BY {columns[0]}
LIMIT {limit}
) t
"""
result = self.execute_query(query)
return result[0][0] if result else None
The Cutover Process
Cutover is where things get nerve‑wracking. I built a multi‑phase process with automatic rollback capability at each stage:
| Phase | Action | Rollback Available |
|---|---|---|
| 1 | Pre‑validation (verify DMS, row counts) | Yes |
| 2 | Wait for sync (CDC lag under threshold) | Yes |
| 3 | Drain connections (terminate source connections) | Yes |
| 4 | Final sync (wait for remaining changes) | Yes |
| 5 | Stop replication | Manual only |
| 6 | Post‑validation | Manual only |
The cutover script saves state to JSON after each phase, so you can resume if something fails:
> **Source:** ...
```sh
# Always do a dry run first
python cutover.py --dry-run
# Execute when ready
python cutover.py --execute
# Resume from saved state if interrupted
python cutover.py --execute --resume
GitHub Actions 통합
모든 작업은 GitHub Actions를 통해 자동화됩니다. Cutover 워크플로우는 프로덕션 환경에서 수동 승인이 필요합니다:
jobs:
approval:
name: Approve Cutover
runs-on: ubuntu-latest
if: github.event.inputs.mode == 'execute'
environment: prod-cutover # Requires manual approval
steps:
- name: Cutover Approved
run: echo "Cutover approved"
cutover:
name: Database Cutover
needs: [approval]
# ... actual cutover steps
이 워크플로우는 AWS Secrets Manager에서 자격 증명을 가져오고, cutover 스크립트를 실행하며, 감사를 위해 상태 아티팩트를 업로드하고, 실패 시 SNS 알림을 전송합니다.
결과
마이그레이션이 성공적으로 완료되었으며, 다음과 같은 지표를 기록했습니다:
| 지표 | 값 |
|---|---|
| 전체 마이그레이션된 데이터 | 512 GB |
| 마이그레이션 시간 (전체 로드) | 3 시간 22 분 |
| 컷오버 중 CDC 지연 | 2.1 초 |
| 애플리케이션 다운타임 | 0 초 |
| 데이터 검증 오류 | 0 |
마이그레이션 후 관찰된 내용:
- 읽기 지연 시간 40 % 감소 (Aurora 읽기 복제본)
- 데이터베이스 유지 관리에 소요된 시간 0
- 자동 백업 및 시점 복구
Lessons Learned
- 검증 스크립트를 철저히 테스트하세요. 처음에 체크섬 쿼리가
NULL값을 올바르게 처리하지 못하는 버그가 있었습니다. 다행히 스테이징 단계에서 발견했습니다. - DMS 인스턴스를 적절히 크기 지정하세요.
dms.r5.2xlarge로 시작했는데 전체 로드 중에 CPU 한계에 걸렸습니다.4xlarge로 업그레이드하니 마이그레이션 시간이 절반으로 줄었습니다. - CDC 지연을 집요하게 모니터링하세요. 지연이 30 초를 초과하면 알람이 울리도록 CloudWatch 알림을 설정했습니다. 마이그레이션 중에 소스에서 배치 작업이 실행돼 지연이 45 초까지 급증한 적이 있었는데, 이를 즉시 파악해 전환을 지연시켜 상황이 안정될 때까지 기다릴 수 있었습니다.
- 실제로 테스트한 롤백 계획을 마련하세요. 전환 후 48 시간 동안 소스 PostgreSQL을 계속 가동했습니다. 마이그레이션과 무관한 사소한 버그가 발생했을 때 롤백 옵션이 있어 모두가 안심하고 원인을 조사할 수 있었습니다.
- 필요하다고 생각하는 것보다 더 많이 소통하세요. 우리는 매시간 업데이트를 전송했습니다.