컨테이너에서 Zero‑Downtime PostgreSQL 메이저 버전 업그레이드: 아무도 말하지 않는 문제

발행: (2026년 5월 10일 PM 05:35 GMT+9)
16 분 소요
원문: Dev.to

I’m happy to translate the article for you, but I’ll need the full text you’d like translated. Could you please paste the content of the article (or the specific sections you want translated) here? I’ll keep the source line exactly as you provided and preserve all formatting, markdown, and code blocks in the translation.

자체 관리형 컨테이너화 PostgreSQL을 선택해야 하는 이유?

컨테이너에서 PostgreSQL을 실행하는 것은 현명한 인프라 결정이지만, 주요 버전으로 업그레이드해야 할 때는 복잡해질 수 있습니다. 업그레이드 과정은 고통스럽게 복잡해질 수 있지만, 팀이 자체 관리를 선택하게 하는 이점은 이러한 도전을 능가합니다.

비용 비교

설정월 비용 (50 GB, 4 vCPU, 16 GB RAM)
Amazon RDS PostgreSQL (db.r6g.xlarge)~$400–600/month
Aurora PostgreSQL (db.r6g.xlarge)~$500–700/month
Self‑managed PostgreSQL on EKS (m6g.xlarge node)~$120–180/month

관리형 서비스보다 3–5배 저렴하며, 데이터 규모가 커질수록 절감 효과가 커집니다.

컨테이너화된 PostgreSQL의 장점

  • Full control over configuration, extensions, and versions
    → 구성, 확장 및 버전에 대한 완전한 제어
  • Portability: the same docker‑compose.yml works in dev, staging, and production
    → 이식성: 동일한 docker‑compose.yml이 개발, 스테이징 및 프로덕션에서 작동합니다
  • No vendor lock‑in – you aren’t tied to a cloud provider’s upgrade schedule or supported version matrix
    → 벤더 종속 없음 – 클라우드 제공업체의 업그레이드 일정이나 지원 버전 매트릭스에 얽매이지 않습니다
  • Extension freedom – install PostGIS, TimescaleDB, pgvector, or any community extension without waiting for managed support
    → 확장 자유도 – PostGIS, TimescaleDB, pgvector 또는 기타 커뮤니티 확장을 관리형 지원을 기다리지 않고 설치할 수 있습니다

Trade‑off: you own the operational complexity, and major version upgrades are where that complexity bites hardest.
Trade‑off: 운영 복잡성을 직접 관리해야 하며, 주요 버전 업그레이드 시 그 복잡성이 가장 크게 작용합니다.

업그레이드 도전 과제

  • 마이너 버전 업그레이드 (예: 15.3 → 15.7): 이미지 태그를 교체하고 재시작 – 데이터 디렉터리 형식은 동일하게 유지됩니다.
  • 메이저 버전 업그레이드 (예: 13 → 16): 내부 데이터 형식이 변경됩니다; PostgreSQL 13으로 만든 데이터 디렉터리를 PostgreSQL 16 바이너리로 지정할 수 없습니다. 서버가 시작을 거부합니다.

공식 도구인 **pg_upgrade**는 데이터 디렉터리를 제자리에서 마이그레이션하지만, 같은 머신에 오래된 바이너리와 새로운 바이너리를 모두 두고 데이터 디렉터리에 대한 공유 접근이 필요합니다—컨테이너 환경에서는 부자연스러운 요구사항입니다.

Source:

일반적인 업그레이드 접근 방식과 그 단점

pg_dumpall / pg_dump + Restore

# Dump from old container
docker exec pg13 pg_dumpall -U postgres > full_dump.sql

# Restore into new container
docker exec -i pg16 psql -U postgres < full_dump.sql

문제점

  • 다운타임이 데이터베이스 크기에 비례함(예: 100 GB → 1–3 시간 덤프 + 2–5 시간 복원).
  • 새 클러스터가 승격된 이후에는 롤백이 불가능함.
  • 무결성 증명이 없음(행, 시퀀스, 물리화된 뷰).
  • 메모리/디스크 압력이 높음: 500 GB DB의 경우 덤프 파일이 200–400 GB가 될 수 있음.

