PostgreSQL 장애 조치, 백업, 스케일링을 모두 사람이 런북대로 처리하다가 새벽 복구 시간이 길어졌습니다. 반복 가능한 운영 절차는 컨트롤러로 자동화할 수 있어야 합니다. Operator 패턴은 도메인 운영 지식을 Kubernetes 컨트롤 루프로 옮기는 방식입니다.
Operator 패턴 — CRD + Controller로 자동화 구축
PostgreSQL 클러스터를 Kubernetes에서 운영한다고 생각해봅시다. Primary 장애 시 Replica를 승격해야 하고, 백업 스케줄을 관리해야 하고, 스토리지가 부족하면 PVC를 확장해야 합니다. 이 모든 것을 수동으로 하려면 전담 DBA가 24시간 대기해야 합니다. Operator는 이 운영 노하우를 Kubernetes Controller 코드로 캡슐화합니다. 사람이 하던 "이 상황에서는 이렇게 한다"는 판단을 Reconcile 루프가 자동으로 실행합니다. CRD가 "무엇을 원하는지" 선언하는 인터페이스라면, Controller는 그 선언을 실현하는 로봇입니다.
Operator 패턴의 동작 원리를 이해하고, postgres-operator와 redis-operator로 실제 스테이트풀 애플리케이션을 선언적으로 운영합니다. Operator를 직접 구현하지 않아도 Controller가 내부에서 무슨 일을 하는지 알면 트러블슈팅이 훨씬 쉬워집니다.
- 1Operator = CRD + Controller — 두 구성요소의 역할 분리
- 2Reconcile 루프 — Watch → Diff → Act의 반복 사이클
- 3postgres-operator로 PostgreSQL 클러스터 선언적 관리
- 4redis-operator로 Redis Sentinel 클러스터 구성
- 5Controller가 하는 일 — Informer, WorkQueue, 멱등성
- 6Operator Hub에서 검증된 Operator 찾기
Helm v3와 kubectl이 설치된 환경이면 됩니다. 실제 PostgreSQL 클러스터를 생성하려면 스토리지 클래스가 필요합니다. 로컬 환경(minikube, kind)에서는 standard 또는 local-path 스토리지 클래스를 사용합니다.
kubectl cluster-infokubectl create namespace operator-labhelm repo add postgres-operator-charts https://opensource.zalando.com/postgres-operator/charts/ && helm repo updatekubectl get crd | wc -lhelm uninstall postgres-operator -n operator-lab && kubectl delete namespace operator-lab
Operator = CRD + Controller — 역할 분리 이해
PostgreSQL을 StatefulSet으로 배포했는데 Primary 파드가 죽자 아무도 자동으로 Replica를 승격시키지 않습니다. 누군가 수동으로 failover 명령을 실행하기 전까지 서비스는 중단된 채로 있습니다. 이런 "이 상황에서는 이렇게 한다"는 운영 판단을 코드로 자동화하려면 Kubernetes의 기본 컨트롤러만으로는 부족합니다. Operator 패턴은 CRD로 새로운 API 타입을 정의하고, Controller가 그 리소스를 watch하며 원하는 상태를 실현하는 구조입니다. Operator라는 단어는 이 두 가지를 합친 개념입니다. 이 CB에서는 CRD와 Controller가 어떻게 분리되고 협력하는지, Operator가 어떤 Kubernetes 리소스를 만들어 운영을 자동화하는지를 다룹니다. CRD(Custom Resource Definition)는 새로운 API 타입을 정의하고, Controller는 그 타입의 리소스를 watch하면서 원하는 상태를 실현합니다. Operator는 이 둘을 묶어서 특정 애플리케이션의 운영 자동화를 담당하는 소프트웨어를 말합니다.

