본문으로 건너뛰기

Barman Cloud S3 백업

· 약 14분

Barman 서버 없이 백업하기​

2~5편은 모두 Barman 서버 한 대가 PostgreSQL에서 백업을 가져오는 구조였어요. 이번 편은 그 서버를 제외하고, PostgreSQL 호스트가 S3 버킷에 직접 백업을 올리는 barman-cloud-* 명령을 다뤄요. 로컬 Docker에 MinIO를 띄워 WAL 아카이브, base backup, 보존 정책, PITR까지 한 번씩 실행해 봤어요.

Barman의 barman-cloud-* 명령은 barman-cli-cloud 패키지로 따로 배포됩니다. 공식 문서는 이 명령을 PostgreSQL 서버와 같은 머신에 설치하고, PGDATA를 읽어야 하므로 보통 postgres 사용자로 실행한다고 안내합니다. 백업 결과는 Barman 서버 카탈로그 대신 오브젝트 스토리지에 객체로 남습니다. CloudNativePG의 in-tree 구현도 복구할 때 barman-cloud-restore를 호출하므로, 7편(CNPG와 Barman Cloud Plugin)으로 넘어가기 전에 명령 단위로 동작을 확인해 두려는 목적도 있습니다.

barman-cloud 명령 9개​

Barman 3.20 문서 기준으로 명령은 아홉 개입니다.

명령역할쓰는 자리
barman-cloud-wal-archiveWAL 세그먼트 업로드archive_command
barman-cloud-wal-restoreWAL 세그먼트 다운로드restore_command
barman-cloud-backupbase backup 생성과 업로드cron, 스케줄러
barman-cloud-backup-list백업 목록수동 확인
barman-cloud-backup-show백업 상세수동 확인
barman-cloud-backup-deleteID 또는 보존 정책으로 삭제cron, 스케줄러
barman-cloud-backup-keep보존 정책 제외 표시수동
barman-cloud-check-wal-archive아카이브 위치가 비었는지 검사최초 연결, 복구 후
barman-cloud-restorebase backup 다운로드복구

모든 명령이 DESTINATION_URL SERVER_NAME 두 인자를 받습니다. s3://pgbackup/lab이 URL이고 pg18이 서버 이름이면 객체는 pgbackup 버킷의 lab/pg18/ 아래에 쌓입니다. S3 호환 스토리지의 주소는 --endpoint-url로 지정하고, 기본 --cloud-provider는 aws-s3입니다.

측정 환경​

측정은 macOS의 Docker Desktop VM(2 CPU, 약 11.7GiB, 다른 작업과 공유)에서 진행했습니다. 시간 값은 모두 한 번씩 측정한 값이라 참고용입니다.

항목값
PostgreSQLpostgres:18 이미지, 18.4 (Debian 13)
BarmanPGDG apt barman-cli-cloud 3.20.0, Python 3.13.5, boto3 1.37.9
MinIOcgr.dev/chainguard/minio, RELEASE.2026-09-22T19-25-18Z
데이터pgbench -i -s 3, DB 53MB, pg_wal 제외 PGDATA 68MB

Docker Hub의 minio/minio와 quay.io/minio/minio는 이 환경에서 pull이 되지 않아 Chainguard 이미지를 썼습니다. Barman 문서는 MinIO를 쓴다면 RELEASE.2025-02-03T21-03-04Z 이상으로 올려야 백업 삭제가 실패하지 않는다고 경고합니다. 측정 버전은 이 조건을 충족합니다. 오래된 S3 호환 스토리지를 boto3 1.36 이상과 함께 써야 한다면 문서가 안내하는 AWS_REQUEST_CHECKSUM_CALCULATION=when_required, AWS_RESPONSE_CHECKSUM_VALIDATION=when_required 환경변수를 확인해 보세요.

설치에는 PGDG apt 저장소의 barman-cli-cloud와 python3-boto3 패키지를 사용했습니다. 처음에는 venv에서 pip install "barman[cloud]==3.20.0"을 시도했지만 psycopg2 소스 빌드에서 실패했습니다. Barman 3.20부터는 Python 3.12 이상이 필요하므로(3.20.0 릴리스 노트), Python 3.11 기반 배포판에서는 패키지 버전을 먼저 확인해야 합니다.

WAL 아카이브 연결​

인증 정보는 PostgreSQL 프로세스의 환경변수(AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION)로 전달했습니다. archive_command는 postmaster 환경을 물려받으므로, 운영에서는 systemd unit의 환경 파일이나 ~postgres/.aws/credentials로 같은 값을 줍니다.

