쿠버네티스 Operator로 구현하는 나만의 관리형 Redis 서비스 개발기 (Managed Redis-as-a-Service)

가오리·2026년 6월 6일

Kubernetes

목록 보기
5/5

쿠버네티스(Kubernetes) 환경에서 애플리케이션을 운영하다 보면 가장 먼저 마주하는 난관 중 하나가 바로 "상태가 있는(Stateful) 애플리케이션을 어떻게 안정적으로 관리할 것인가?" 입니다. 특히 메모리 기반의 빠른 캐시 및 데이터 저장소로 자주 쓰이는 Redis는 복제본(Replica) 구성, Primary 장애 시 자동 Failover, 데이터 백업 등 운영에 손이 많이 가는 요소가 가득합니다.

이러한 문제를 해결하기 위해, 쿠버네티스의 선언적 프로그래밍 모델을 활용하여 Redis의 생성, 확장, 장애 복구 등 전체 생명주기를 자동화하는 '관리형 Redis 서비스'를 직접 개발했습니다.

본 글에서는 API 서버와 쿠버네티스 컨트롤러(Operator)를 분리한 시스템 아키텍처부터, Custom Resource Definition(CRD) 설계, 핵심 Operator 로직, 그리고 실제 서비스 운영 중 발생할 수 있는 장애 극복 사례까지 자세히 공유하고자 합니다.


1. 전체 시스템 아키텍처

우선 확장성과 유연성을 확보하기 위해 API 서버쿠버네티스 컨트롤러(Operator) 를 별도의 프로젝트로 분리했습니다. 각 컴포넌트가 명확한 책임을 가지는 느슨한 결합(Loose Coupling) 구조입니다.

  • API 서버 (api-server): 사용자의 REST API 요청을 받아 유효성을 검증하고, 쿠버네티스 API 서버와 통신하여 ManagedRedis Custom Resource(CR)를 생성/수정/삭제합니다. Redis의 내부 동작 원리는 모른 채 사용자와의 계약(API Spec)에만 집중합니다.
  • 컨트롤러 (operator): java-operator-sdk를 기반으로 동작하며, ManagedRedis CR의 변경 사항을 감시(Watch)합니다. 실제 Redis Pod, Service 등의 리소스 상태가 사용자가 선언한 '원하는 상태(Spec)'와 일치하도록 끊임없이 조정(Reconcile)하는 두뇌 역할을 합니다.

2. Custom Resource Definition (CRD) 설계

사용자가 Redis 클러스터의 상태를 명확히 정의하고, Operator가 상세한 운영 정보를 투명하게 전달할 수 있도록 Custom Resource를 다음과 같이 설계했습니다.

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: managedredises.hamcloud.com
spec:
  group: hamcloud.com
  scope: Namespaced
  names:
    plural: managedredises
    singular: managedredis
    kind: ManagedRedis
    shortNames: [mredis]
  versions:
    - name: v1alpha1
      served: true
      storage: true
      additionalPrinterColumns:
        - name: PHASE
          type: string
          jsonPath: .status.phase
        # ... 생략 ...
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              required: [replicas, image]
              properties:
                replicas:
                  type: integer
                  minimum: 1
                  description: "총 Redis 인스턴스 수(Primary 포함)"
                image:
                  type: string
                  description: "redis 컨테이너 이미지"
                promoteReplica:
                  type: string
                  description: "새로운 Primary로 승격시킬 Replica Pod의 이름 (Switchover용)"
            status:
              type: object
              properties:
                phase: { type: string }
                currentPrimary: { type: string }
                primaryEndpoint: { type: string }
                readerEndpoint: { type: string }
                readyReplicas: { type: integer }
                nodes:
                  type: array
                  items:
                    type: object
                    properties:
                      name: { type: string }
                      role: { type: string }
                      ready: { type: boolean }
                      podIP: { type: string }
                conditions:
                  type: array
                  # ... 생략 ...

주요 설계 포인트

  1. promoteReplica (Spec): 무중단 계획 점검이나 롤링 업그레이드를 위해, 관리자가 직접 특정 Replica를 Primary로 승격(Switchover)시킬 수 있도록 지원하는 제어 필드입니다.
  2. conditions (Status): 쿠버네티스의 표준 방식을 준수하여 ScalingUp, SwitchoverSucceeded, PodReady 등 클러스터의 상태 변화 이력을 시간 기록과 함께 남겨 운영 중 디버깅이 쉽도록 돕습니다.
  3. additionalPrinterColumns: kubectl get mredis 명령만 쳐도 현재 클러스터의 Phase, Replicas, Ready 상태, Primary Pod 등을 한눈에 모니터링할 수 있도록 편의성을 극대화했습니다.

3. Redis Operator Controller 구현 디테일