수시간에 달하는 다운타임 윈도우는 프로덕션 워크로드에 대해 종종 허용되지 않음.

논리 복제(pglogical) 또는 내장 슬롯

PG 13 (primary) ──logical replication──► PG 16 (replica)

                               [catch up, then promote]

문제점

  • 전환 시 복제 지연 발생; DDL 변경은 복제되지 않으며 두 클러스터에 모두 수동으로 적용해야 함.
  • 설정 복잡도 높음: wal_level = logical, 복제 슬롯, 대용량 객체 처리, 시퀀스 관리 등.
  • 슬롯이 WAL을 무한히 보관할 수 있어, 쓰기 부하가 큰 워크로드에서는 디스크가 빠르게 가득 찰 수 있음.
  • 특정 데이터 타입(pg_largeobject, 언로그 테이블 등)은 깨끗하게 복제되지 않음.
  • 여전히 기존 클러스터에서 쓰기를 중단하는 전환 윈도우가 필요함.

볼륨 스냅샷

  1. 기존 클러스터를 백업하고 있는 EBS/PD 볼륨을 스냅샷한다.
  2. PostgreSQL 16이 설치된 새 인스턴스에 마운트한다.
  3. 그곳에서 pg_upgrade를 실행한다.

문제점

  • 새 노드에 PostgreSQL 16이 설치되지 않을 수 있어, 바이너리 가용성 문제가 다시 발생함.
  • 재현성이 없음: 수동적이고 반복 불가능한 단계이며 명령 로그가 남지 않음.
  • 업그레이드 후 체계적인 무결성 검사가 없음.
  • 스테이징 환경에서 테스트하기 어려움(볼륨 구성이 다름).

컨테이너 내부에서 pg_upgrade 수행

FROM ubuntu:22.04
RUN apt-get install postgresql-13 postgresql-16

문제점

  • 표준 이미지가 없음; 각 팀이 경로와 권한이 다른 커스텀 이미지를 직접 빌드함.
  • 권한 관리가 까다로움: pg_upgradepostgres OS 사용자로 실행돼야 하며, 데이터 디렉터리는 해당 사용자 소유여야 함.
  • CI 검증이 없으며, 업그레이드가 프로덕션에서 수동으로 수행됨.
  • 오래된 베이스 이미지(예: Debian Stretch/Buster)는 EOL에 도달해 레거시 PostgreSQL 버전 설치가 실패함.

Introducing pg‑upgrade

pg-upgrade구버전신버전 PostgreSQL 바이너리를 모두 포함하는 Docker 이미지 세트를 제공합니다. 이 이미지들은 Docker 볼륨을 통해 연결된 세 개의 조정된 컨테이너 단계—init‑old, upgrade, verify—를 오케스트레이션합니다.

Upgrade workflow diagram

┌──────────────────────────────────────────────────────────────────┐
│  Step 1 — init‑old                                                │
│  Seeds the old cluster with real‑world schema, data, etc.       │
└──────────────────────┬───────────────────────────────────────────┘
                       │  pg‑old‑data volume
┌──────────────────────▼───────────────────────────────────────────┐
│  Step 2 — upgrade                                                │
│  Runs pg_upgrade --check (dry run) then the real upgrade.       │
│  Shows before/after snapshots, file sizes, structural changes.  │
└──────────────────────┬───────────────────────────────────────────┘
                       │  pg‑new‑data volume
┌──────────────────────▼───────────────────────────────────────────┐
│  Step 3 — verify                                                 │
│  Starts the upgraded cluster and asserts:                       │
│  • databases exist                                            │
│  • row counts match                                            │
│  • indexes, views, sequences, foreign keys survive            │
└──────────────────────────────────────────────────────────────────┘

프로덕션 업그레이드에서는 init‑old 단계를 건너뛰고 기존 PVC를 Step 2에 직접 마운트합니다. 기존 클러스터는 먼저 0으로 스케일링해야 하며(다운타임 창), 업그레이드 자체는 몇 초에서 몇 분 안에 완료되며, 몇 시간 걸리지 않습니다.

성능 비교