연결하기 전에 아카이브 위치부터 검사합니다.

barman-cloud-check-wal-archive \
--endpoint-url http://barman-cloud-s3-backup-minio:9000 \
s3://pgbackup/lab pg18
# exit=0

이 명령은 아카이브 위치가 비어 있으면 성공합니다. WAL이 하나라도 있으면 실패하고, --timeline을 지정하면 그보다 앞선 timeline의 WAL만 있을 때 통과합니다. 다른 클러스터의 WAL과 섞이는 사고를 막는 검사라서, 새 클러스터를 기존 경로에 연결하기 전에 실행해 두는 편이 좋습니다.

ALTER SYSTEM SET archive_mode = on;
ALTER SYSTEM SET archive_command =
'barman-cloud-wal-archive --endpoint-url http://barman-cloud-s3-backup-minio:9000 --zstd s3://pgbackup/lab pg18 %p';
-- archive_mode 변경은 재시작 필요

재시작한 뒤 pg_switch_wal()을 한 번 호출하자 pg_stat_archiver가 archived_count=1, failed_count=0으로 바뀌었습니다. 16MB 세그먼트는 --zstd 덕분에 버킷에 1.2MiB, 500KiB처럼 작게 올라갔고, 거의 빈 세그먼트는 700B 안팎이었습니다.

base backup 올리기​

barman-cloud-backup --endpoint-url http://barman-cloud-s3-backup-minio:9000 \
--immediate-checkpoint -n plain s3://pgbackup/lab pg18
barman-cloud-backup --endpoint-url http://barman-cloud-s3-backup-minio:9000 \
--immediate-checkpoint --zstd -n zstd s3://pgbackup/lab pg18
백업압축소요 시간 (time real)버킷 크기
plain없음2.893초data.tar 69MiB
zstd--zstd2.964초data.tar.zst 4.7MiB

압축 비율이 이렇게 큰 것은 pgbench 초기 데이터가 반복 패턴이 많은 합성 데이터이기 때문입니다. 실제 데이터에서는 이 비율이 나오지 않을 가능성이 높으니 일반화하지 않는 편이 맞습니다. 이 크기에서는 압축 여부가 소요 시간에 거의 영향을 미치지 않았습니다. 병렬 업로드 기본값은 -J 2이고, backup.info의 copy_stats에도 number_of_workers: 2로 기록됐습니다.

버킷에 남은 객체는 다음과 같습니다(mc ls -r, 일부 생략).

69MiB lab/pg18/base/20260928T070251/data.tar
1.3KiB lab/pg18/base/20260928T070251/backup.info
4.7MiB lab/pg18/base/20260928T070254/data.tar.zst
1.3KiB lab/pg18/base/20260928T070254/backup.info
1.2MiB lab/pg18/wals/0000000100000000/000000010000000000000001.zst
203B lab/pg18/wals/0000000100000000/000000010000000000000005.00000028.backup.zst
699B lab/pg18/wals/0000000100000000/000000010000000000000005.zst
...

이 lab에서는 백업마다 base/<백업ID>/ 디렉터리에 tar 한 개와 backup.info 한 개가 생겼습니다. -S/--max-archive-size는 tar 한 개의 최대 크기이며 기본값은 100GB입니다. WAL은 wals/<timeline+log 번호>/ 아래에 세그먼트 단위로 쌓입니다. backup.info는 key=value 텍스트이고 begin_wal, end_wal, timeline, systemid, backup_name 같은 값이 들어 있어서, 삭제와 복구 명령이 이 파일을 읽어 판단합니다.

Backup ID End Time Begin Wal Archival Status Name
20260928T070251 2026-09-28 07:02:52 000000010000000000000005 plain
20260928T070254 2026-09-28 07:02:54 000000010000000000000007 zstd

-n으로 지정한 이름은 barman-cloud-backup-list의 Name 열에 표시됩니다. 백업 ID 자리에는 first/oldest, last/latest, last-failed 단축어도 사용합니다. 뒤의 복구에서 사용한 latest가 그 예입니다.

보존 정책과 삭제​

Barman 서버 모드에서는 barman cron이 보존 정책을 주기적으로 적용합니다(4편). barman-cloud-*에는 그런 상주 프로세스가 없어서, barman-cloud-backup-delete -r를 백업 스케줄 옆에 직접 등록해야 합니다. 정책 문법은 서버 모드와 같은 REDUNDANCY n, RECOVERY WINDOW OF n {DAYS|WEEKS|MONTHS}이고, -m/--minimum-redundancy로 최소 개수를 지정합니다.

