5.3. Go 기반 Operator 생성
Operator 개발자는 Operator SDK의 Go 프로그래밍 언어 지원을 활용하여 분산형 키-값 저장소인 Memcached에 대한 Go 기반 Operator 예제를 빌드하고 라이프사이클을 관리할 수 있습니다.
Kubebuilder 는 Go 기반 Operator의 스캐폴딩 솔루션으로 Operator SDK에 포함되어 있습니다.
5.3.1. Operator SDK를 사용하여 Go 기반 Operator 생성
Operator SDK를 사용하면 Kubernetes 네이티브 애플리케이션을 더 쉽게 빌드할 수 있으며 애플리케이션별 운영 지식이 필요할 수 있습니다. SDK는 이러한 장벽을 낮출 뿐만 아니라 미터링 또는 모니터링과 같은 많은 공통 관리 기능에 필요한 상용구 코드의 양을 줄이는 데 도움이 됩니다.
이 절차에서는 SDK에서 제공하는 툴과 라이브러리를 사용하여 간단한 Memcached Operator를 생성하는 예를 설명합니다.
사전 요구 사항
- 개발 워크스테이션에 Operator SDK v0.19.4 CLI가 설치되어 있습니다.
-
Kubernetes 기반 클러스터에 설치된 OLM(Operator Lifecycle Manager) (애플리케이션
/v1beta2
API 그룹을 지원하기 위해 v1.8 이상) (예: OpenShift Container Platform 4.6 -
cluster
-admin 권한이 있는 계정을 사용하여 클러스터에
대한 액세스 -
OpenShift CLI (
oc
) v4.6 이상이 설치됨
절차
Operator 프로젝트를 생성합니다.
프로젝트에 사용할 디렉터리를 생성합니다.
$ mkdir -p $HOME/projects/memcached-operator
디렉터리로 변경합니다.
$ cd $HOME/projects/memcached-operator
Go 모듈에 대한 지원을 활성화합니다.
$ export GO111MODULE=on
operator-sdk init
명령을 실행하여 프로젝트를 초기화합니다.$ operator-sdk init \ --domain=example.com \ --repo=github.com/example-inc/memcached-operator
참고operator-sdk init
명령은 기본적으로go.kubebuilder.io/v2
플러그인을 사용합니다.
지원되는 이미지를 사용하도록 Operator를 업데이트합니다.
프로젝트 루트 수준 Dockerfile에서 기본 러너 이미지 참조를 에서 변경합니다.
FROM gcr.io/distroless/static:nonroot
다음으로 변경합니다.
FROM registry.access.redhat.com/ubi8/ubi-minimal:latest
-
Go 프로젝트 버전에 따라 Dockerfile에
USER 65532:65532
또는USER nonroot:nonroot
지시문이 포함될 수 있습니다. 두 경우 모두 지원되는 실행기 이미지에 행이 필요하지 않으므로 행을 제거합니다. config/default/manager_auth_proxy_patch.yaml
파일에서image
값을 변경합니다.gcr.io/kubebuilder/kube-rbac-proxy:<tag>
지원되는 이미지를 사용하려면 다음을 실행합니다.
registry.redhat.io/openshift4/ose-kube-rbac-proxy:v4.6
다음 행을 대체하여 이후 빌드 중에 필요한 종속성을 설치하도록 Makefile에서
테스트
대상을 업데이트합니다.예 5.1. 기존
테스트
대상test: generate fmt vet manifests go test ./... -coverprofile cover.out
다음 행을 사용하여 다음을 수행합니다.
예 5.2. 업데이트된
테스트
대상ENVTEST_ASSETS_DIR=$(shell pwd)/testbin test: manifests generate fmt vet ## Run tests. mkdir -p ${ENVTEST_ASSETS_DIR} test -f ${ENVTEST_ASSETS_DIR}/setup-envtest.sh || curl -sSLo ${ENVTEST_ASSETS_DIR}/setup-envtest.sh https://raw.githubusercontent.com/kubernetes-sigs/controller-runtime/v0.7.2/hack/setup-envtest.sh source ${ENVTEST_ASSETS_DIR}/setup-envtest.sh; fetch_envtest_tools $(ENVTEST_ASSETS_DIR); setup_envtest_env $(ENVTEST_ASSETS_DIR); go test ./... -coverprofile cover.out
CRD(사용자 정의 리소스 정의) API 및 컨트롤러를 생성합니다.
다음 명령을 실행하여
캐시
버전v1
및 종류의Memcached
그룹이 있는 API를 생성합니다.$ operator-sdk create api \ --group=cache \ --version=v1 \ --kind=Memcached
메시지가 표시되면 리소스 및 컨트롤러 모두 생성하도록
y
를 입력합니다.Create Resource [y/n] y Create Controller [y/n] y
출력 예
Writing scaffold for you to edit... api/v1/memcached_types.go controllers/memcached_controller.go ...
이 프로세스는
api/v1/memcached_types.go
에 Memcached 리소스 API를 생성하고controllers/memcached_controller.go
에 컨트롤러를 생성합니다.spec
및status
가 다음과 같도록api/v1/memcached_types.go
에서 Go 유형 정의를 수정합니다.// MemcachedSpec defines the desired state of Memcached type MemcachedSpec struct { // +kubebuilder:validation:Minimum=0 // Size is the size of the memcached deployment Size int32 `json:"size"` } // MemcachedStatus defines the observed state of Memcached type MemcachedStatus struct { // Nodes are the names of the memcached pods Nodes []string `json:"nodes"` }
+kubebuilder:subresource:status
마커를 추가하여 CRD 매니페스트에status
하위 리소스를 추가합니다.// Memcached is the Schema for the memcacheds API // +kubebuilder:subresource:status 1 type Memcached struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec MemcachedSpec `json:"spec,omitempty"` Status MemcachedStatus `json:"status,omitempty"` }
- 1
- 이 행을 추가합니다.
그러면 컨트롤러에서 CR 오브젝트의 나머지 부분을 변경하지 않고도 CR 상태를 업데이트할 수 있습니다.
리소스 유형에 대해 생성된 코드를 업데이트합니다.
$ make generate
작은 정보*_types.go
파일을 수정한 후에는make generate
명령을 실행하여 해당 리소스 유형에 대해 생성된 코드를 업데이트해야 합니다.위의 Makefile 대상은
controller-gen
유틸리티를 호출하여api/v1/zz_generated.deepcopy.go
파일을 업데이트합니다. 이렇게 하면 API Go 유형 정의에서 모든종류의
유형에서 구현해야 하는runtime.Object
인터페이스를 구현할 수 있습니다.
CRD 매니페스트를 생성하고 업데이트합니다.
$ make manifests
이 Makefile 대상은
controller-gen
유틸리티를 호출하여config/crd/bases/cache.example.com_memcacheds.yaml
파일에서 CRD 매니페스트를 생성합니다.선택 사항: CRD에 사용자 지정 검증을 추가합니다.
매니페스트가 생성될 때
spec.validation
블록의 CRD 매니페스트에 OpenAPI v3.0 스키마가 추가됩니다. 이 검증 블록을 사용하면 Kubernetes가 생성 또는 업데이트될 때Memcached
CR(사용자 정의 리소스)의 속성을 검증할 수 있습니다.Operator 작성자는 Kubebuilder 마커 라는 주석과 같은 단일 줄 주석을 사용하여 API에 대한 사용자 정의 검증을 구성할 수 있습니다. 이러한 마커에는 항상
+kubebuilder:validation
접두사가 있어야 합니다. 예를 들어 열거형 사양을 추가하는 작업은 다음 마커를 추가하여 수행할 수 있습니다.// +kubebuilder:validation:Enum=Lion;Wolf;Dragon type Alias string
API 코드의 마커 사용은 Kubebuilder Generating CRD 및 Markers for Config/Code Generation 설명서에서 설명합니다. 전체 OpenAPIv3 검증 마커 목록은 Kubebuilder CRD 검증 설명서에서도 사용할 수 있습니다.
사용자 정의 검증을 추가하는 경우 다음 명령을 실행하여 CRD에 대한 OpenAPI 검증 섹션을 업데이트합니다.
$ make manifests
새 API 및 컨트롤러를 생성하면 컨트롤러 논리를 구현할 수 있습니다. 이 예제에서는 생성된 컨트롤러 파일
controllers/memcached_controller.go
를 다음 예제 구현으로 교체합니다.예 5.3.
memcached_controller.go
의 예/* 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 controllers import ( "context" "reflect" "github.com/go-logr/logr" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" cachev1 "github.com/example-inc/memcached-operator/api/v1" ) // MemcachedReconciler reconciles a Memcached object type MemcachedReconciler struct { client.Client Log logr.Logger 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=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list; func (r *MemcachedReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { ctx := context.Background() log := r.Log.WithValues("memcached", req.NamespacedName) // Fetch the Memcached instance memcached := &cachev1.Memcached{} err := r.Get(ctx, req.NamespacedName, memcached) if err != nil { if errors.IsNotFound(err) { // Request object not found, could have been deleted after reconcile request. // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. // Return and don't requeue log.Info("Memcached resource not found. Ignoring since object must be deleted") return ctrl.Result{}, nil } // Error reading the object - requeue the request. log.Error(err, "Failed to get Memcached") return ctrl.Result{}, err } // Check if the deployment already exists, if not create a new one found := &appsv1.Deployment{} err = r.Get(ctx, types.NamespacedName{Name: memcached.Name, Namespace: memcached.Namespace}, found) if err != nil && errors.IsNotFound(err) { // Define a new deployment dep := r.deploymentForMemcached(memcached) log.Info("Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) err = r.Create(ctx, dep) if err != nil { log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) return ctrl.Result{}, err } // Deployment created successfully - return and requeue return ctrl.Result{Requeue: true}, nil } else if err != nil { log.Error(err, "Failed to get Deployment") return ctrl.Result{}, err } // Ensure the deployment size is the same as the spec size := memcached.Spec.Size if *found.Spec.Replicas != size { found.Spec.Replicas = &size err = r.Update(ctx, found) if err != nil { log.Error(err, "Failed to update Deployment", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name) return ctrl.Result{}, err } // Spec updated - return and requeue return ctrl.Result{Requeue: true}, nil } // Update the Memcached status with the pod names // List the pods for this memcached's deployment podList := &corev1.PodList{} listOpts := []client.ListOption{ client.InNamespace(memcached.Namespace), client.MatchingLabels(labelsForMemcached(memcached.Name)), } if err = r.List(ctx, podList, listOpts...); err != nil { log.Error(err, "Failed to list pods", "Memcached.Namespace", memcached.Namespace, "Memcached.Name", memcached.Name) return ctrl.Result{}, err } podNames := getPodNames(podList.Items) // Update status.Nodes if needed if !reflect.DeepEqual(podNames, memcached.Status.Nodes) { memcached.Status.Nodes = podNames err := r.Status().Update(ctx, memcached) if err != nil { log.Error(err, "Failed to update Memcached status") return ctrl.Result{}, err } } return ctrl.Result{}, nil } // deploymentForMemcached returns a memcached Deployment object func (r *MemcachedReconciler) deploymentForMemcached(m *cachev1.Memcached) *appsv1.Deployment { ls := labelsForMemcached(m.Name) replicas := m.Spec.Size dep := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: m.Name, Namespace: m.Namespace, }, Spec: appsv1.DeploymentSpec{ Replicas: &replicas, Selector: &metav1.LabelSelector{ MatchLabels: ls, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: ls, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{{ Image: "memcached:1.4.36-alpine", Name: "memcached", Command: []string{"memcached", "-m=64", "-o", "modern", "-v"}, Ports: []corev1.ContainerPort{{ ContainerPort: 11211, Name: "memcached", }}, }}, }, }, }, } // Set Memcached instance as the owner and controller ctrl.SetControllerReference(m, dep, r.Scheme) return dep } // labelsForMemcached returns the labels for selecting the resources // belonging to the given memcached CR name. func labelsForMemcached(name string) map[string]string { return map[string]string{"app": "memcached", "memcached_cr": name} } // getPodNames returns the pod names of the array of pods passed in func getPodNames(pods []corev1.Pod) []string { var podNames []string for _, pod := range pods { podNames = append(podNames, pod.Name) } return podNames } func (r *MemcachedReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&cachev1.Memcached{}). Owns(&appsv1.Deployment{}). Complete(r) }
예제 컨트롤러는 각
Memcached
CR에 대해 다음 조정 논리를 실행합니다.- Memcached 배포가 없는 경우 생성합니다.
-
배포 크기가
Memcached
CR 사양에 지정된 것과 같은지 확인합니다. -
Memcached
CR 상태를memcached
Pod의 이름으로 업데이트합니다.
다음 두 하위 단계에서는 컨트롤러에서 리소스를 감시하는 방법과 조정 반복문이 트리거되는 방법을 검사합니다. 이러한 단계를 건너뛰어 Operator를 직접 빌드하고 실행할 수 있습니다.
controllers/memcached_controller.go
파일에서 컨트롤러 구현을 검사하여 컨트롤러에서 리소스를 감시하는 방법을 확인합니다.SetupWithManager()
함수는 해당 컨트롤러가 소유하고 관리하는 CR 및 기타 리소스를 조사하기 위해 컨트롤러가 빌드되는 방법을 지정합니다.예 5.4.
SetupWithManager()
함수import ( ... appsv1 "k8s.io/api/apps/v1" ... ) func (r *MemcachedReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&cachev1.Memcached{}). Owns(&appsv1.Deployment{}). Complete(r) }
NewControllerManagedBy()
에서는 다양한 컨트롤러 구성을 허용하는 컨트롤러 빌더를 제공합니다.For(&cachev1.Memcached{})
는 조사할 기본 리소스로Memcached
유형을 지정합니다.Memcached
유형에 대한 각 추가, 업데이트 또는 삭제 이벤트의 경우 조정 반복문은 해당Memcached
오브젝트의 조정Request
인수(네임스페이스 및 이름 키로 구성됨)로 전송됩니다.Owns(&appsv1.Deployment{})
는 조사할 보조 리소스로Deployment
유형을 지정합니다. 이벤트 핸들러는Deployment
유형, 즉 추가, 업데이트 또는 삭제 이벤트가 발생할 때마다 각 이벤트를 배포 소유자의 조정 요청에 매핑합니다. 이 경우 소유자는 배포가 생성된Memcached
오브젝트입니다.모든 컨트롤러에는 조정 반복문을 구현하는
Reconcile()
메서드가 포함된 조정기 오브젝트가 있습니다. 조정 반복문에는 캐시에서 기본 리소스 오브젝트인Memcached
를 찾는 데 사용되는 네임스페이스 및 이름 키인Request
인수가 전달됩니다.예 5.5. 조정 반복문
import ( ctrl "sigs.k8s.io/controller-runtime" cachev1 "github.com/example-inc/memcached-operator/api/v1" ... ) func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { // Lookup the Memcached instance for this reconcile request memcached := &cachev1.Memcached{} err := r.Get(ctx, req.NamespacedName, memcached) ... }
Reconcile()
함수의 반환 값에 따라 조정요청이
다시 큐에 추가될 수 있으며 루프가 다시 트리거될 수 있습니다.예 5.6. 대기열 논리 재지정
// Reconcile successful - don't requeue return reconcile.Result{}, nil // Reconcile failed due to error - requeue return reconcile.Result{}, err // Requeue for any reason other than error return reconcile.Result{Requeue: true}, nil
유예 기간 후 요청을 다시 큐에 추가하도록
Result.RequeueAfter
를 설정할 수 있습니다.예 5.7. 유예 기간 후 다시 큐에 추가
import "time" // Reconcile for any reason other than an error after 5 seconds return ctrl.Result{RequeueAfter: time.Second*5}, nil
참고주기적으로 CR을 조정하도록
RequeueAfter
를 설정하여Result
를 반환할 수 있습니다.조정기, 클라이언트, 리소스 이벤트와의 상호 작용에 대한 자세한 내용은 Controller Runtime Client API 설명서를 참조하십시오.
추가 리소스
- CRD의 OpenAPI v3.0 검증 스키마에 대한 자세한 내용은 Kubernetes 설명서를 참조하십시오.