DB 크기pg_dump + 복원논리 복제 전환pg‑upgrade (복사 모드)pg‑upgrade (링크 모드 -k)
10 GB30–90 분5–30 분~45 초< 5 초
100 GB3–8 시간5–30 분~7 분< 5 초
1 TB30+ 시간5–30 분~70 분< 5 초

**링크 모드 (-k)**는 파일을 복사하는 대신 하드 링크를 사용하므로 업그레이드 시간은 클러스터 크기에 영향을 받지 않습니다. 트레이드오프: 업그레이드 후에는 이전 데이터 디렉터리가 독립적으로 유효하지 않으므로, 새 클러스터가 정상임을 확인한 뒤에만 삭제하십시오.

CI‑생성 보고서

업그레이드 요약

──────────────────────────────────────────────────────────────────────
  Old cluster — PostgreSQL 9.6
──────────────────────────────────────────────────────────────────────
  Path:                /var/lib/postgresql/9.6/main
  Total size:          47M

  Notable structural changes applied during this upgrade:
    pg_xlog/    → pg_wal/    (WAL directory, renamed in PG 10)
    pg_clog/    → pg_xact/   (transaction status, renamed in PG 10)
    pg_log/     → log/       (server log directory, renamed in PG 10)

──────────────────────────────────────────────────────────────────────
  Upgrade complete
──────────────────────────────────────────────────────────────────────
  Cluster size:        47M → 49M  (+4%)
  Upgrade duration:    8s
  PostgreSQL version:  9.6 → 16
──────────────────────────────────────────────────────────────────────

검증 결과

──────────────────────────────────────────────────────────────────────
  Verification result — PostgreSQL 16
──────────────────────────────────────────────────────────────────────
  Passed:    9
  Failed:    0
──────────────────────────────────────────────────────────────────────

이 보고서는 데이터가 온전하게 유지되었음을 체계적이고 스크립트화된 방식으로 입증합니다—다른 접근 방식에서는 부족한 점입니다.

다른 방법에 대한 장점

  • 실시간 데이터베이스 연결 없음pg_upgradepostgres OS 사용자로서 데이터 파일에 직접 작동합니다. 비밀번호가 필요 없고, pg_hba.conf 변경도 없습니다.
  • 자격 증명의 자동 마이그레이션pg_authid 항목이 데이터와 함께 이동합니다.
  • 모든 업그레이드 경로에 대해 CI 검증됨 – GitHub Actions가 지원되는 각 버전 쌍에 대해 전체 3단계 파이프라인(init → upgrade → verify)을 실행합니다.
  • 동일한 이미지가 Kubernetes에서 작동 – Docker 볼륨을 PersistentVolumeClaims로 교체하고 Jobs로 실행합니다.

Kubernetes에서 pg‑upgrade 실행

# pg-upgrade Job (example)
apiVersion: batch/v1
kind: Job
metadata:
  name: pg-upgrade-run
spec:
  template:
    spec:
      containers:
      - name: pg-upgrade
        image: abhsss/pg-upgrade:13-to-16
        args: ["upgrade"]
        volumeMounts:
        - name: pg-old-data
          mountPath: /var/lib/postgresql/13/main
        - name: pg-new-data
          mountPath: /var/lib/postgresql/16/main
      restartPolicy: Never
      volumes:
      - name: pg-old-data
        persistentVolumeClaim:
          claimName: pg13-pvc
      - name: pg-new-data
        persistentVolumeClaim:
          claimName: pg16-pvc
# Apply the upgrade Job
kubectl apply -f pg-upgrade-job.yaml

# Wait for completion (timeout 30 min)
kubectl wait --for=condition=complete job/pg-upgrade-run --timeout=30m

# View the output
kubectl logs job/pg-upgrade-run

프로덕션 StatefulSet 워크플로우

  1. kubectl scale statefulset postgres --replicas=0 (다운타임)
  2. 기존 PVC를 마운트하여 업그레이드 Job 실행.
  3. StatefulSet 이미지 태그를 PostgreSQL 16으로 업데이트.
  4. 검증 Job 실행.
  5. kubectl scale statefulset postgres --replicas=<desired>

링크 모드에서는 DB 크기에 관계없이 단계 2–4가 1분 이내에 완료됩니다.