백업을 두 개 더 만들어 네 개로 늘린 뒤, 가장 오래된 plain에 keep --target standalone을 설정하고 REDUNDANCY 2를 --dry-run으로 먼저 실행했습니다.

barman-cloud-backup-keep --endpoint-url ... --target standalone \
s3://pgbackup/lab pg18 20260928T070251
barman-cloud-backup-delete --endpoint-url ... -r "REDUNDANCY 2" --dry-run \
s3://pgbackup/lab pg18
Skipping deletion of objects ['lab/pg18/base/20260928T070254/data.tar.zst',
'lab/pg18/base/20260928T070254/backup.info'] due to --dry-run option
Skipping deletion of objects ['.../000000010000000000000001.zst', ... '0002', '0003', '0004',
'0006', '0007.00000028.backup', '0007', '0008', '0009', '000A', '000B', '000C'] due to --dry-run option

삭제 대상 백업은 zstd 하나였습니다. 네 개 가운데 plain은 keep 표시 때문에 개수 계산에서 빠지고, 최신 두 개(b3, b4)가 REDUNDANCY 2를 채웠기 때문입니다. WAL 쪽 결과가 더 흥미로웠습니다. plain의 시작 세그먼트인 ...05와 ...05.00000028.backup은 목록에서 빠졌지만, plain 이후부터 b3 시작 전까지인 ...06~...0C는 삭제 대상에 들어갔습니다.

문서의 WAL 삭제 조건이 이 결과를 설명합니다. 남은 백업 중 가장 오래된 비archival 백업의 begin_wal보다 앞서고, 클라우드에 있는 어떤 archival 백업에도 필요하지 않은 WAL이 삭제 대상입니다. standalone keep은 백업 자체의 일관성에 필요한 WAL만 보존하기 때문에, plain 시점으로는 되돌려도 plain과 b3 사이의 임의 시점으로는 PITR이 되지 않습니다. 그 구간이 필요하면 --target full로 표시해야 합니다.

실제 삭제에는 0.253초가 걸렸고, 버킷은 30개 객체 97MiB에서 16개 객체 85MiB로 줄었습니다. plain이 압축하지 않은 69MiB라서 크기는 많이 줄지 않았습니다.

Backup ID End Time Begin Wal Archival Status Name
20260928T070251 2026-09-28 07:02:52 000000010000000000000005 KEEP:STANDALONE plain
20260928T070408 2026-09-28 07:04:08 00000001000000000000000D b3
20260928T070415 2026-09-28 07:04:15 000000010000000000000011 b4

keep 표시는 base/20260928T070251/annotations/keep라는 10바이트 객체로 저장됐습니다. 버킷 수명 주기 규칙(lifecycle rule)으로 오래된 객체를 삭제하는 방식과 함께 쓰면 이 관계가 깨지므로, 삭제는 barman-cloud-backup-delete에 맡기는 편이 안전합니다. AWS S3의 Object Lock을 쓰는 경우 3.17.0부터 --check-object-lock으로 잠긴 객체가 있는 백업의 삭제를 중단합니다(3.17.0 릴리스 노트).

복구와 PITR​

PITR 대상 시점을 만들기 위해 orders_marker 테이블에 1,000행을 넣었습니다. 2초 뒤 시각 2026-09-28 07:03:20+00을 기록하고, 다시 2초 뒤 테이블을 삭제했습니다.

barman-cloud-restore --endpoint-url ... \
s3://pgbackup/lab pg18 latest /var/lib/postgresql/restore
# real 0m0.330s

barman-cloud-restore는 파일만 풀어 놓습니다. 문서도 "This command does not automatically prepare Postgres for recovery"라고 적고 있습니다. restore_command, recovery target, signal 파일, 필요한 WAL도 직접 준비해야 한다고 안내합니다. 풀린 디렉터리에는 backup_label이 있었고 pg_wal은 비어 있었습니다.

# restore/postgresql.auto.conf 에 추가
port = 5433
archive_mode = off
restore_command = 'barman-cloud-wal-restore --endpoint-url http://barman-cloud-s3-backup-minio:9000 s3://pgbackup/lab pg18 %f %p'
recovery_target_time = '2026-09-28 07:03:20+00'
recovery_target_action = 'promote'
touch /var/lib/postgresql/restore/recovery.signal
chmod 700 /var/lib/postgresql/restore
pg_ctl -D /var/lib/postgresql/restore -l restore.log -w start

