TT Lab
요금제

한국어

시작하기
블로그

블로그

CloudNativePG 읽기: PID 1 의 매니저, 소켓 너머의 플러그인, WAL 한 조각의 여행

대화로 듣기 · 12분두 사람이 이 글을 풀어 줍니다

상태: 초안 (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 옆에 붙은 「상주 관리인」과 「외주 택배 기사」

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 네임스페이스 CNPG 오퍼레이터 Cluster 컨트롤러 Backup 컨트롤러 Plugin 컨트롤러 플러그인 저장소 (gRPC 연결 풀) barman-cloud operator 반쪽 Identity ReconcilerHooks OperatorLifecycle Deployment 1개 gRPC mTLS 인스턴스 파드 (DB 한 대) postgres 컨테이너 PID 1: manager (instance run) └ postgres (자식) └ wal-archive (매번) plugin-barman-cloud instance 반쪽 네이티브 사이드카 WAL · Backup Metrics 소켓 init: bootstrap-controller → manager 복사 상태 조회 /pg/status 사이드카 주입 패치 barman-cloud-* S3 호환 객체 저장소 ObjectStore CR 어디에·어떤 자격으로 소켓: 두 컨테이너가 같이 붙인 /plugins 볼륨의 유닉스 소켓. 오퍼레이터 ↔ 플러그인 Deployment 는 Service 로 찾는다.
그림: 오퍼레이터, 플러그인 두 반쪽, 인스턴스 파드

그림 읽는 법:

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 은 길지만 순서가 명확합니다.

  1. 기본값 채우기, 이미지 결정, 플러그인 상태 갱신, 보조 객체(Secret·Service·PDB) 만들기 (createPostgresClusterObjects, cluster_create.go:57).
  2. 플러그인의 pre-reconcile 훅 (preReconcilePluginHooks, plugins.go:36). barman-cloud 는 여기서 사이드카가 ObjectStore 와 Secret 을 읽을 Role·RoleBinding 을 만듭니다(4.5절).
  3. 각 인스턴스의 상태를 HTTPS 로 묻기 (GetStatusFromInstances, pkg/management/postgres/webserver/client/remote/instance.go:183). primary 가 둘로 보이면 아무것도 하지 않고 5초 뒤 다시 봅니다. 돌아온 옛 primary 가 스스로 강등할 때까지 기다려, 데이터가 갈라지는 것을 막습니다.
  4. 전환(switchover/failover) 처리, 파드·PVC 맞추기 (reconcileResources, :825).
  5. 마지막에 플러그인 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 하나와 소켓 폴더 하나

플러그인은 두 길로 발견됩니다.

어느 길이든 연결 직후 하는 일은 같습니다. 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 까지

WAL 한 조각과 백업 한 번이 가는 길 ① WAL 아카이브 ② 백업(Backup CR) postgres archiver archive_command 실행 manager wal-archive PID 1 과 같은 바이너리 archiveWALViaPlugins /plugins 소켓을 훑는다 WAL.Archive (gRPC) 사이드카가 받는다 barman-cloud-wal-archive --gzip, 한 조각 약 0.25초 조건 갱신 ContinuousArchiving=True ScheduledBackup 매일 한 번 Backup 을 만든다 Backup 컨트롤러 checkPrerequisites method 가 plugin 인가? 예 → 플러그인 이름만 확인 아니오 → .spec.backup 필요 파드 안에서 exec manager backup 「이름」 Backup.Backup (gRPC) 로컬 웹서버 → 사이드카 barman-cloud-backup 끝나면 phase=completed destinationPath / serverName / wals·base serverName 은 Cluster 의 플러그인 파라미터에서 #1166 이 다룬 오류 method 생략 → 기본값 → no backup section #1164 이 다룬 자리 serverName 은 ObjectStore 가 아니라 플러그인 파라미터
그림: WAL 한 조각과 백업 한 번이 가는 길
  1. archive_command 는 사용자가 못 바꾼다. CNPG 는 이 값을 「필수 설정」으로 고정합니다 (pkg/postgres/configuration.go:589, CnpgConfigurationSettings.MandatorySettings). 값은 /controller/manager wal-archive --log-destination … %p 입니다. 어떤 백업 수단을 쓰든 PostgreSQL 쪽 설정은 한 줄로 같고, 바뀌는 것은 그 뒤의 Go 코드뿐입니다.
  2. wal-archive 명령. NewCmd, internal/cmd/manager/walarchive/cmd.go:40 은 로컬 웹서버의 캐시에서 Cluster 를 받아 archiver.Run, pkg/management/postgres/archiver/archiver.go:124 을 부릅니다. 자기가 현재 primary 가 아니면(전환 중) 거절합니다. 같은 WAL 을 두 노드가 동시에 올리는 일을 막으려는 것입니다. 끝나면 성공·실패를 ContinuousArchiving 조건으로 Cluster 에 남깁니다.
  3. 플러그인 먼저. 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).

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. 어디부터 읽을까

  1. cnpg-i/proto/identity.proto 와 wal.proto. 계약서부터 보면 양쪽 코드가 쉽게 읽힙니다.
  2. cnpg-i-machinery pkg/pluginhelper/http/server.go:152. 플러그인이 서버를 여는 방식(TCP 대 소켓)을 봅니다.
  3. plugin-barman-cloud internal/cnpgi/operator/lifecycle.go:88 과 common/wal.go:107. 두 반쪽이 각각 하는 일입니다.
  4. cloudnative-pg internal/controller/plugin_controller.go:135 → internal/cnpi/plugin/connection/connection.go:442. 오퍼레이터가 플러그인을 찾고 능력을 읽는 곳입니다.
  5. pkg/management/postgres/archiver/archiver.go:156. in-tree 와 플러그인 경로가 한 함수에서 갈립니다.
  6. internal/controller/cluster_controller.go:310. 마지막으로 큰 루프를 읽으면 앞에서 본 조각들이 어디에 끼워지는지 보입니다.

8. 첫 기여하기 좋은 곳

소스를 읽으며 실제로 눈에 띈 것들입니다. 아직 아무 데도 올리지 않았고, 올리기 전에 기존 이슈를 먼저 찾아봐야 합니다.

두 저장소 모두 CONTRIBUTING 이 CloudNativePG 공통 기여 지침(행동 강령, AI 사용, 작업 흐름)을 가리킵니다. AI 도움을 받았으면 커밋에 Assisted-by 트레일러로 밝히는 것이 이 프로젝트의 방식입니다. 2026-10-17 에 good first issue 라벨로 열린 이슈는 본체 저장소에 2건, 플러그인 저장소에 0건이었습니다.

9. 한계

티티 한마디. PostgreSQL 은 집 안에서 편지만 쓰고, 우체통(소켓)에 넣으면 옆집 택배 기사(사이드카)가 들고 나가요. 편지 봉투에 적는 주소가 바로 serverName 이라서, 이사할 때 주소를 바꿔 적으면 옛 편지함 옆에 새 편지함이 생겨 버린답니다!

참고

모든 자료는 2026-10-17 에 열람했습니다.

인용한 코드는 cloudnative-pg 와 plugin-barman-cloud 의 것으로, 두 저장소 모두 Apache License 2.0 입니다.

로그인하면 좋아요를 누를 수 있습니다

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다