상태: 초안 (2026-10-17), 일부 실측. 기준 판 cloudnative-pg v1.30.0(커밋 4b5e244a7d03), plugin-barman-cloud v0.14.0(1e13020fe5f0), cnpg-i v0.6.0(844d20b2b783), cnpg-i-machinery v0.4.2(e3ead2b31db4). 작은 클러스터가 바로 이 판(오퍼레이터 1.30.0, 플러그인 v0.14.0)을 돌리고 있어서, 실측은 2026-10-17 그 클러스터에서 읽기만 해서 얻었습니다(CR 읽기, 파드 구성 읽기, 실행 중 파드 안의
/proc·설정 파일 읽기, 로그 읽기). 쓰기·재시작·복구 시험은 하지 않았습니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」, 확인하지 못한 것은 「미확인」으로 표시합니다.
목차
- 1. 한 줄 요약: PostgreSQL 옆에 붙은 「상주 관리인」과 「외주 택배 기사」
- 2. 구조도
- 3. 코드 지도
- 4. 핵심 경로 따라가기
- 5. 글쓴이의 기여와 이어지는 곳
- 6. 실측: 작은 클러스터에서 읽은 것
- 7. 어디부터 읽을까
- 8. 첫 기여하기 좋은 곳
- 9. 한계
- 참고
1. 한 줄 요약: PostgreSQL 옆에 붙은 「상주 관리인」과 「외주 택배 기사」
PostgreSQL 을 쿠버네티스에서 돌릴 때 어려운 것은 파드를 띄우는 일이 아니라, primary 를 정하고, 죽으면 바꾸고, WAL 을 빠짐없이 바깥에 실어 나르는 일입니다. CloudNativePG(CNPG)는 StatefulSet 을 쓰지 않고 파드와 PVC 를 직접 만들며, 각 파드 안에 상주 관리인(인스턴스 매니저)을 PID 1 로 앉혀 PostgreSQL 을 자식 프로세스로 기릅니다.
최근 판에서는 백업과 WAL 아카이브라는 「물건 실어 나르기」를 오퍼레이터 본체에서 떼어 내 외주 택배 기사(플러그인)에게 맡기는 쪽으로 옮겨 가고 있습니다. 둘 사이의 계약서가 CNPG-I 라는 gRPC 인터페이스이고, CNPG 프로젝트가 직접 만드는 택배 기사가 barman-cloud 플러그인입니다. 본체가 S3·GCS·Azure 의 사정을 몰라도 되고, 플러그인은 본체 릴리스와 따로 고칠 수 있게 하려는 설계입니다.
| 구성요소 | 하는 일 | 어디서 도나 |
|---|---|---|
| CNPG 오퍼레이터 | Cluster·Backup·Pooler 등 reconcile, 파드·PVC·서비스 생성, failover 결정 | cnpg-system 의 Deployment |
인스턴스 매니저(manager instance run) |
PostgreSQL 을 자식으로 실행, 설정 파일 작성, 상태 HTTP 엔드포인트 제공 | 인스턴스 파드의 PID 1 |
manager wal-archive / wal-restore |
PostgreSQL 의 archive_command·restore_command 가 부르는 일회성 명령 |
인스턴스 파드, WAL 마다 한 번 |
| barman-cloud 플러그인, operator 반쪽 | Cluster 를 보고 RBAC 준비, 파드에 사이드카를 주입하는 패치 생성 | cnpg-system 의 Deployment |
| barman-cloud 플러그인, instance 반쪽 | WAL 아카이브·복원, 베이스 백업, 지표 | 인스턴스 파드의 사이드카 |
| ObjectStore CR | 어느 버킷에, 어떤 자격으로, 어떤 압축으로 | 앱 네임스페이스 |
2. 구조도
그림 읽는 법:
- 위 칸은
cnpg-system입니다. 오퍼레이터는 플러그인 Deployment 와 TCP + mTLS gRPC 로 이야기합니다. 플러그인을 찾는 방법은 설정 파일이 아니라 Service 의 라벨과 주석입니다(4.2절). - 아래 칸은 인스턴스 파드 하나입니다. PostgreSQL 컨테이너의 PID 1 은 오퍼레이터와 같은 바이너리(
manager)이고, 이 바이너리는 init 컨테이너가 오퍼레이터 이미지에서 꺼내 공유 볼륨에 복사해 둔 것입니다. 그래서 PostgreSQL 이미지에는 CNPG 코드가 없어도 됩니다. - 플러그인의 파드 안 반쪽은 사이드카로 붙고, PostgreSQL 컨테이너와는 공유 볼륨
/plugins의 유닉스 소켓으로 gRPC 를 주고받습니다. - 플러그인 하나가 두 곳에 나뉘어 사는 이유는 일의 성격이 달라서입니다. 파드 스펙을 고치는 일은 파드가 생기기 전에 오퍼레이터 옆에서 해야 하고, WAL 파일을 읽는 일은 PGDATA 가 보이는 파드 안에서 해야 합니다.
3. 코드 지도
네 저장소를 함께 봅니다. 최상위에서 중요한 곳만 골랐습니다.
| 경로 | 무엇 | 처음 읽을 파일 |
|---|---|---|
cloudnative-pg api/v1/ |
Cluster·Backup 등 CRD 의 Go 타입과 도우미 함수 | cluster_types.go, backup_funcs.go |
cloudnative-pg internal/controller/ |
오퍼레이터의 reconcile 루프(Cluster, Backup, Plugin …) | cluster_controller.go, backup_controller.go |
cloudnative-pg internal/cnpi/plugin/ |
CNPG-I 클라이언트: 플러그인 발견, 연결 풀, RPC 래퍼 | repository/setup.go, client/wal.go |
cloudnative-pg internal/cmd/manager/ |
한 바이너리의 하위 명령들(instance run, wal-archive, backup …) |
instance/run/cmd.go, walarchive/cmd.go |
cloudnative-pg pkg/management/postgres/ |
인스턴스 매니저 본체: PostgreSQL 실행, 웹서버, 아카이버 | archiver/archiver.go, webserver/local.go |
cloudnative-pg pkg/postgres/ |
PostgreSQL 설정값 규칙(필수값 포함) | configuration.go |
cnpg-i proto/ |
인터페이스 정의(서비스 9개) | identity.proto, wal.proto |
cnpg-i-machinery pkg/pluginhelper/ |
플러그인 쪽 gRPC 서버 뼈대, 객체 디코더 | http/server.go |
plugin-barman-cloud internal/cnpgi/ |
플러그인 구현: operator/(Deployment 반쪽), instance/(사이드카 반쪽), common/(WAL) |
operator/lifecycle.go, common/wal.go |
plugin-barman-cloud api/v1/ |
ObjectStore CRD | objectstore_types.go |
4. 핵심 경로 따라가기
4.1 Cluster reconcile 루프: 플러그인부터 불러 놓고 시작한다
Reconcile
(internal/controller/cluster_controller.go:169)
은 Cluster 를 읽은 다음 가장 먼저 이 Cluster 가 쓰는 플러그인 이름을 모읍니다
(getPluginsNeededForReconcile, :298).
.spec.plugins 와 외부 클러스터에 적힌 플러그인을 합친 목록입니다. 그 이름들로 5초 안에 연결을 얻어
(WithPlugins, internal/cnpi/plugin/client/client.go:79)
context 에 넣어 두고 안쪽 루프로 들어갑니다. 플러그인을 못 찾으면 phase 를 「Unknown plugin」으로 바꾸고 10초 뒤 다시 봅니다. 적혀 있는데 안 보이는 플러그인을 조용히 넘기지 않으려는 것입니다.
안쪽 reconcile, :310 은
길지만 순서가 명확합니다.
- 기본값 채우기, 이미지 결정, 플러그인 상태 갱신, 보조 객체(Secret·Service·PDB) 만들기
(
createPostgresClusterObjects,cluster_create.go:57). - 플러그인의 pre-reconcile 훅
(
preReconcilePluginHooks,plugins.go:36). barman-cloud 는 여기서 사이드카가 ObjectStore 와 Secret 을 읽을 Role·RoleBinding 을 만듭니다(4.5절). - 각 인스턴스의 상태를 HTTPS 로 묻기
(
GetStatusFromInstances,pkg/management/postgres/webserver/client/remote/instance.go:183). primary 가 둘로 보이면 아무것도 하지 않고 5초 뒤 다시 봅니다. 돌아온 옛 primary 가 스스로 강등할 때까지 기다려, 데이터가 갈라지는 것을 막습니다. - 전환(switchover/failover) 처리, 파드·PVC 맞추기
(
reconcileResources,:825). - 마지막에 플러그인 post 훅과 상태 동기화, 그리고 「healthy」 phase 기록
(
finalizeReconciliation,plugins.go:105).
오퍼레이터가 쿠버네티스에 객체를 만들 때도 플러그인이 끼어듭니다. 오퍼레이터의 클라이언트는 Create·Patch·Delete 를
감싸서 먼저 플러그인의 LifecycleHook 을 부릅니다
(extendedClient.Create, internal/cnpi/plugin/operatorclient/client.go:71).
인스턴스 파드 스펙을 만드는 NewInstance, pkg/specs/pods.go:485
도 끝에서 같은 훅을 「evaluate」 동작으로 부릅니다. 플러그인이 돌려준 JSON 패치가 적용된 파드가 실제로 만들어지므로, 사이드카
주입이 웹훅 없이 이루어집니다.
4.2 오퍼레이터가 플러그인을 찾는 법: Service 하나와 소켓 폴더 하나
플러그인은 두 길로 발견됩니다.
- 원격(TCP): 오퍼레이터에는 Service 를 지켜보는 컨트롤러가 따로 있습니다
(
PluginReconciler.reconcile,internal/controller/plugin_controller.go:135). 오퍼레이터 네임스페이스의 Service 에cnpg.io/pluginName라벨이 있으면, 주석에서 포트와 서버·클라이언트 인증서 Secret 이름을 읽어<서비스>.<네임스페이스>.svc:<포트>로 TLS 1.3 mTLS 연결 설정을 만들고 저장소에 등록합니다 (RegisterRemotePlugin,internal/cnpi/plugin/repository/setup.go:177). 플러그인을 설치하는 일이 「Deployment 와 Service 를 하나 만드는 일」로 끝나도록 한 설계입니다. - 로컬(유닉스 소켓): 인스턴스 파드 안의 매니저는
/plugins폴더를 훑어 파일 이름을 플러그인 이름으로 삼습니다 (RegisterUnixSocketPluginsInPath,setup.go:193).
어느 길이든 연결 직후 하는 일은 같습니다.
LoadPlugin, internal/cnpi/plugin/connection/connection.go:442
이 Identity 서비스로 이름·버전과 어떤 서비스를 구현했는지를 묻고, 구현한 서비스마다 다시 세부 능력(capability)을 묻습니다.
이후의 모든 호출은 이 능력 목록으로 걸러집니다. 예를 들어 WAL 아카이브는 ARCHIVE_WAL 능력이 있는 플러그인에게만 갑니다.
CNPG-I 의 서비스는 cnpg-i/proto/ 에 아홉 개가 있습니다.
Identity(identity.proto:23), Operator, OperatorLifecycle,
ReconcilerHooks, WAL(wal.proto:23), Backup, RestoreJobHooks,
Postgres, Metrics 입니다. 모든 요청에 Cluster 전체가 JSON 으로 실려 갑니다. 플러그인이 쿠버네티스에서 Cluster 를 다시 읽지 않아도 되게 하고, 오퍼레이터가 본
그 순간의 Cluster 로 판단하게 하려는 것입니다.
기준 판의 오퍼레이터 코드에서 실제로 부르는 RPC 를 세어 보면(*Client().<RPC> 호출 위치를 grep), WAL 서비스의 Status·
SetFirstRequired 와 Operator 서비스의 Deregister 는 호출하는 곳을 찾지 못했습니다. proto 에는 있지만 1.30.0 오퍼레이터가 아직
쓰지 않는 자리로 보입니다(의도는 미확인).
플러그인 쪽 서버 뼈대는 cnpg-i-machinery 가 줍니다.
Server.Start, pkg/pluginhelper/http/server.go:152
는 Identity 를 먼저 등록하고 「enricher」 함수들이 나머지 서비스를 붙이게 합니다. 서버 주소가 주어지면 TCP, 아니면 플러그인 이름으로
유닉스 소켓을 엽니다(createListener, :304).
4.3 인스턴스 매니저: PID 1 은 PostgreSQL 이 아니다
인스턴스 파드의 명령은 /controller/manager instance run 입니다(6절 실측). 이 바이너리는 bootstrap-controller init 컨테이너가
자기 자신을 공유 볼륨에 복사해 둔 것입니다(bootstrap NewCmd, internal/cmd/manager/bootstrap/cmd.go:34).
runSubCommand, internal/cmd/manager/instance/run/cmd.go:157
을 읽으면 이 PID 1 은 사실상 작은 컨트롤러 매니저입니다. controller-runtime 위에 인스턴스·데이터베이스·역할 reconciler, 로그 파이프,
PostgreSQL 수명 관리자, 원격·로컬 웹서버, 지표 서버를 올립니다.
PostgreSQL 은 수명 관리자가 띄웁니다
(runPostgresAndWait, internal/cmd/manager/instance/run/lifecycle/run.go:40).
초기화(어떤 역할로 시작할지 signal 파일을 맞추는 일)가 끝날 때까지 기다린 뒤 postmaster 를 자식으로 실행하고, postmaster 가 끝나면
매니저도 그 사실을 알고 다음 행동을 정합니다. PID 1 이 PostgreSQL 이 아니라 매니저인 이유가 여기 있습니다. 쿠버네티스가 컨테이너를
죽이고 살리는 단위와 PostgreSQL 을 멈추고 다시 띄우는 단위를 분리해야, 설정 다시 읽기나 primary 강등 같은 일을 컨테이너 재시작 없이
할 수 있습니다.
오퍼레이터가 4.1절 3단계에서 묻는 곳은 원격 웹서버의 /pg/status 입니다
(pgStatus, pkg/management/postgres/webserver/remote.go:320).
반대로 파드 안의 일회성 명령(wal-archive, backup)은 로컬 웹서버로 PID 1 에게 일을 넘기거나 Cluster 캐시를 받아 갑니다.
4.4 WAL 한 조각의 여행: archive_command 부터 S3 까지
archive_command는 사용자가 못 바꾼다. CNPG 는 이 값을 「필수 설정」으로 고정합니다 (pkg/postgres/configuration.go:589,CnpgConfigurationSettings.MandatorySettings). 값은/controller/manager wal-archive --log-destination … %p입니다. 어떤 백업 수단을 쓰든 PostgreSQL 쪽 설정은 한 줄로 같고, 바뀌는 것은 그 뒤의 Go 코드뿐입니다.wal-archive명령.NewCmd,internal/cmd/manager/walarchive/cmd.go:40은 로컬 웹서버의 캐시에서 Cluster 를 받아archiver.Run,pkg/management/postgres/archiver/archiver.go:124을 부릅니다. 자기가 현재 primary 가 아니면(전환 중) 거절합니다. 같은 WAL 을 두 노드가 동시에 올리는 일을 막으려는 것입니다. 끝나면 성공·실패를ContinuousArchiving조건으로 Cluster 에 남깁니다.- 플러그인 먼저.
internalRun,:156의 첫 동작은 플러그인에게 맡기기입니다.
// We allow plugins to archive WALs even if there is no plugin
// directly enabled by the user, to retain compatibility with
// the old API.
if err := archiveWALViaPlugins(ctx, cluster, pgData, walName); err != nil {
return err
}
// If the used chosen a plugin to do WAL archiving, we don't
// trigger the legacy archiving process.
if cluster.GetEnabledWALArchivePluginName() != "" {
return nil
}
출처: cloudnative-pg v1.30.0 pkg/management/postgres/archiver/archiver.go, Apache-2.0
.spec.plugins[] 중 isWALArchiver: true 인 플러그인이 있으면 기존(in-tree) barman 경로는 아예 타지 않습니다. 없으면 그 아래에서
.spec.backup.barmanObjectStore 를 보고, 그것도 없으면 아무것도 하지 않고 성공을 돌려줍니다. 백업을 설정하지 않은 Cluster 도
archive_mode 는 켜져 있고 같은 명령이 돌지만, WAL 은 어디에도 올라가지 않고 성공으로 처리됩니다(6절 실측).
4. 소켓을 훑어 gRPC. archiveWALViaPlugins, :277
는 /plugins 의 소켓 중 Cluster 에 켜진 플러그인만 골라 연결하고,
ArchiveWAL, internal/cnpi/plugin/client/wal.go:34
이 Cluster JSON 과 WAL 경로를 실어 WAL.Archive 를 부릅니다. WAL 하나마다 프로세스가 새로 뜨고 연결도 새로 맺는 구조입니다.
5. 사이드카가 받는다. 플러그인의
WALServiceImplementation.Archive, internal/cnpgi/common/wal.go:107
은 Cluster JSON 에서 플러그인 파라미터(barmanObjectName, serverName)를 꺼내고, ObjectStore 를 읽어 자격 증명을 환경 변수로
만들고, 처음이면 대상 경로가 비어 있는지 확인한 다음, maxParallel 만큼 준비된 WAL 을 더 모아 barman-cloud-wal-archive 를
병렬로 실행합니다. 경로를 정하는 인자에 configuration.ServerName 이 들어갑니다
(:196). 이 값이 5절의
serverName 이야기와 이어집니다.
archive_command 가 실패하면 PostgreSQL 은 같은 WAL 을 다시 시도하고 그 사이 pg_wal 이 쌓이므로, 이 사슬의 어느 고리가 끊겨도
결국 디스크가 차는 것으로 나타납니다. barman CLI 의 종료 코드는 gRPC 오류 문자열로 감싸져 돌아옵니다(6절).
4.5 barman-cloud 플러그인: 한 바이너리, 두 반쪽
플러그인 바이너리의 하위 명령은 instance·operator·restore·healthcheck 넷입니다
(cmd/manager/main.go:52).
- operator 반쪽은 Identity 에서
RECONCILER_HOOKS,LIFECYCLE_SERVICE,INSTANCE_SIDECAR_INJECTION세 능력을 알립니다 (operator/identity.go:45; 서버 구성은operator/start.go:44). pre 훅(ReconcilerImplementation.Pre,operator/reconciler.go:62) 은 Cluster 가 가리키는 ObjectStore 들을 읽고, 없으면 「다시 와 달라(REQUEUE)」 를 돌려주며, 있으면 사이드카용 Role·RoleBinding 을 맞춥니다. Lifecycle 훅(LifecycleHook,operator/lifecycle.go:88) 은 파드와 Job 을 받아reconcileInstancePod,:325→reconcilePodSpec,:370에서 사이드카 컨테이너를 만들고, 그 컨테이너에RestartPolicy: Always를 붙여 (:433) init 컨테이너 목록에 넣습니다(injectPluginSidecarPodSpec,:539). 쿠버네티스 네이티브 사이드카라서, PostgreSQL 보다 먼저 떠서 소켓을 열어 두고 PostgreSQL 이 끝난 뒤에 내려갑니다. 마지막 WAL 을 올리는 도중에 사이드카가 먼저 죽는 일을 막는 순서입니다. PostgreSQL 컨테이너의 볼륨 마운트를 그대로 복사해 붙이므로 사이드카도 PGDATA 를 봅니다. - instance 반쪽은 WAL·Backup·Metrics 를 알리고(
instance/identity.go:46), 주소 없이 시작하므로 유닉스 소켓으로 열립니다(instance/start.go:47).
4.6 Backup 한 번: method 가 길을 고른다
BackupReconciler.Reconcile, internal/controller/backup_controller.go:127
은 Cluster 를 찾은 뒤 형식적인 전제 조건부터 봅니다
(checkPrerequisites, :390).
if backup.Spec.Method == apiv1.BackupMethodPlugin {
if len(cluster.Spec.Plugins) == 0 {
const message = "cannot proceed with the backup as the cluster has no plugin configured"
return flagMissingPrerequisite(message, "ClusterHasNoBackupExecutorPlugin")
}
return nil, nil
}
if cluster.Spec.Backup == nil {
const message = "cannot proceed with the backup as the cluster has no backup section"
return flagMissingPrerequisite(message, "ClusterHasBackupConfigured")
}
출처: cloudnative-pg v1.30.0 internal/controller/backup_controller.go, Apache-2.0
method: plugin 이면 플러그인이 하나라도 있는지만 보고 통과합니다. 그 밖의 method 는 모두 .spec.backup 을 요구합니다. 실패는
TerminalError 로 끝나 다시 시도하지 않습니다. 설정이 틀린 백업을 끝없이 재시도해 봐야 소용없기 때문입니다.
통과하면 method 로 길이 나뉩니다. plugin 과 barmanObjectStore 는 인스턴스 매니저가 하는 백업이고
(IsManagedByInstance, api/v1/backup_funcs.go:319),
volumeSnapshot 은 오퍼레이터가 직접 스냅숏을 찍습니다. 인스턴스 쪽 백업은 대상 파드(target 설정에 따라 primary 또는 가장 앞선 standby)를
고른 뒤 그 파드의 PostgreSQL 컨테이너에서 /controller/manager backup <이름> 을 exec 합니다
(startInstanceManagerBackup, :968).
이 명령은 로컬 웹서버에 백업을 요청하고
(requestBackup, pkg/management/postgres/webserver/local.go:131),
plugin 이면
PluginBackupCommand.invokeStart, webserver/plugin_backup.go:72
이 소켓으로 Backup.Backup 을 부릅니다. 사이드카의
BackupServiceImplementation.Backup, instance/backup.go:71
이 barman-cloud-backup 을 실행하고(서버 이름 인자는 역시 configuration.ServerName), 결과를 Backup 의 status 로 돌려줍니다.
5. 글쓴이의 기여와 이어지는 곳
글쓴이는 in-tree barmanObjectStore 로 백업하던 CNPG 1.30 클러스터를 플러그인(v0.14.0)으로 옮기다가 두 번 걸렸고, 그 경험을 문서
PR 두 개로 보냈습니다. 둘 다 2026-10-17 기준 리뷰 대기 중입니다.
① serverName 은 어디로 가나 — plugin-barman-cloud#1164(이슈 #1163).
이전 안내서는 in-tree 의 barmanObjectStore 절을 ObjectStore 의 spec.configuration 으로 그대로 옮기라고 합니다. 그런데 거기에
serverName 이 있으면 ObjectStore CRD 가 거절합니다. 규칙은
api/v1/objectstore_types.go:61
의 CEL 한 줄(!has(self.serverName))이고, 메시지는 「Cluster 의 serverName 플러그인 파라미터를 쓰라」 입니다. 플러그인이 이 값을
읽는 곳은 NewFromCluster, internal/cnpgi/operator/config/config.go:142
입니다.
serverName := cluster.Name
for _, plugin := range cluster.Spec.Plugins {
if plugin.IsEnabled() && plugin.Name == metadata.PluginName {
if pluginServerName, ok := plugin.Parameters["serverName"]; ok {
serverName = pluginServerName
}
}
}
출처: plugin-barman-cloud v0.14.0 internal/cnpgi/operator/config/config.go, Apache-2.0
이 값이 4.4절의 WAL 경로와 4.6절의 백업 경로에 그대로 들어갑니다. 즉 serverName 은 「버킷 안의 폴더 이름」이고, 옮기면서 이 값이
달라지면 플러그인은 기존 백업 옆에 새 폴더를 만들어 쓰기 시작합니다. 외부 클러스터(복구·replica 원본) 쪽은 기본값도 다릅니다.
in-tree 는 외부 클러스터 이름으로 떨어지는데
(ExternalCluster.GetServerName, cloudnative-pg/api/v1/cluster_funcs.go:403),
플러그인은 지금 만드는 Cluster 의 이름으로 떨어집니다(config.go:163,
:173). PR 은
이전 안내서에 이 두 차이를 적었습니다. 외부 클러스터 쪽은 코드 대조로만 확인했고 복구로 재 보지는 않았습니다(미실측).
② 「no backup section」 — plugin-barman-cloud#1166(이슈 #1165).
플러그인으로 옮기며 Cluster 에서 .spec.backup 을 지우면, method 를 적지 않은 Backup·ScheduledBackup 은 위 4.6절 인용의 두 번째
분기로 떨어집니다. Backup CRD 의 method 기본값이 barmanObjectStore 이기 때문입니다(6절에서 설치된 CRD 로 확인). 오류 문구가 플러그인
문서의 문제 해결 절에 없어서, 그 절에 원인과 고치는 법(method: plugin + pluginConfiguration.name, kubectl cnpg backup -m plugin)을
더했습니다.
③ #679 댓글. SeaweedFS 로 WAL 아카이브가 종료 코드 4 로 실패한다는 보고에, 같은 사이드카 이미지(v0.14.0)로 재현되지 않는다는 표와
「종료 코드 4 는 barman 의 일반 오류이니 사이드카 로그에서 바로 앞의 예외 줄을 보라」는 안내를 남겼습니다. 4.4절의 5단계가 바로 그
명령을 실행하는 자리이고, 종료 코드가 gRPC 오류로 감싸져 manager wal-archive 까지 올라옵니다.
6. 실측: 작은 클러스터에서 읽은 것
2026-10-17, 모든 명령은 읽기(kubectl get·logs, 실행 중 파드에 exec 로 /proc·설정 파일 cat)입니다. 앱 네임스페이스와
클러스터 이름은 가렸고, 원자료는 따로 보관했습니다.
설치된 판과 클러스터. cnpg-system 에 오퍼레이터(cloudnative-pg:1.30.0)와 플러그인(plugin-barman-cloud:v0.14.0) Deployment
가 하나씩 있습니다. Cluster 는 넷이고 모두 Cluster in healthy state, PostgreSQL 18.4 이미지입니다.
| Cluster | 인스턴스 | 플러그인 | .spec.backup |
ObjectStore |
|---|---|---|---|---|
| 앱 DB 클러스터 A | 2 | barman-cloud(isWALArchiver: true) |
없음 | 있음 |
| 인증 서비스 DB 클러스터 B | 2 | barman-cloud | 없음 | 있음 |
| 클러스터 C, D | 1, 2 | 없음 | 없음 | 없음 |
플러그인 발견(4.2절). 플러그인 Service 의 라벨은 cnpg.io/pluginName: barman-cloud.cloudnative-pg.io, 주석은
cnpg.io/pluginPort: 9090 과 서버·클라이언트 인증서 Secret 이름 둘이었습니다. 오퍼레이터 로그에는 지난 24시간 Registered plugin 이
7번 있었습니다. 클러스터 A 의 .status.pluginStatus 는 버전 0.14.0, 능력 TYPE_RECONCILER_HOOKS·TYPE_LIFECYCLE_SERVICE·
TYPE_INSTANCE_SIDECAR_INJECTION 으로, 4.5절 operator 반쪽의 Identity 응답과 정확히 같습니다.
인스턴스 파드(4.3·4.5절). 클러스터 A 의 두 파드 모두 같은 구성입니다.
| 항목 | 값 |
|---|---|
| initContainers | bootstrap-controller(오퍼레이터 이미지 1.30.0), plugin-barman-cloud(사이드카 이미지 v0.14.0, restartPolicy: Always) |
| containers | postgres 하나 |
| postgres 컨테이너 PID 1 | /controller/manager instance run --status-port-tls --log-level=info |
| postmaster | PID 37, 부모 PID 1. 그 아래 checkpointer·walwriter·archiver(「last was …66」)·walsender 등 |
/plugins |
소켓 파일 barman-cloud.cloudnative-pg.io 하나, /proc/net/unix 로는 대기 1·연결 1 |
사이드카는 containers 가 아니라 initContainers 에 restartPolicy: Always 로 들어가 있었습니다. 4.5절에서 읽은 그대로입니다.
플러그인 Deployment 의 디버그 로그에는 주입 패치가 남아 있었는데, 그 로그의 caller 가 internal/cnpgi/operator/lifecycle.go:364 였습니다.
v0.14.0 태그에서 같은 파일의 364번째 줄이 바로 "generated patch" 로그 줄이어서, 돌고 있는 빌드와 읽은 소스가 같은 판임을 덤으로
확인했습니다. 패치 내용은 /spec/volumes 에 plugins emptyDir, /spec/initContainers/1 에 사이드카를 더하는 JSON 패치였습니다.
archive_command(4.4절). PGDATA 의 설정 파일을 읽었습니다.
| 파일 | 값 |
|---|---|
custom.conf |
archive_command = '/controller/manager wal-archive --log-destination /controller/log/postgres.json %p' |
custom.conf |
archive_mode = 'on' |
override.conf |
restore_command = '/controller/manager wal-restore --log-destination /controller/log/postgres.json %f %p' |
Cluster 설정의 archive_timeout 은 5분입니다. 백업도 플러그인도 없는 클러스터 C 의 custom.conf 에도 똑같은 archive_command 와
archive_mode = 'on' 이 있었습니다. 4.4절 3단계대로라면 그 WAL 들은 아무 데도 가지 않고 「성공」으로 끝납니다.
WAL 아카이브 로그. 사이드카 로그에서 지난 24시간 Archived WAL file 이 322건이었습니다. 5분마다 강제로 WAL 을 바꾸면 하루
288개이니, 나머지는 실제 쓰기로 찬 조각으로 보입니다. 최근 네 조각(UTC 16:06~16:21, 5분 간격)의 걸린 시간(elapsedWalTime)은
0.237~0.256초였습니다. 각 줄 앞에는 Executing barman-cloud-wal-archive 와 인자 목록(--gzip, --endpoint-url <가림>, s3://<버킷>/, 서버 이름)이 찍혀
있습니다. 반면 postgres 컨테이너 로그에는 같은 24시간 동안 wal-archive 가 들어간 줄이 0건이었습니다. 성공한 wal-archive 는 info
수준으로 아무것도 남기지 않으므로, WAL 아카이브를 보려면 사이드카 로그를 봐야 합니다.
백업(4.6절). ScheduledBackup 은 두 개, 둘 다 method: plugin 과 pluginConfiguration.name 을 적고 있습니다. 설치된 CRD 를
읽으니 Backup 의 spec.method 기본값은 정말 barmanObjectStore 였고, ObjectStore CRD 의 spec.configuration 에는
!has(self.serverName) 규칙이 그대로 있었습니다(5절의 두 PR 의 근거).
| Cluster | Backup 수 | 결과 | 비고 |
|---|---|---|---|
| A | 31(09-16 ~ 10-16, 매일) | 모두 completed | 최근 것: 03:30:00 시작, 03:33:40 끝(약 3분 40초) |
| B | 28(첫 수동 1 + 매일 27) | completed 26, failed 2 | 10-06·10-07 실패: rpc error: code = Unknown desc = exit status 1 |
B 의 실패 문구는 4.4절 끝에서 말한 「barman CLI 종료 코드가 gRPC 오류 문자열로 감싸진 모양」 그대로입니다. 원인 자체는 이 글에서 쫓지 않았습니다(미확인).
상태가 두 곳에 있다. 클러스터 A 의 .status.firstRecoverabilityPoint 와 .status.lastSuccessfulBackup 은 2026-08-23 에 멈춰
있었습니다. in-tree 로 백업하던 마지막 날입니다. API 타입 주석에 「Deprecated: 백업 플러그인에는 설정되지 않는다」고 적힌 필드라서
플러그인으로 옮긴 뒤로는 갱신되지 않습니다. 지금의 값은 ObjectStore 쪽에 있습니다.
| 어디 | 첫 복구 가능 시점 | 마지막 성공 백업 |
|---|---|---|
Cluster .status(in-tree 시절 값) |
2026-08-23 07:25 | 2026-08-23 07:32 |
ObjectStore .status.serverRecoveryWindow |
2026-09-16 03:33 | 2026-10-16 03:33 |
보존 정책이 30일이라 ObjectStore 쪽 첫 복구 시점은 한 달 전에서 잘립니다. 경보가 Cluster 쪽 필드를 보고 있다면 옮긴 뒤 조용히 멈춥니다.
연결 누수는 없었다. 4.4절의 archiveWALViaPlugins 를 비롯해 네 곳이 쓰지 않는 플러그인 저장소를 하나 더 만들고 닫는 코드를
갖고 있어서(8절), 실제 클라이언트 쪽 연결 풀이 닫히지 않고 쌓이는지 봤습니다. 26일 동안 매일 백업한 primary 파드에서
/proc/net/unix 의 플러그인 소켓은 대기 1·연결 1 뿐이었습니다. 누수의 흔적은 없습니다.
7. 어디부터 읽을까
cnpg-i/proto/identity.proto와wal.proto. 계약서부터 보면 양쪽 코드가 쉽게 읽힙니다.cnpg-i-machinery pkg/pluginhelper/http/server.go:152. 플러그인이 서버를 여는 방식(TCP 대 소켓)을 봅니다.plugin-barman-cloud internal/cnpgi/operator/lifecycle.go:88과common/wal.go:107. 두 반쪽이 각각 하는 일입니다.cloudnative-pg internal/controller/plugin_controller.go:135→internal/cnpi/plugin/connection/connection.go:442. 오퍼레이터가 플러그인을 찾고 능력을 읽는 곳입니다.pkg/management/postgres/archiver/archiver.go:156. in-tree 와 플러그인 경로가 한 함수에서 갈립니다.internal/controller/cluster_controller.go:310. 마지막으로 큰 루프를 읽으면 앞에서 본 조각들이 어디에 끼워지는지 보입니다.
8. 첫 기여하기 좋은 곳
소스를 읽으며 실제로 눈에 띈 것들입니다. 아직 아무 데도 올리지 않았고, 올리기 전에 기존 이슈를 먼저 찾아봐야 합니다.
- 이유(reason) 이름이 뜻과 반대.
.spec.backup이 없어서 실패할 때 이벤트 reason 이ClusterHasBackupConfigured입니다 (backup_controller.go:422). 경보 규칙이 이 문자열을 볼 수 있으니 바로 바꾸기보다 이슈로 먼저 묻는 편이 좋습니다. - 로그 필드 오타.
startBarmanBackup이"backupNamespace"에backup.Name을 넣습니다 (pkg/management/postgres/webserver/local.go:213). 플러그인 쪽에도 비슷한 것이 있습니다. 백업용 ObjectStore 를 읽다 실패하면 로그에는 복구용 키를 찍습니다 (instance/backup.go:86,instance/metrics.go:119). - 쓰지 않는 플러그인 저장소.
plugins := repository.New(); defer plugins.Close()다음에plugins를 쓰지 않는 함수가 넷 있습니다 (archiver.go:285,walrestore/cmd.go:275,plugin_backup.go:78,restore.go:1067). 실제 연결은NewClient안에서 만든 다른 저장소로 맺습니다(create.go:40, 바로 아래 주석에는 「make che socketDir」 오타). 6절 실측으로는 누수가 없었으니 정리 성격의 PR 입니다. - in-tree 와 플러그인의 병렬 아카이브 오류 처리 차이. in-tree 는 병렬로 함께 올린 WAL 중 요청받은 첫 조각의 오류만 돌려준다고
주석으로 밝힙니다(
archiver.go:264). 플러그인은 묶음 중 아무 조각이라도 실패하면 오류를 돌려줍니다(common/wal.go:222). 의도된 차이인지 확인해 볼 만합니다(미확인). - 문서: 옮긴 뒤 멈추는 Cluster 상태 필드. 6절에서 본 대로
firstRecoverabilityPoint·lastSuccessfulBackup은 옮긴 날에 멈춥니다. 이전 안내서에 「이제 ObjectStore 의 status 를 보라」는 한 줄을 더하는 것은 #1164 와 같은 결의 문서 기여입니다.
두 저장소 모두 CONTRIBUTING 이 CloudNativePG 공통 기여 지침(행동 강령, AI 사용, 작업 흐름)을 가리킵니다. AI 도움을 받았으면 커밋에
Assisted-by 트레일러로 밝히는 것이 이 프로젝트의 방식입니다. 2026-10-17 에 good first issue 라벨로 열린 이슈는 본체 저장소에 2건,
플러그인 저장소에 0건이었습니다.
9. 한계
- 복구 경로(
wal-restore, 복구 Job, RestoreJobHooks), replica 클러스터, volume snapshot 백업, 전환·failover 의 세부 판단, Pooler 는 다루지 않았습니다. barman-cloud-*명령 자체(파이썬 barman)와, 플러그인이 쓰는 Go 라이브러리(cloudnative-pg/barman-cloud)의 내부는 열어 보지 않았습니다. 6절의Executing barman-cloud-wal-archive로그도 그 라이브러리가 남기는 줄입니다.- 클러스터 B 의 백업 실패 두 건, 외부 클러스터의 serverName 기본값 차이가 실제 복구에서 어떻게 나타나는지는 확인하지 않았습니다(미확인·미실측).
- WAL 하나에 새 프로세스와 새 gRPC 연결이 드는 비용을 따로 재지는 않았습니다. 6절의 0.25초는 사이드카가 잰 업로드 시간입니다(미실측).
티티 한마디. PostgreSQL 은 집 안에서 편지만 쓰고, 우체통(소켓)에 넣으면 옆집 택배 기사(사이드카)가 들고 나가요. 편지 봉투에 적는 주소가 바로 serverName 이라서, 이사할 때 주소를 바꿔 적으면 옛 편지함 옆에 새 편지함이 생겨 버린답니다!
참고
모든 자료는 2026-10-17 에 열람했습니다.
- cloudnative-pg v1.30.0 소스: https://github.com/cloudnative-pg/cloudnative-pg/tree/v1.30.0
- plugin-barman-cloud v0.14.0 소스·문서: https://github.com/cloudnative-pg/plugin-barman-cloud/tree/v0.14.0, 이전 안내서 https://github.com/cloudnative-pg/plugin-barman-cloud/blob/v0.14.0/web/docs/migration.md, 문제 해결 https://github.com/cloudnative-pg/plugin-barman-cloud/blob/v0.14.0/web/docs/troubleshooting.md
- cnpg-i v0.6.0 프로토콜: https://github.com/cloudnative-pg/cnpg-i/tree/v0.6.0/proto, 설명 https://github.com/cloudnative-pg/cnpg-i/blob/v0.6.0/docs/protocol.md
- cnpg-i-machinery v0.4.2: https://github.com/cloudnative-pg/cnpg-i-machinery/tree/v0.4.2
- CloudNativePG 기여 지침: https://github.com/cloudnative-pg/governance/blob/main/CONTRIBUTING.md
- 글쓴이의 PR·이슈·댓글: https://github.com/cloudnative-pg/plugin-barman-cloud/pull/1164, https://github.com/cloudnative-pg/plugin-barman-cloud/pull/1166, https://github.com/cloudnative-pg/plugin-barman-cloud/issues/679
- 같은 시리즈: MetalLB 읽기
인용한 코드는 cloudnative-pg 와 plugin-barman-cloud 의 것으로, 두 저장소 모두 Apache License 2.0 입니다.