Operator 내부 구조는 단일 Reconciler 클래스가 모든 것을 처리하지 않고, 역할과 책임에 따라 여러 Manager 클래스로 구성하는 객체지향적 설계를 취했습니다.

ManagedRedisReconciler (지휘자)
 ├─ PodManager (Pod 생명주기 및 확장 담당)
 ├─ RedisStateManager (실제 Redis 프로세스 통신 및 복제/장애 복구 담당)
 ├─ ServiceManager (네트워크 엔드포인트 제공)
 └─ StatusManager (CRD Phase 및 Kubernetes Event 발행)

3.1. StatefulSet을 쓰지 않는 Pod 고유성 제어

상태가 있는 애플리케이션에 주로 쓰이는 StatefulSet을 사용하는 대신, 쿠버네티스의 로우레벨 Pod 제어 방식을 직접 구현했습니다.

  • PodManager는 Pod 생성 시 [클러스터명]-redis-[순번] 형태로 고유한 이름을 직접 할당하고, 레이블에 순번(ordinal)을 부여합니다.
  • Scale-Out 시 새로 생성되는 Replica Pod에 --replicaof [기존 Primary DNS]를 인자로 넘겨 자동으로 초기 복제에 합류시킵니다.
  • Scale-In 시에는 데이터 유실 위험이 없는 Replica 중에서 순번(ordinal)이 가장 높은 Pod부터 안전하게 타겟팅하여 삭제합니다.

3.2. 멱등성(Idempotency) 보장 전략

Reconcile 루프는 언제, 어떤 이유로든 반복 실행될 수 있으므로 멱등성 보장이 핵심입니다.
이를 위해 Read-Before-Write(조회 후 변경) 패턴을 철저히 구현했습니다.

  • Service 생성: 서비스가 이미 존재하는지 먼저 확인한 후에만 없는 경우 새로 생성합니다.
  • 복제본 연결: 무조건 REPLICAOF 명령을 내리는 것이 아니라, INFO replication 명령을 실행해 현재 설정 상태를 확인하고 실제 설정과 다를 때만 쉘 명령을 통해 설정을 보정합니다.

3.3. Finalizer를 통한 안전한 정리(Clean-up)

사용자가 ManagedRedis CR을 지웠을 때, Operator가 만든 하위 Pod와 Service가 방치되는 고아 리소스 문제를 해결하고자 Finalizer 패턴을 적용했습니다.

  • java-operator-sdk의 Cleaner 인터페이스를 상속받아 cleanup() 콜백 메서드에서 하위 리소스를 완전히 삭제하도록 보장한 뒤에 비로소 CR 리소스가 삭제되도록 보장합니다.

4. 계획된 전환(Switchover) vs 자동 장애 조치(Failover)

서비스 운영의 지속성을 위해 두 가지 형태의 마스터 승격 메커니즘을 설계했습니다.

4.1. Failover (자동 장애 조치)

Operator는 주기적인 Proactive 헬스 체크를 통해 Primary Pod의 상태를 감시합니다.
1. 장애 감지: Pod의 상태가 비정상(isHardFailure)이거나, 혹은 Pod는 작동 중이지만 내부 Redis 프로세스가 PING 명령에 응답하지 않는 '좀비' 상태를 감지합니다.
2. 대상 선정: Ready 상태의 Replica 중 순번(ordinal)이 가장 낮은 최적의 대상(혹은 데이터 복제 오프셋 차이가 가장 적은 대상)을 선출합니다.
3. 승격 및 강등: 선출된 Replica에 REPLICAOF NO ONE을 실행하여 새 Primary로 승격시키고, 장애가 났던 기존 Primary는 Split-brain 방지를 위해 Replica로 강등 조치합니다.

4.2. Switchover (계획된 전환)

관리 목적의 무중단 마스터 이동 절차입니다.
1. 관리자가 spec.promoteReplica 필드에 승격할 대상을 명시합니다.
2. Operator는 대상 Replica의 데이터 동기화 완료 여부(isReady)를 확인합니다.
3. 안전한 시점에 승격과 강등 작업을 진행하고 Service Endpoint(Primary)를 스위칭합니다.


5. 마치며

이번 프로젝트를 진행하며 단순히 애플리케이션을 배포하는 것을 넘어, 인프라의 생명주기를 다루는 컨트롤러 소프트웨어를 설계하는 방법에 대해 깊이 고민할 수 있었습니다.

쿠버네티스 Operator의 핵심은 최종 일관성(Eventually Consistency)멱등성(Idempotency) 을 유지하는 루프를 견고히 하는 데 있습니다. 앞으로는 분산 캐시 환경에서 완벽한 데이터 정합성 보장을 위해 복제 오프셋(master_repl_offset)을 비교하는 Failover 정밀 선출 알고리즘을 추가하고, 자동 백업/복구 고도화를 진행할 예정입니다.

profile
가오리의 개발 이야기

0개의 댓글