[Daily morning study] Kubernetes Operator ํจํด โ Custom Resource์ Controller
#daily morning study
Kubernetes Operator ํจํด
Operator๋?
Kubernetes๋ ๊ธฐ๋ณธ์ ์ผ๋ก Deployment, StatefulSet, Service ๊ฐ์ ๋ด์ฅ ๋ฆฌ์์ค๋ฅผ ์ ๊ณตํ์ง๋ง, ๋ณต์กํ ์ํ๋ฅผ ๊ฐ์ง ์ ํ๋ฆฌ์ผ์ด์ (๋ฐ์ดํฐ๋ฒ ์ด์ค ํด๋ฌ์คํฐ, ๋ฉ์์ง ํ, ML ํ์ดํ๋ผ์ธ ๋ฑ)์ ์ด์ํ๋ ๋ฐ ํ์ํ ๋๋ฉ์ธ ์ง์๊น์ง ๋ด์ฅํ๊ณ ์์ง ์๋ค.
Operator ํจํด์ ์ด ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ฑ์ฅํ๋ค.
Operator = CRD (Custom Resource Definition) + Custom Controller
ํน์ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ด์ ์ง์์ ์ฝ๋๋ก ๊ตฌํํด์, ์ฌ๋์ด ์๋์ผ๋ก ํด์ผ ํ๋ ์์ (๋กค๋ง ์ ๊ทธ๋ ์ด๋, ๋ฐฑ์ , ์ฅ์ ๋ณต๊ตฌ ๋ฑ)์ ์๋ํํ๋ Kubernetes ํ์ฅ ๋ฐฉ์์ด๋ค.
์ Operator๊ฐ ํ์ํ๊ฐ?
๊ฐ๋จํ ์น ์๋ฒ๋ Deployment๋ง์ผ๋ก ์ถฉ๋ถํ๋ค. ๊ทธ๋ฐ๋ฐ Elasticsearch๋ PostgreSQL ํด๋ฌ์คํฐ๋ฅผ ์ด์ํ๋ค๊ณ ์๊ฐํด๋ณด๋ฉด:
- ๋ ธ๋ ์ถ๊ฐ โ ์ค๋ ์ฌ๋ฐฐ๋ถ
- ๋ฒ์ ์ ๊ทธ๋ ์ด๋ โ ์์ ์๋ ๋กค๋ง ์ ๋ฐ์ดํธ
- ์ฅ์ ๋ฐ์ โ ํน์ ๋ ธ๋ ์ ์ธ ํ ํ์ผ์ค๋ฒ
- ์ฃผ๊ธฐ์ ์ค๋ ์ท ๋ฐฑ์
์ด๋ฐ ์ ์ฐจ๋ ๋จ์ํ replicas: 3 ์ค์ ์์ค์ด ์๋๋ค. Operator๋ ์ด ์ด์ ์ ์ฐจ๋ฅผ ์ปจํธ๋กค๋ฌ ์ฝ๋์ ๋
น์ฌ์ K8s๊ฐ ์ค์ค๋ก ์ฒ๋ฆฌํ๊ฒ ๋ง๋ ๋ค.
CRD โ API ํ์ฅ
CRD(Custom Resource Definition)๋ K8s API์ ์๋ก์ด ๋ฆฌ์์ค ํ์
์ ์ถ๊ฐํ๋ค. ์ ์ฉํ๋ฉด kubectl get myapps ๊ฐ์ ๋ช
๋ น์ด ๋์ํ๋ค.
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: myapps.example.com
spec:
group: example.com
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
replicas:
type: integer
minimum: 1
image:
type: string
scope: Namespaced
names:
plural: myapps
singular: myapp
kind: MyApp
CRD ๋ฑ๋ก ํ, ์ด ํ์ ์ผ๋ก Custom Resource(CR)๋ฅผ ๋ง๋ค ์ ์๋ค:
apiVersion: example.com/v1
kind: MyApp
metadata:
name: my-app-instance
namespace: default
spec:
replicas: 3
image: my-image:v1.2.3
์ด YAML์ etcd์ ์ ์ฅ๋๊ณ , ์ปจํธ๋กค๋ฌ๊ฐ ์ฝ์ด์ ์ค์ Deployment, Service ๋ฑ์ ์์ฑํ๋ค.
Custom Controller โ Reconciliation Loop
์ปจํธ๋กค๋ฌ์ ํต์ฌ์ Reconciliation Loop๋ค. ๊ด์ฐฐ โ ์ฐจ์ด ๊ณ์ฐ โ ํ๋์ ๋ฐ๋ณตํ๋ฉฐ ์ํ๋ ์ํ(Desired State)๋ฅผ ์ ์งํ๋ค.
Observe โ Diff โ Act โ Observe โ ...
Go ๊ธฐ๋ฐ ์์ (controller-runtime ๋ผ์ด๋ธ๋ฌ๋ฆฌ):
func (r *MyAppReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// 1. CR ์กฐํ
var myApp myv1.MyApp
if err := r.Get(ctx, req.NamespacedName, &myApp); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 2. ํ์ฌ Deployment ํ์ธ
var deployment appsv1.Deployment
err := r.Get(ctx, req.NamespacedName, &deployment)
if errors.IsNotFound(err) {
// 3. ์์ผ๋ฉด ์์ฑ
newDeploy := r.buildDeployment(&myApp)
return ctrl.Result{}, r.Create(ctx, newDeploy)
}
// 4. ์์ผ๋ฉด ์ํ๋ ์ํ์ ๋น๊ต ํ ์
๋ฐ์ดํธ
if *deployment.Spec.Replicas != myApp.Spec.Replicas {
deployment.Spec.Replicas = &myApp.Spec.Replicas
return ctrl.Result{}, r.Update(ctx, &deployment)
}
return ctrl.Result{}, nil
}
Reconcile์ ๋ฐ๋์ ๋ฉฑ๋ฑ์ฑ(Idempotent)์ ๋ณด์ฅํด์ผ ํ๋ค. ๋์ผํ ์ํ์์ ์ฌ๋ฌ ๋ฒ ์คํํด๋ ๊ฒฐ๊ณผ๊ฐ ๊ฐ์์ผ ํ๋ค.
Operator ๊ฐ๋ฐ ๋๊ตฌ
Operator SDK
Red Hat์ด ๋ง๋ ๋๊ตฌ. Go / Helm / Ansible ๊ธฐ๋ฐ Operator๋ฅผ ์ง์ํ๋ค.
# Go ๊ธฐ๋ฐ ์ด๊ธฐํ
operator-sdk init --domain example.com --repo github.com/example/my-operator
operator-sdk create api --group apps --version v1 --kind MyApp --resource --controller
Kubebuilder
K8s SIG์์ ๊ด๋ฆฌํ๋ ๊ณต์ ์ค์บํด๋ฉ ๋๊ตฌ.
kubebuilder init --domain example.com
kubebuilder create api --group apps --version v1 --kind MyApp
๋ ๋๊ตฌ ๋ชจ๋ ๋ด๋ถ์ ์ผ๋ก controller-runtime ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๋ฉฐ ๊ตฌ์กฐ๊ฐ ๋น์ทํ๋ค.
Spec vs Status
| ํ๋ | ์ญํ | ๋๊ฐ ์ฐ๋ |
|---|---|---|
spec | ์ํ๋ ์ํ (Desired State) | ์ฌ์ฉ์ |
status | ๊ด์ฐฐ๋ ํ์ฌ ์ํ | ์ปจํธ๋กค๋ฌ |
์ปจํธ๋กค๋ฌ๋ ์์
๊ฒฐ๊ณผ๋ฅผ status์ ๊ธฐ๋กํ๋ค:
myApp.Status.Phase = "Running"
myApp.Status.ReadyReplicas = 3
r.Status().Update(ctx, &myApp)
status:
phase: Running
readyReplicas: 3
conditions:
- type: Ready
status: "True"
lastTransitionTime: "2026-08-14T09:00:00Z"
Finalizer โ ์ญ์ ํ๋ฆ ์ ์ด
๋ฆฌ์์ค ์ญ์ ์ ์ ์ธ๋ถ ๋ฆฌ์์ค ์ ๋ฆฌ(์: ํด๋ผ์ฐ๋ ๋ณผ๋ฅจ ์ญ์ , ์ธ๋ถ DB ํด์ )๊ฐ ํ์ํ ๋ Finalizer๋ฅผ ์ฌ์ฉํ๋ค.
const myFinalizer = "example.com/finalizer"
// CR ์์ฑ ์ Finalizer ์ถ๊ฐ
controllerutil.AddFinalizer(&myApp, myFinalizer)
r.Update(ctx, &myApp)
// ์ญ์ ์์ฒญ ๊ฐ์ง
if !myApp.DeletionTimestamp.IsZero() {
// ์ ๋ฆฌ ์์
์ํ
if err := r.cleanupExternalResources(&myApp); err != nil {
return ctrl.Result{}, err
}
// Finalizer ์ ๊ฑฐ โ K8s๊ฐ ์ค์ ์ญ์ ์งํ
controllerutil.RemoveFinalizer(&myApp, myFinalizer)
r.Update(ctx, &myApp)
}
Finalizer๊ฐ ๋จ์ ์์ผ๋ฉด K8s๋ ์ค๋ธ์ ํธ๋ฅผ ์ค์ ๋ก ์ญ์ ํ์ง ์๋๋ค. DeletionTimestamp๋ง ์ฐ์ด๋๊ณ ๊ธฐ๋ค๋ฆฐ๋ค. ์ปจํธ๋กค๋ฌ๊ฐ ์ ๋ฆฌ ์์
์ ๋ง์น๊ณ Finalizer๋ฅผ ์ ๊ฑฐํด์ผ ๋น๋ก์ etcd์์ ์ญ์ ๋๋ค.
Owner Reference โ ๊ฐ๋น์ง ์ปฌ๋ ์
์ปจํธ๋กค๋ฌ๊ฐ ์์ฑํ ํ์ ๋ฆฌ์์ค(Deployment, Service ๋ฑ)์ OwnerReference๋ฅผ ๋ฌ๋ฉด, CR์ด ์ญ์ ๋ ๋ ํ์ ๋ฆฌ์์ค๋ ์๋์ผ๋ก ์ญ์ ๋๋ค.
ctrl.SetControllerReference(&myApp, deployment, r.Scheme)
์ด๋ ๊ฒ ํ๋ฉด ์ง์ Finalizer ๋ก์ง ์์ด๋ ์ข ์ ๋ฆฌ์์ค๊ฐ ๊น๋ํ๊ฒ ์ ๋ฆฌ๋๋ค.
Operator Maturity Model
CoreOS(ํ Red Hat)๊ฐ ์ ์ํ Operator ์ฑ์๋ ๋ชจ๋ธ:
| Level | ์ด๋ฆ | ์ง์ ๊ธฐ๋ฅ |
|---|---|---|
| Level 1 | Basic Install | ์๋ ์ค์น ๋ฐ ์ค์ |
| Level 2 | Seamless Upgrades | ํจ์น ๋ฐ ๋ง์ด๋ ๋ฒ์ ์ ๊ทธ๋ ์ด๋ |
| Level 3 | Full Lifecycle | ๋ฐฑ์ , ๋ณต๊ตฌ, ์ฅ์ ๋์ |
| Level 4 | Deep Insights | ๋ฉํธ๋ฆญ, ์๋ฆผ, ๋ก๊ทธ ๋ถ์ ํตํฉ |
| Level 5 | Auto Pilot | ์ํ ํ์ฅ, ์ด์ ๊ฐ์ง, ์๋ ์กฐ์ |
๋จ์ํ Helm Chart๋ Level 1์ ํด๋นํ๋ค. ์์ ํ Operator๋ Level 3 ์ด์์ ๋ชฉํ๋ก ํ๋ฉฐ, ๋ณต์กํ ์ํ ๊ด๋ฆฌ์ ๋ณต๊ตฌ ์ ์ฐจ๋ฅผ ์ฝ๋๋ก ๊ตฌํํ๋ค.
์ค์ ๋ก ์ฐ์ด๋ Operator ์์
| Operator | ์ญํ |
|---|---|
| Prometheus Operator | PrometheusRule, ServiceMonitor CRD๋ก ๋ชจ๋ํฐ๋ง ์ค์ ์๋ํ |
| Cert-Manager | Certificate CRD๋ก TLS ์ธ์ฆ์ ์๋ ๋ฐ๊ธ ๋ฐ ๊ฐฑ์ |
| ArgoCD | Application CRD๋ก GitOps ๊ธฐ๋ฐ ๋ฐฐํฌ ๊ด๋ฆฌ |
| Strimzi | Kafka ํด๋ฌ์คํฐ ์์ฑยท์ ๊ทธ๋ ์ด๋ยทํ ํฝ ๊ด๋ฆฌ ์๋ํ |
| CloudNativePG | PostgreSQL ํด๋ฌ์คํฐ HA ๊ตฌ์ฑ ๋ฐ ์๋ ํ์ผ์ค๋ฒ |
์ ๋ฆฌ
- Operator๋ CRD + Custom Controller ์กฐํฉ์ผ๋ก K8s๋ฅผ ๋๋ฉ์ธ๋ณ๋ก ํ์ฅํ๋ ํจํด์ด๋ค.
- ์ปจํธ๋กค๋ฌ๋ Reconciliation Loop๋ฅผ ํตํด ์ํ๋ ์ํ์ ํ์ฌ ์ํ์ ์ฐจ์ด๋ฅผ ์ง์์ ์ผ๋ก ๋ง์ถฐ๋๊ฐ๋ค.
spec์ ์ฌ์ฉ์๊ฐ ์ํ๋ ์ํ,status๋ ์ปจํธ๋กค๋ฌ๊ฐ ๊ธฐ๋กํ๋ ํ์ฌ ์ํ๋ค.- Finalizer๋ก ์ญ์ ์ ์ ๋ฆฌ ์์ ์ ์ ์ดํ๊ณ , OwnerReference๋ก ํ์ ๋ฆฌ์์ค์ ๊ฐ๋น์ง ์ปฌ๋ ์ ์ ์๋ํํ๋ค.
- ๊ฐ๋ฐ ๋๊ตฌ๋ Operator SDK๋ Kubebuilder ์ค ํ๋๋ฅผ ์ ํํ๋ฉด ๋๋ค.