Docker 명령줄 사용법

# 적절한 이미지 가져오기
docker pull abhsss/pg-upgrade:13-to-16

# Docker 볼륨 생성
docker volume create pg-old-data
docker volume create pg-new-data

# 단계 1 — 테스트 데이터 시드 (프로덕션에서는 건너뛰기)
docker run --rm \
  -v pg-old-data:/var/lib/postgresql/13/main \
  abhsss/pg-upgrade:13-to-16 init-old

# 단계 2 — 업그레이드
docker run --rm \
  -v pg-old-data:/var/lib/postgresql/13/main \
  -v pg-new-data:/var/lib/postgresql/16/main \
  abhsss/pg-upgrade:13-to-16 upgrade

# 단계 3 — 검증
docker run --rm \
  -v pg-new-data:/var/lib/postgresql/16/main \
  abhsss/pg-upgrade:13-to-16 verify

# 정리
docker volume rm pg-old-data pg-new-data

올바른 접근 방식 선택

상황권장 접근 방식
작은 DB, 장시간 다운타임 허용 가능pg_dump / restore – 간단하고 별도 도구 필요 없음
무중단 요구, 전환 중 DDL 변경이 있는 복잡한 스키마논리 복제 + 신중한 전환 스크립팅
컨테이너화된 PostgreSQL, 예측 가능한 다운타임 창pg-upgrade – 재현 가능, CI 검증, 빠름
컨테이너화된 PostgreSQL, 절대 최소 다운타임pg-upgrade with link mode -k

pg‑upgrade 기여하기

이 프로젝트는 오픈 소스이며 모든 규모의 기여를 환영합니다.

좋은 첫 번째 이슈 (범위가 작고 정의가 명확함)

  1. pgvector CI 매트릭스 항목 및 픽스처 추가 – 벡터 유사도 쿼리가 업그레이드 후에도 정상 작동하도록 합니다.
  2. pg_upgrade에 대한 --jobs N 병렬 처리 지원 – 대규모 클러스터에서 빠른 업그레이드를 위해 환경 변수로 노출합니다.
  3. delete-old 엔트리포인트 명령 추가delete_old_cluster.sh를 일등급 명령으로 래핑합니다.
  4. README 업데이트.
  5. 누락된 업그레이드 매트릭스 경로 채우기 – 10→12, 11→13, 12→13에 대한 Dockerfile 및 CI 항목을 추가합니다.

더 큰 기여 (도움 필요)

  1. pg_partman, pg_cron, pgaudit에 대한 CI 커버리지.
  2. PG 17을 업그레이드 대상에 추가COPY --fromldconfig 조정을 처리합니다.
  3. --link--clone 업그레이드 모드 지원 – 기본 copy‑mode 동작을 깨지 않으면서 -k와 clone 옵션을 노출합니다.

Note: 풀 리퀘스트를 제출하기 전에 범위를 논의하기 위해 먼저 이슈를 열어 주세요.

최종 생각

컨테이너화된 PostgreSQL은 큰 비용 절감, 완전한 제어, 벤더 종속성 없음이라는 장점을 제공하지만, 주요 버전 업그레이드 시점이 운영 책임이 가장 뚜렷하게 드러나는 순간입니다. 기존 방식인 덤프/복원, 논리 복제, 수동 pg_upgrade는 수시간의 다운타임, 재현 불가능한 절차, 체계적인 무결성 검증 부재를 초래합니다.

**pg-upgrade**는 이러한 문제점을 해결합니다. CI에서 실행하는 동일한 세 단계를 프로덕션에서도 그대로 수행하는 재현 가능한 Docker‑네이티브 파이프라인을 제공하므로, 업그레이드가 몇 초에서 몇 분 안에 완료되고, 구조화된 기계 판독 가능한 보고서를 출력하며, 새로운 클러스터를 승격하기 전에 서명된 검증을 제공합니다.

Source code & contribution guidelines: https://github.com/abhsss96/postgres-upgrade-kit
Docker images: https://hub.docker.com/r/abhsss/pg-upgrade

0 조회
Back to Blog

관련 글

더 보기 »