Operator

노재원·2022년 4월 14일
0

Kubernetes

목록 보기
4/8

Kubernetes Controller


쿠버네티스에 배포된 어플리케이션의 선언적 상태 와 현재 상태를 정기적으로 주시하고 재조정하는 것

선언적 상태란?
쿠버네티스는 선언적인 자원 중심 API를 기반으로 한다고 한다. 여기서 선언적이란 대상이 어떻게 동작하는가가 아닌, 어떻게 보여야 하는지 기술하는 것이다. 예를 들어 deployment의 replicas수가 변경되었을 때가 있다.

쿠버네티스는 deployment, daemonset, statefulset 등 다양한 내장 컨트롤러를 제공하고 있다.

하지만 여기서 문제는 현재 내장 컨트롤러들은 주로 stateless한 어플리케이션들을 위한 것이다. statefulset은 말 그대로 stateful한 어플리케이션들을 위한 것이긴 하지만 data sync 등 여전히 관리자가 직접 설정해야되는 부분들이 많다.

대부분의 어플리케이션들은 stateful 성격을 많이 띄고 있으므로 내장 컨트롤러만으로는 관리자가 직접 건드려야 하는 부분들이 많고, 배포할 때마다 매번 이런 귀찮은 과정들이 들어가게 되었고 이를 해결하기 위해 탄생한 것이 Operator 이다

CRD


Custom Resource Definition의 약자로, Deployment나 Pod와 같은 내장 리소스가 아닌 사용자가 정의한 리소스를 일컫는다. 완전히 새로운 리소스 라기보단 기존 리소스를 조합해 새로운 이름을 붙이는 것이다.

CR (Custom Resource) 는 사용자가 정의한 리소스를 일컬으며, CRD 는 사용자가 정의한 리소스의 스펙이다. 즉 CR 을 실제 쿠버네티스에 사용하려면 CR 의 스펙이 etcd에 등록되어 있어야 하므로 CRD 를 만들어서 API 서버에 등록하면 비로소 CR 을 사용할 수 있게 된다.

Operator (Controller)


앞서 정의한 CRD 의 상태를 주기적으로 관찰하고, 업데이트하는 등, 관리하는 것이 바로 Operator 이다. 즉, CRD 를 관리하는 컨트롤러이다.

CR 은 단순히 사용자가 정의한 리소스라는 etcd에 저장되는 데이터 일뿐 실제로 어떤 Pod나 Service가 생성되서 서비스를 제공하지 않는다. 따라서 이 CR이 실제로 어떻게 동작할 지 정의하려면 선언적 컨트롤러인 Operator를 구현해야 한다.

Controller Architecture & Component


  1. K8s Cache

    k8s api server의 부하를 줄이기 위해서 api server로부터 가져온 정보를 저장한다. 이 때 api server의 정보와 cache의 정보가 맞아야 하므로 sync 작업이 별도로 들어간다. informer는 api server를 통해서 controller가 관리해야 하는 object를 계속 주시(watch) 한다. 주시하고 있는 Object에 상태 변화에 대한 event가 발생하여 informer가 수신하게 되면 해당 Object의 이름과 네임스페이스 정보를 controller의 work queue에 저장(enqueue)한다.

  2. K8s Client

    k8s api server와 통신하는 역할을 하며 Object에 Write 액션은 바로 api server로 요청을 보내며, Read 액션은 cache로 요청을 보낸다.

  3. Work Queue

    Informer가 넣어준 Object NS, Name 정보가 저장된다. controller가 여러 개 있을 시 controller마다 work queue가 존재한다.

  4. Reconciler

    Work Queue에 정보가 있으면 순서대로 정보를 빼내서(Dequeue) Object spec가 현재 상태를 비교하여 현재 상태가 spec과 같아지도록 재조정(reconcile) 과정이 진행된다. 만약 어떠한 이유로 재조정이 실패하면 해당 Object를 다시 Work Queue에 삽입(Requeue)하여 나중에 다시 처리하도록 한다.

Mysql CRD & Controller Example


