Red Hat이 주도하는 오픈소스 프로젝트로, 오퍼레이터 개발을 단순화하고 자동화하기 위한 다양한 도구와 라이브러리를 제공합니다. Operator SDK는 Kubebuilder 기반으로 만들어졌지만, Helm, Ansible 등 다양한 언어와 프레임워크를 지원합니다.
또한 레드햇이 주도하여 개발 중으로 보이는 openstack operator 프로젝트에 사용되었습니다.
쿠버네티스의 오퍼레이터와 오퍼레이터 패턴은 공식 문서 https://kubernetes.io/ko/docs/concepts/extend-kubernetes/operator/에 정리되어 있습니다.
오퍼레이터(Operator)는 사용자 정의 리소스를 사용하여 애플리케이션 및 해당 컴포넌트를 관리하는 쿠버네티스의 소프트웨어 익스텐션이다. 오퍼레이터는 쿠버네티스 원칙, 특히 컨트롤 루프를 따른다.
쿠버네티스 코드 자체를 수정하지 않고 기능을 확대한다는 점에서 큰 이점을 가지고 있습니다. 이미 cncf 의 많은 프로젝트들이 해당 기법을 사용하여 기능을 제공하고 있습니다. CKA 시험에서도 24년 11월 25일 이후부터 시험항목에 관련 내용이 추가됩니다.
Operator Framework 는 크게 세 가지 특성 Operator SDK(build, test, iterate), Operator Lifecycle manager(install, manage, update), Operatorhub.io (Publish & share) 을 제공 합니다.
이번 글에서는 코드 수준의 오퍼레이터 코드 작성 및 빌드 방식에 집중합니다.
operator framework 가 내포한 kubebuilder는 아래와 같은 구조를 가지고 있습니다.
kubebuilder 는 controller-runtime 이 핵심 라이브러리 입니다. [ https://github.com/kubernetes/sample-controller/blob/master/controller.go 이런 예시 프로젝트가 code-generator 기반 인 것과 비교할 수 있습니다. ]

이미지에서 controller-runtime의 각 구성 요소(Client, Cache, Controller, Predicate, Reconciler, Webhook)에 대한 설명을 아래에 정리했습니다:
1. Client
위치: sigs.k8s.io/controller-runtime/pkg/client
역할:
특징:
2. Cache
위치: sigs.k8s.io/controller-runtime/pkg/cache
역할:
특징:
3. Controller
위치: sigs.k8s.io/controller-runtime/pkg/controller
역할:
특징:
4. Predicate
위치: sigs.k8s.io/controller-runtime/pkg/predicate
역할:
특징:
5. Reconciler
위치: sigs.k8s.io/controller-runtime/pkg/reconcile
역할:
특징:
6. Webhook
위치: sigs.k8s.io/controller-runtime/pkg/webhook
역할:
리소스의 Admission Request(생성, 업데이트 요청)를 가로채어 검증 및 수정 작업을 수행합니다.
종류:
특징:
Client는 Kubernetes API와의 통신을 담당하고, Cache는 데이터를 캐싱하여 성능을 향상시킵니다.
Controller는 리소스 변경 사항을 감지하고, 이를 Reconciler로 전달하여 상태를 조정합니다.
Predicate는 이벤트 필터링을 통해 불필요한 Reconciliation을 방지합니다.
Reconciler는 리소스의 현재 상태와 의도된 상태를 동기화하는 핵심 로직을 수행합니다.
Webhook은 리소스의 Admission Request를 처리하여 기본값 설정 및 유효성 검사를 수행합니다.
https://sdk.operatorframework.io/docs/building-operators/golang/tutorial/
git clone https://github.com/operator-framework/operator-sdk
cd operator-sdk
git checkout master
make installmkdir memcached-operator
cd memcached-operator
operator-sdk init --domain example.com --repo github.com/example/memcached-operator 환경 확인~/memcached-operator $ ls
cmd config Dockerfile go.mod go.sum hack Makefile PROJECT README.md test **** --domain 은 API group의 prefix가 됩니다. k8s에서의 대표적 그룹 예시는 apps 나 rbac.authoriztion.k8s.io 가 있습니다. 좀 더 구체적인 내용(쿠버네티스의 api를 이해하는 데 도움이 됩니다)은 https://book.kubebuilder.io/cronjob-tutorial/gvks.html 에서 확인할 수 있습니다. 또한 생성된 프로젝트의 기본 구조는 https://book.kubebuilder.io/cronjob-tutorial/basic-project.html 문서를 확인하면 좋습니다.go.mod
Makefile : 프로젝트 작업에 필요한 여러 명령어들이 정리되어 있습니다. 해당 파일을 분석하면 어떤 작업을 할 수 있는지 대략적으로 감을 잡을 수 있습니다.
PROJECT : Kubebuilder 가 사용할 메타데이터 입니다. 새로운 구성요소를 scaffolding (리소스를 빠르게 초기화하고 설정할 수 있는 템플릿이나 자동화된 생성 도구를 의미) 하는 데 필요 합니다.
config 폴더 아래에는 Kustomize YAML 정의들을 가지고 있고, 추후에 컨트롤러를 작성하기 시작하면, 커스텀 리소스 정의나 RBAC 설정 그리고 웹훅 설정 등도 생성되게 됩니다.
참고로 --repo=<path> 설정은 $GOPATH/src 밖에서 프로젝트를 생성할 때 사용 되어야 합니다.
Manager
cmd/main.go 는 오퍼레이터의 메인 프로그램이고 Manager를 초기화하고 구동 시킵니다.
매니저는 네임스페이스의 제한 둘 수 있습니다. 해당 네임스페이스에서만 모든 컨트롤러는 리소스를 watch 할 수 있습니다.
mgr, err := ctrl.NewManager(cfg, manager.Options{Namespace: namespace})
만약 모든 네임스페이스를 가능하게 하려면, 빈 값으로 두면 됩니다.
mgr, err := ctrl.NewManager(cfg, manager.Options{Namespace: ""})
Operator scope에 대한 더 심도있는 내용은 https://sdk.operatorframework.io/docs/building-operators/golang/operator-scope/ 문서를 살펴보아야 합니다.
~/memcached-operator $ operator-sdk create api --group cache --version v1alpha1 --kind Memcached --resource --controller
INFO[0000] Writing kustomize manifests for you to edit...
INFO[0000] Writing scaffold for you to edit...
INFO[0000] api/v1alpha1/memcached_types.go
INFO[0000] api/v1alpha1/groupversion_info.go
INFO[0000] internal/controller/suite_test.go
INFO[0000] internal/controller/memcached_controller.go
INFO[0000] internal/controller/memcached_controller_test.go
INFO[0000] Update dependencies:
$ go mod tidy
INFO[0000] Running make:
$ make generate
mkdir -p /home/syyang/memcached-operator/bin
Downloading sigs.k8s.io/controller-tools/cmd/controller-gen@v0.15.0
go: downloading sigs.k8s.io/controller-tools v0.15.0
go: downloading k8s.io/api v0.30.0
go: downloading k8s.io/apiextensions-apiserver v0.30.0
go: downloading k8s.io/apimachinery v0.30.0
go: downloading golang.org/x/tools v0.20.0
go: downloading golang.org/x/sys v0.19.0
go: downloading golang.org/x/net v0.24.0
/home/syyang/memcached-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
Next: implement your new API and generate the manifests (e.g. CRDs,CRs) with:
$ make manifests 환경 확인~/memcached-operator $ ls
api cmd Dockerfile go.sum internal PROJECT test
bin config go.mod hack Makefile README.md api/v1alpha1/memcached_types.go 를 통해 api 리소스가 만들어지고 controllers/memcached_controller.go 를 통해 컨트롤러가 만들어집니다. (정확히는 scaffold 합니다)참고
더 완벽한 튜토리얼을 위해서는 아래 명령을 시도해볼 수 있습니다.
$ operator-sdk create api --group cache --version v1alpha1 --kind Memcached --plugins="deploy-image/v1-alpha" --image=memcached:1.4.36-alpine --image-container-command="memcached,-m=64,modern,-v" --run-as-user="1001"
이번 예제는 single group API 경우에 대해서 다룹니다. multi-group 은 https://book.kubebuilder.io/migration/multi-group.html 문서를 확인해야 합니다.
controller-runtime에서 설정한 설계 목표를 제대로 따르기 위해, 프로젝트에서 생성된 각 API를 관리하는 전용 컨트롤러를 하나씩 두는 것이 권장됩니다.
// MemcachedSpec defines the desired state of Memcached
type MemcachedSpec struct {
// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
// Important: Run "make" to regenerate code after modifying this file
// The following markers will use OpenAPI v3 schema to validate the value
// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=5
// +kubebuilder:validation:ExclusiveMaximum=false
// Size defines the number of Memcached instances
// +operator-sdk:csv:customresourcedefinitions:type=spec
Size int32 `json:"size,omitempty"`
// Port defines the port that will be used to init the container with the image
// +operator-sdk:csv:customresourcedefinitions:type=spec
ContainerPort int32 `json:"containerPort,omitempty"`
}
// MemcachedStatus defines the observed state of Memcached
type MemcachedStatus struct {
// Represents the observations of a Memcached's current state.
// Memcached.status.conditions.type are: "Available", "Progressing", and "Degraded"
// Memcached.status.conditions.status are one of True, False, Unknown.
// Memcached.status.conditions.reason the value should be a CamelCase string and producers of specific
// condition types may define expected values and meanings for this field, and whether the values
// are considered a guaranteed API.
// Memcached.status.conditions.Message is a human readable message indicating details about the transition.
// For further information see: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties
// Conditions store the status conditions of the Memcached instances
// +operator-sdk:csv:customresourcedefinitions:type=status
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"`
} CRD 매니페스트에 status 서브리소스를 추가하려면, +kubebuilder:subresource:status 마커를 사용해야 합니다. // Memcached is the Schema for the memcacheds API
//+kubebuilder:subresource:status
type Memcached struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec MemcachedSpec `json:"spec,omitempty"`
Status MemcachedStatus `json:"status,omitempty"`
} Spec은 사용자가 원하는 상태(desired state)를 정의합니다. Status 는 현재의 실제 상태(actual state)를 나타냅니다. 추가한 마커를 통해, 서브리소스를 추가하고, 이를 통해 컨트롤러는 CR 객체의 나머지 부분을 변경하지 않고, status 필드만 업데이트할 수 있습니다. 참고로 *_types.go 파일을 수정한 후, 해당 리소스 타입의 생성된 코드를 업데이트하기 위해 항상 make generate 명령어를 수행해야 합니다. Makefile 타겟은 controller-gen 유틸리티를 호출하여 api/v1alpha1/zz_generated.deepcopy.go 파일을 업데이트합니다. 이를 통해 모든 Kind 타입이 구현해야 하는 runtime.Object 인터페이스를 우리의 API Go 타입 정의가 구현하도록 보장합니다. 위의 설명을 좀 더 풀어서 zz_generated.deepcopy.go 파일에 대해 설명하자면, Kubernetes는 컨트롤러, 캐시, 인덱서 등에서 객체를 복사하고 조작합니다. 이 과정에서 안전한 객체 복사(deepcopy)가 필수적입니다. DeepCopyObject() 함수는 runtime.Object 인터페이스를 구현합니다.type Object interface {
GetObjectKind() schema.ObjectKind
DeepCopyObject() Object
}// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Memcached) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
} 이후 make manifests 를 통해 CRD 매니페스트 파일을 생성할 수 있습니다. config/crd/bases/cache.example.com_memcacheds.yaml 경로에 생성됩니다.internal/controllers/memcached_controller.go 파일을 아래 내용으로 교체합니다.원 파일
```go
/*
Copyright 2024.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
import (
"context"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
cachev1alpha1 "github.com/example/memcached-operator/api/v1alpha1"
)
// MemcachedReconciler reconciles a Memcached object
type MemcachedReconciler struct {
client.Client
Scheme *runtime.Scheme
}
// +kubebuilder:rbac:groups=cache.example.com,resources=memcacheds,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=cache.example.com,resources=memcacheds/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=cache.example.com,resources=memcacheds/finalizers,verbs=update
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the Memcached object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.18.4/pkg/reconcile
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
}
// SetupWithManager sets up the controller with the Manager.
func (r *MemcachedReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&cachev1alpha1.Memcached{}).
Complete(r)
}
```
이를 원래 파일과 비교하면 내용이 꽤나 많이 추가된 것을 볼 수 있는데, 아래의 내용은 컨트롤러가 어떻게 리소스를 감시하고 reconcile loop 이 작동하는 지 설명합니다.
Setup a Recorder
main.go 에 Recorder**:** mgr**.**GetEventRecorderFor**(**"memcached-controller"**), 를 추가합니다.**
if err = (&controller.MemcachedReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("memcached-controller"),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Memcached")
os.Exit(1)
}
Reconciler의 개념은 다음과 같습니다. Reconciler는 Kubernetes에서 컨트롤러의 핵심 로직을 담당하는 구조체입니다. Kubernetes에서 컨트롤러는 목표 상태(desired state)와 현재 상태(actual state)를 비교하고, 이를 일치시키는 작업을 수행합니다. Reconciler는 이 과정에서 각 리소스(Custom Resource 포함)를 관찰하고, 필요한 업데이트나 변경 사항을 처리합니다. Event Recorder는 Kubernetes 리소스에서 발생하는 이벤트를 기록하는 역할을 합니다.
Resources watched by the Controller
controllers/memcached_controller.go 파일의 SetupWithManager() 함수는 컨트롤러가 어떻게 구축되어, 해당 CR 및 컨트롤러가 소유하고 관리하는 다른 리소스들을 감시할지를 정의합니다.
import (
...
appsv1 "k8s.io/api/apps/v1"
...
)
func (r *MemcachedReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&cachev1alpha1.Memcached{}).
Owns(&appsv1.Deployment{}).
Complete(r)
}
NewControllerManagedBy() 함수는 다양한 컨트롤러 구성을 가능하게 하는 컨트롤러 빌더를 제공합니다.
For(&cachev1alpha1.Memcached{})는 Memcached 타입을 주요 감시 대상 리소스로 지정합니다. 각 Memcached 객체에 대해 Add/Update/Delete 이벤트가 발생할 때마다, 해당 Memcached 객체에 대한 reconcile 요청(네임스페이스/이름 키)이 Reconcile 루프로 전송됩니다.
Owns(&appsv1.Deployment{})는 Deployment 타입을 보조 감시 대상 리소스로 지정합니다. 각 Deployment 객체에 대해 Add/Update/Delete 이벤트가 발생하면, 이벤트 핸들러는 이를 해당 Deployment의 소유자에 대한 reconcile 요청으로 매핑합니다. 이 경우, Deployment의 소유자는 해당 Memcached 객체입니다.
이 경우, 의존 객체(Deployment)는 Owner References 필드에 자신의 소유 객체를 참조해야 합니다. 이 필드는 ctrl.SetControllerReference 메서드를 사용하여 추가할 수 있습니다.
참고: k8s API는 ownerRef 필드에 따라 리소스를 관리합니다. 이 메서드를 사용하면 ownerRef가 올바르게 설정됩니다. 따라서, K8s API는 Memcached Kind의 Custom Resource에 의존하는 리소스(예: Memcached Operand 이미지를 실행하는 Deployment)를 인식하게 됩니다. 이렇게 하면 Custom Resource가 삭제될 때, 모든 의존 리소스도 함께 삭제될 수 있습니다.
Controller Configurations
이외에 컨트롤러를 시작하기 위한 여러 유용한 설정들이 존재합니다. 아래 내용 말고도 builder 와 controller godoc를 통해 다른 설정들을 더 자세하게 살펴볼 수 있습니다.
concurrent Reconciles 의 최대 수를 지정하는 옵션은 아래와 같습니다. (기본 값은 1 입니다)
func (r *MemcachedReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&cachev1alpha1.Memcached{}).
Owns(&appsv1.Deployment{}).
WithOptions(controller.Options{MaxConcurrentReconciles: 2}).
Complete(r)
}
predicates 를 통해 watch events를 필터링 할 수 있습니다.
감시 이벤트가 Reconcile 루프로의 요청으로 어떻게 변환될지를 결정하려면 적절한 EventHandler 타입을 선택해야 합니다. 기본(primary) 리소스와 보조(secondary) 리소스 관계보다 더 복잡한 Operator 관계에서는, EnqueueRequestsFromMapFunc 핸들러를 사용하여 감시 이벤트를 임의의 Reconcile 요청 세트로 변환할 수 있습니다.
Reconcile 함수는 감시 중인 CR 또는 리소스에서 이벤트가 발생할 때마다 이 함수가 실행되며, 목표 상태와 현재 상태가 일치하는지 여부에 따라 반환값이 달라집니다.
이와 같이, 모든 컨트롤러는 Reconcile() 메서드를 구현하는 Reconciler 객체를 가지고 있으며, 이 메서드는 Reconcile 루프를 구현합니다. Reconcile 루프는 Request 인자를 전달받는데, 이 인자는 네임스페이스/이름 키로, 캐시에서 주요 리소스 객체인 Memcached를 조회하는 데 사용됩니다.
import (
ctrl "sigs.k8s.io/controller-runtime"
cachev1alpha1 "github.com/example/memcached-operator/api/v1alpha1"
...
)
func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// Lookup the Memcached instance for this reconcile request
memcached := &cachev1alpha1.Memcached{}
err := r.Get(ctx, req.NamespacedName, memcached)
...
}
Reconciler, Client, 그리고 리소스 이벤트와 상호작용하는 방법에 대한 자세한 안내는 Client API 문서를 참고 하시면 됩니다.
Reconciler에서 사용할 수 있는 반환 옵션 몇 가지는 다음과 같습니다. 더 자세한 것 내용은 Reconcile godoc을 참고하셔야 합니다.
오류가 있는 경우:
return ctrl.Result{}, err
오류가 발생하면, ctrl.Result{}와 함께 오류를 반환합니다. 이 경우, Reconcile 함수는 오류를 처리하고 재시도할 수 있습니다.
오류 없이 재시도 요청:
return ctrl.Result{Requeue: true}, nil
오류는 없지만, Reconcile 루프를 다시 실행하고 싶을 때 사용합니다. Requeue: true 옵션은 즉시 재시도를 요청합니다.
Reconcile 중지:
return ctrl.Result{}, nil
리소스의 현재 상태가 목표 상태와 일치할 때 사용합니다. Reconcile 루프를 멈추고, 추가적인 재시도는 요청하지 않습니다.
X 시간 후에 다시 Reconcile:
`return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil`
일정 시간이 지난 후에 Reconcile을 다시 실행하고 싶을 때 사용합니다. 위의 예시에서는 5분 후에 재시도합니다.
Specify permissions and generate RBAC manifests
컨트롤러가 관리하는 리소스와 상호작용하기 위해서는 특정 ****RBAC 권한이 필요합니다. 이러한 권한은 다음과 같은 RBAC 마커(RBAC markers)를 통해 지정할 수 있습니다.
//+kubebuilder:rbac:groups=cache.example.com,resources=memcacheds,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=cache.example.com,resources=memcacheds/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=cache.example.com,resources=memcacheds/finalizers,verbs=update
//+kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch
func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
...
}
make manifests 명령을 통해 ClusterRole 매니페스트를 config/rbac/role.yaml 경로에 생성합니다.
이후 문서 내용은 배포 과정을 설명하고 있습니다.