들어가며 — 왜 데이터베이스가 가장 어려운가
스테이트리스 애플리케이션은 쿠버네티스가 잘 다룹니다. Deployment에 replicas를 적으면 파드가 뜨고, 죽으면 다시 뜨고, 어느 파드가 죽든 똑같습니다. 그런데 데이터베이스는 다릅니다.
데이터베이스 파드는 서로 동등하지 않습니다. 하나는 쓰기를 받는 프라이머리이고 나머지는 읽기 전용 레플리카입니다. 파드를 지우면 그 안의 데이터가 함께 사라질 수 있습니다. 프라이머리가 죽으면 누군가를 승격시켜야 하는데, 잘못 승격시키면 데이터가 갈라집니다(split-brain). 스케일 다운은 단순히 파드를 줄이는 게 아니라 데이터 재배치를 동반합니다. 백업 없이 운영한다는 건 상상할 수 없습니다.
이 모든 절차가 바로 사람 DBA가 머릿속에 가지고 있던 운영 지식입니다. 이 글에서는 그 지식을 Operator로 코드화하는 과정을, Kubebuilder와 controller-runtime을 사용해 단계별로 따라가 보겠습니다. 실제 프로덕션 데이터베이스 엔진을 다 구현하지는 않지만, **Operator로 스테이트풀 시스템을 운영하는 핵심 패턴**을 손에 익히는 것이 목표입니다.
참고로 2026년 현재 Kubebuilder는 Kubernetes 1.36 / Go 1.26을 지원하고, controller-runtime은 v0.24 계열, controller-tools는 v0.21 계열입니다. 과거 사이드카로 붙이던 kube-rbac-proxy는 제거되었고, 대신 controller-runtime의 메트릭 서버가 제공하는 WithAuthenticationAndAuthorization를 사용합니다.
설계 — CR 스펙을 먼저 정한다
Operator 개발은 코드가 아니라 API 설계에서 시작합니다. 사용자가 무엇을 선언하게 할지를 먼저 정해야 합니다. 우리의 Database CR은 이렇게 생겼습니다.
apiVersion: db.example.com/v1alpha1
kind: Database
metadata:
name: orders-db
spec:
replicas: 3
version: "16.3"
storage:
size: 50Gi
storageClassName: fast-ssd
backup:
enabled: true
schedule: "0 3 * * *"
retention: 14
destination: s3://my-backups/orders-db
status:
phase: Running
primary: orders-db-0
readyReplicas: 3
lastBackupTime: "2026-06-15T03:00:12Z"
설계 원칙은 **작은 API**입니다. 사용자가 알아야 할 최소한의 손잡이만 노출합니다. replicas, version, storage, backup이면 충분합니다. 내부 구현 세부(StatefulSet 이름, 복제 프로토콜 같은 것)는 절대 spec에 노출하지 않습니다. 그것은 Operator의 책임이지 사용자의 관심사가 아닙니다.
Go 타입으로는 다음과 같이 정의합니다. Kubebuilder 마커(kubebuilder 주석)가 CRD의 OpenAPI 스키마와 검증을 생성합니다.
// DatabaseSpec defines the desired state of Database.
type DatabaseSpec struct {
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=9
// +kubebuilder:default=1
Replicas int32 `json:"replicas"`
// +kubebuilder:validation:Required
Version string `json:"version"`
Storage StorageSpec `json:"storage"`
// +optional
Backup *BackupSpec `json:"backup,omitempty"`
}
type StorageSpec struct {
Size string `json:"size"`
StorageClassName *string `json:"storageClassName,omitempty"`
}
type BackupSpec struct {
Enabled bool `json:"enabled"`
Schedule string `json:"schedule"`
Retention int32 `json:"retention"`
Destination string `json:"destination"`
}
// DatabaseStatus defines the observed state of Database.
type DatabaseStatus struct {
Phase string `json:"phase,omitempty"`
Primary string `json:"primary,omitempty"`
ReadyReplicas int32 `json:"readyReplicas,omitempty"`
LastBackupTime *metav1.Time `json:"lastBackupTime,omitempty"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
replicas 범위를 1에서 9로 제한한 것은 의도적입니다. 홀수 권장(쿼럼)과 무리한 스케일을 막기 위한 검증입니다. API 단계에서 막을 수 있는 실수는 API에서 막는 것이 좋습니다.
reconcile — 핵심 루프
Operator의 심장은 Reconcile 함수입니다. 이 함수는 "Database CR이 이런 상태이길 원한다"는 선언을 받아, 현실을 그 선언에 맞춥니다. 가장 중요한 원칙은 **멱등성**입니다. 같은 입력으로 몇 번을 호출해도 결과가 같아야 합니다.
func (r *DatabaseReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
// 1. 대상 CR 가져오기
var db dbv1alpha1.Database
if err := r.Get(ctx, req.NamespacedName, &db); err != nil {
// 이미 삭제됐다면 무시
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 2. 삭제 처리(finalizer)
if !db.DeletionTimestamp.IsZero() {
return r.reconcileDelete(ctx, &db)
}
if !controllerutil.ContainsFinalizer(&db, dbFinalizer) {
controllerutil.AddFinalizer(&db, dbFinalizer)
if err := r.Update(ctx, &db); err != nil {
return ctrl.Result{}, err
}
}
// 3. desired 리소스들을 차례로 보장
if err := r.reconcileHeadlessService(ctx, &db); err != nil {
return ctrl.Result{}, err
}
if err := r.reconcileStatefulSet(ctx, &db); err != nil {
return ctrl.Result{}, err
}
if err := r.reconcileBackupCronJob(ctx, &db); err != nil {
return ctrl.Result{}, err
}
// 4. 상태 갱신
if err := r.updateStatus(ctx, &db); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
각 reconcileXxx 함수는 "원하는 객체를 만들어 두고, 없으면 만들고, 다르면 맞춘다"는 동일한 패턴을 따릅니다. controller-runtime의 CreateOrUpdate 헬퍼가 이 패턴을 깔끔하게 표현합니다.
func (r *DatabaseReconciler) reconcileStatefulSet(ctx context.Context, db *dbv1alpha1.Database) error {
sts := &appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Name: db.Name,
Namespace: db.Namespace,
},
}
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, sts, func() error {
// 원하는 스펙을 채운다 (멱등)
sts.Spec.Replicas = &db.Spec.Replicas
sts.Spec.ServiceName = db.Name + "-headless"
sts.Spec.Selector = &metav1.LabelSelector{
MatchLabels: labelsFor(db),
}
sts.Spec.Template = podTemplateFor(db)
sts.Spec.VolumeClaimTemplates = pvcTemplatesFor(db)
// owner reference: CR 삭제 시 StatefulSet도 GC
return controllerutil.SetControllerReference(db, sts, r.Scheme)
})
return err
}
여기서 **owner reference**가 핵심입니다. StatefulSet의 소유자를 Database CR로 지정하면, CR이 삭제될 때 쿠버네티스 가비지 컬렉터가 StatefulSet을 자동으로 정리합니다. 또한 StatefulSet의 변화 이벤트가 다시 reconcile을 트리거하도록 watch를 걸어 둡니다.
StatefulSet·Service·PVC — 왜 이 셋인가
스테이트풀 데이터베이스를 쿠버네티스에서 운영할 때, 이 세 가지 리소스가 기본 골격입니다.
+------------------------------------------+
| Database CR |
+-------------------+----------------------+
| reconcile
+-----------+-----------+-----------+
v v v
StatefulSet Headless CronJob
(파드 N개) Service (백업)
| |
v v
안정적 식별자 DNS 레코드
db-0, db-1 db-0.db-headless
db-2 ... (파드별 고정 주소)
|
v
VolumeClaimTemplate
→ 파드마다 전용 PVC
→ 파드 재시작에도 데이터 유지
StatefulSet을 쓰는 이유는 세 가지입니다. 첫째, **안정적인 네트워크 식별자**입니다. 파드 이름이 db-0, db-1처럼 순서대로 고정되고, 헤드리스 Service와 결합해 db-0.db-headless 같은 고정 DNS를 가집니다. 레플리카가 프라이머리를 찾을 때 이 안정적 주소가 필수입니다.
둘째, **순차적 생성·삭제**입니다. db-0이 Ready가 된 뒤 db-1이 뜹니다. 데이터베이스 클러스터 부트스트랩에서 보통 db-0을 프라이머리로 초기화하고 나머지가 거기 붙는 순서가 자연스럽습니다.
셋째, **안정적인 스토리지**입니다. VolumeClaimTemplate으로 각 파드에 전용 PVC가 붙고, 파드가 재스케줄돼도 같은 PVC가 따라옵니다. 데이터가 보존됩니다.
헤드리스 Service는 ClusterIP가 None인 Service로, 로드밸런싱 대신 파드별 DNS 레코드를 제공합니다.
apiVersion: v1
kind: Service
metadata:
name: orders-db-headless
spec:
clusterIP: None
selector:
app: orders-db
ports:
- port: 5432
name: postgres
쓰기용 Service는 별도로 두되, 프라이머리만 가리키도록 셀렉터에 role 라벨을 추가합니다. Operator가 페일오버 시 이 라벨을 옮겨 트래픽을 새 프라이머리로 보냅니다.
주기적 백업 — CronJob 생성
백업은 Operator가 만드는 CronJob에 위임할 수 있습니다. spec.backup이 켜져 있으면 백업 CronJob을, 꺼져 있으면 삭제하는 식으로 reconcile합니다.
func (r *DatabaseReconciler) reconcileBackupCronJob(ctx context.Context, db *dbv1alpha1.Database) error {
name := db.Name + "-backup"
cj := &batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: db.Namespace},
}
// 백업이 꺼져 있으면 CronJob을 정리
if db.Spec.Backup == nil || !db.Spec.Backup.Enabled {
err := r.Delete(ctx, cj)
return client.IgnoreNotFound(err)
}
_, err := controllerutil.CreateOrUpdate(ctx, r.Client, cj, func() error {
cj.Spec.Schedule = db.Spec.Backup.Schedule
cj.Spec.JobTemplate.Spec.Template.Spec = backupPodSpec(db)
return controllerutil.SetControllerReference(db, cj, r.Scheme)
})
return err
}
백업 파드는 pg_dump나 물리 백업 도구를 실행하고 결과를 spec.backup.destination(예: S3)로 올립니다. 보존 정책(retention)은 백업 스크립트가 오래된 백업을 정리하도록 구현합니다.
대안으로 CronJob에 의존하지 않고 Operator 안에 자체 스케줄러를 두는 방식도 있습니다. requeue 타이밍을 계산해 다음 백업 시각에 reconcile을 재호출하는 것입니다. 다만 CronJob을 쓰는 편이 단순하고, 쿠버네티스가 스케줄링과 재시도를 대신 해 주므로 권장합니다.
페일오버와 리더 선출 — 개념
프라이머리가 죽었을 때의 처리가 데이터베이스 Operator의 가장 어려운 부분입니다. 핵심 위험은 **split-brain**입니다. 옛 프라이머리가 일시적 네트워크 단절로 "죽은 것처럼 보였다가" 다시 살아나면, 두 개의 프라이머리가 동시에 쓰기를 받아 데이터가 갈라집니다.
안전한 페일오버의 원칙은 다음과 같습니다.
페일오버 절차 (안전판 포함)
1. 프라이머리 헬스 감시 (예: 5초마다 liveness probe)
2. N회 연속 실패 → "의심" 상태로 전환 (즉시 승격 금지)
3. 펜싱(fencing): 옛 프라이머리의 쓰기 차단
- 쓰기 Service 셀렉터에서 옛 프라이머리 제거
- 또는 네트워크/스토리지 수준 격리
4. 가장 앞선 레플리카 선택 (복제 지연 최소)
5. 해당 레플리카를 프라이머리로 승격
6. 쓰기 Service 라벨을 새 프라이머리로 이동
7. 나머지 레플리카를 새 프라이머리에 재연결
8. status.primary 갱신, 이벤트 기록
리더 선출 자체는 직접 구현하기보다 검증된 합의 메커니즘에 위임하는 것이 안전합니다. 실제 프로덕션 Operator들은 다음 중 하나를 씁니다.
- **Raft/합의 라이브러리 내장**: etcd, Consul처럼 합의 알고리즘이 내장된 시스템.
- **Patroni 같은 전용 HA 에이전트**: Zalando Postgres Operator가 이 방식. Patroni가 etcd/쿠버네티스를 분산 락 저장소로 써서 리더를 선출합니다.
- **쿠버네티스 Lease 객체**: 쿠버네티스 자체의 리더 선출 메커니즘(coordination.k8s.io/Lease)을 활용.
직접 split-brain 방지를 구현하는 것은 분산 시스템 박사 논문급 난이도임을 인정해야 합니다. 그래서 "데이터베이스 페일오버를 새로 구현하지 말라"는 것이 현장의 정설입니다. 우리 Operator도 합의는 Patroni나 엔진 내장 메커니즘에 맡기고, Operator는 그 상태를 관찰해 쿠버네티스 리소스(Service 라벨 등)를 조정하는 역할에 집중하는 것이 현실적입니다.
버전 업그레이드 — 순차 롤링
버전 업그레이드는 한 번에 모든 파드를 바꾸면 안 됩니다. 가용성을 유지하며 한 대씩 교체해야 합니다. StatefulSet의 RollingUpdate 전략이 기본 틀을 제공하지만, 데이터베이스는 추가 제약이 있습니다.
업그레이드 순서 (Postgres 류 기준)
1. 모든 레플리카가 건강한지 확인 (불건강하면 중단)
2. 백업을 한 번 강제 수행 (롤백 대비)
3. 레플리카부터 업그레이드 (db-2 → db-1 ...)
- 한 대 업그레이드 → Ready 확인 → 복제 따라잡기 확인 → 다음
4. 마지막으로 프라이머리:
- 레플리카 하나를 새 버전으로 승격 (계획된 페일오버)
- 옛 프라이머리를 새 버전으로 재시작 후 레플리카로 합류
5. status.version 갱신
핵심은 **각 단계 사이의 헬스 게이트**입니다. 한 파드를 올린 뒤 무작정 다음으로 넘어가는 게 아니라, 복제가 따라잡았는지를 확인하고 진행합니다. Operator의 reconcile은 이 진행 상태를 status에 기록하고, 한 단계가 끝날 때마다 requeue로 다음 단계를 진행합니다.
// 업그레이드를 단계적으로 진행하는 의사 코드
func (r *DatabaseReconciler) reconcileUpgrade(ctx context.Context, db *dbv1alpha1.Database) (ctrl.Result, error) {
target := db.Spec.Version
// 가장 높은 인덱스의 구버전 레플리카를 찾는다
pod, found := r.findOutdatedReplica(ctx, db, target)
if !found {
return ctrl.Result{}, nil // 모두 최신
}
if !r.isHealthyAndCaughtUp(ctx, db) {
// 아직 따라잡는 중 → 잠시 후 재확인
return ctrl.Result{RequeueAfter: 15 * time.Second}, nil
}
if err := r.upgradePod(ctx, pod, target); err != nil {
return ctrl.Result{}, err
}
// 한 대 처리했으니 다시 reconcile
return ctrl.Result{RequeueAfter: 5 * time.Second}, nil
}
마이너 버전 건너뛰기는 금지하는 검증을 두는 것이 좋습니다. 예를 들어 Postgres에서 메이저 버전 업그레이드는 pg_upgrade 같은 별도 절차가 필요하므로, Operator가 "16에서 18로 직접 점프"를 거부하고 17 경유를 강제하거나 명시적 절차를 요구해야 합니다.
status — 헬스를 밖으로 노출하기
reconcile이 끝날 때마다 status를 갱신해 클러스터 사용자와 다른 도구가 데이터베이스 상태를 알 수 있게 합니다. 표준 패턴은 metav1.Condition 배열입니다.
func (r *DatabaseReconciler) updateStatus(ctx context.Context, db *dbv1alpha1.Database) error {
var sts appsv1.StatefulSet
if err := r.Get(ctx, types.NamespacedName{Name: db.Name, Namespace: db.Namespace}, &sts); err != nil {
return client.IgnoreNotFound(err)
}
db.Status.ReadyReplicas = sts.Status.ReadyReplicas
if sts.Status.ReadyReplicas == db.Spec.Replicas {
db.Status.Phase = "Running"
meta.SetStatusCondition(&db.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: metav1.ConditionTrue,
Reason: "AllReplicasReady",
Message: "all database replicas are ready",
})
} else {
db.Status.Phase = "Progressing"
meta.SetStatusCondition(&db.Status.Conditions, metav1.Condition{
Type: "Ready",
Status: metav1.ConditionFalse,
Reason: "ReplicasNotReady",
Message: "waiting for replicas to become ready",
})
}
return r.Status().Update(ctx, db)
}
중요한 원칙: **status는 Operator만 쓴다.** 사용자는 spec을 쓰고 status를 읽습니다. status를 spec처럼 입력 채널로 쓰는 것은 흔한 안티패턴입니다. 또 status 서브리소스를 활성화해(Kubebuilder 마커로) spec과 status 업데이트가 충돌하지 않게 합니다.
kubectl에서 보기 좋게 출력 컬럼도 마커로 정의할 수 있습니다.
// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase`
// +kubebuilder:printcolumn:name="Replicas",type=integer,JSONPath=`.status.readyReplicas`
// +kubebuilder:printcolumn:name="Primary",type=string,JSONPath=`.status.primary`
// +kubebuilder:subresource:status
관측 — 메트릭과 이벤트
운영 가능한 Operator는 자기 상태를 관측 가능하게 만듭니다. controller-runtime은 reconcile 횟수, 큐 깊이, 처리 시간 같은 메트릭을 기본 제공합니다. 여기에 도메인 메트릭(예: 페일오버 횟수, 백업 성공/실패)을 추가합니다.
var (
failoverTotal = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "database_failover_total",
Help: "Number of failovers performed per database",
},
[]string{"database", "namespace"},
)
)
func init() {
metrics.Registry.MustRegister(failoverTotal)
}
또한 중요한 사건은 쿠버네티스 이벤트로 남깁니다. 사용자가 kubectl describe로 무슨 일이 있었는지 추적할 수 있습니다.
r.Recorder.Event(db, corev1.EventTypeNormal, "FailoverCompleted",
fmt.Sprintf("promoted %s to primary", newPrimary))
운영 시나리오 — 실제로 어떻게 굴러가나
만들어진 Operator가 현장에서 어떤 일을 겪는지 시나리오로 정리합니다.
시나리오 1. 노드 장애로 프라이머리 파드 소멸
→ 스케줄러가 다른 노드에 파드 재생성, PVC 따라옴
→ 그 사이 HA 에이전트가 레플리카 승격, Operator가 쓰기 Service 라벨 이동
→ 옛 파드가 새 노드에서 뜨면 레플리카로 합류
시나리오 2. 디스크가 가득 참
→ status에 경고 condition 노출, 이벤트 발생
→ 사용자가 spec.storage.size 증가 → Operator가 PVC 확장(허용 시)
시나리오 3. 부하 증가로 읽기 레플리카 추가 필요
→ spec.replicas 3 → 5 → StatefulSet 스케일 → 새 레플리카가 베이스 백업으로 동기화
시나리오 4. 정기 백업 실패 (S3 권한 만료)
→ 백업 Job 실패, status.lastBackupTime 미갱신
→ 알림 규칙이 "X시간 이상 백업 없음"을 감지해 호출
이 시나리오들이 사람의 새벽 대응 없이 굴러간다는 것이 Operator의 가치입니다. 다만 시나리오 4처럼 **자동화가 실패했을 때 사람을 부르는 경로**(알림)가 반드시 함께 있어야 합니다. 조용히 실패하는 자동화가 가장 위험합니다.
기성 Operator vs 직접 구현 — 트레이드오프
여기까지 읽고 나면 한 가지가 분명해집니다. **데이터베이스 Operator를 제대로 만드는 것은 매우 어렵다.** 그렇다면 굳이 만들 필요가 있을까요?
| 관점 | 직접 구현 | 기성 Operator(CloudNativePG 등) |
| --- | --- | --- |
| 초기 비용 | 매우 높음 | 낮음 (helm install) |
| split-brain 안전성 | 직접 검증 필요(위험) | 수년간 프로덕션 검증됨 |
| 백업/PITR | 직접 구현 | 내장, 검증됨 |
| 커스터마이즈 | 무한 | 제공 범위 내 |
| 유지보수 | 영원히 우리 몫 | 커뮤니티가 분담 |
| 학습 가치 | 매우 높음 | 낮음 |
결론은 명확합니다. **프로덕션 데이터베이스라면 기성 Operator를 쓰십시오.** CloudNativePG, Zalando Postgres Operator, Strimzi는 split-brain, 백업 무결성, 버전 업그레이드 같은 함정을 이미 수년간 다듬어 왔습니다. 이를 재발명하는 것은 거의 항상 손해입니다.
직접 만들 가치가 있는 경우는 두 가지입니다. 첫째, **학습 목적** — Operator 패턴을 체화하는 데 데이터베이스만 한 교보재가 없습니다. 둘째, **기성 솔루션이 없는 사내 고유 스테이트풀 시스템** — 특수한 내부 데이터 저장소나 레거시 엔진을 쿠버네티스로 옮길 때입니다.
함정 — 직접 만든다면 반드시 마주칠 것들
[ ] PVC를 함부로 삭제 — owner reference에 PVC를 묶으면 CR 삭제 시 데이터 증발.
데이터 보존이 필요하면 PVC는 GC 대상에서 제외하고 별도 정책으로.
[ ] reconcile에서 블로킹 — 긴 작업(백업 등)을 reconcile 안에서 동기로 기다리면
워커 큐가 막힘. 별도 Job/CronJob에 위임할 것.
[ ] 멱등성 위반 — reconcile 호출마다 새 백업 Job을 만들면 무한 증식.
항상 "이미 있는가"를 먼저 확인.
[ ] status를 입력으로 사용 — split-brain 판단을 status에만 의존하면 위험.
[ ] 무한 requeue — 에러 시 즉시 재시도하면 폭주. 지수 백오프 활용.
[ ] 과도한 RBAC — 데이터베이스 Operator라고 cluster-admin을 요구하지 말 것.
[ ] 버전 스킵 방치 — 메이저 버전 점프를 막지 않으면 데이터 손상.
특히 첫 번째, PVC와 데이터 보존은 가장 치명적입니다. "Operator를 지웠더니 프로덕션 데이터가 같이 날아갔다"는 사고는 실제로 일어납니다. CR 삭제와 데이터 삭제를 분리하는 정책(예: deletionPolicy: Retain)을 반드시 두십시오.
마치며
데이터베이스 Operator를 만드는 여정은 reconcile 루프의 진짜 위력과 동시에 한계를 보여 줍니다. StatefulSet·Service·PVC를 조정하고, 백업을 CronJob에 위임하고, status로 헬스를 노출하는 패턴은 어떤 스테이트풀 Operator에도 그대로 적용됩니다. 이것이 이 글에서 가져갈 핵심 근육입니다.
그러나 split-brain과 데이터 무결성이라는 분산 시스템의 깊은 늪 앞에서는 겸손해야 합니다. 프로덕션 데이터베이스는 검증된 기성 Operator에 맡기고, 직접 구현은 학습이나 진짜로 대안이 없는 사내 시스템에 한정하는 것이 현명합니다. 다음 글에서는 이런 판단을 포함해 좋은 Operator와 나쁜 Operator를 가르는 베스트 프랙티스와 안티패턴을 정리하겠습니다.
참고 자료
- Kubebuilder Book: https://book.kubebuilder.io/
- Operator SDK: https://sdk.operatorframework.io/
- controller-runtime(pkg.go.dev): https://pkg.go.dev/sigs.k8s.io/controller-runtime
- Kubernetes Operator 패턴: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/
- kubebuilder GitHub: https://github.com/kubernetes-sigs/kubebuilder
- controller-runtime GitHub: https://github.com/kubernetes-sigs/controller-runtime
- StatefulSet 공식 문서: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/
- CloudNativePG(참고 구현): https://cloudnative-pg.io/
- Zalando Postgres Operator(Patroni 기반): https://github.com/zalando/postgres-operator
- 쿠버네티스 리더 선출(Lease): https://kubernetes.io/docs/concepts/architecture/leases/
- Operator Capability Levels: https://sdk.operatorframework.io/docs/overview/operator-capabilities/
현재 단락 (1/316)
스테이트리스 애플리케이션은 쿠버네티스가 잘 다룹니다. Deployment에 replicas를 적으면 파드가 뜨고, 죽으면 다시 뜨고, 어느 파드가 죽든 똑같습니다. 그런데 데이터베...