들어가며 — 운영자의 지식을 코드로 옮긴다는 것
쿠버네티스를 처음 배울 때 우리는 Deployment, Service, ConfigMap 같은 빌트인 리소스를 다룹니다. 이들은 잘 정의된 동작을 가지고 있어서, YAML을 적용하면 클러스터가 알아서 원하는 상태를 맞춰 줍니다. Deployment에 replica를 3으로 적으면 파드가 3개로 유지되고, 하나가 죽으면 다시 살아납니다. 이 "선언하면 알아서 맞춰지는" 경험이 쿠버네티스의 핵심 매력입니다.
그런데 실제 운영 현장에는 빌트인 리소스만으로는 표현되지 않는 복잡한 운영 지식이 넘쳐납니다. 예를 들어 PostgreSQL 클러스터를 운영한다고 합시다. 단순히 파드 3개를 띄우는 것으로 끝나지 않습니다. 누가 프라이머리이고 누가 레플리카인지 정해야 하고, 프라이머리가 죽으면 레플리카 중 하나를 승격(failover)해야 하며, 백업을 주기적으로 떠야 하고, 버전 업그레이드 시에는 순서를 지켜야 합니다. 이런 운영 절차는 보통 운영자의 머릿속이나 런북(runbook) 문서에 들어 있습니다.
**Operator 패턴은 바로 이 "운영자의 지식"을 코드로 옮겨 클러스터 안에서 24시간 자동으로 돌게 만드는 방법입니다.** CoreOS가 2016년에 처음 제안한 이 개념은 이제 쿠버네티스 생태계의 표준적인 확장 방식으로 자리 잡았습니다. 이 글에서는 Operator가 정확히 무엇을 푸는지, 그 밑바탕인 컨트롤 루프와 reconcile이 어떻게 동작하는지, 그리고 실무에서 언제 도입해야 하고 언제 피해야 하는지를 깊이 있게 다룹니다.
Operator가 푸는 문제 — 스테이트풀과 Day-2 운영
스테이트리스 vs 스테이트풀
쿠버네티스의 빌트인 컨트롤러는 스테이트리스(stateless) 워크로드에 매우 강합니다. 웹 서버처럼 어느 파드가 죽든 다른 파드로 대체하면 그만인 경우, Deployment 하나로 충분합니다. 파드 간 정체성도 필요 없고, 종료 순서도 중요하지 않습니다.
문제는 스테이트풀(stateful) 워크로드입니다. 데이터베이스, 메시지 브로커, 분산 캐시, 합의(consensus) 기반 시스템은 각 인스턴스가 고유한 정체성과 상태를 가집니다. StatefulSet이 안정적인 네트워크 ID와 스토리지를 제공하긴 하지만, StatefulSet은 "어떻게 클러스터를 부트스트랩하고, 멤버를 추가하고, 장애를 복구하는가"는 모릅니다. 그것은 애플리케이션 고유의 도메인 지식이기 때문입니다.
Day-1과 Day-2 운영
운영을 시간 축으로 나누면 이렇게 구분합니다.
- **Day-0**: 설계와 계획
- **Day-1**: 설치와 배포 (한 번만 하는 일)
- **Day-2**: 그 이후의 모든 운영 — 업그레이드, 스케일링, 백업/복원, 장애 복구, 설정 변경, 보안 패치
대부분의 도구는 Day-1을 잘 도와줍니다. Helm 차트로 설치는 쉽게 합니다. 하지만 진짜 어려운 것은 Day-2입니다. Helm은 한 번 설치하고 나면 그 뒤에 일어나는 일을 모릅니다. 프라이머리가 죽었을 때 누군가 손으로 개입해야 한다면, 그건 자동화가 아닙니다.
**Operator는 Day-2 운영을 자동화하는 데 특화되어 있습니다.** 컨트롤러가 클러스터 상태를 끊임없이 관찰하다가, 원하는 상태와 어긋나면 운영자가 했을 법한 조치를 스스로 수행합니다. 새벽 3시에 페이저가 울리지 않게 하는 것, 그것이 Operator의 핵심 가치입니다.
비교: 빌트인 vs Helm vs Operator
| 측면 | 빌트인 컨트롤러 | Helm | Operator |
| --- | --- | --- | --- |
| 주 대상 | 스테이트리스 | 패키징/설치 | 스테이트풀/복잡 운영 |
| Day-1 설치 | 수동 YAML | 자동 | 자동 |
| Day-2 운영 | 제한적 | 거의 없음 | 핵심 강점 |
| 지속적 조정 | 빌트인 한정 | 없음(템플릿 1회) | 항상 동작 |
| 도메인 지식 | 없음 | 없음 | 코드로 내장 |
| 학습 곡선 | 낮음 | 낮음 | 높음 |
컨트롤 루프와 reconcile 원리
선언적 모델: desired vs observed
쿠버네티스의 모든 동작은 하나의 단순한 원리로 환원됩니다. 바로 **원하는 상태(desired state)와 관찰된 상태(observed state)를 비교하고, 그 차이를 좁히는 것**입니다. 사용자가 "이렇게 되어야 한다"고 desired state를 선언하면, 컨트롤러는 현재 클러스터의 observed state를 읽어서 둘을 비교하고, 차이가 있으면 메우는 행동을 합니다.
이 끊임없이 도는 비교-조정 과정을 **컨트롤 루프(control loop)** 또는 **reconcile loop**라고 부릅니다. 글로 풀면 이렇습니다.
무한 반복:
desired = 사용자가 선언한 원하는 상태 읽기
observed = 클러스터의 실제 상태 읽기
if desired != observed:
차이를 좁히는 행동 수행
잠시 대기 또는 이벤트 발생까지 블록
이 단순한 루프가 쿠버네티스 전체를 떠받칩니다. Deployment 컨트롤러, ReplicaSet 컨트롤러, Node 컨트롤러 모두 이 패턴을 따릅니다. Operator도 정확히 같은 원리로 동작하며, 다만 그 대상이 사용자 정의 리소스라는 점만 다릅니다.
ASCII 다이어그램으로 보는 흐름
+-------------------+
| 사용자 / Git |
| desired state |
| (CR 적용) |
+---------+---------+
| apply
v
+-------------------+ watch +-----------------+
| API Server |<-------------------+| 컨트롤러 |
| (etcd 저장) | | (reconcile) |
+---------+---------+ +--------+--------+
^ |
| status 업데이트 | 1. observed 읽기
| / 리소스 생성·수정 | 2. desired 비교
| | 3. 차이 조정
+----------------------------------------+
실제 클러스터 상태를 맞춰감
핵심은 컨트롤러가 직접 etcd를 건드리지 않고, **항상 API Server를 통해 상태를 읽고 쓴다**는 점입니다. 컨트롤러는 API Server의 watch 메커니즘으로 변경을 통지받고, 변경이 생긴 객체에 대해 reconcile을 호출합니다.
멱등성(idempotency)이 핵심이다
reconcile 함수를 설계할 때 가장 중요한 원칙은 **멱등성**입니다. 같은 입력에 대해 몇 번을 실행하든 결과가 동일해야 한다는 뜻입니다. reconcile은 같은 객체에 대해 수십 번, 수백 번 호출될 수 있습니다. watch 이벤트가 중복으로 올 수도 있고, 주기적으로 재실행될 수도 있고, 에러 후 재시도될 수도 있습니다.
따라서 reconcile은 "X를 생성하라"가 아니라 "X가 원하는 모습으로 존재하는지 확인하고, 아니면 맞춰라"로 작성해야 합니다. 다음 의사코드를 비교해 봅시다.
잘못된 방식 (비멱등):
create Deployment # 두 번째 호출에서 "이미 존재함" 에러
올바른 방식 (멱등):
desired = buildDesiredDeployment()
existing = get Deployment
if not found:
create desired
else if existing != desired:
update to desired
이미 같으면 아무것도 안 함
멱등성을 지키지 못하면 컨트롤러는 중복 생성 에러를 내거나, 무한 루프에 빠지거나, 리소스를 계속 흔들어 클러스터를 불안정하게 만듭니다.
CRD + 컨트롤러 = Operator
CRD: 새로운 리소스 타입을 정의하다
Operator는 두 부분으로 구성됩니다. 첫 번째는 **CRD(CustomResourceDefinition)** 입니다. CRD는 쿠버네티스 API에 새로운 리소스 타입(kind)을 등록합니다. 예를 들어 `PostgresCluster`라는 새 종류를 정의하면, 사용자는 다음처럼 빌트인 리소스와 똑같은 방식으로 다룰 수 있게 됩니다.
apiVersion: db.example.com/v1
kind: PostgresCluster
metadata:
name: orders-db
spec:
replicas: 3
version: "16"
storage: 50Gi
status:
phase: Running
primary: orders-db-0
CRD를 등록하면 `kubectl get postgrescluster` 같은 명령이 동작하고, RBAC도 적용되고, etcd에 저장됩니다. 즉 CRD는 쿠버네티스 API의 어휘를 늘려 줍니다. 하지만 CRD만으로는 아무 일도 일어나지 않습니다. 그냥 데이터가 etcd에 저장될 뿐입니다.
컨트롤러: 정의에 생명을 불어넣다
두 번째 부분이 **컨트롤러**입니다. 컨트롤러는 위에서 정의한 CR(Custom Resource)을 watch하다가, 생성·수정·삭제가 일어나면 reconcile을 돌려 실제 동작을 만들어 냅니다. `PostgresCluster`를 보고 StatefulSet, Service, Secret, ConfigMap을 만들고, 프라이머리 선출을 관리하고, 백업 CronJob을 거는 일이 모두 컨트롤러 코드 안에 있습니다.
정리하면 다음 등식이 성립합니다.
Operator = CRD (새로운 API 타입) + 컨트롤러 (그 타입을 위한 reconcile 로직)
CRD가 "무엇을 원하는지 말하는 언어"라면, 컨트롤러는 "그것을 실현하는 운영자의 두뇌"입니다. 이 둘이 합쳐져야 비로소 도메인 특화된 자동 운영이 완성됩니다.
owner reference로 가비지 컬렉션
컨트롤러가 만든 하위 리소스(StatefulSet 등)에는 보통 **owner reference**를 답니다. 이렇게 하면 부모인 CR이 삭제될 때 쿠버네티스의 가비지 컬렉터가 자식 리소스를 자동으로 정리합니다. owner reference는 멱등성과 함께 Operator 설계의 기본기입니다. 이 덕분에 컨트롤러는 "내가 만든 것"을 추적하고, 정리 로직을 단순하게 유지할 수 있습니다.
컨트롤 플레인 확장 메커니즘 — CRD vs API Aggregation
쿠버네티스 API를 확장하는 방법은 사실 두 가지입니다. Operator는 거의 항상 첫 번째를 씁니다.
1. CRD 방식
CRD는 쿠버네티스가 제공하는 기본 저장소(etcd)와 API 처리 파이프라인을 그대로 빌려 씁니다. 스키마(OpenAPI v3)만 등록하면, 검증·버전 변환·저장·watch를 쿠버네티스가 알아서 해 줍니다. 별도 서버를 운영할 필요가 없어 압도적으로 간단합니다. 오늘날 거의 모든 Operator가 CRD 방식입니다.
2. API Aggregation 방식
API Aggregation은 사용자가 직접 확장 API 서버를 띄우고, 그것을 메인 API Server 뒤에 등록하는 방식입니다. 자체 저장 백엔드를 쓸 수 있고, 더 복잡한 검증·변환 로직을 구현할 수 있습니다. 하지만 별도 서버를 운영·인증·스케일링해야 해서 부담이 큽니다. `metrics.k8s.io`처럼 특수한 경우에만 씁니다.
비교 표
| 측면 | CRD | API Aggregation |
| --- | --- | --- |
| 저장소 | etcd 공용 | 자체 백엔드 가능 |
| 운영 부담 | 낮음 | 높음(서버 운영) |
| 검증 | OpenAPI 스키마 + 웹훅 | 임의 코드 |
| 일반적 용도 | 대부분의 Operator | 특수 케이스(메트릭 등) |
| 권장도 | 기본 선택 | 꼭 필요할 때만 |
추가로, 검증이나 기본값 주입이 더 필요하면 **어드미션 웹훅**(validating/mutating webhook)을 CRD와 함께 붙입니다. CRD + 웹훅 조합이면 API Aggregation 없이도 상당히 정교한 확장이 가능합니다.
Operator Capability Level — 성숙도 5단계
Operator Framework는 Operator의 성숙도를 5단계로 정의합니다. 자신의 Operator가 어디쯤 있는지 가늠하는 좋은 기준입니다.
| 레벨 | 이름 | 핵심 능력 |
| --- | --- | --- |
| 1 | Basic Install | 자동 설치, 기본 설정 프로비저닝 |
| 2 | Seamless Upgrades | 무중단 버전 업그레이드, 패치 관리 |
| 3 | Full Lifecycle | 백업/복원, failover, 스케일링 등 전체 수명주기 |
| 4 | Deep Insights | 메트릭/알림/로그, 워크로드 분석 노출 |
| 5 | Auto Pilot | 오토스케일링, 자동 튜닝, 이상 감지·자동 복구 |
- **레벨 1(Basic Install)**: CR 하나 적용하면 앱이 설치됩니다. 대부분의 Operator가 여기서 시작합니다.
- **레벨 2(Seamless Upgrades)**: 버전을 바꾸면 다운타임 없이 롤링 업그레이드합니다. 스테이트풀 앱에서는 이게 생각보다 어렵습니다.
- **레벨 3(Full Lifecycle)**: 백업 스케줄, 자동 failover, 멤버 추가/제거 같은 진짜 Day-2 운영을 자동화합니다.
- **레벨 4(Deep Insights)**: Prometheus 메트릭을 노출하고, 의미 있는 status conditions와 이벤트를 발행합니다.
- **레벨 5(Auto Pilot)**: 부하에 따라 알아서 스케일하고, 성능을 튜닝하고, 이상 징후를 감지해 스스로 복구합니다.
레벨이 높을수록 가치도 크지만 구현 난이도와 유지보수 비용도 급격히 올라갑니다. 모든 Operator가 레벨 5를 목표로 할 필요는 없습니다.
언제 Operator를 쓰고, 언제 쓰지 말아야 하나
쓰면 좋은 경우
- **복잡한 스테이트풀 애플리케이션**: 데이터베이스, 메시지 브로커, 분산 스토리지처럼 부트스트랩·failover·백업 로직이 도메인 특화된 경우.
- **반복되는 Day-2 운영**: 사람이 런북 보고 손으로 하던 절차가 명확히 정의되어 있고 자주 반복되는 경우.
- **여러 팀·환경에 동일 앱을 배포**: CR 하나로 표준화된 배포를 제공하면 일관성이 크게 올라갑니다.
- **자가 치유가 운영상 중요한 경우**: 새벽 장애에 사람이 개입해야 하는 위험을 줄이고 싶을 때.
쓰지 말아야 할 경우
- **스테이트리스 단순 앱**: Deployment + HPA로 충분한 것을 Operator로 만들면 과도한 엔지니어링입니다.
- **한 번 설치하고 끝**: Day-2 운영이 거의 없다면 Helm 차트가 더 적절합니다.
- **유지보수 인력이 없을 때**: Operator는 코드입니다. 쿠버네티스 버전이 올라가면 따라 유지보수해야 합니다. 만들고 방치하면 부채가 됩니다.
- **이미 좋은 공식 Operator가 있을 때**: 직접 만들기 전에 OperatorHub를 먼저 뒤져 보세요.
한 줄 판단 기준
> "운영자가 새벽에 일어나 손으로 해야 할 절차가 명확히 있고, 그게 반복되며, 자동화의 가치가 유지보수 비용보다 크다면 Operator를 고려하라."
그렇지 않다면 더 단순한 도구(Deployment, StatefulSet, Helm, GitOps)가 거의 항상 정답입니다. Operator는 강력하지만 공짜가 아닙니다.
생태계 — OperatorHub와 프레임워크
직접 만들기 전에 이미 검증된 Operator가 있는지 확인하는 것이 중요합니다.
- **OperatorHub.io**: 커뮤니티와 벤더가 공개한 Operator 카탈로그입니다. PostgreSQL, Kafka, Redis, Prometheus 등 주요 소프트웨어 대부분의 Operator를 찾을 수 있습니다.
- **OLM(Operator Lifecycle Manager)**: Operator 자체를 설치·업그레이드·의존성 관리하는 메타 운영 도구입니다. Operator의 Operator라고 보면 됩니다.
- **Kubebuilder**: Go로 Operator를 만드는 사실상 표준 SDK입니다. controller-runtime 위에서 프로젝트 스캐폴딩과 코드 생성을 제공합니다.
- **Operator SDK**: Kubebuilder를 감싸면서 Go뿐 아니라 Helm·Ansible 기반 Operator도 지원하는 상위 도구입니다.
다음 글에서는 Kubebuilder로 직접 Operator를 만드는 과정을, 그다음 글에서는 reconcile 루프를 심화해서 다룹니다.
명령형에서 선언형으로의 사고 전환
Operator를 제대로 이해하려면 사고방식 자체를 바꿔야 합니다. 전통적인 자동화 스크립트는 명령형(imperative)입니다. "이걸 만들고, 저걸 설정하고, 그다음 이걸 실행하라"는 절차의 나열입니다. 문제는 스크립트가 중간에 실패하면 시스템이 어중간한 상태에 빠진다는 것입니다. 다시 실행하면 "이미 존재함" 에러가 나거나 중복이 생깁니다.
reconcile는 선언형(declarative)입니다. "최종적으로 이런 모습이어야 한다"만 선언하고, 현재 상태와 비교해 그 모습으로 수렴시킵니다. 어느 지점에서 실패하든, 다음 reconcile가 처음부터 다시 돌면서 끝나지 않은 부분만 마저 합니다. 이 차이를 표로 정리하면 다음과 같습니다.
| 측면 | 명령형 스크립트 | 선언형 reconcile |
| --- | --- | --- |
| 표현 | "이 절차를 실행하라" | "이 결과가 되어야 한다" |
| 실패 후 | 어중간한 상태, 수동 복구 | 다음 실행이 자동 복구 |
| 재실행 | 위험(중복/에러) | 안전(멱등) |
| 드리프트 | 감지 못 함 | 자동 교정 |
이 사고 전환이 Operator의 본질입니다. "어떻게 할까"가 아니라 "무엇이 되어야 하는가"를 코드로 표현하는 것, 그리고 그 간극을 끊임없이 메우는 루프를 신뢰하는 것입니다. 이 관점에 익숙해지면 쿠버네티스 전체가 하나의 거대한 선언형 시스템으로 보이기 시작합니다.
2026년의 맥락
2026년 현재 Operator 패턴은 완전히 주류가 되었습니다. 클라우드 벤더의 매니지드 서비스 상당수가 내부적으로 Operator로 구현되어 있고, 플랫폼 엔지니어링 팀은 내부 개발자 플랫폼(IDP)을 만들 때 CRD와 Operator로 셀프서비스 추상화를 제공합니다. Kubebuilder는 Kubernetes 1.36과 Go 1.26을 지원하며, controller-runtime은 v0.24.x 계열로 안정화되었습니다.
특히 주목할 변화는 보안 측면입니다. 과거에는 메트릭 엔드포인트 보호를 위해 별도의 kube-rbac-proxy 사이드카를 붙였지만, 이제는 controller-runtime이 제공하는 인증·인가 미들웨어(WithAuthenticationAndAuthorization)를 사용해 추가 컨테이너 없이 메트릭을 보호합니다. 운영 표면이 줄어들어 더 단순하고 안전해졌습니다.
실제 Operator 사례로 보는 패턴
추상적인 설명보다 실제 사례를 보면 Operator가 무엇을 자동화하는지 더 선명해집니다. 대표적인 오픈소스 Operator 몇 가지를 살펴봅시다.
데이터베이스 Operator
PostgreSQL이나 MySQL Operator는 Operator 패턴의 교과서적 사례입니다. 이들이 자동화하는 운영을 나열해 보면 다음과 같습니다.
- 프라이머리/레플리카 토폴로지 부트스트랩
- 프라이머리 장애 시 자동 failover와 레플리카 승격
- 정기 백업과 시점 복구(PITR)
- 무중단 마이너 버전 업그레이드
- 커넥션 풀러(예: PgBouncer) 통합 관리
운영자가 새벽에 깨어나 수동으로 했을 이 모든 일이, CR 한 장의 선언과 컨트롤러의 reconcile로 대체됩니다. 이것이 레벨 3(Full Lifecycle) 이상의 가치입니다.
모니터링 Operator
Prometheus Operator는 또 다른 유명한 사례입니다. 여기서는 `Prometheus`, `ServiceMonitor`, `PrometheusRule` 같은 CRD를 정의해, 모니터링 설정 자체를 쿠버네티스 리소스로 다룹니다. 개발자는 거대한 Prometheus 설정 파일을 직접 편집하는 대신, `ServiceMonitor`라는 작은 CR을 만들어 "내 서비스의 메트릭을 수집하라"고 선언합니다. Operator가 이 선언들을 모아 실제 Prometheus 설정으로 변환하고 리로드합니다.
인증서 Operator
cert-manager는 인증서 발급과 갱신을 자동화하는 Operator입니다. `Certificate`라는 CR을 만들면 Operator가 ACME(예: Let's Encrypt)와 통신해 인증서를 발급하고, 만료 전에 자동으로 갱신합니다. 여기서 RequeueAfter의 주기적 점검이 빛을 발합니다. 인증서 만료는 watch 이벤트로 잡히지 않으므로, Operator는 주기적으로 다시 reconcile해 갱신 시점을 확인합니다.
공통 패턴 추출
이 사례들에서 반복되는 공통 구조가 보입니다.
1. 사용자가 작은 CR로 "원하는 결과"를 선언
2. Operator가 그것을 여러 하위 리소스/외부 호출로 변환
3. 지속적으로 관찰하며 드리프트를 교정
4. 시간 의존 작업(갱신/백업)은 주기적 reconcile로 처리
즉 Operator는 "복잡한 운영 의도를 단순한 선언으로 압축"하는 추상화 계층입니다. 사용자는 결과를 말하고, 그 결과를 만드는 절차는 Operator가 책임집니다.
CR의 일생 — 생성부터 삭제까지
Operator를 깊이 이해하려면 하나의 CR이 태어나서 죽기까지 어떤 단계를 거치는지 따라가 보는 것이 좋습니다. 추상적인 원리를 구체적인 흐름으로 바꿔 봅시다.
1. 생성 단계
사용자가 CR을 적용하면 API Server가 이를 검증하고 etcd에 저장합니다. 이 순간 컨트롤러의 informer가 watch로 생성 이벤트를 받고, 해당 객체 키를 workqueue에 넣습니다. 컨트롤러는 reconcile를 호출해 "이 CR이 원하는 하위 리소스가 아직 없다"는 사실을 발견하고, 필요한 Deployment·Service 등을 만듭니다.
사용자 apply
-> API Server 검증/저장
-> informer 생성 이벤트
-> reconcile 1회차: 하위 리소스 생성
-> reconcile 2회차: 하위 리소스 상태 관찰, status 갱신
흥미로운 점은 보통 한 번의 reconcile로 끝나지 않는다는 것입니다. 첫 reconcile에서 Deployment를 만들면, 그 Deployment의 파드가 준비되기까지 시간이 걸립니다. 컨트롤러는 RequeueAfter나 하위 리소스 watch를 통해 다시 호출되어, 준비 상태가 갖춰질 때까지 status를 점진적으로 갱신합니다.
2. 정상 운영 단계
CR이 원하는 상태에 도달하면 reconcile는 "할 일 없음"을 확인하고 조용히 끝납니다. 이때부터 컨트롤러는 감시자 역할을 합니다. 누군가 하위 Deployment를 손으로 수정하면 watch가 이를 감지해 reconcile가 다시 돌고, 원하는 상태로 되돌립니다. 노드 장애로 파드가 죽어도 빌트인 컨트롤러가 파드를 살리고, Operator는 그 위에서 도메인 레벨의 일관성을 유지합니다.
3. 변경 단계
사용자가 CR의 spec을 바꾸면(예: replica 3 → 5), generation이 증가하고 새 reconcile가 트리거됩니다. 컨트롤러는 새 desired state를 계산해 하위 리소스를 갱신합니다. 멱등하게 작성되어 있으므로, 무엇이 바뀌었는지 일일이 추적할 필요 없이 "원하는 전체 모습"을 다시 적용하면 됩니다.
4. 삭제 단계와 finalizer
CR을 삭제하면 두 가지 경로가 있습니다. owner reference로 연결된 단순한 하위 리소스는 가비지 컬렉터가 알아서 정리합니다. 하지만 클러스터 밖의 리소스(예: 클라우드 로드밸런서, 외부 DB 사용자, S3 버킷)는 쿠버네티스가 모르므로 자동 정리되지 않습니다. 이때 **finalizer**가 등장합니다.
finalizer는 객체에 붙는 일종의 "삭제 잠금"입니다. finalizer가 남아 있는 한 객체는 삭제 표시(deletionTimestamp)만 찍힌 채 실제로 사라지지 않습니다. 컨트롤러는 deletionTimestamp가 찍힌 것을 보고 외부 정리 작업을 수행한 뒤, finalizer를 제거합니다. finalizer가 모두 사라지면 그제서야 객체가 etcd에서 삭제됩니다.
삭제 요청
-> deletionTimestamp 설정 (객체는 아직 존재)
-> reconcile: 외부 리소스 정리 수행
-> finalizer 제거
-> 객체 실제 삭제
finalizer 덕분에 Operator는 "삭제 시 반드시 해야 할 정리"를 빠뜨리지 않고 보장할 수 있습니다. 외부 자원을 다루는 Operator라면 finalizer는 선택이 아니라 필수입니다.
패턴의 한계와 안티패턴
Operator는 강력하지만, 잘못 쓰면 오히려 시스템을 복잡하고 취약하게 만듭니다. 자주 보이는 안티패턴을 정리합니다.
안티패턴 1: 모든 것을 Operator로
단순한 배포까지 Operator로 감싸려는 욕심은 흔한 실수입니다. Deployment + ConfigMap으로 끝나는 일에 CRD와 컨트롤러를 도입하면, 운영자가 배워야 할 추상화만 늘어나고 디버깅은 어려워집니다. "이걸 Helm으로 못 하나?"를 먼저 물어보세요.
안티패턴 2: reconcile에 부작용 폭탄
reconcile는 멱등해야 합니다. 그런데 reconcile 안에서 이메일을 보내거나, 외부 시스템에 비멱등 요청을 보내거나, 카운터를 증가시키면 어떻게 될까요? reconcile는 수십 번 불릴 수 있으므로, 이메일이 수십 통 가고 카운터가 엉망이 됩니다. 부작용은 반드시 멱등하게 설계하거나, "한 번만" 보장이 필요하면 status에 완료 표시를 남겨 중복을 막아야 합니다.
안티패턴 3: 거대한 단일 reconcile
하나의 reconcile에 수백 줄의 분기를 욱여넣으면 추론과 테스트가 불가능해집니다. 앞서 본 단계적 설계처럼, 각 단계를 작은 멱등 함수로 쪼개는 것이 좋습니다.
안티패턴 4: status를 신뢰의 원천으로
status는 컨트롤러가 관찰한 결과를 캐시한 것일 뿐, 진실의 원천이 아닙니다. 진짜 상태는 항상 실제 리소스(observed state)에 있습니다. status만 보고 분기하면 status가 낡았을 때 잘못된 결정을 내립니다.
도입 전 체크리스트
Operator를 만들기로 결정하기 전에 다음을 자문해 보세요.
[ ] 이미 공식/커뮤니티 Operator가 있는가? (OperatorHub 확인)
[ ] Helm + GitOps로는 정말 안 되는가?
[ ] 자동화하려는 Day-2 운영이 명확히 정의되어 있는가?
[ ] 장기적으로 유지보수할 인력과 의지가 있는가?
[ ] reconcile를 멱등하게 설계할 수 있는가?
이 다섯 질문에 자신 있게 "예"라고 답할 수 있다면 Operator 도입은 좋은 선택입니다. 하나라도 망설여진다면, 더 단순한 길을 한 번 더 검토할 가치가 있습니다.
흔한 오해 풀기
Operator를 처음 접할 때 자주 생기는 오해 몇 가지를 짚고 넘어갑니다.
| 오해 | 사실 |
| --- | --- |
| "Operator는 마법 같은 자동화다" | 결국 reconcile 함수에 사람이 짠 운영 로직일 뿐입니다. 코드가 모르는 것은 자동화되지 않습니다. |
| "CRD만 만들면 Operator다" | CRD는 데이터 정의일 뿐, 컨트롤러가 없으면 아무 동작도 하지 않습니다. |
| "한 번 만들면 끝이다" | 쿠버네티스 버전 업과 함께 유지보수가 필요한 살아 있는 코드입니다. |
| "Operator는 항상 Go로 짜야 한다" | Go가 표준이지만 Helm·Ansible·Python(kopf) 등 다른 방식도 있습니다. |
| "Operator는 스테이트풀에만 쓴다" | 주로 그렇지만, 복잡한 설정 조정이나 정책 강제 등 다른 용도도 있습니다. |
특히 첫 번째 오해가 중요합니다. Operator는 사람의 판단을 코드로 옮긴 것이지, 사람보다 똑똑한 무언가가 아닙니다. reconcile 함수에 잘못된 운영 로직을 넣으면, 그 실수가 24시간 자동으로 반복됩니다. 그래서 Operator 코드의 품질과 테스트가 일반 애플리케이션보다 더 중요할 수 있습니다.
마지막 오해도 짚어 둘 만합니다. Operator가 스테이트풀 전용이라는 통념과 달리, 실제로는 설정 일관성 강제, 정책 적용, 외부 시스템과의 동기화 같은 스테이트리스 성격의 자동화에도 널리 쓰입니다. 핵심은 "상태를 가지느냐"가 아니라 "지속적으로 reconcile할 가치가 있는 도메인 로직이 있느냐"입니다. 그 기준으로 보면 Operator의 적용 범위는 생각보다 넓습니다.
마치며
Operator 패턴의 본질은 화려한 기술이 아니라 **"운영자의 지식을 선언적 모델 위의 reconcile 루프로 코드화한다"** 는 단순한 아이디어입니다. desired와 observed를 비교해 차이를 메우는 루프, 그 루프를 멱등하게 작성하는 규율, CRD로 어휘를 늘리고 컨트롤러로 생명을 불어넣는 구조 — 이 세 가지만 확실히 이해하면 어떤 Operator를 만나도 그 동작 원리를 꿰뚫어 볼 수 있습니다.
다만 잊지 말아야 할 것은, Operator가 모든 문제의 답은 아니라는 점입니다. 단순한 것은 단순하게 유지하고, 진짜 복잡한 Day-2 운영을 자동화해야 할 때 비로소 Operator를 꺼내 드는 것이 성숙한 선택입니다.
이어지는 글에서는 이 개념들을 실제 코드로 옮깁니다. 다음 글에서 Kubebuilder로 첫 Operator를 직접 만들고, 그다음 글에서 reconcile 루프의 내부 동작과 성능 튜닝을 깊이 파고듭니다. 원리를 이해한 지금이, 손으로 만들어 보며 그 이해를 단단히 굳힐 가장 좋은 때입니다.
참고 자료
- [Operator 패턴 (쿠버네티스 공식 문서)](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/)
- [CustomResourceDefinition 확장 (쿠버네티스 공식 문서)](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/)
- [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-sigs/kubebuilder (GitHub)](https://github.com/kubernetes-sigs/kubebuilder)
- [kubernetes-sigs/controller-runtime (GitHub)](https://github.com/kubernetes-sigs/controller-runtime)
- [Operator Lifecycle Manager 문서](https://olm.operatorframework.io/)
- [OperatorHub.io](https://operatorhub.io/)
- [Operator Capability Levels](https://sdk.operatorframework.io/docs/overview/operator-capabilities/)
현재 단락 (1/196)
쿠버네티스를 처음 배울 때 우리는 Deployment, Service, ConfigMap 같은 빌트인 리소스를 다룹니다. 이들은 잘 정의된 동작을 가지고 있어서, YAML을 적용하...