Zero-Downtime 데이터베이스 마이그레이션 파이프라인 구축 (PostgreSQL to Aurora)

발행: (2025년 12월 4일 오후 12:22 GMT+9)
9 분 소요
원문: Dev.to

Source: Dev.to

번역을 진행하려면 번역하고자 하는 본문 텍스트를 제공해 주세요. 현재는 소스 링크만 포함되어 있어 번역할 내용이 없습니다. 텍스트를 알려주시면 바로 한국어로 번역해 드리겠습니다.

문제

작년에 저는 자체 관리형 PostgreSQL을 실행하고 있는 프로젝트를 인계받았습니다. 데이터베이스는 500 GB까지 성장했으며, 패치, 백업, 복제 문제 등 유지 관리에 너무 많은 시간을 소비하고 있었습니다. 관리형 운영, 더 나은 확장성, 그리고 AWS와의 네이티브 통합을 위해 Aurora PostgreSQL으로 이전하기로 결정했습니다.

문제는? 이 데이터베이스는 하루에 50,000명의 활성 사용자를 서비스하는 프로덕션 환경이었습니다. 다운타임이 발생하면 매출 손실과 고객 불만이 발생합니다. 비즈니스 측에서는 유지 보수 창을 0분으로 제시했습니다. 압박감이 심하네요.

내가 처음 시도한 방법 (그리고 왜 실패했는지)

내 초기 생각은 간단했습니다: 조용한 시간에 pg_dumppg_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

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:

CheckDescription
Row CountsCompare exact row counts between source and target
ChecksumsMD5 hash of sample data from each table
SequencesVerify sequence values are synchronized
Primary KeysEnsure all tables have PKs (required for CDC)
DMS StatusTask 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:

PhaseActionRollback Available
1Pre‑validation (verify DMS, row counts)Yes
2Wait for sync (CDC lag under threshold)Yes
3Drain connections (terminate source connections)Yes
4Final sync (wait for remaining changes)Yes
5Stop replicationManual only
6Post‑validationManual 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

  1. 검증 스크립트를 철저히 테스트하세요. 처음에 체크섬 쿼리가 NULL 값을 올바르게 처리하지 못하는 버그가 있었습니다. 다행히 스테이징 단계에서 발견했습니다.
  2. DMS 인스턴스를 적절히 크기 지정하세요. dms.r5.2xlarge 로 시작했는데 전체 로드 중에 CPU 한계에 걸렸습니다. 4xlarge 로 업그레이드하니 마이그레이션 시간이 절반으로 줄었습니다.
  3. CDC 지연을 집요하게 모니터링하세요. 지연이 30 초를 초과하면 알람이 울리도록 CloudWatch 알림을 설정했습니다. 마이그레이션 중에 소스에서 배치 작업이 실행돼 지연이 45 초까지 급증한 적이 있었는데, 이를 즉시 파악해 전환을 지연시켜 상황이 안정될 때까지 기다릴 수 있었습니다.
  4. 실제로 테스트한 롤백 계획을 마련하세요. 전환 후 48 시간 동안 소스 PostgreSQL을 계속 가동했습니다. 마이그레이션과 무관한 사소한 버그가 발생했을 때 롤백 옵션이 있어 모두가 안심하고 원인을 조사할 수 있었습니다.
  5. 필요하다고 생각하는 것보다 더 많이 소통하세요. 우리는 매시간 업데이트를 전송했습니다.
Back to Blog

관련 글

더 보기 »