전통적 운영 vs Operator 패턴
전통적 운영:
DBA가 PostgreSQL 장애 감지 → 수동으로 failover 명령 실행
→ Replica를 Primary로 승격 → DNS 업데이트 → 애플리케이션 재연결
(수 분~수십 분 소요, 사람 개입 필요)
Operator 패턴:
postgres-operator Controller가 Primary 파드 실패 감지 (수 초)
→ 자동으로 Replica 중 하나를 Primary로 승격
→ Service 엔드포인트 업데이트
→ CRD status 업데이트 (새 Primary 정보)
(수십 초 이내, 자동)
Controller의 Watch → Diff → Act 루프
┌─────────────────────────────────────────────────────┐
│ Controller 루프 │
│ │
│ ┌─────────┐ 이벤트 ┌──────────┐ 큐에 추가 ┌─────┐│
│ │ Informer│ ──────→ │ Event │ ──────────→ │Work ││
│ │ (Watch) │ │ Handler │ │Queue││
│ └─────────┘ └──────────┘ └──┬──┘│
│ ↑ │ │
│ │ ┌─────▼─┐ │
│ API Server │Reconcil│ │
│ 변경 이벤트 │ Loop │ │
│ (Added/Modified/Deleted) └─────┬─┘ │
│ │ │
│ ┌─────────────▼─┐ │
│ │ 현재 상태 조회 │ │
│ │ (API Server) │ │
│ └───────┬───────┘ │
│ │ │
│ ┌───────▼───────┐ │
│ │ Diff 계산 │ │
│ │ (원하는 vs │ │
│ │ 현재 상태) │ │
│ └───────┬───────┘ │
│ │ │
│ ┌───────▼───────┐ │
│ │ Act (실행) │ │
│ │ Create/Update │ │
│ │ Delete/Patch │ │
│ └───────┬───────┘ │
│ │ │
│ ┌───────▼───────┐ │
│ │ Status 업데이트 │ │
│ └───────────────┘ │
└─────────────────────────────────────────────────────┘
Operator가 관리하는 Kubernetes 리소스들
PostgresCluster (CRD) 하나가 생성되면 Operator가 만드는 것:
├── StatefulSet (Primary, Replica 각각)
├── Services (Primary 엔드포인트, Replica 엔드포인트)
├── PersistentVolumeClaims (데이터 디렉토리)
├── ConfigMaps (postgresql.conf, pg_hba.conf)
├── Secrets (패스워드, TLS 인증서)
├── CronJobs (백업 스케줄)
└── ServiceAccounts + RBAC (Operator 권한)
postgres-operator — PostgreSQL 클러스터 선언적 관리
PostgreSQL HA 클러스터를 직접 구성하려면 Patroni 설정, pg_hba.conf, 백업 CronJob, failover 스크립트, 서비스 엔드포인트 관리를 모두 손으로 작성해야 합니다. 레플리카를 추가할 때마다 복제 설정을 수동으로 해줘야 하고, 버전 업그레이드는 순서를 지키지 않으면 데이터 손실 위험이 있습니다. postgres-operator는 이 모든 운영 노하우를 postgresql CRD 하나로 캡슐화합니다. Zalando의 postgres-operator는 CRD + Controller 패턴의 교과서적인 구현입니다. 이 CB에서는 postgresql CRD를 선언하면 Operator가 StatefulSet, Service, Secret, CronJob을 자동으로 생성하는 과정을 실습합니다. postgresql CRD 하나만 작성하면 HA PostgreSQL 클러스터 전체를 자동으로 구성합니다. Patroni를 사용한 자동 failover, WAL 백업, connection pooler(PgBouncer)까지 포함됩니다.

