[Daily morning study] Kubernetes Operator ํŒจํ„ด โ€” Custom Resource์™€ Controller

#daily morning study

Image


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 1Basic Install์ž๋™ ์„ค์น˜ ๋ฐ ์„ค์ •
Level 2Seamless UpgradesํŒจ์น˜ ๋ฐ ๋งˆ์ด๋„ˆ ๋ฒ„์ „ ์—…๊ทธ๋ ˆ์ด๋“œ
Level 3Full Lifecycle๋ฐฑ์—…, ๋ณต๊ตฌ, ์žฅ์•  ๋Œ€์‘
Level 4Deep Insights๋ฉ”ํŠธ๋ฆญ, ์•Œ๋ฆผ, ๋กœ๊ทธ ๋ถ„์„ ํ†ตํ•ฉ
Level 5Auto Pilot์ˆ˜ํ‰ ํ™•์žฅ, ์ด์ƒ ๊ฐ์ง€, ์ž๋™ ์กฐ์ •

๋‹จ์ˆœํ•œ Helm Chart๋Š” Level 1์— ํ•ด๋‹นํ•œ๋‹ค. ์™„์ „ํ•œ Operator๋Š” Level 3 ์ด์ƒ์„ ๋ชฉํ‘œ๋กœ ํ•˜๋ฉฐ, ๋ณต์žกํ•œ ์ƒํƒœ ๊ด€๋ฆฌ์™€ ๋ณต๊ตฌ ์ ˆ์ฐจ๋ฅผ ์ฝ”๋“œ๋กœ ๊ตฌํ˜„ํ•œ๋‹ค.


์‹ค์ œ๋กœ ์“ฐ์ด๋Š” Operator ์˜ˆ์‹œ

Operator์—ญํ• 
Prometheus OperatorPrometheusRule, ServiceMonitor CRD๋กœ ๋ชจ๋‹ˆํ„ฐ๋ง ์„ค์ • ์ž๋™ํ™”
Cert-ManagerCertificate CRD๋กœ TLS ์ธ์ฆ์„œ ์ž๋™ ๋ฐœ๊ธ‰ ๋ฐ ๊ฐฑ์‹ 
ArgoCDApplication CRD๋กœ GitOps ๊ธฐ๋ฐ˜ ๋ฐฐํฌ ๊ด€๋ฆฌ
StrimziKafka ํด๋Ÿฌ์Šคํ„ฐ ์ƒ์„ฑยท์—…๊ทธ๋ ˆ์ด๋“œยทํ† ํ”ฝ ๊ด€๋ฆฌ ์ž๋™ํ™”
CloudNativePGPostgreSQL ํด๋Ÿฌ์Šคํ„ฐ HA ๊ตฌ์„ฑ ๋ฐ ์ž๋™ ํŽ˜์ผ์˜ค๋ฒ„

์ •๋ฆฌ

  • Operator๋Š” CRD + Custom Controller ์กฐํ•ฉ์œผ๋กœ K8s๋ฅผ ๋„๋ฉ”์ธ๋ณ„๋กœ ํ™•์žฅํ•˜๋Š” ํŒจํ„ด์ด๋‹ค.
  • ์ปจํŠธ๋กค๋Ÿฌ๋Š” Reconciliation Loop๋ฅผ ํ†ตํ•ด ์›ํ•˜๋Š” ์ƒํƒœ์™€ ํ˜„์žฌ ์ƒํƒœ์˜ ์ฐจ์ด๋ฅผ ์ง€์†์ ์œผ๋กœ ๋งž์ถฐ๋‚˜๊ฐ„๋‹ค.
  • spec์€ ์‚ฌ์šฉ์ž๊ฐ€ ์›ํ•˜๋Š” ์ƒํƒœ, status๋Š” ์ปจํŠธ๋กค๋Ÿฌ๊ฐ€ ๊ธฐ๋กํ•˜๋Š” ํ˜„์žฌ ์ƒํƒœ๋‹ค.
  • Finalizer๋กœ ์‚ญ์ œ ์ „ ์ •๋ฆฌ ์ž‘์—…์„ ์ œ์–ดํ•˜๊ณ , OwnerReference๋กœ ํ•˜์œ„ ๋ฆฌ์†Œ์Šค์˜ ๊ฐ€๋น„์ง€ ์ปฌ๋ ‰์…˜์„ ์ž๋™ํ™”ํ•œ๋‹ค.
  • ๊ฐœ๋ฐœ ๋„๊ตฌ๋Š” Operator SDK๋‚˜ Kubebuilder ์ค‘ ํ•˜๋‚˜๋ฅผ ์„ ํƒํ•˜๋ฉด ๋œ๋‹ค.