쿠버네티스에 배포된 어플리케이션의 선언적 상태 와 현재 상태를 정기적으로 주시하고 재조정하는 것
선언적 상태란?
쿠버네티스는 선언적인 자원 중심 API를 기반으로 한다고 한다. 여기서 선언적이란 대상이 어떻게 동작하는가가 아닌, 어떻게 보여야 하는지 기술하는 것이다. 예를 들어 deployment의 replicas수가 변경되었을 때가 있다.
쿠버네티스는 deployment, daemonset, statefulset 등 다양한 내장 컨트롤러를 제공하고 있다.
하지만 여기서 문제는 현재 내장 컨트롤러들은 주로 stateless한 어플리케이션들을 위한 것이다. statefulset은 말 그대로 stateful한 어플리케이션들을 위한 것이긴 하지만 data sync 등 여전히 관리자가 직접 설정해야되는 부분들이 많다.
대부분의 어플리케이션들은 stateful 성격을 많이 띄고 있으므로 내장 컨트롤러만으로는 관리자가 직접 건드려야 하는 부분들이 많고, 배포할 때마다 매번 이런 귀찮은 과정들이 들어가게 되었고 이를 해결하기 위해 탄생한 것이 Operator
이다
Custom Resource Definition의 약자로, Deployment나 Pod와 같은 내장 리소스가 아닌 사용자가 정의한 리소스를 일컫는다. 완전히 새로운 리소스 라기보단 기존 리소스를 조합해 새로운 이름을 붙이는 것이다.
CR (Custom Resource)
는 사용자가 정의한 리소스를 일컬으며, CRD
는 사용자가 정의한 리소스의 스펙이다. 즉 CR
을 실제 쿠버네티스에 사용하려면 CR
의 스펙이 etcd에 등록되어 있어야 하므로 CRD
를 만들어서 API 서버에 등록하면 비로소 CR
을 사용할 수 있게 된다.
앞서 정의한 CRD
의 상태를 주기적으로 관찰하고, 업데이트하는 등, 관리하는 것이 바로 Operator
이다. 즉, CRD
를 관리하는 컨트롤러이다.
CR
은 단순히 사용자가 정의한 리소스라는 etcd에 저장되는 데이터 일뿐 실제로 어떤 Pod나 Service가 생성되서 서비스를 제공하지 않는다. 따라서 이 CR
이 실제로 어떻게 동작할 지 정의하려면 선언적 컨트롤러인 Operator를 구현해야 한다.
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)한다.
K8s Client
k8s api server와 통신하는 역할을 하며 Object에 Write 액션은 바로 api server로 요청을 보내며, Read 액션은 cache로 요청을 보낸다.
Work Queue
Informer가 넣어준 Object NS, Name 정보가 저장된다. controller가 여러 개 있을 시 controller마다 work queue가 존재한다.
Reconciler
Work Queue에 정보가 있으면 순서대로 정보를 빼내서(Dequeue) Object spec가 현재 상태를 비교하여 현재 상태가 spec과 같아지도록 재조정(reconcile) 과정이 진행된다. 만약 어떠한 이유로 재조정이 실패하면 해당 Object를 다시 Work Queue에 삽입(Requeue)하여 나중에 다시 처리하도록 한다.
Prerequisite
- operator-sdk tutorial
- install operator-sdk
전체 코드
- rjwharry2003/k8s-operator
개요
이번에 예제로 구현한 Mysql CRD & Controller
는 operator-sdk의 기본적인 것만 다루었으며, 튜토리얼 목적이기 때문에 실제로 mysql 데이터베이스를 위한 실질적인 controller를 구현한 것은 아니다. 단순히 operator-sdk로 어떤 것들을 할 수 있는지 간략하게 파악하기 위한 예제일 뿐이다.
설명
구현하기 전에 mysql을 쿠버네티스에 띄울 때 기본적으로 필요한 리소스가 어떤 것들이 있는지부터 파악해보자. 우선 쿠버네티스에서 데이터베이스와 같은 Stateful한 workload를 띄울 땐 Deployment
대신 Statefulset
을 사용한다. 그리고 다른 어플리케이션에서 mysql에 접근하기 때문에 Service
가 필요할 것이며, mysql의 비밀번호와 같은 민감한 정보를 담기위한 Secret
도 필요하다. 즉, mysql를 하나 띄우기 위해 3개의 리소스가 필요하므로, Mysql
이라는 CRD를 정의하고 클러스터에 생성하면 위의 3개 리소스가 자동으로 생성되도록 구현해야 한다.
프로젝트 시작
operator-sdk init --domain example.com --repo github.com/example/mysql-operator
API와 Controller 생성
operator-sdk create api --group operator --version v1alpha1 --kind Mysql --resource --controller
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"`
}
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
}
생성 로직
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
}
업데이트 로직
Mysql
스펙 중, replicas에 대해서만 구현했다. 즉 Mysql
리소스에서 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
}
...
}
한계
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
Develop and deploy a basic Kubernetes operator