postgres-operator 설치
# Helm으로 postgres-operator 설치
helm install postgres-operator \
postgres-operator-charts/postgres-operator \
--namespace operator-lab \
--create-namespace
# Operator Pod 실행 확인
kubectl get pods -n operator-lab
# NAME READY STATUS RESTARTS
# postgres-operator-5d97b6f4d9-x8p2k 1/1 Running 0
# 설치된 CRD 확인
kubectl get crd | grep acid.zalan.do
# postgresqls.acid.zalan.do 2026-05-16T10:00:00Z
# operatorconfigurations.acid.zalan.do
# postgresteams.acid.zalan.do
- NAME—조회 대상 리소스 이름이 예상한 대상과 일치하는지 확인합니다.
- STATUS/READY—Running, Ready, Available처럼 정상 상태를 나타내는 필드가 있는지 봅니다.
- RESTARTS/EVENTS—재시작 횟수나 Warning 이벤트가 증가하지 않는지 확인합니다.
PostgreSQL 클러스터 생성
# postgres-cluster.yaml
apiVersion: acid.zalan.do/v1
kind: postgresql
metadata:
name: my-postgres-cluster
namespace: operator-lab
spec:
teamId: "platform" # 팀 식별자 (파드 이름 접두사)
volume:
size: 5Gi # 데이터 볼륨 크기
numberOfInstances: 2 # Primary 1개 + Replica 1개
users:
app_user: # 생성할 DB 유저
- login
- superuser
databases:
app_db: app_user # 생성할 DB: 소유자
postgresql:
version: "16"
parameters:
shared_buffers: "256MB"
max_connections: "100"
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: 1000m
memory: 2Gi
kubectl apply -f postgres-cluster.yaml
# 클러스터 생성 과정 관찰
kubectl get pods -n operator-lab -w
# NAME READY STATUS RESTARTS
# my-postgres-cluster-0 0/1 Pending 0
# my-postgres-cluster-0 1/1 Running 0 ← Primary
# my-postgres-cluster-1 1/1 Running 0 ← Replica
# 생성된 리소스 확인
kubectl get all -n operator-lab | grep -v "postgres-operator"
# pod/my-postgres-cluster-0 1/1 Running
# pod/my-postgres-cluster-1 1/1 Running
# service/my-postgres-cluster ClusterIP (Primary)
# service/my-postgres-cluster-repl ClusterIP (Replica)
# statefulset.apps/my-postgres-cluster
# 클러스터 상태 확인
kubectl get postgresql -n operator-lab
# NAME TEAM VERSION PODS VOLUME STATUS AGE
# my-postgres-cluster platform 16 2 5Gi Running 5m
자동 생성된 Secret으로 연결
# postgres-operator가 생성한 패스워드 Secret
kubectl get secrets -n operator-lab | grep my-postgres
# app_user.my-postgres-cluster.credentials.postgresql.acid.zalan.do
# 패스워드 추출
APP_PASS=$(kubectl get secret \
app_user.my-postgres-cluster.credentials.postgresql.acid.zalan.do \
-n operator-lab \
-o jsonpath='{.data.password}' | base64 -d)
# psql로 연결 테스트
kubectl run -it --rm psql-client \
--image=postgres:16-alpine \
--restart=Never \
-n operator-lab \
-- psql -h my-postgres-cluster -U app_user -d app_db \
-c "SELECT version();"
redis-operator — Redis Sentinel 클러스터 관리
Redis HA를 직접 구성하려면 Primary, Replica, Sentinel 파드를 각각 관리하고 Sentinel이 올바른 Primary를 가리키도록 설정해야 합니다. Sentinel 수는 홀수여야 하고, Redis 설정 파일은 파드마다 다르게 주입해야 합니다. Primary가 바뀌면 연결 풀도 재설정이 필요합니다. redis-operator는 RedisFailover CRD 하나로 이 구성을 선언하면 Primary/Replica/Sentinel 파드를 자동으로 배포하고 failover 시 Sentinel 투표를 통해 새 Primary를 선출합니다. 이 CB에서는 RedisFailover CRD를 선언해 Redis HA 클러스터를 배포하고 연결하는 방법을 다룹니다. Primary/Replica/Sentinel 구성을 자동으로 처리하고 장애 시 자동 failover를 수행합니다.
redis-operator 설치 및 RedisFailover 생성
# redis-operator Helm 저장소 추가
helm repo add redis-operator https://spotahome.github.io/redis-operator
helm repo update
# redis-operator 설치
helm install redis-operator redis-operator/redis-operator \
--namespace operator-lab
# RedisFailover CRD 생성
kubectl apply -f - << 'EOF'
apiVersion: databases.spotahome.com/v1
kind: RedisFailover
metadata:
name: my-redis
namespace: operator-lab
spec:
sentinel:
replicas: 3 # Sentinel 인스턴스 수 (홀수 권장)
resources:
requests:
cpu: 50m
memory: 32Mi
redis:
replicas: 3 # Redis 인스턴스 (Primary 1 + Replica 2)
resources:
requests:
cpu: 100m
memory: 128Mi
storage:
persistentVolumeClaim:
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
EOF
# Operator가 생성한 파드 확인
kubectl get pods -n operator-lab | grep redis
# rfr-my-redis-0 1/1 Running (Redis Replica/Primary)
# rfr-my-redis-1 1/1 Running
# rfr-my-redis-2 1/1 Running
# rfs-my-redis-0 1/1 Running (Sentinel)
# rfs-my-redis-1 1/1 Running
# rfs-my-redis-2 1/1 Running
# RedisFailover 상태 확인
kubectl get redisfailover my-redis -n operator-lab
# NAME AGE
# my-redis 3m
Redis 연결 방법
# redis-operator는 두 개의 Service를 생성
kubectl get svc -n operator-lab | grep redis
# rfs-my-redis ClusterIP 10.96.x.x 26379/TCP (Sentinel)
# rfr-my-redis ClusterIP 10.96.x.x 6379/TCP (Redis)
# 애플리케이션에서는 Sentinel 주소로 연결 (Primary 자동 감지)
# 예: redis-py에서
# from redis.sentinel import Sentinel
# sentinel = Sentinel([('rfs-my-redis.operator-lab.svc', 26379)])
# master = sentinel.master_for('mymaster')
실습 — Operator 동작 관찰하기
1단계: Operator 설치 및 CRD 확인
# postgres-operator 설치 (이미 설치했다면 생략)
helm install postgres-operator \
postgres-operator-charts/postgres-operator \
-n operator-lab
# Operator가 등록한 CRD 목록
kubectl get crd | grep acid
# postgresqls.acid.zalan.do
# operatorconfigurations.acid.zalan.do
# Operator Pod 로그 확인 (Reconcile 루프 관찰)
kubectl logs -n operator-lab \
-l app.kubernetes.io/name=postgres-operator \
--follow &
2단계: PostgresCluster 생성 및 Reconcile 관찰
kubectl apply -f postgres-cluster.yaml
# 다른 터미널에서 이벤트 스트리밍
kubectl get events -n operator-lab --watch &
# Operator 로그에서 Reconcile 사이클 관찰
# 2026-05-16 10:00:01 INFO cluster has been created (my-postgres-cluster)
# 2026-05-16 10:00:01 INFO creating statefulset my-postgres-cluster
# 2026-05-16 10:00:01 INFO creating service my-postgres-cluster
# 2026-05-16 10:00:01 INFO creating service my-postgres-cluster-repl
# 2026-05-16 10:00:01 INFO cluster my-postgres-cluster is in the Running state
3단계: 수동 장애 시뮬레이션 및 자동 복구 확인
# Primary 파드가 어느 것인지 확인
kubectl exec -n operator-lab my-postgres-cluster-0 -- \
patronictl -c /home/postgres/postgres.yml list
# + Cluster: my-postgres-cluster (7891234567) ------+----+-----------+
# | Member | Host | Role | State | TL |
# +--------------------------+----------+---------+--------+----+
# | my-postgres-cluster-0 | 10.x.x.x | Leader | running| 1 | ← Primary
# | my-postgres-cluster-1 | 10.x.x.x | Replica | running| 1 |
# Primary 파드 강제 삭제 (장애 시뮬레이션)
kubectl delete pod my-postgres-cluster-0 -n operator-lab
# 30초 내로 Replica가 Primary로 승격 확인
kubectl exec -n operator-lab my-postgres-cluster-1 -- \
patronictl -c /home/postgres/postgres.yml list
# | my-postgres-cluster-1 | 10.x.x.x | Leader | running| 2 | ← 새 Primary!
# | my-postgres-cluster-0 | 10.x.x.x | Replica | running| 2 | ← 재시작 후 Replica
4단계: postgresql CRD status 확인
kubectl get postgresql my-postgres-cluster -n operator-lab -o yaml | \
grep -A 20 "status:"
# status:
# PostgresClusterStatus: Running
# instances: 2
# latestCRDVersion: v1
# latestRestoreJobId: ""
# masterServiceName: my-postgres-cluster
문제 상황
# postgresql CRD를 적용했지만 파드, StatefulSet이 생성되지 않음
kubectl apply -f postgres-cluster.yaml
# postgresql.acid.zalan.do/my-postgres-cluster created ← CRD 적용은 됨
kubectl get pods -n operator-lab
# NAME READY STATUS
# postgres-operator-5d97b6f4d9-x8p2k 1/1 Running
# (PostgreSQL 파드가 없음)
kubectl get postgresql -n operator-lab
# NAME STATUS
# my-postgres-cluster (비어 있음 또는 Creating 상태에서 멈춤)
원인 1: Operator Pod 자체 문제
# Operator 로그에서 오류 확인
kubectl logs -n operator-lab \
-l app.kubernetes.io/name=postgres-operator \
--tail=50
# 흔한 오류들:
# "failed to watch resources: watch error" → RBAC 권한 부족
# "no storage class found" → 스토리지 클래스 미설정
# "quota exceeded" → 네임스페이스 리소스 쿼터 초과
원인 2: RBAC 권한 부족
# Operator의 ServiceAccount가 필요한 권한을 가졌는지 확인
kubectl get clusterrolebinding | grep postgres-operator
kubectl describe clusterrole postgres-operator
# 권한 부족 로그 예시:
# "failed to create statefulset: statefulsets.apps is forbidden:
# User "system:serviceaccount:operator-lab:postgres-operator"
# cannot create resource"
# 해결: Helm 재설치 또는 ClusterRole 재적용
helm upgrade postgres-operator postgres-operator-charts/postgres-operator \
-n operator-lab
원인 3: 스토리지 클래스 없음
# 스토리지 클래스 확인
kubectl get storageclass
# (출력 없음) ← 스토리지 클래스 없음!
# Operator 로그에서 확인
# "could not create persistent volume claim: no persistent volumes available"
# 해결 A: minikube에서 스토리지 활성화
minikube addons enable storage-provisioner
# 해결 B: kind에서 local-path provisioner 설치
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/main/deploy/local-path-storage.yaml
kubectl patch storageclass local-path -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
# 해결 C: postgresql CRD에 스토리지 클래스 명시
# spec:
# volume:
# storageClass: standard ← 존재하는 스토리지 클래스 이름
원인 4: 리소스 쿼터 초과
# 네임스페이스 리소스 사용량 확인
kubectl describe resourcequota -n operator-lab
# 출력 예시:
# Resource Used Hard
# pods 8 10
# requests.memory 3Gi 4Gi ← 거의 꽉 찼을 때
# 해결: 쿼터 증가 또는 리소스 요청량 줄이기
# spec.resources.requests.memory 값 줄이기
일반 디버깅 체크리스트
# 1. CRD 적용 확인
kubectl get crd | grep acid.zalan.do
# 2. Operator Pod 상태
kubectl get pods -n operator-lab -l app.kubernetes.io/name=postgres-operator
# 3. Operator 로그 (가장 중요)
kubectl logs -n operator-lab \
-l app.kubernetes.io/name=postgres-operator \
--since=5m
# 4. Events (리소스 생성 실패 이유)
kubectl get events -n operator-lab --sort-by=.creationTimestamp | tail -20
# 5. postgresql 리소스 상세 상태
kubectl describe postgresql my-postgres-cluster -n operator-lab
배경
중견 이커머스 회사의 인프라팀. 15개 MySQL 인스턴스를 Kubernetes 외부에서 전통적으로 운영하다가 Kubernetes 마이그레이션을 결정했습니다. DB 파드를 직접 StatefulSet으로 관리할지, Operator를 쓸지 고민했습니다.
직접 StatefulSet 관리의 문제
- Primary 파드가 죽으면? → 수동으로 failover 스크립트 실행
- PostgreSQL 마이너 버전 업그레이드? → 롤링 업데이트 순서 수동 조율
- Replica 추가? → postgresql.conf, pg_hba.conf 수동 설정
- 백업 스케줄? → 별도 CronJob을 직접 관리
Operator 도입 후 달라진 것
# 이것만 변경하면 Replica 추가
spec:
numberOfInstances: 3 # 2 → 3으로 변경
# Operator가 자동으로:
# 1. 새 Replica 파드 생성
# 2. Primary에서 베이스 백업 복사
# 3. Streaming Replication 설정
# 4. Service 엔드포인트 업데이트
# → 운영자는 spec만 수정, 나머지는 Operator
현실적인 주의사항
1. Operator 자체도 관리 대상이다
→ Operator 버전 업그레이드 시 breaking change 확인 필수
→ Operator Pod 자체 장애 시 DB는 영향 없지만 자동화는 멈춤
2. Operator가 모든 것을 해결하지 않는다
→ 스토리지 확장, 대용량 마이그레이션은 여전히 주의 필요
→ Operator의 동작 방식을 이해해야 문제 발생 시 대응 가능
3. 운영 노하우가 Operator에 종속된다
→ postgres-operator를 쓰면 Patroni 이해도 필요
→ Operator 오픈소스 이슈 트래커를 구독할 것
핵심 요약
| 개념 | 설명 |
|---|---|
| Operator | CRD(인터페이스) + Controller(구현)로 구성된 Kubernetes 애플리케이션 자동화 소프트웨어 |
| Reconcile 루프 | Watch → Diff → Act를 반복하며 선언 상태와 실제 상태를 맞추는 제어 루프 |
| Informer | Watch API를 추상화한 client-go 메커니즘 — 이벤트 캐싱과 리스트 기능 포함 |
| 멱등성 | 같은 Reconcile이 여러 번 실행되어도 동일한 결과 — Controller 설계의 핵심 원칙 |
| postgres-operator | Zalando의 PostgreSQL Operator — Patroni 기반 HA, 자동 failover, 백업 |
| redis-operator | Spotahome의 Redis Operator — Sentinel 기반 HA 클러스터 관리 |
| Controller 로그 | Operator 트러블슈팅의 시작점 — kubectl logs -l app=operator |
| OperatorHub | operatorhub.io — 검증된 Operator 카탈로그 |