archive_mode = off는 복구한 인스턴스가 원본과 같은 경로에 WAL을 올리지 않게 하려고 넣었습니다. 복구 인스턴스가 promote된 뒤 timeline 2의 WAL을 같은 SERVER_NAME으로 올리면 두 클러스터의 이력이 한 경로에 섞입니다.

LOG: restored log file "000000010000000000000007" from archive
LOG: starting point-in-time recovery to 2026-09-28 07:03:20+00
LOG: restored log file "000000010000000000000008" from archive
LOG: consistent recovery state reached at 0/7000120
LOG: recovery stopping before commit of transaction 767, time 2026-09-28 07:03:22.355701+00
LOG: selected new timeline ID: 2
LOG: archive recovery complete

pg_ctl start는 1.118초 만에 반환됐습니다. orders_marker는 1,000행 그대로 돌아왔고 pg_is_in_recovery()는 f였습니다. 로그의 transaction 767이 DROP TABLE이고, 기록한 시각 이후 첫 commit에서 replay가 멈췄습니다. 대량 WAL을 받아야 하는 복구라면 3.19.0에서 추가된 barman-cloud-wal-restore -p/--parallel 옵션을 검토해 볼 만합니다. 여러 세그먼트를 동시에 받아 --spool-dir에 저장하는 방식입니다.

아카이브가 실패하면​

MinIO 컨테이너를 멈춘 상태에서 WAL 세그먼트 두 개를 채웠습니다. 약 40초 뒤 상태는 다음과 같았습니다.

pg_stat_archiver: archived_count=21, failed_count=5, last_failed_wal=000000010000000000000012
archive_status: ...12.ready, ...13.ready

ERROR: Barman cloud WAL archiver exception: Could not connect to the endpoint URL: "http://.../000000010000000000000012.zst"
LOG: archive command failed with exit code 4
WARNING: archiving write-ahead log file "000000010000000000000012" failed too many times, will try again later

PostgreSQL archiver는 같은 세그먼트를 순서대로 재시도합니다. 연속 3회 실패하면 잠시 쉬었다가 다시 시도합니다. 실패한 세그먼트는 .ready 상태로 pg_wal에 남아 있습니다. MinIO를 다시 켜자 70초 안에 두 세그먼트 모두 .done으로 바뀌었습니다. 스토리지 장애가 길어지면 그동안 pg_wal이 계속 커지므로, pg_stat_archiver.failed_count와 last_failed_time을 알람 대상에 넣어 두어야 합니다. Barman 서버의 barman check처럼 이 상태를 대신 확인해 주는 프로세스가 없다는 점이 서버 모드와 가장 크게 다릅니다.

rsync, streaming 모드와 비교​

항목rsync 모드 (3편)streaming 모드 (2편)barman-cloud
백업 저장 위치Barman 서버 디스크Barman 서버 디스크오브젝트 스토리지
Barman 서버필요필요불필요
base backup 전송rsync over SSHpg_basebackupPostgreSQL 호스트에서 업로드
WAL 전송archive_command (SSH)pg_receivewalarchive_command
보존 정책 적용barman cronbarman cronbarman-cloud-backup-delete 직접 실행
파일 단위 dedupreuse_backup = link——
백업 부하 위치PostgreSQL + Barman 서버PostgreSQL + Barman 서버PostgreSQL 호스트

서버 모드에서는 Barman 서버가 여러 PostgreSQL을 한곳에서 관리하고 barman check, barman cron, 카탈로그를 제공합니다. barman-cloud는 그 관리 계층을 포기하는 대신 서버 한 대를 따로 운영하지 않아도 됩니다. 압축과 업로드에 PostgreSQL 호스트의 CPU를 쓴다는 점도 설계할 때 고려해야 합니다. 이번 측정 규모에서는 부하 차이가 드러나지 않았고, 대용량에서 비교한 수치는 없습니다.

두 방식을 함께 쓰는 구성도 있습니다. 문서는 barman-cloud-backup을 Barman 서버의 post_backup_retry_script hook으로 사용해 서버에 받은 백업을 클라우드로 복사하는 구성을 안내합니다. 로컬에서 빠르게 복구하고 원격 사본은 버킷에 두는 구조입니다.

장점​

PostgreSQL 호스트와 버킷만 있으면 백업이 성립합니다. 서버 대수를 줄이려는 환경이나 이미 오브젝트 스토리지를 표준 백업 위치로 쓰는 조직에 적합합니다. 버킷의 내구성과 수명 비용 구조를 그대로 가져오므로 원격 보관을 따로 설계하지 않아도 됩니다.