Prerequisite
- operator-sdk tutorial
- install operator-sdk
전체 코드
- rjwharry2003/k8s-operator

  1. 개요
    이번에 예제로 구현한 Mysql CRD & Controller는 operator-sdk의 기본적인 것만 다루었으며, 튜토리얼 목적이기 때문에 실제로 mysql 데이터베이스를 위한 실질적인 controller를 구현한 것은 아니다. 단순히 operator-sdk로 어떤 것들을 할 수 있는지 간략하게 파악하기 위한 예제일 뿐이다.

  2. 설명
    구현하기 전에 mysql을 쿠버네티스에 띄울 때 기본적으로 필요한 리소스가 어떤 것들이 있는지부터 파악해보자. 우선 쿠버네티스에서 데이터베이스와 같은 Stateful한 workload를 띄울 땐 Deployment 대신 Statefulset을 사용한다. 그리고 다른 어플리케이션에서 mysql에 접근하기 때문에 Service가 필요할 것이며, mysql의 비밀번호와 같은 민감한 정보를 담기위한 Secret도 필요하다. 즉, mysql를 하나 띄우기 위해 3개의 리소스가 필요하므로, Mysql이라는 CRD를 정의하고 클러스터에 생성하면 위의 3개 리소스가 자동으로 생성되도록 구현해야 한다.

  3. 프로젝트 시작

    operator-sdk init --domain example.com --repo github.com/example/mysql-operator
    • init 명령어를 실행하면 operator를 개발할 수 있는 프로젝트 템플릿이 생성된다.
  4. API와 Controller 생성

    operator-sdk create api --group operator --version v1alpha1 --kind Mysql --resource --controller
    
    • Custom Resource API는 api/v1alpha1/mysql_types.go에서 정의
    • Controller는 controllers/mysql_controller.go에서 정의
  5. API 정의

    • Mysql라는 커스텀 리소스의 스펙을 정의하는 것
    apiVersion: operator.example.com/v1alpha1
    kind: Mysql
    metadata:
      name: mysql-sample
    spec:
      rootPassword: root # mysql root password
      image: mysql:5.6 # mysql image
      replicas: 1 # statefulset replicas
      dataPvcName: data # mysql persistentvolumeclaim name
    // MysqlSpec defines the desired state of Mysql
    type MysqlSpec struct {
        RootPassword string `json:"rootPassword"`
        Image        string `json:"image"`
        Replicas     int32  `json:"replicas"`
        DataPvcName  string `json:"dataPvcName"`
    }
    
    // MysqlStatus defines the observed state of Mysql
    type MysqlStatus struct {
        // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
        // Important: Run "make" to regenerate code after modifying this file
    }
    
    //+kubebuilder:object:root=true
    //+kubebuilder:subresource:status
    
    // Mysql is the Schema for the mysqls API
    type Mysql struct {
        metav1.TypeMeta   `json:",inline"`
        metav1.ObjectMeta `json:"metadata,omitempty"`
    
        Spec   MysqlSpec   `json:"spec,omitempty"`
        Status MysqlStatus `json:"status,omitempty"`
    }
    
    //+kubebuilder:object:root=true
    
    // MysqlList contains a list of Mysql
    type MysqlList struct {
        metav1.TypeMeta `json:",inline"`
        metav1.ListMeta `json:"metadata,omitempty"`
        Items           []Mysql `json:"items"`
    }
  6. Controller 정의

    • Reconcile 함수만 구현하면, controller가 Mysql 리소스를 Reconcile의 로직대로 다루게 된다

      // controllers/mysql_controller.go
      func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
          _ = log.FromContext(ctx)
      
          // TODO(user): your logic here
      
          return ctrl.Result{}, nil
      }
    1. 생성 로직

      • reqMysql은 관리자가 새로 생성한 Mysql Custom Resource이며, 생성요청이 들어왔을 때 Reconcile함수에서 아까 언급했던 Secret, Service, Statefulset 리소스를 생성해준다. getSecretObject, getStsObject, getServiceObject 함수에서 reqMysql에 입력된 정보를 기반으로 리소스를 정의하고 반환하며, 반환된 object를 클러스터에 생성한다.
      • Mysql리소스를 삭제할 때 자동으로 3개 리소스가 다같이 삭제되기 위해선 리소스 생성하기 전에 controllerutil.SetControllerReference(reqMysql, &secret, r.Scheme) 코드를 추가해준다. 해당 리소스가 어떤 리소스로부터 생성된 것인지 알려준다. 여기선 Secret, Service, Statefulset 리소스들이 Mysql 리소스로부터 생성됐다는 것을 알려주는 것이다.
       func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
          _ = log.FromContext(ctx)
      	...
          err := r.createSecret(ctx, reqMysql)
      	if err != nil {
      		return ctrl.Result{}, err
      	}
      	err = r.createSts(ctx, reqMysql)
      	if err != nil {
      		return ctrl.Result{}, err
      	}
      	err = r.createService(ctx, reqMysql)
      	if err != nil {
      		return ctrl.Result{}, err
      	}
      	...
          return ctrl.Result{}, nil
      }
      
      func (r *MysqlReconciler) createSecret(ctx context.Context, reqMysql *operatorv1alpha1.Mysql) error {
          secret := getSecretObject(*reqMysql)
          controllerutil.SetControllerReference(reqMysql, &secret, r.Scheme)
          err := r.Create(ctx, &secret)
          return err
      }
      
      func (r *MysqlReconciler) createSts(ctx context.Context, reqMysql *operatorv1alpha1.Mysql) error {
          sts := getStsObject(*reqMysql)
          controllerutil.SetControllerReference(reqMysql, &sts, r.Scheme)
          err := r.Create(ctx, &sts)
          return err
      }
      
      func (r *MysqlReconciler) createService(ctx context.Context, reqMysql *operatorv1alpha1.Mysql) error {
          svc := getServiceObject(*reqMysql)
          controllerutil.SetControllerReference(reqMysql, &svc, r.Scheme)
          err := r.Create(ctx, &svc)
          return err
      }
      
    2. 업데이트 로직

      • 업데이트는 Mysql 스펙 중, replicas에 대해서만 구현했다. 즉 Mysql 리소스에서 replicas 수를 변경하면 자동으로 관련된 statefulset의 replicas를 변경하여 스케일링을 한다.
      • 현재 배포되어 있는 statefulset의 replicas 수와 변경된 Mysql의 replicas 수를 비교하고 적용한 후, 클러스터에 있는 statefulset을 업데이트 시켜준다.
      func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
         _ = log.FromContext(ctx)
         ...
         originalSts := &appsv1.StatefulSet{}
              err := r.Get(ctx, req.NamespacedName, originalSts)
              if err != nil {
                  log.Log.Error(err, fmt.Sprintf("Failed to find %s statefulets", reqMysql.Name))
                  return ctrl.Result{}, err
              }
              replicas := originalSts.Spec.Replicas
              if *replicas != reqMysql.Spec.Replicas {
                  *replicas = reqMysql.Spec.Replicas
              }
              err = r.Update(ctx, originalSts)
              if err != nil {
                  log.Log.Error(err, "Something wrong when updating statefulsets")
                  return ctrl.Result{}, err
              }
          ...
       }
  7. 한계

    • Custom Resource의 Status에 대해 정의하지 않았다. 아직 정확히 어떻게 하는지 몰라서 더 공부하고 반영할 예정이다.
    • 업데이트할 때 replicas만 고려했기 때문에, 다른 field를 변경하지 못하게 막아야 한다. 그럴려면 webhook을 사용해야 하는데, 이 부분도 아직 미숙하여 따로 하나의 주제로 다루면서 이 예제에 추가할 예정이다.

