근묵자흑
Kubernetes Patterns: Operator 본문
Controller 패턴(Chapter 27)에서는 Annotation 기반으로 ConfigMap 변경을 감지하고 Pod를 재시작하는 구조를 살펴보았습니다.
이 방식은 동작하지만, Annotation은 자유 형식 문자열이라 유효성 검증이 없고, 어떤 ConfigMap이 어떤 Pod와 연결되는지 파악하기 어렵습니다. Operator 패턴은 Custom Resource Definition(CRD) 을 도입하여 이 한계를 해결합니다. 도메인 특화 리소스를 Kubernetes API의 일급 객체로 등록하고, 커스텀 Controller가 이를 관리하는 것이 Operator의 핵심입니다.
1. Operator 개요
1.1 문제
Controller 패턴은 기존 Kubernetes 리소스(ConfigMap, Pod, Deployment 등)만을 감시합니다. 설정 정보를 Annotation에 저장하면 다음과 같은 한계가 있습니다.
- 스키마 검증 불가: Annotation은 자유 형식 문자열이므로 잘못된 값이 들어가도 에러가 발생하지 않습니다
- 관계 파악 어려움: ConfigMap의 Annotation에 Pod 선택자가 묻혀 있어 전체 매핑을 한눈에 볼 수 없습니다
- 도메인 지식 부재: Kubernetes API가 도메인 특화 개념을 이해하지 못합니다
- 1:1 제약: Annotation 기반 접근은 하나의 ConfigMap에 하나의 애플리케이션만 연결할 수 있습니다
예를 들어 Prometheus를 Kubernetes의 모니터링 솔루션으로 채택했다고 가정합니다. Prometheus 설치 설정과 모니터링 대상 서비스를 Kubernetes 리소스처럼 선언적으로 정의할 수 있다면 운영이 훨씬 편리해집니다. 이러한 상황이 CRD가 필요한 전형적인 사례입니다.
1.2 해결책
CRD(Custom Resource Definition) 로 새로운 리소스 타입을 정의하고, 이 리소스의 변화를 감시하는 커스텀 Controller를 작성합니다. CRD와 Controller의 조합이 Operator 입니다.
Jimmy Zelinskie의 정의가 Operator의 특성을 잘 설명합니다.
An operator is a Kubernetes controller that understands two domains: Kubernetes and something else. By combining knowledge of both areas, it can automate tasks that usually require a human operator that understands both domains.
flowchart LR
subgraph "Controller 패턴"
C1[기존 리소스 감시] --> C2[Annotation 읽기] --> C3[Pod 재시작]
end
subgraph "Operator 패턴"
O1[기존 리소스 감시] --> O2[CRD 조회] --> O3[Pod 재시작]
O4[CRD 등록] -.-> O2
end
2. Controller vs Operator
Operator는 Controller의 상위 개념(is-a 관계)입니다. CRD를 통해 Kubernetes API를 확장하고, 도메인 특화 운영 지식을 코드로 표현합니다.
| 구분 | Controller | Operator |
|---|---|---|
| API 확장 | 기존 리소스만 사용 | CRD로 새 리소스 타입 정의 |
| 설정 방식 | Annotation 기반 (자유 형식) | CR 인스턴스 (스키마 검증) |
| 관계 표현 | ConfigMap Annotation에 매핑 정보 내장 | 별도 CR 리소스로 매핑 관계 명시 |
| 조회 가능성 | kubectl get cm에서 Annotation 확인 필요 |
kubectl get cw로 전체 매핑 조회 |
| 도메인 지식 | 범용적 | 도메인 특화 |
| 유효성 검증 | 없음 | OpenAPIV3Schema로 검증 |
| 매핑 구조 | 1:1 (ConfigMap → 단일 앱) | N:M (CR로 임의 조합 가능) |
다만 이 경계가 항상 명확한 것은 아닙니다. ConfigMap을 CRD 대신 사용하여 도메인 로직을 담는 중간 형태도 존재합니다. CRD 등록에 cluster-admin 권한이 필요하므로, 공유 클러스터에서 CRD를 등록할 수 없는 경우에는 ConfigMap 기반 Controller가 현실적인 대안이 됩니다.
flowchart LR
subgraph "Controller 분류 스펙트럼"
direction LR
A["Simple Controller<br/>(기존 리소스 감시)"] --> B["ConfigMap 기반<br/>Controller<br/>(CRD 없이 도메인 로직)"] --> C["Operator<br/>(CRD + Controller)"] --> D["API Aggregation<br/>(커스텀 API 서버)"]
end
style A fill:#e8f5e9
style B fill:#fff3e0
style C fill:#e3f2fd
style D fill:#f3e5f5
3. Operator 동작 원리
Operator도 Controller와 동일한 Observe-Analyze-Act 사이클을 따릅니다. 핵심 차이는 Analyze 단계에서 Annotation 대신 CRD를 조회한다는 점입니다.
sequenceDiagram
participant User
participant API as API Server
participant Op as Operator
participant CRD as ConfigWatcher CR
participant Pod
Note over API: CRD 등록 (configwatchers.k8spatterns.com)
User->>API: ConfigMap 수정
API->>Op: MODIFIED 이벤트 전송
Op->>API: ConfigWatcher CRD 조회
API-->>Op: spec.configMap 매칭 CR 반환
Op->>Op: podSelector.matchLabels 추출
Op->>API: Pod 삭제 요청 (labelSelector)
API->>Pod: Pod 종료
Note over API,Pod: Deployment가 새 Pod 생성
3.1 Level-Triggered vs Edge-Triggered
Operator의 Reconciliation은 Level-Triggered 방식입니다. 개별 이벤트에 반응하는 것이 아니라, 매 Reconciliation 사이클마다 현재 상태와 원하는 상태를 비교하여 수렴시킵니다. 이 설계가 멱등성(Idempotency) 을 자연스럽게 요구합니다.
- Edge-Triggered: "이벤트 A가 발생했으니 A에 대한 처리를 수행" → 이벤트 유실 시 상태 불일치 발생
- Level-Triggered: "현재 상태가 원하는 상태와 다르니 수렴시킴" → 이벤트가 유실되어도 다음 Reconcile에서 차이를 감지하여 보정
flowchart TB
subgraph "Edge-Triggered (비권장)"
E1[이벤트 A 발생] --> E2[A에 대한 처리]
E3[이벤트 B 발생] --> E4[B에 대한 처리]
E5[이벤트 유실] --> E6["상태 불일치"]
end
subgraph "Level-Triggered (Operator 방식)"
L1[Reconcile 호출] --> L2[현재 상태 조회]
L2 --> L3[원하는 상태와 비교]
L3 --> L4[차이 있으면 수렴]
L4 --> L5["항상 일관된 상태"]
end
controller-runtime 내부에서 이 사이클은 다음과 같이 동작합니다.
API Server → Watch → SharedInformer → Local Cache → Event Handler → Work Queue → Reconciler → API Server (writes)
읽기는 로컬 캐시(Eventually Consistent)를 통하고, 쓰기는 API Server로 직접 전달됩니다. Reconcile() 함수는 namespace/name 키만 받으며, 어떤 이벤트가 트리거했는지 알지 못합니다. 이 설계가 Level-Triggered 동작을 자연스럽게 강제합니다.
4. Custom Resource Definition (CRD)
CRD는 Kubernetes API를 확장하여 새로운 리소스 타입을 등록하는 메커니즘입니다. 등록된 CRD는 네이티브 리소스와 동일하게 kubectl, API Server, RBAC을 통해 관리됩니다.
4.1 CRD 구조
Prometheus Operator의 CRD를 예로 핵심 필드를 살펴보겠습니다.
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: prometheuses.monitoring.coreos.com # ① 이름: <plural>.<group>
spec:
group: monitoring.coreos.com # ② API 그룹
names:
kind: Prometheus # ③ 리소스 타입명
plural: prometheuses # ④ 복수형 (URL 경로에 사용)
singular: prometheus
scope: Namespaced # ⑤ 네임스페이스/클러스터 범위
versions:
- name: v1 # ⑥ 버전
storage: true # ⑦ 정확히 하나만 storage version
served: true # ⑧ REST API에서 제공 여부
schema:
openAPIV3Schema: .... # ⑨ 유효성 검증 스키마
| 항목 | 설명 |
|---|---|
metadata.name |
<plural>.<group> 형식으로 고유 이름을 구성합니다 |
spec.group |
API 그룹으로, 리소스의 소유 도메인을 나타냅니다 |
names.kind |
리소스 타입명으로, CR 인스턴스의 kind 필드에 사용됩니다 |
scope |
Namespaced 또는 Cluster 범위를 지정합니다 |
versions[].storage |
정확히 하나의 버전만 true로 설정하여 etcd에 저장되는 형식을 결정합니다 |
versions[].served |
REST API에서 해당 버전을 제공할지 결정합니다 |
openAPIV3Schema |
OpenAPI V3 스키마로 CR 생성 시 유효성을 자동 검증합니다 |
4.2 CRD 분류
CRD는 용도에 따라 크게 두 가지로 나뉩니다.
- Installation CRD: 애플리케이션 자체의 설치와 운영을 관리합니다. Prometheus CRD가 대표적인 예시로, Prometheus 서버의 배포 설정을 선언적으로 정의합니다.
- Application CRD: 애플리케이션 도메인 개념을 표현합니다.
ServiceMonitorCRD는 Prometheus가 스크래핑할 대상 서비스를 정의하며, Prometheus Operator가 이를 읽어 서버 설정을 자동으로 갱신합니다.
하나의 Operator가 여러 종류의 CRD를 관리할 수 있습니다. Prometheus Operator는 Prometheus, Alertmanager, ServiceMonitor, PodMonitor, PrometheusRule 등 10개 이상의 CRD를 관리합니다.
5. 구현 예제: ConfigWatcher Operator
5.1 CRD 정의
ConfigWatcher라는 새 리소스를 Kubernetes에 등록합니다. 이 리소스는 "어떤 ConfigMap이 변경되면 어떤 Pod를 재시작할 것인가"를 선언적으로 정의합니다.
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: configwatchers.k8spatterns.com
spec:
scope: Namespaced
group: k8spatterns.com
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
configMap:
type: string
description: "감시할 ConfigMap 이름"
podSelector:
type: object
description: "재시작할 Pod의 라벨 선택자"
properties:
matchLabels:
type: object
additionalProperties:
type: string
required:
- configMap
- podSelector
additionalPrinterColumns:
- name: ConfigMap
type: string
jsonPath: .spec.configMap
names:
kind: ConfigWatcher
singular: configwatcher
plural: configwatchers
shortNames:
- cw
CRD의 핵심 요소를 정리하면 다음과 같습니다.
| 항목 | 값 | 설명 |
|---|---|---|
| API Group | k8spatterns.com |
리소스 소유 도메인 |
| Kind | ConfigWatcher |
리소스 타입명 |
| Scope | Namespaced |
네임스페이스 범위 리소스 |
| Short Name | cw |
kubectl get cw로 조회 가능 |
spec.configMap |
string, required | 감시 대상 ConfigMap |
spec.podSelector |
object, required | 재시작 대상 Pod 라벨 |
| OpenAPIV3Schema | 정의됨 | 생성 시 유효성 자동 검증 |
| additionalPrinterColumns | ConfigMap | kubectl get cw 출력에 ConfigMap 이름 표시 |
5.2 CR 인스턴스
Controller 패턴에서는 ConfigMap에 k8spatterns.com/podDeleteSelector: "app=webapp" Annotation을 직접 넣었습니다. Operator 패턴에서는 별도의 ConfigWatcher CR로 이 매핑을 분리합니다.
apiVersion: k8spatterns.com/v1
kind: ConfigWatcher
metadata:
name: webapp-config-watcher
spec:
configMap: webapp-config
podSelector:
matchLabels:
app: webapp
이 분리로 인해 ConfigMap은 Operator/Controller를 알 필요가 없어지고, 하나의 ConfigMap에 여러 ConfigWatcher CR을 연결하거나, 하나의 ConfigWatcher가 여러 Pod를 대상으로 할 수 있는 N:M 매핑이 가능해집니다.
5.3 아키텍처
flowchart TB
subgraph "Operator Pod"
proxy[kubeapi-proxy<br/>Ambassador Container]
watcher[config-watcher<br/>Operator Script]
proxy <--> watcher
end
subgraph "Kubernetes API"
api[API Server]
crd_api["CRD API<br/>k8spatterns.com/v1"]
watch["Watch API<br/>configmaps?watch=true"]
end
subgraph "Custom Resources"
crd["ConfigWatcher CRD<br/>configwatchers.k8spatterns.com"]
cr["ConfigWatcher CR<br/>webapp-config-watcher"]
crd -.->|인스턴스| cr
end
subgraph "Application"
cm["ConfigMap<br/>webapp-config"]
deploy[Deployment<br/>webapp]
pod["Pod<br/>app=webapp"]
deploy --> pod
cm -.->|환경변수| pod
end
watcher -->|"1. Watch ConfigMaps"| watch
watch -->|"2. MODIFIED 이벤트"| watcher
watcher -->|"3. Query ConfigWatcher CRDs"| crd_api
crd_api -->|"4. 매칭 CR 반환"| watcher
watcher -->|"5. DELETE Pod"| api
api -->|"6. Pod 삭제"| pod
cr -.->|spec.configMap| cm
cr -.->|spec.podSelector| pod
style crd fill:#e1f5fe
style cr fill:#e1f5fe
style crd_api fill:#e1f5fe
5.4 Operator Script
Controller 패턴과의 핵심 차이는 ConfigMap MODIFIED 이벤트 수신 후 Annotation이 아닌 ConfigWatcher CRD를 조회하는 것입니다.
#!/bin/bash
namespace=${WATCH_NAMESPACE:-default}
base=http://localhost:8001
ns=namespaces/$namespace
start_event_loop() {
echo "::: Starting to watch ConfigMaps in namespace $namespace"
curl -N -s $base/api/v1/${ns}/configmaps?watch=true | while read -r event
do
event=$(echo "$event" | tr '\r\n' ' ')
local type=$(echo "$event" | jq -r .type)
local config_map=$(echo "$event" | jq -r .object.metadata.name)
echo "::: Event: $type on ConfigMap $config_map"
if [ "$type" = "MODIFIED" ]; then
echo "::: ConfigMap $config_map modified, checking ConfigWatcher CRDs..."
# ★ 핵심: Annotation 대신 ConfigWatcher CRD를 조회
local watchers=$(curl -s \
$base/apis/k8spatterns.com/v1/${ns}/configwatchers | \
jq -r ".items[] | select(.spec.configMap == \"$config_map\")")
if [ -n "$watchers" ]; then
echo "$watchers" | jq -r \
'.spec.podSelector.matchLabels | to_entries |
map(.key + "=" + .value) | join(",") | @uri' | \
while read -r selector; do
if [ -n "$selector" ]; then
delete_pods_with_selector "$selector"
fi
done
fi
fi
done
}
delete_pods_with_selector() {
local selector=${1}
echo "::::: Deleting pods with selector: $selector"
local pods=$(curl -s $base/api/v1/${ns}/pods?labelSelector=$selector | \
jq -r .items[].metadata.name)
for pod in $pods; do
exit_code=$(curl -s -X DELETE -o /dev/null \
-w "%{http_code}" $base/api/v1/${ns}/pods/$pod)
if [ $exit_code -eq 200 ]; then
echo "::::: Deleted pod $pod"
else
echo "::::: Error deleting pod $pod: $exit_code"
fi
done
}
start_event_loop
Controller와 Operator 스크립트의 핵심 차이를 비교하면 다음과 같습니다.
| 항목 | Controller | Operator |
|---|---|---|
| Pod 선택자 획득 | ConfigMap Annotation에서 추출 | ConfigWatcher CRD 조회 |
| API 엔드포인트 | /api/v1/.../configmaps |
/apis/k8spatterns.com/v1/.../configwatchers |
| 매핑 정보 위치 | ConfigMap 자체에 내장 | 별도 CR 리소스 |
| ConfigMap 의존성 | ConfigMap이 Controller를 알아야 함 | ConfigMap은 변경 없음 |
5.5 RBAC 구성
Controller 패턴에서는 기본 edit ClusterRole만으로 충분했습니다. Operator 패턴에서는 CRD 리소스에 대한 접근 권한이 추가로 필요합니다.
apiVersion: v1
kind: ServiceAccount
metadata:
name: config-watcher-operator
---
# 기본 리소스 관리 권한 (Pod 삭제, ConfigMap 감시)
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: config-watcher-operator
subjects:
- kind: ServiceAccount
name: config-watcher-operator
roleRef:
name: edit
kind: ClusterRole
apiGroup: rbac.authorization.k8s.io
---
# ★ CRD 접근 권한 (Controller에 없는 부분)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: config-watcher-crd
rules:
- apiGroups: ["k8spatterns.com"]
resources: ["configwatchers"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: config-watcher-crd
subjects:
- kind: ServiceAccount
name: config-watcher-operator
roleRef:
name: config-watcher-crd
kind: ClusterRole
apiGroup: rbac.authorization.k8s.io
flowchart LR
SA[ServiceAccount<br/>config-watcher-operator]
SA -->|RoleBinding| CR1["ClusterRole: edit<br/>Pod 삭제, ConfigMap 감시"]
SA -->|RoleBinding| CR2["ClusterRole: config-watcher-crd<br/>ConfigWatcher get/list/watch"]
style CR2 fill:#fff3e0
config-watcher-crd ClusterRole은 Operator에만 있는 부분입니다. k8spatterns.com API 그룹의 configwatchers 리소스에 대한 읽기 권한을 부여합니다.
5.6 Operator Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
pattern: Operator
name: config-watcher-operator
spec:
replicas: 1 # Singleton 패턴
selector:
matchLabels:
app: config-watcher-operator
template:
spec:
serviceAccountName: config-watcher-operator
containers:
- name: kubeapi-proxy
image: k8spatterns/kubeapi-proxy
- name: config-watcher
image: k8spatterns/curl-jq
env:
- name: WATCH_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace # Downward API
command: ["sh", "/watcher/config-watcher-operator.sh"]
volumeMounts:
- mountPath: "/watcher"
name: config-watcher-operator
volumes:
- name: config-watcher-operator
configMap:
name: config-watcher-operator
이 Deployment에는 여러 Kubernetes 패턴이 적용되어 있습니다.
flowchart TB
Operator[ConfigWatcher Operator]
Operator --> P1["Singleton Service<br/>replicas: 1"]
Operator --> P2["Ambassador<br/>kubeapi-proxy"]
Operator --> P3["Self Awareness<br/>Downward API"]
Operator --> P4["Controller<br/>Watch + Reconcile"]
Operator --> P5["CRD (API 확장)"]
P1 -.- D1[동시성 문제 방지]
P2 -.- D2[API Server 접근 단순화]
P3 -.- D3[네임스페이스 자동 인식]
P4 -.- D4[Observe-Analyze-Act 사이클]
P5 -.- D5[도메인 특화 리소스 정의]
style P5 fill:#fff3e0
5.7 Web Application
Controller 패턴과 달리, ConfigMap에 Annotation이 필요 없습니다. 매핑 정보가 ConfigWatcher CR에 분리되어 있기 때문입니다.
apiVersion: v1
kind: ConfigMap
metadata:
name: webapp-config
# ★ Annotation 없음 - ConfigWatcher CR이 매핑을 담당
data:
message: "Welcome to Kubernetes Patterns !"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
spec:
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- name: app
image: k8spatterns/mini-http-server
ports:
- containerPort: 8080
env:
- name: MESSAGE
valueFrom:
configMapKeyRef:
name: webapp-config
key: message
6. 테스트 결과
minikube 환경에서 총 56개 테스트를 수행한 결과를 공유드립니다.
6.1 테스트 환경
Kubernetes: minikube v1.37.0
kubectl: v1.34.1
Namespace: operator-test
6.2 오프라인 테스트 (Test 1-5)
클러스터 연결 없이 매니페스트와 스크립트의 구조적 정합성을 검증합니다.
| 테스트 | 항목 수 | 결과 |
|---|---|---|
| Test 1: YAML 구문 검증 | 4 | 전체 통과 |
| Test 2: CRD 구조 검증 | 14 | 전체 통과 |
| Test 3: Operator 스크립트 로직 검증 | 12 | 전체 통과 |
| Test 4: 매니페스트 구조 검증 | 18 | 전체 통과 |
| Test 5: README 핵심 개념 검증 | 8 | 전체 통과 |
6.3 클러스터 배포 테스트 (Test 6)
minikube에서 Operator의 실제 동작을 E2E로 검증합니다.
테스트 과정
- CRD 등록 (
configwatchers.k8spatterns.com) - Operator 배포 (ServiceAccount, RBAC, Deployment)
- Web App 배포 (ConfigMap, Deployment, Service)
- ConfigWatcher CR 생성
- ConfigMap 수정 → Operator가 Pod 재시작 트리거
- Pod 이름 변경 확인
- Operator 로그 확인
CRD 등록 및 CR 조회
$ kubectl get crd configwatchers.k8spatterns.com
NAME CREATED AT
configwatchers.k8spatterns.com 2026-02-07T...
$ kubectl get cw -n operator-test
NAME CONFIGMAP
webapp-config-watcher webapp-config
kubectl get cw로 전체 ConfigWatcher 매핑을 한눈에 확인할 수 있습니다. additionalPrinterColumns 설정 덕분에 CONFIGMAP 열이 표시됩니다. Controller 패턴에서는 kubectl get cm으로 각 ConfigMap의 Annotation을 일일이 확인해야 했던 것과 비교됩니다.
ConfigMap 수정 및 Pod 재시작
$ kubectl patch configmap webapp-config -n operator-test \
--type merge -p '{"data":{"message":"Updated at 193701"}}'
configmap/webapp-config patched
Operator 로그
::: Starting to watch ConfigMaps in namespace operator-test
::: Event: ADDED on ConfigMap config-watcher-operator
::: Event: ADDED on ConfigMap kube-root-ca.crt
::: Event: ADDED on ConfigMap webapp-config
::: Event: MODIFIED on ConfigMap webapp-config
::: ConfigMap webapp-config modified, checking ConfigWatcher CRDs...
::::: Deleting pods with selector: app%3Dwebapp
::::: Deleted pod webapp-f4ddf5b47-gwzm8
로그에서 Operator의 동작 흐름을 확인할 수 있습니다.
- ConfigMap Watch 시작 → ADDED 이벤트 수신
webapp-configMODIFIED 이벤트 감지- ConfigWatcher CRD 조회 →
spec.configMap == "webapp-config"매칭 podSelector.matchLabels에서app=webapp추출- Pod 삭제 → Deployment가 새 Pod 자동 생성
동작 결과 확인
flowchart LR
subgraph Before
CM1["ConfigMap<br/>message: Welcome..."]
Pod1["Pod<br/>webapp-...-gwzm8"]
CW["ConfigWatcher CR<br/>webapp-config-watcher"]
end
subgraph "ConfigMap 수정"
Patch[kubectl patch]
end
subgraph After
CM2["ConfigMap<br/>message: Updated..."]
Pod2["Pod<br/>webapp-...-8kcmf"]
CW2["ConfigWatcher CR<br/>변경 없음"]
end
CM1 --> Patch
Patch --> CM2
Pod1 -.->|Operator가 삭제| Patch
Patch -.->|Deployment가 생성| Pod2
CW --> CW2
| 단계 | 내용 | 값 |
|---|---|---|
| 수정 전 | Pod 이름 | webapp-f4ddf5b47-gwzm8 |
| 수정 전 | MESSAGE | Welcome to Kubernetes Patterns ! |
| 수정 후 | Pod 이름 | webapp-f4ddf5b47-8kcmf |
| 수정 후 | MESSAGE | Updated at 193701 |
리소스 상태
NAME READY STATUS AGE
pod/config-watcher-operator-6ff656dd9b-8mqjf 2/2 Running 12s
pod/webapp-f4ddf5b47-8kcmf 1/1 Running 5s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/webapp NodePort 10.101.25.125 <none> 8080:30754/TCP 12s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/config-watcher-operator 1/1 1 1 12s
deployment.apps/webapp 1/1 1 1 12s
클러스터 배포 테스트: 14개 항목 전부 통과.
7. Operator Capability Levels
Operator Framework는 다섯 단계의 Operator Capability Level을 정의합니다. 이 모델은 Operator의 성숙도를 평가하는 기준으로 사용됩니다.
flowchart TB
L1["Level 1: Basic Install<br/>CR로 프로비저닝"]
L2["Level 2: Seamless Upgrades<br/>무중단 업그레이드"]
L3["Level 3: Full Lifecycle<br/>백업/복원, 장애조치"]
L4["Level 4: Deep Insights<br/>메트릭, 알림, 로그"]
L5["Level 5: Auto Pilot<br/>자동 스케일링, 자가 치유"]
L1 --> L2 --> L3 --> L4 --> L5
style L1 fill:#c8e6c9
style L2 fill:#c8e6c9
style L3 fill:#fff9c4
style L4 fill:#fff9c4
style L5 fill:#ffccbc
| Level | 설명 | 핵심 기능 | 예시 |
|---|---|---|---|
| Level 1 | Basic Install | CR로 자동 프로비저닝, 기본 상태 보고 | 이 예제의 ConfigWatcher |
| Level 2 | Seamless Upgrades | Operator/Operand 무중단 업그레이드, 버전 마이그레이션 | Helm 기반 Operator |
| Level 3 | Full Lifecycle | 백업/복원, 장애조치, 클러스터 멤버 관리, 쿼럼 인식 롤링 업데이트 | CloudNativePG, Strimzi |
| Level 4 | Deep Insights | Prometheus 메트릭, 커스텀 알림, 로그 집계, 성능 베이스라인 | Prometheus Operator |
| Level 5 | Auto Pilot | 자동 스케일링, 자가 치유, 성능 베이스라인 학습, 워크로드 마이그레이션 | Strimzi + Cruise Control |
각 Level은 독립적입니다. Level 4의 모니터링을 달성하면서 Level 3의 백업 기능이 없을 수도 있습니다. Level 5를 완전히 달성한 Operator는 매우 드뭅니다. 이 예제의 ConfigWatcher Operator는 Level 1 (Basic Install) 수준으로, ConfigMap 변경 감지 → Pod 재시작이라는 단일 운영 작업을 자동화합니다.
8. 프로덕션 Operator 구축
Shell Script Operator는 학습용으로 Operator의 핵심 개념을 이해하는 데 적합하지만, 프로덕션 환경에서는 전용 프레임워크를 사용해야 합니다.
8.1 Operator 개발 프레임워크 현황 (2025-2026)
| 프레임워크 | 버전 | 언어 | 특징 |
|---|---|---|---|
| Kubebuilder | v4.11 | Go | 레퍼런스 구현, controller-runtime 기반, SIG API Machinery 관리 |
| Operator SDK | v1.42 | Go/Ansible/Helm | OLM 통합, Scorecard 테스트, Kubebuilder를 라이브러리로 사용 |
| Java Operator SDK | v5.2 | Java | Strimzi, Keycloak에서 사용, Quarkus 런타임 |
| kube-rs | v3.0 | Rust | CNCF Sandbox, 고성능, CustomResource derive 매크로 |
| Metacontroller | v4.12 | Any (webhook) | 빠른 프로토타이핑, HTTP/JSON 기반 위임 |
Kubebuilder와 Operator SDK의 관계: 초기에는 기능이 크게 겹쳤으나, 현재는 공통 부분이 Kubebuilder로 통합되고 Operator SDK는 Kubebuilder를 의존성으로 사용합니다. Operator SDK는 OLM 통합, Scorecard 테스트, Ansible/Helm 기반 Operator 같은 추가 기능을 제공합니다.
Metacontroller는 독특한 접근을 취합니다. Kubernetes API와의 모든 상호작용을 대행하고, 사용자는 비즈니스 로직만 담긴 Webhook 함수를 작성합니다. HTTP와 JSON을 이해하는 어떤 언어로든 Controller 함수를 구현할 수 있어 Go에 익숙하지 않은 팀에 적합합니다.
8.2 controller-runtime으로 구현하면
Go 기반 controller-runtime을 사용하면 Shell Script Operator를 다음과 같이 구현할 수 있습니다.
func (r *ConfigWatcherReconciler) Reconcile(
ctx context.Context, req ctrl.Request,
) (ctrl.Result, error) {
// 1. ConfigMap 가져오기
configMap := &corev1.ConfigMap{}
if err := r.Get(ctx, req.NamespacedName, configMap); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 2. 이 ConfigMap을 참조하는 ConfigWatcher CR 조회
var watchers v1.ConfigWatcherList
if err := r.List(ctx, &watchers,
client.InNamespace(req.Namespace)); err != nil {
return ctrl.Result{}, err
}
for _, watcher := range watchers.Items {
if watcher.Spec.ConfigMap != configMap.Name {
continue
}
// 3. 매칭되는 Pod 삭제
selector := labels.SelectorFromSet(
watcher.Spec.PodSelector.MatchLabels)
var pods corev1.PodList
r.List(ctx, &pods,
client.InNamespace(req.Namespace),
client.MatchingLabelsSelector{Selector: selector})
for _, pod := range pods.Items {
r.Delete(ctx, &pod)
}
}
return ctrl.Result{}, nil
}
8.3 Reconcile 에러 처리 패턴
controller-runtime의 반환 값에 따라 재시도 동작이 결정됩니다.
| 반환 값 | 동작 |
|---|---|
ctrl.Result{}, err |
지수 백오프로 재시도 (최대 약 16.7분) |
ctrl.Result{Requeue: true}, nil |
지수 백오프로 재시도 |
ctrl.Result{RequeueAfter: 5*time.Second}, nil |
고정 간격 후 재시도, Rate Limiting 미적용 |
ctrl.Result{}, nil |
성공, 재시도 없음 |
영구적 에러(잘못된 spec, 필수 리소스 부재)의 경우 reconcile.TerminalError(err)를 사용하여 재시도를 중단하고, Status Condition을 통해 사용자에게 알리는 것이 좋습니다.
8.4 프로덕션 핵심 요소
| 항목 | 설명 |
|---|---|
| 멱등성 | 같은 입력에 여러 번 Reconcile해도 동일한 결과를 보장합니다 |
| Leader Election | 다중 레플리카 배포 시 Lease 기반으로 하나만 활성화합니다 |
| Status Subresource | /status로 사용자 의도(spec)와 관측 결과(status)를 분리합니다 |
| ObservedGeneration | status.observedGeneration = metadata.generation으로 최신 spec 처리 여부를 표시합니다 |
| Finalizer | 외부 리소스 정리 시 삭제 전 cleanup을 보장합니다. 삭제 후 반드시 finalizer를 제거해야 합니다 |
| Owner Reference | 클러스터 내 부모-자식 관계를 설정하여 가비지 컬렉션을 자동화합니다. 네임스페이스를 넘지 못합니다 |
| Server-Side Apply | GET → UPDATE 대신 PATCH로 필드 소유권을 관리하여 충돌을 방지합니다 |
| Event Filtering | predicate.GenerationChangedPredicate{}로 status-only 변경을 필터링합니다 |
Server-Side Apply (SSA)
SSA는 Kubernetes 1.22에서 GA가 된 이후 Operator의 표준 쓰기 패턴으로 자리잡고 있습니다. 기존 GET → modify → UPDATE 방식은 동시 수정 시 충돌 위험이 있었지만, SSA는 Operator가 관리하는 필드만 선언적으로 PATCH하고, API Server가 필드 소유권을 추적하여 병합합니다.
desired := buildStatefulSet(cr) // Operator가 관리하는 필드만 포함
err := r.Patch(ctx, desired, client.Apply,
client.FieldOwner("my-operator"), client.ForceOwnership)
여러 액터(Operator, GitOps, Webhook)가 같은 객체의 서로 다른 필드를 안전하게 수정할 수 있습니다. Strimzi Kafka Operator도 SSA 도입을 진행하고 있습니다.
Finalizer 패턴
Finalizer는 CR 삭제 시 외부 리소스(클라우드 스토리지, DNS 레코드, 데이터베이스 등) 정리가 필요한 경우 사용합니다.
flowchart TB
A["CR 생성/업데이트<br/>deletionTimestamp 없음"] --> B{Finalizer<br/>등록됨?}
B -->|No| C[Finalizer 추가]
C --> D[정상 Reconcile]
B -->|Yes| D
E["CR 삭제 요청<br/>deletionTimestamp 설정됨"] --> F{Finalizer<br/>존재?}
F -->|Yes| G[외부 리소스 정리]
G --> H[Finalizer 제거]
H --> I[Kubernetes가 CR 삭제]
F -->|No| I
주의할 점은 CRD를 삭제할 때 Finalizer가 있는 CR이 남아 있으면 데드락이 발생할 수 있다는 것입니다. CRD 삭제 시 해당 API가 사라지므로 Finalizer를 제거하기 위한 UPDATE가 불가능해집니다.
9. CEL Validation (Kubernetes 1.29+ GA)
Kubernetes 1.29에서 GA가 된 CEL(Common Expression Language) 검증은 Operator 개발에 큰 변화를 가져왔습니다. CRD에 검증 규칙을 직접 내장하여 Validating Admission Webhook을 대부분의 경우 대체할 수 있습니다.
spec:
versions:
- name: v1
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
x-kubernetes-validations:
- rule: "self.configMap.size() > 0"
message: "configMap name must not be empty"
- rule: "size(self.podSelector.matchLabels) > 0"
message: "at least one label selector required"
properties:
configMap:
type: string
podSelector:
type: object
properties:
matchLabels:
type: object
additionalProperties:
type: string
CEL 검증의 장점은 다음과 같습니다.
- Webhook 인프라 불필요: TLS 인증서 관리, Webhook 서버 배포, 네트워크 지연이 없습니다
- CRD 등록 시 컴파일: 런타임이 아닌 CRD 생성 시 규칙이 검증되어 안정성이 높습니다
- Transition Rule 지원:
oldSelf를 사용하여 immutable 필드를 구현할 수 있습니다
x-kubernetes-validations:
- rule: "oldSelf.name == self.name"
message: "name is immutable once set"
Gateway API 프로젝트는 CEL 검증으로 모든 Validating Webhook을 완전히 대체했습니다.
CRD Validation Ratcheting (Kubernetes 1.33 GA)
Kubernetes 1.33에서 GA가 된 Validation Ratcheting은 CRD 스키마 진화를 크게 용이하게 합니다. 스키마 위반이 있는 기존 리소스라도, 변경되지 않은 필드의 위반은 무시하여 업데이트를 허용합니다. 이를 통해 CRD 작성자가 새 버전에서 검증을 강화해도 기존 리소스의 즉시 수정을 강제하지 않아도 됩니다.
10. CRD vs API Aggregation
| 항목 | CRD | API Aggregation |
|---|---|---|
| 복잡도 | 낮음 (YAML 정의) | 높음 (별도 API 서버 구현) |
| 인프라 | 추가 없음 | TLS, 배포, 모니터링 필요 |
| 검증 | CEL, OpenAPIV3Schema | 코드로 구현 |
| 커스텀 서브리소스 | /status, /scale만 |
/exec, /logs 등 자유롭게 정의 가능 |
| 저장소 | etcd (Kubernetes가 관리) | 자유 선택 (자체 구현) |
| 적합한 경우 | 대부분의 Operator | metrics-server, 커스텀 메트릭 어댑터 |
CRD가 기본 선택지입니다. API Aggregation은 /exec, /logs 같은 커스텀 서브리소스나 etcd 외 저장소가 필요한 특수한 경우에만 사용합니다. metrics-server가 API Aggregation의 대표적인 실제 사례입니다.
# API Aggregation 예시: 커스텀 API 서버 등록
apiVersion: apiregistration.k8s.io/v1
kind: APIService
metadata:
name: v1alpha1.sample-api.k8spatterns.io
spec:
group: sample-api.k8spatterns.io
service:
name: custom-api-server
version: v1alpha1
이 설정 이후 https://<api-server>/apis/sample-api.k8spatterns.io/v1alpha1/...로의 모든 요청이 커스텀 서비스로 전달됩니다.
11. Operator Lifecycle Manager (OLM)
CRD는 클러스터 범위 리소스이므로 등록에 cluster-admin 권한이 필요합니다. 이는 일반 사용자가 Operator를 설치하기 어렵게 만드는 제약입니다. Operator Lifecycle Manager(OLM)는 이 문제를 해결합니다.
11.1 OLM v0 (기존)
OLM v0은 ClusterServiceVersion(CSV) 리소스를 통해 Operator의 배포, CRD 등록, 종속성 관리를 자동화했습니다. 6개의 CRD(CatalogSource, Subscription, InstallPlan, CSV, OperatorGroup, PackageManifest)를 사용하며, Operator Hub를 통해 검색과 설치가 가능합니다.
11.2 OLM v1 (2025 GA)
OLM v1은 2025년 OpenShift 4.18에서 GA가 된 완전한 재설계입니다. 6개의 CRD를 ClusterExtension과 ClusterCatalog 2개로 단순화했으며, 보안 모델을 근본적으로 강화했습니다.
apiVersion: olm.operatorframework.io/v1
kind: ClusterExtension
metadata:
name: my-operator
spec:
namespace: my-operator-system
serviceAccount:
name: my-operator-installer-sa # ★ 필수: 명시적 ServiceAccount
source:
sourceType: Catalog
catalog:
packageName: my-operator
channels: [stable]
version: ">=1.0.0 <2.0.0" # Semver 범위 지원
핵심 변경사항은 다음과 같습니다.
- ServiceAccount 필수: 각 ClusterExtension에 명시적 RBAC이 적용된 ServiceAccount를 요구하여 최소 권한 원칙을 강제합니다
- catalogd 통합: gRPC 대신 RESTful HTTPS API로 카탈로그를 제공하여 API Server 부하를 줄였습니다
- 멀티 테넌시 제거: CRD가 클러스터 싱글톤이라는 근본적 제약으로 인해 의도적으로 제외되었습니다
- v0에서 v1로의 자동 마이그레이션 미지원: 두 버전은 같은 클러스터에서 공존할 수 있습니다
12. Operator를 사용하지 말아야 하는 경우
Operator는 모든 상황에서 필요한 것이 아닙니다. 도입 전에 실제로 런타임 Reconciliation이 필요한지, 도메인 특화 로직이 있는지 판단해야 합니다.
flowchart TB
Q1{"런타임 조정이<br/>필요한가?"}
Q2{"도메인 특화<br/>운영 로직이 있는가?"}
Q3{"CRD 수준의<br/>API 확장이 필요한가?"}
Q1 -->|No| H["Helm / Kustomize<br/>배포 시점만 관리"]
Q1 -->|Yes| Q2
Q2 -->|No| C["Simple Controller<br/>기존 리소스 감시"]
Q2 -->|Yes| Q3
Q3 -->|No| C
Q3 -->|Yes| O["Operator<br/>CRD + Controller"]
style H fill:#e8f5e9
style C fill:#fff3e0
style O fill:#e3f2fd
| 도구 | 적합한 경우 |
|---|---|
| Helm | Stateless 앱 배포, 환경별 설정 템플릿 |
| Kustomize | 템플릿 없는 오버레이 기반 커스터마이징 |
| Simple Controller | 기존 리소스 감시, 라벨/Annotation 자동 추가 |
| Operator | Stateful 앱 라이프사이클, 백업, 장애조치, 도메인 특화 스케일링 |
| Crossplane | 클라우드 인프라 선언적 프로비저닝 (DB, 네트워크, 스토리지) |
실무에서는 이들을 조합하여 사용하는 것이 일반적입니다. Crossplane으로 인프라를 프로비저닝하고, Helm으로 Operator를 설치하며, ArgoCD/Flux로 GitOps 배포를 관리하고, Operator가 Stateful 앱의 라이프사이클을 처리하는 구조입니다.
Kubernetes 공식 문서에서도 Controller, Operator, API Aggregation, 독립 API 중 무엇을 선택할지에 대한 가이드를 제공합니다. 사용 사례가 선언적이지 않거나, 데이터가 Kubernetes 리소스 모델에 맞지 않거나, 플랫폼과의 긴밀한 통합이 필요 없다면 독립 API와 Service/Ingress 조합이 더 나은 선택일 수 있습니다.
13. 실제 운영 Operator 사례
| Operator | 버전 (2025-2026) | 관리 대상 | CRD 수 | Capability Level |
|---|---|---|---|---|
| Prometheus Operator | v0.89 | 모니터링 스택 | 10+ | Level 3-4 |
| Strimzi | v0.50 | Kafka 클러스터 (KRaft 모드) | 10+ | Level 4+ |
| cert-manager | v1.19 | TLS 인증서 (ACME, Vault, CA) | 6 | Level 3 |
| CloudNativePG | v1.28 | PostgreSQL HA, 백업/PITR | 3 | Level 4 |
| ArgoCD Operator | v0.14 | ArgoCD 인스턴스 | 4 | Level 2 |
Prometheus Operator는 10개 이상의 CRD(Prometheus, Alertmanager, ServiceMonitor, PodMonitor, ScrapeConfig, PrometheusRule 등)를 관리하며, 6주 주기로 릴리스됩니다. kube-prometheus-stack Helm 차트가 가장 널리 사용되는 배포 방법입니다.
Strimzi는 CNCF Incubating 프로젝트로, v0.49에서 모든 CR의 v1 API 버전에 도달했습니다. 내부적으로 Cluster Operator, Topic Operator, User Operator 3개의 Operator를 실행하며, Cruise Control 통합으로 파티션 자동 리밸런싱(Level 5에 근접)을 제공합니다.
CloudNativePG는 CNCF Sandbox의 유일한 PostgreSQL Operator로, v1.26에서 선언적 오프라인 메이저 버전 업그레이드를 도입했습니다. HA, 백업/PITR, PgBouncer 연결 풀링, 레플리카 클러스터를 지원합니다.
14. Operator 보안 모범 사례
RBAC 최소 권한
- 와일드카드(
*) verb나 리소스 그룹을 사용하지 않습니다 - Kubebuilder RBAC 마커(
//+kubebuilder:rbac:...)로 ClusterRole을 자동 생성합니다 - 클러스터 범위 리소스를 관리하지 않는다면 네임스페이스 스코프 Role/RoleBinding을 우선합니다
escalate,bind,impersonateverb 부여를 피합니다
ServiceAccount 관리
- 반드시 전용 ServiceAccount를 생성하며,
defaultSA를 사용하지 않습니다 - Kubernetes 1.21 이후 기본값인 Projected Bound Token(시간/대상/객체 제한)을 활용합니다
- API 접근이 불필요한 Pod에는
automountServiceAccountToken: false를 설정합니다
Pod Security
Operator Pod에는 restricted Pod Security Standard를 적용합니다.
securityContext:
runAsNonRoot: true
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
15. Operator 테스트 도구
| 계층 | 도구 | 특징 |
|---|---|---|
| Unit/Integration | envtest | 최소 Control Plane(API Server + etcd) 기동, Pod 미실행, Docker 불필요, CI에서 빠르게 실행 |
| E2E | Chainsaw (Kyverno 프로젝트) | YAML 기반 선언적 테스트, KUTTL 대체, 멀티 클러스터 지원, 풍부한 assertion |
| OLM 호환성 | Operator SDK Scorecard | 번들 유효성, OLM 통합 검증 |
16. 정리
Operator 패턴은 Controller 패턴을 CRD로 확장하여 도메인 특화 운영 지식을 Kubernetes API의 일급 객체로 표현합니다.
- CRD로 새 리소스 타입을 정의하여 스키마 검증과
kubectl네이티브 조회가 가능합니다 - Annotation 기반 설정을 별도 CR로 분리하여 관계의 가시성이 향상됩니다
- ConfigMap이 Controller/Operator를 알 필요가 없어 관심사 분리를 달성합니다
- Level-Triggered Reconciliation으로 이벤트 유실에도 일관된 상태를 유지합니다
- 프로덕션에서는 Kubebuilder v4 또는 Operator SDK v1.42 사용을 권장합니다
- CEL Validation(GA in 1.29)으로 Webhook 없이 CRD 유효성 검증이 가능합니다
- Validation Ratcheting(GA in 1.33)으로 기존 리소스를 깨뜨리지 않고 스키마를 강화할 수 있습니다
- Server-Side Apply가 Operator의 표준 쓰기 패턴으로 자리잡고 있습니다
- OLM v1이 GA에 도달하여 보안이 강화된 Operator 라이프사이클 관리를 제공합니다
- Operator가 필요 없는 경우 Helm, Kustomize, Simple Controller가 더 적합합니다
참고
'k8s > kubernetes-pattern' 카테고리의 다른 글
| Kubernetes Patterns : Elastic Scale (0) | 2026.02.21 |
|---|---|
| Kubernetes Patterns : Operator (Datadog Operator) (0) | 2026.02.13 |
| Kubernetes Pattern: Controller (0) | 2026.01.31 |
| Kubernetes Pattern: Access Control (0) | 2026.01.24 |
| Kubernetes Pattern: Secure Configuration (0) | 2026.01.17 |