12.2 Kubernetes 위의 Patroni
3.4에서 Kubernetes API를 DCS 백엔드로 쓰는 법을 다뤘습니다. etcd 없이 Endpoints나 ConfigMap에 leader lock과 클러스터 상태를 저장하는 방식이었습니다. 이 절은 그 위 계층입니다. Patroni를 컨테이너 이미지로 포장한 Spilo, 그 이미지를 대량으로 관리하는 Zalando postgres-operator, 그리고 Patroni 없이 같은 문제를 푸는 CloudNativePG와의 관계를 정리합니다.
Kubernetes 백엔드 요약
patroni.yml에 kubernetes 섹션을 두면 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 스크립트를 트리거합니다.
같은 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 범위라는 제약과 함께 옵니다.