배포


operator-sdk에서 Makefile 을 제공하고 있어 make 명령어로 빌드, 이미지 생성, 배포까지 할 수 있다.

# *_types.go 파일 수정 시 실행
# api/v1alpha1/zz_generated.deepcopy.go 코드와 CRD를 업데이트 해준다.
make generate

# rbac yaml 파일을 생성해준다.
make manifests

# controller 이미지 빌드 및 푸시
export IMG=<DOCKER-ID>/<IMAGE-NAME>:<IMAGE_TAG>
make docker-build IMG=$IMG
make docker-push IMG=$IMG

# CRD 설치
make install

# Controller 배포
make deploy

# 정리
make uninstall
make undeploy

API Aggregation


  • kube api server가 기본으로 제공하는 것 이외에도 더 많은 기능을 추가할 수 있게 해주며, Custom Resource를 생성하는 또 다른 방법이다.
  • 사용자가 원하는 기능을 수행하는 API를 추가하고, 그 API를 구현하는 Custom API server를 만들어 kube API server에 등록한다.
  • API Registration 기능이 있어 하나의 엔드포인트를 유지한 채 계속해서 API를 확장할 수 있다.
  • CRD보다 어려우나 더 정교하게 API를 다룰 수 있음

CRD vs API Aggregation(AA)


  1. CRD는 custom apiserver를 구현할 필요가 없다.
  2. AA보다 훨씬 간편하며, api aggregation에 대한 이해없이 CR을 생성할 수 있다
  3. AA는 custom api server를 구현해야 한다.
  4. 하지만 CRD에 비해 데이터 저장, API 버전 간 변환과 같은 API 동작을 보다 더 강력하게 제어할 수 있다.
  5. 어떤 방법이든 생성된 리소스는 Custom Resource라고 한다.

Refernce


Go Operator Tutorial

alice_k106님의 블로그 : 네이버 블로그

Develop and deploy a basic Kubernetes operator

Kubernetes Kubebuilder를 이용한 Operator 개발

커스텀 리소스

0개의 댓글