본문으로 건너뛰기
12.2 Kubernetes 위의 Patroni

12.2 Kubernetes 위의 Patroni

3.4에서 Kubernetes API를 DCS 백엔드로 쓰는 법을 다뤘다. etcd 없이 Endpoints나 ConfigMap에 leader lock과 클러스터 상태를 저장하는 방식이었다. 이 절은 그 위 계층이다. Patroni를 컨테이너 이미지로 포장한 Spilo, 그 이미지를 대량으로 관리하는 Zalando postgres-operator, 그리고 Patroni 없이 같은 문제를 푸는 CloudNativePG와의 관계를 정리한다.

Kubernetes 백엔드 요약

patroni.ymlkubernetes 섹션을 두면 Patroni는 Pod 안에서 평소처럼 돌되, leader key와 멤버 정보를 Kubernetes 객체에 기록한다. use_endpoints: true의 Endpoints 방식이 권장되는데, leader 정보 annotation과 실제 주소가 한 번의 갱신으로 함께 바뀌기 때문이다. ConfigMap 방식은 leader 변경에 최소 두 번의 갱신이 필요해 그만큼 창이 넓고, 다만 OpenShift에서는 ConfigMap 외의 대안이 없다고 문서가 밝힌다. leader로 트래픽을 보내는 Service는 role_label selector로 primary Pod를 가리키고, ServiceAccount에는 configmaps, endpoints, pods에 대한 RBAC 권한이 필요하다. 설정 키와 권한 목록의 상세는 3.4에 있다.

Patroni REST API에는 Kubernetes probe 전용 엔드포인트가 있다. GET /liveness는 Patroni heartbeat 루프가 정상일 때 200을 반환하며 primary에서 ttl, replica에서 2 * ttl을 넘기면 503이 된다. GET /readiness?lag=<bytes>&mode=apply|write는 leader이거나 lag이 임계값 이내인 replica일 때 200을 반환하고, lag 기본값은 maximum_lag_on_failover다. 각각 livenessProbe와 readinessProbe로 사용한다.

Spilo: PostgreSQL과 Patroni 번들 이미지

Spilo는 Zalando가 유지하는 Docker 이미지로, 공식 설명 그대로 “PostgreSQL and Patroni bundled together"다. 이미지 하나로 Pod를 여러 개 띄우면 각 Pod의 Patroni가 Kubernetes API를 DCS 삼아 HA 클러스터를 이룬다. 이미지에는 Patroni 본체 외에 두 가지가 더 들어 있다.

첫째, WAL-E/WAL-G 기반 백업 스크립트다. WAL archiving과 base backup을 오브젝트 스토리지로 보내는 경로가 이미지 안에 준비되어 있다.

둘째, in-place major upgrade 스크립트다. 10.2에서 본 대로 Patroni 자체에는 major 업그레이드 명령이 없고 pg_upgrade를 동반하는 수동 절차만 문서화되어 있는데, Spilo 13+ 이미지의 inplace_upgrade.py가 그 절차를 스크립트로 구현한다. 목표 버전은 PGVERSION 환경변수로 정하고, primary Pod 안에서 멤버 수를 인자로 실행한다.

# primary Pod 안에서, 멤버 3대 클러스터의 major upgrade
python3 /scripts/inplace_upgrade.py 3

Zalando postgres-operator

Spilo Pod를 손으로 배치하는 대신, Zalando postgres-operator는 Spilo 이미지를 기반으로 Patroni 클러스터 다수를 operator 패턴으로 관리한다. 매니페스트를 선언하면 operator가 Spilo Pod들을 만들고, 많은 수의 클러스터를 한꺼번에 관리하는 운영을 겨냥한다. Patroni 공식 문서의 kubernetes 페이지도 프로덕션 규모 예시로 Spilo와 postgres-operator를 참조한다.

운영 자동화에서 눈여겨볼 것은 업그레이드 처리다. minor 업그레이드는 Pod rolling update에 switchover를 결합해 자동으로 수행하며, operator 문서는 “The switch should usually take less than 5 seconds, still clients have to reconnect.“라고 기술한다. 전환 자체는 짧아도 클라이언트 재연결은 피하지 못한다는 조건이 붙어 있다. major 업그레이드는 major_version_upgrade_mode 설정으로 제어하는데, off(기본)면 손대지 않고, manual/full이면 Spilo의 in-place upgrade 스크립트를 트리거한다.

    flowchart TD
  subgraph Z["Patroni 계열"]
    OP["postgres-operator"]
    SP["Spilo Pod<br/>Patroni + PostgreSQL"]
    K1["K8s API = DCS"]
    OP -->|Pod 생성/관리| SP
    SP -->|leader lock| K1
  end
  subgraph C["CloudNativePG"]
    OP2["CNPG operator"]
    IM["Pod<br/>instance manager<br/>+ PostgreSQL"]
    K2["K8s API"]
    OP2 -->|failover 결정| IM
    IM -->|상태 보고| K2
  end
  

같은 Kubernetes 위라도 두 스택의 HA 주체가 다르다. 왼쪽은 각 Pod 안의 Patroni가 leader race를 벌이고 operator는 배치를 관리한다. 오른쪽은 operator와 instance manager가 failover 자체를 담당한다.

CloudNativePG와의 관계

사실 관계부터 분명히 하면, CloudNativePG는 Patroni를 쓰지 않는다. 프로젝트 README는 “Instead of relying on external high-availability tools (like Patroni, repmgr, or Stolon), it integrates directly with the Kubernetes API to automate database operations.“라고 밝힌다. operator와 각 Pod의 instance manager가 Kubernetes API를 직접 사용해 primary 선출과 failover를 자체 처리하므로, DCS라는 별도 개념 없이 Kubernetes 자체가 그 자리를 맡는다.

범위 제한도 원문 그대로 옮기면, 단일 Kubernetes cluster 밖으로는 “CloudNativePG cannot perform any cross-cluster automated failover"다. 여러 K8s cluster에 걸친 자동 failover는 범위 밖이라는 뜻이다. CloudNativePG의 동작 원리는 별도 노트 dbalog.dev/cnpg에 정리해 두었다.

무엇을 고를 것인가

공식 문서가 답을 주는 문제는 아니고, 배치 환경에 따른 일반적인 판단은 이렇다. 신규 구축이 Kubernetes 전용이라면 CloudNativePG처럼 Kubernetes API에 직접 통합된 operator 쪽이 부품 수가 적고 Kubernetes의 운영 관행과 자연스럽게 맞물린다. 반면 VM/베어메탈과 Kubernetes가 섞여 있거나, 이미 운영 중인 etcd 같은 Kubernetes 밖 DCS를 재사용하고 싶거나, Citus HA(12.3)가 필요하다면 Patroni 계열이 맞는 자리다. Zalando postgres-operator는 그 중간에서, Kubernetes 안에 있으면서도 Patroni의 동작 방식을 그대로 유지한 채 대량 관리를 얹는 선택지가 된다.

정리

Kubernetes 위의 Patroni 스택은 세 층으로 읽으면 된다. DCS 백엔드로서의 Kubernetes API(3.4), 그것을 이미지 하나에 담은 Spilo, 그리고 Spilo를 대량으로 운영하는 postgres-operator다. CloudNativePG는 이 스택의 변형이 아니라 Patroni 없이 Kubernetes API에 직접 통합한 별개의 답이며, 단일 K8s cluster 범위라는 제약과 함께 온다.