객체 배치가 단순합니다. base/<ID>/data.tar*, backup.info, wals/<prefix>/<segment> 구조라서 mc나 aws s3 ls로도 상태를 확인할 수 있고, 명령이 없는 환경에서도 tar를 직접 받아 풀 여지가 있습니다.

CloudNativePG와 쓰는 도구가 겹칩니다. in-tree 구현이 barman-cloud-restore 같은 명령을 호출하므로, 여기서 익힌 명령과 객체 배치가 Kubernetes 쪽 문제를 분석할 때도 쓰입니다. Barman Cloud Plugin 내부 구조는 이번에 확인하지 않았고 7편에서 다룹니다.

단점과 제약​

  • 상주 프로세스 없음: 보존 정책 적용, 아카이브 감시, 백업 스케줄을 전부 외부(cron, 모니터링)에 맡겨야 함
  • 복구 준비 수동: barman-cloud-restore는 파일만 복원하고 recovery 설정은 직접 작성
  • 증분 없음: 3.20 barman-cloud-backup --help에 incremental 옵션이 없고, 매번 전체 tar를 업로드
  • 부하 위치: 압축과 업로드가 PostgreSQL 호스트의 CPU와 네트워크를 사용
  • 삭제 시점: REDUNDANCY 계산에서 keep 백업이 빠지고, standalone keep은 사이 구간 WAL을 보존하지 않음

증분 백업은 서버 쪽 기능으로 추가되고 있습니다. 3.18.0은 backup_method = postgres에 basebackups_directory, wals_directory를 클라우드 URL로 지정하는 block-level 증분 백업을 실험 기능으로 추가했습니다. 이번 글에서는 측정하지 않았습니다.

버전별 변화와 CloudNativePG​

최근 Barman 릴리스는 클라우드 기능을 barman 본체로 옮기는 방향입니다.

버전날짜클라우드 관련 변경
3.17.02026-01-07barman-cloud-backup-delete --check-object-lock (AWS S3)
3.18.02026-03-12backup_method = local-to-cloud 추가, 클라우드 증분 백업(실험)
3.19.02026-05-20클라우드 백업을 barman restore로 복원, barman cloud-wal-restore, barman-cloud-wal-restore 병렬
3.20.02026-08-27Python 3.12 이상 필요, AWS S3 SSE-C (--sse-customer-key)

3.18.0 릴리스 노트는 local-to-cloud를 설명하면서 이것이 "the first step toward deprecating the standalone barman-cloud-* scripts"라고 적었습니다. 3.20.0 기준으로 스크립트 제거 일정은 찾지 못했습니다. 지금 스크립트로 구성하더라도 이 방향은 알고 있어야 합니다.

CloudNativePG 쪽은 공식 릴리스 노트에서 다음 범위까지 확인했습니다.

CNPG 릴리스날짜in-tree Barman Cloud 제거 예정 버전
1.26.02025-05-231.28.0 (deprecated 발표)
1.27.42026-03-311.29.0에서 1.30.0으로 변경
1.30.02026-06-291.30.0에서 1.31.0으로 변경

1.28.0이 1.29.0으로 바뀐 시점은 이번에 확인하지 못했습니다. 1.30.0 릴리스 노트는 "Users are still encouraged to migrate to the Barman Cloud Plugin"이라고 적고 있습니다. 2026-09-23에 나온 1.30.1에도 in-tree Barman Cloud 복구 관련 수정이 포함되어 있습니다. 제거 일정이 여러 번 미뤄졌지만 방향은 바뀌지 않았으므로, 1.31 전에 플러그인 전환을 계획해 두는 편이 안전합니다. 전환 절차는 7편에서 다룹니다.

정리​

항목내용
구성PostgreSQL 호스트에 barman-cli-cloud, archive_command에 barman-cloud-wal-archive
객체 배치<prefix>/<server>/base/<ID>/, <prefix>/<server>/wals/<prefix>/
보존 정책barman-cloud-backup-delete -r를 직접 스케줄, --dry-run으로 먼저 확인
keepstandalone은 백업 시점만, full은 이후 WAL까지 보존
복구barman-cloud-restore 후 restore_command, recovery target, recovery.signal 직접 작성
감시pg_stat_archiver.failed_count, pg_wal 크기
CNPGin-tree Barman Cloud는 1.31.0 제거 예정 (1.30.0 릴리스 노트)

서버 없이 버킷 하나로 백업이 돌아가는 건 편하지만, Barman 서버가 담당하던 감시와 정리를 이제 직접 챙겨야 해요. 저는 --dry-run 출력으로 WAL 삭제 범위를 한 번 확인해 보고 나서야 standalone keep의 의미가 분명해졌어요.

참고 자료​