쿠버네티스 오퍼레이터 구축

전제 조건:


  • Kubernetes 클러스터(일부 Openshift 기본 리소스를 추가할 예정이므로 PC에 설치할 수 있는 로컬 개발 kubernetes 클러스터인 CRC를 사용합니다)
  • 해당 클러스터에 대한 클러스터 관리자 액세스 권한

  • Operator-sdk를 사용하여 프로젝트 부트스트랩



    먼저 쿠버네티스 오퍼레이터란? 오퍼레이터를 사용하면 사용자 정의 리소스를 클러스터에 추가하여 Kubernetes API를 확장할 수 있습니다. 이것은 제가 만들 수 있는 가장 기본적인 연산자입니다. 마이크로 서비스용 포드를 생성하고 마이크로 서비스용 경로를 생성하며 복제본의 양을 지정할 수 있습니다. 내가 취한 모든 단계를 설명하겠습니다. 먼저 생성 명령을 사용하여 연산자 프로젝트를 스캐폴드합니다.

    mkdir pod-route
    cd pod-route
    # --domain example.com is used in the operator-sdk quickstart guide this is used to create the api group, think of package in java. 
    # This was my first gotcha following my misreading of the docs. You need to be careful when choosing domain name as is difficult to revert after its generated.I will continue with quay.io for now.  
    # --repo is your git repo where you operator code will live 
    operator-sdk init --domain quay.io --repo github.com/austincunningham/pod-route
    # Add a controller
    # --version I use v1alpha1 (this is a Kubernetes API version for early candidates)
    # --kind name of Custom Resource
    operator-sdk create api --version v1alpha1 --kind Podroute --resource --controller
    # build and push the operator image make docker-build docker-push IMG="quay.io/austincunningham/pod-route:v0.0.1"
    


    파일은 this과 같아야 하고 컨테이너repo가 푸시됩니다.
    다음으로 내 api/v1alpha1/podroute_types.go 파일 사양PodrouteSpec을 편집합니다. 스펙은 기본적으로 운영자가 관리하고 싶은 것입니다.

    // PodrouteSpec defines the desired state of Podroute
    type PodrouteSpec struct {
        // Image container image string e.g. "quay.io/austincunningham/always200:latest"
        // Replicas number of containers to spin up
        Image string `json:"image,omitempty"`
        Replicas int32 `json:"replicas,omitempty"`
    } 
    


    유형 파일을 변경한 후 다음 명령을 실행하여 연산자에서 파일을 업데이트해야 합니다.

    make generate
    make manifests
    


    컨트롤러 로직 추가



    이제 controllers/podroute_controller.go에서 내 조정 논리를 살펴볼 수 있습니다. 포드 및 배포에 대한 일부 RBAC 규칙을 추가하고 클라이언트가 CR(Custom Resource)을 찾기 위해 클러스터에 도착하도록 합니다. 나머지는 생성된 코드입니다.

    package controllers
    
    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"
    
        quayiov1alpha1 "github.com/austincunningham/pod-route/api/v1alpha1"
    )
    
    // PodrouteReconciler reconciles a Podroute object
    type PodrouteReconciler struct {
        client.Client
        Scheme *runtime.Scheme
    }
    
    //+kubebuilder:rbac:groups=quay.io,resources=podroutes,verbs=get;list;watch;create;update;patch;delete
    //+kubebuilder:rbac:groups=quay.io,resources=podroutes/status,verbs=get;update;patch
    //+kubebuilder:rbac:groups=quay.io,resources=podroutes/finalizers,verbs=update
    //+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 *PodrouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
        _ = log.FromContext(ctx)
    
        // your logic here
        // Create a Custom Resource object for Podroute, quayio part of the name is due to my earlier mistake
        cr := &quayiov1alpha1.Podroute{}
        // do a kubernetes client get to check if the CR is on the Cluster
        err := r.Client.Get(ctx, req.NamespacedName, cr)
        if err != nil {
            return ctrl.Result{}, err
        }
    
        return ctrl.Result{}, nil
    }
    
    // SetupWithManager sets up the controller with the Manager.
    func (r *PodrouteReconciler) SetupWithManager(mgr ctrl.Manager) error {
        return ctrl.NewControllerManagedBy(mgr).
            For(&quayiov1alpha1.Podroute{}).
            Complete(r)
    }
    


    따라서 가장 먼저 해야 할 일은 기존 배포를 확인하고 존재하지 않는 경우 생성하는 것입니다.
    마지막Reconcile 앞의 return ctrl.Result{}, nil 함수에서 다음과 같이 createDeployment 함수에 대한 호출을 추가합니다.

        deployment, err := r.createDeployment(cr, r.podRouteDeployment(cr))
        if err != nil {
            return reconcile.Result{}, err
        }
        // just logging here to keep Go happy will use later
        log.Log.Info("deployment", deployment)
    


    모든 리소스에 대해 이것을 사용할 레이블 기능을 만듭니다.

    func labels(cr *quayiov1alpha1.Podroute, tier string) map[string]string {
        // Fetches and sets labels
    
        return map[string]string{
            "app":         "PodRoute",
            "podroute_cr": cr.Name,
            "tier":        tier,
        }
    }
    


    배포 개체를 만듭니다.

    // This is the equivalent of creating a deployment yaml and returning it
    // It doesn't create anything on cluster
    func (r *PodrouteReconciler) podRouteDeployment(cr *quayiov1alpha1.Podroute) *appsv1.Deployment {
        // Build a Deployment
        labels := labels(cr, "backend-podroute")
        size := cr.Spec.Replicas
        podRouteDeployment := &appsv1.Deployment{
            ObjectMeta: metav1.ObjectMeta{
                Name:      "pod-route",
                Namespace: cr.Namespace,
            },
            Spec: appsv1.DeploymentSpec{
                Replicas: &size,
                Selector: &metav1.LabelSelector{
                    MatchLabels: labels,
                },
                Template: corev1.PodTemplateSpec{
                    ObjectMeta: metav1.ObjectMeta{
                        Labels: labels,
                    },
                    Spec: corev1.PodSpec{
                        Containers: []corev1.Container{{
                            Image:           cr.Spec.Image,
                            ImagePullPolicy: corev1.PullAlways,
                            Name:            "podroute-pod",
                            Ports: []corev1.ContainerPort{{
                                ContainerPort: 8080,
                                Name:          "podroute",
                            }},
                        }},
                    },
                },
            },
        }
    
        // sets the this controller as owner
        controllerutil.SetControllerReference(cr, podRouteDeployment, r.Scheme)
        return podRouteDeployment
    }
    


    기존 배포에 대해 Client.Get을 사용하여 클러스터를 확인한 다음 위에서 만든 배포 개체를 사용하여 클러스터를 만듭니다.

    // check for a deployment if it doesn't exist it creates one on cluster using the deployment created in deployment
    func (r PodrouteReconciler) createDeployment(cr *quayiov1alpha1.Podroute, deployment *appsv1.Deployment) (*appsv1.Deployment, error) {
        // check for a deployment in the namespace
        found := &appsv1.Deployment{}
        err := r.Client.Get(context.TODO(), types.NamespacedName{Name: deployment.Name, Namespace: cr.Namespace}, found)
        if err != nil {
            log.Log.Info("Creating Deployment")
            err = r.Client.Create(context.TODO(), deployment)
            if err != nil {
                log.Log.Error(err, "Failed to create deployment")
                return found, err
            }
        }
        return found, nil
    }
    


    다음으로 조정 기능에서 배포 복제본이 CR(사용자 지정 리소스)의 번호와 일치하는지 확인하여 주석log.Log.Info("deployment", deployment)을 제거하고 다음으로 바꿉니다.

        // If the spec.Replicas in the CR changes, update the deployment number of replicas
        if deployment.Spec.Replicas != &cr.Spec.Replicas {
            controllerutil.CreateOrUpdate(context.TODO(), r.Client, deployment, func() error {
                deployment.Spec.Replicas = &cr.Spec.Replicas
                return nil
            })
        }
    


    그래서 지금까지 우리는 이미지(컨테이너)와 복제본 수를 가져오고 이에 대한 배포를 생성하는 CR을 가지고 있습니다. 다음으로 서비스 및 경로를 생성합니다. 이들은 배포와 유사한 패턴을 갖습니다. 즉, 경로/서비스 객체를 생성하고 생성되지 않은 경우 존재하는지 확인합니다. 서비스부터 시작하겠습니다. 마지막 반환 전에 reconcile 함수에서 createService 함수 호출을 추가합니다return ctrl.Result{}, nil.

        err = r.createService(cr, r.podRouteService(cr))
        if err != nil {
            return reconcile.Result{}, err
        }
    


    이 함수를 사용하여 서비스 개체를 생성합니다.

    // This is the equivalent of creating a service yaml and returning it
    // It doesnt create anything on cluster
    func (r PodrouteReconciler) podRouteService(cr *quayiov1alpha1.Podroute) *corev1.Service {
        labels := labels(cr, "backend-podroute")
    
        podRouteService := &corev1.Service{
            ObjectMeta: metav1.ObjectMeta{
                Name:      "podroute-service",
                Namespace: cr.Namespace,
            },
            Spec: corev1.ServiceSpec{
                Selector: labels,
                Ports: []corev1.ServicePort{{
                    Protocol:   corev1.ProtocolTCP,
                    Port:       8080,
                    TargetPort: intstr.FromInt(8080),
                }},
            },
        }
    
        controllerutil.SetControllerReference(cr, podRouteService, r.Scheme)
        return podRouteService
    }
    


    위 서비스 개체에서 서비스를 생성하는 기능 추가

    // check for a service if it doesn't exist it creates one on cluster using the service created in podRouteService
    func (r PodrouteReconciler) createService(cr *quayiov1alpha1.Podroute, podRouteServcie *corev1.Service) error {
        // check for a service in the namespace
        found := &corev1.Service{}
        err := r.Client.Get(context.TODO(), types.NamespacedName{Name: podRouteServcie.Name, Namespace: cr.Namespace}, found)
        if err != nil {
            log.Log.Info("Creating Service")
            err = r.Client.Create(context.TODO(), podRouteServcie)
            if err != nil {
                log.Log.Error(err, "Failed to create Service")
                return err
            }
        }
        return nil
    }
    


    마지막으로 마지막 전에 조정에서 경로 추가 createRoute 함수 호출return ctrl.Result{}, nil
        err = r.createRoute(cr, r.podRouteRoute(cr))
        if err != nil{
            return reconcile.Result{}, err
        }
    


    경로 개체에 대한 함수 만들기

    // This is the equivalent of creating a route yaml file and returning it
    // It doesn't create anything on cluster
    func (r PodrouteReconciler) podRouteRoute(cr *quayiov1alpha1.Podroute) *routev1.Route {
        labels := labels(cr, "backend-podroute")
    
        podRouteRoute := &routev1.Route{
            ObjectMeta: metav1.ObjectMeta{
                Name:      "podroute-route",
                Namespace: cr.Namespace,
                Labels:    labels,
            },
            Spec: routev1.RouteSpec{
                To: routev1.RouteTargetReference{
                    Kind: "Service",
                    Name: "podroute-service",
                },
                Port: &routev1.RoutePort{
                    TargetPort: intstr.FromInt(8080),
                },
            },
        }
        controllerutil.SetControllerReference(cr, podRouteRoute, r.Scheme)
        return podRouteRoute
    }
    


    위의 경로 개체에서 경로를 만드는 기능을 추가하십시오.

    // check for a route if it doesn't exist it creates one on cluster using the route created in podRouteRoute
    func (r PodrouteReconciler) createRoute(cr *quayiov1alpha1.Podroute, podRouteRoute *routev1.Route) error {
        // check for a route in the namespace
        found := &routev1.Route{}
        err := r.Client.Get(context.TODO(), types.NamespacedName{Name: podRouteRoute.Name, Namespace: cr.Namespace}, found)
        if err != nil {
            log.Log.Info("Creating Route")
            err = r.Client.Create(context.TODO(), podRouteRoute)
            if err != nil {
                log.Log.Error(err, "Failed to create Route")
                return err
            }
        }
        return nil
    }
    


    NOTE: imports did change with these code changes



    import (
        "context"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "k8s.io/apimachinery/pkg/types"
        "k8s.io/apimachinery/pkg/util/intstr"
        "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
        "sigs.k8s.io/controller-runtime/pkg/reconcile"
    
        routev1 "github.com/openshift/api/route/v1"
        appsv1 "k8s.io/api/apps/v1"
        corev1 "k8s.io/api/core/v1"
        "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"
    
        quayiov1alpha1 "github.com/austincunningham/pod-route/api/v1alpha1"
    )
    


    또한 route는 kubernetes 네이티브가 아니라 openshift이기 때문에 main.go 파일의 체계에 추가해야 했습니다.

        if err := routev1.AddToScheme(mgr.GetScheme()); err != nil {
            setupLog.Error(err, "failed to add routev1 to scheme")
            os.Exit(1)
        }
    


    오퍼레이터 테스트


    crc start로 CRC(code ready containers) 시작

    crc start
    INFO Adding crc-admin and crc-developer contexts to kubeconfig... 
    Started the OpenShift cluster.
    
    The server is accessible via web console at:
      https://console-openshift-console.apps-crc.testing
    
    Log in as administrator:
      Username: kubeadmin
      Password: KUBEADMIN_PASSWORD
    
    Log in as user:
      Username: developer
      Password: developer
    
    Use the 'oc' command line interface:
      $ eval $(crc oc-env)
      $ oc login -u developer https://api.crc.testing:6443
    


    kubeadmin으로 CRC 클러스터에 로그인합니다.

    oc login -u kubeadmin -p KUBEADMIN_PASSWORD https://api.crc.testing:6443
    


    프로젝트 생성, Makefile에는 우리가 사용할 수 있는 opeator-sdk에 의해 생성된 많은 명령이 있습니다.

    oc new-project podroute
    # Installs the custom resource definitions onto the cluster
    make install
    # Create the CR on cluster
    oc apply -f - <<EOF
    ---
    apiVersion: quay.io/v1alpha1
    kind: Podroute
    metadata:
      name: test-podroute
      namespace: podroute
    spec:
      image: quay.io/austincunningham/always200:latest
      replicas: 3
    EOF
    # We can then run the operator locally
    make run
    # Should see something like
    2022-06-10T14:41:28.854+0100    INFO    Creating Deployment
    2022-06-10T14:41:28.980+0100    INFO    Creating Service
    2022-06-10T14:41:29.114+0100    INFO    Creating Route
    


    모두 정상임을 확인할 수 있습니다

    # get the servic
    oc get service
    NAME               TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)    AGE
    podroute-service   ClusterIP   10.217.5.14   <none>        8080/TCP   4m38s
    # get the route
    oc get route
    NAME             HOST/PORT                                  PATH   SERVICES           PORT   TERMINATION   WILDCARD
    podroute-route   podroute-route-podroute.apps-crc.testing          podroute-service   8080                 None
    # should be 3 pod replicas 
    oc get pods
    NAME                        READY   STATUS    RESTARTS   AGE
    pod-route-96b87c455-6sw2h   1/1     Running   0          4m12s
    pod-route-96b87c455-ghdm8   1/1     Running   0          4m12s
    pod-route-96b87c455-md426   1/1     Running   0          4m12s
    # the get route should be alive and return ok
    curl http://podroute-route-podroute.apps-crc.testing/get
    OK%  
    


    참조:
    Operator-sdk quickstart guide
    Operator-sdk Golang tutorial
    Git repo

    NOTE: built with operator-sdk v1.15.0

    좋은 웹페이지 즐겨찾기