Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Testing Kubernetes Controller

kakao
December 09, 2022

Testing Kubernetes Controller

#Kubernetes #Controller #Operator #Test

카카오에서는 Kubernetes 상에서 동작하는 다양한 컨트롤러들을 개발하고 사용하고 있습니다. 이러한 컨트롤러를 통해 Loadbalancer, DNS, ACL, Vault 등 다양한 리소스들을 쿠버네티스상에서 다룰 수 있게 되었지만, 컨트롤러에 버그가 있을경우 같은 클러스터안에서 작동중인 모든 서비스에 치명적인 결과를 가져올 수 있어 가능하면 단순한 로직과 높은 안정성이 요구됩니다. 본 세션에서는 어떻게 이 컨트롤러를 테스트 가능하게 개발할 수 있는지에 대해서 다룹니다.

발표자 : scotty.scott
카카오에서 클라우드 플랫폼을 개발하고 있는 스카티입니다. Human Computer Interaction (HCI) 에 관심이 많으며, 일하면서 틈틈히 개발자를 위한 HCI 라는 주제에 대해 생각해보고 있습니다. 하지만 이번발표에서는 순수하게 개발적인 내용만 다룹니다.

kakao

December 09, 2022
Tweet

More Decks by kakao

Other Decks in Programming

Transcript

  1. Kubernetes Controller를 위한 테스트코드 작성 Copyright 2022. Kakao Corp. All

    rights reserved. Redistribution or public display is not permitted without written permission from Kakao. Testing Kubernetes Controller 이완해 scotty.scott 카카오 if(kakao)2022
  2. Controller 란? In Kubernetes, controllers are control loops that watch

    the state of your cluster, then make or request changes where needed. Each controller tries to move the current cluster state closer to the desired state.1) 1) “Kubernetes Documentation / Concepts / Cluster Architecture / Controllers”, Kubernetes, accessed Nov 01, 2022, https://kubernetes.io/docs/concepts/architecture/controller/
  3. Controller 란? Reconcile Observe Check Diff Action Control Loop 현재온도

    확인 설정온도와 비교 온도 조절 실행 - 보일러의 경우 - -
  4. Controller 란? Reconcile Observe Check Diff Action Control Loop 현재

    Pod 개수 확인 설정 Pod 개수와 비교 Pod 생성 or 삭제 - ReplicaSet Controller 의 경우 - -
  5. 수많은 리소스들이 컨트롤러로 관리 Deployment LoadBalancer ReplicaSet StatefulSet Service PersistentVolume

    PersistentVolumeClaim Endpoints Ingress NetworkPolicy HorizontalPodAutoscalers DaemonSet
  6. Controller 혹은 Operator 의 형태로 자동화 L4 LoadBalancer DNS Vault

    Secret fi rewall L7 LoadBalancer Remote Storage 백신설치 자동화 Component HealthCheck ElasticSearch Backup SnapShotter
  7. Cluster API Autoscaler Scale to Zero D elete All C

    lusters Database D elete All D atabases
  8. Cluster API Autoscaler Scale to Zero 치명적 버그 발생시 서비스의

    영구적 복구불가 상태까지 가능 😭 D elete All C lusters Database D elete All D atabases
  9. BlueGreen Controller apiVersion: app.demo.kakao.com/v1 kind: BlueGreen metadata: name: demo spec:

    routeTo: Blue blueSpec: containers: - name: blue image: demo:version-blue greenSpec: containers: - name: green image: demo:version-green BlueGreen 배포를 알아서 해주는 Custom Controller -
  10. BlueGreen Controller apiVersion: app.demo.kakao.com/v1 kind: BlueGreen metadata: name: demo spec:

    routeTo: Blue blueSpec: containers: - name: blue image: demo:version-blue greenSpec: containers: - name: green image: demo:version-green Service Blue Deployment Green Deployment phase: Blue phase: G reen phase: Blue
  11. BlueGreen Controller apiVersion: app.demo.kakao.com/v1 kind: BlueGreen metadata: name: demo spec:

    routeTo: Blue blueSpec: containers: - name: blue image: demo:version-blue greenSpec: containers: - name: green image: demo:version-green routeTo: Green Service Blue Deployment Green Deployment phase: Blue phase: G reen phase: G reen
  12. BlueGreen Controller apiVersion: app.demo.kakao.com/v1 kind: BlueGreen metadata: name: demo spec:

    routeTo: Blue blueSpec: containers: - name: blue image: demo:version-blue greenSpec: containers: - name: green image: demo:version-green Kubebuilder 를 사용할 예정 - Reference 적인 
 Framework - 제약은 많지만 
 고려할 요소가 적음 - 대부분의 사용케이스에 대해 
 자동화가 잘 되어있음
  13. func (r *BlueGreenReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {

    log := log.FromContext(ctx) log.Info("Reconcile Start", "name", req.Name, "namespace", req.Namespace) defer log.Info("Reconcile Finished", "name", req.Name, "namespace", req.Namespace) bluegreen := new(v1.BlueGreen) if err := r.Client.Get(ctx, req.NamespacedName, bluegreen); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } // Create or Update Service svc := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Namespace: req.Namespace, Name: req.Name}} if _, err := ctrl.CreateOrUpdate(ctx, r.Client, svc, func() error { if err := ctrl.SetControllerReference(bluegreen, svc, r.Scheme); err != nil { return fmt.Errorf("fail to set owner reference for a service: %w", err) } svc.Spec.Ports = []corev1.ServicePort{ {Name: "http", Protocol: corev1.ProtocolTCP, Port: 80}, } svc.Spec.Selector = map[string]string{ "app.kubernetes.io/managed-by": "app.demo.kakao.com", "app.kubernetes.io/name": req.Name, "app.kubernetes.io/phase": string(bluegreen.Spec.RouteTo), } BlueGreen Controller BlueGreen Resource 읽어오기
  14. func (r *BlueGreenReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {

    log := log.FromContext(ctx) log.Info("Reconcile Start", "name", req.Name, "namespace", req.Namespace) defer log.Info("Reconcile Finished", "name", req.Name, "namespace", req.Namespace) bluegreen := new(v1.BlueGreen) if err := r.Client.Get(ctx, req.NamespacedName, bluegreen); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } // Create or Update Service svc := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Namespace: req.Namespace, Name: req.Name}} if _, err := ctrl.CreateOrUpdate(ctx, r.Client, svc, func() error { if err := ctrl.SetControllerReference(bluegreen, svc, r.Scheme); err != nil { return fmt.Errorf("fail to set owner reference for a service: %w", err) } svc.Spec.Ports = []corev1.ServicePort{ {Name: "http", Protocol: corev1.ProtocolTCP, Port: 80}, } svc.Spec.Selector = map[string]string{ "app.kubernetes.io/managed-by": "app.demo.kakao.com", "app.kubernetes.io/name": req.Name, "app.kubernetes.io/phase": string(bluegreen.Spec.RouteTo), } return nil }); err != nil { return ctrl.Result{}, fmt.Errorf("fail to create (or update) service: %w", err) } // Create or Update BlueDeployment for _, tgt := range []struct { Phase v1.BlueOrGreen Spec *corev1.PodSpec BlueGreen Controller Service 를 생성하고
  15. svc.Spec.Selector = map[string]string{ "app.kubernetes.io/managed-by": "app.demo.kakao.com", "app.kubernetes.io/name": req.Name, "app.kubernetes.io/phase": string(bluegreen.Spec.RouteTo), }

    return nil }); err != nil { return ctrl.Result{}, fmt.Errorf("fail to create (or update) service: %w", err) } // Create or Update BlueDeployment for _, tgt := range []struct { Phase v1.BlueOrGreen Spec *corev1.PodSpec }{ {Phase: v1.Blue, Spec: bluegreen.Spec.BlueSpec}, {Phase: v1.Green, Spec: bluegreen.Spec.GreenSpec}, } { if tgt.Spec == nil { continue } deploy := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Namespace: req.Namespace, Name: deploymentName(req.Name, tgt.Phase)}} if _, err := ctrl.CreateOrUpdate(ctx, r.Client, deploy, func() error { if err := ctrl.SetControllerReference(bluegreen, deploy, r.Scheme); err != nil { return fmt.Errorf("fail to set owner reference for a deployment: %w", err) } label := map[string]string{ "app.kubernetes.io/managed-by": "app.demo.kakao.com", "app.kubernetes.io/name": req.Name, "app.kubernetes.io/phase": string(tgt.Phase), } deploy.Spec.Selector = &metav1.LabelSelector{MatchLabels: label} BlueGreen Controller Blue / Green Spec 정의되어있다면
  16. Phase v1.BlueOrGreen Spec *corev1.PodSpec }{ {Phase: v1.Blue, Spec: bluegreen.Spec.BlueSpec}, {Phase:

    v1.Green, Spec: bluegreen.Spec.GreenSpec}, } { if tgt.Spec == nil { continue } deploy := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Namespace: req.Namespace, Name: deploymentName(req.Name, tgt.Phase)}} if _, err := ctrl.CreateOrUpdate(ctx, r.Client, deploy, func() error { if err := ctrl.SetControllerReference(bluegreen, deploy, r.Scheme); err != nil { return fmt.Errorf("fail to set owner reference for a deployment: %w", err) } label := map[string]string{ "app.kubernetes.io/managed-by": "app.demo.kakao.com", "app.kubernetes.io/name": req.Name, "app.kubernetes.io/phase": string(tgt.Phase), } deploy.Spec.Selector = &metav1.LabelSelector{MatchLabels: label} deploy.Spec.Template = corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{Labels: label}, Spec: *tgt.Spec, } return nil }); err != nil { return ctrl.Result{}, fmt.Errorf("fail to create (or update) deployment: %w", err) } } bluegreen.Status = v1.BlueGreenStatus{ BlueGreen Controller Deployment 생성
  17. "app.kubernetes.io/managed-by": "app.demo.kakao.com", "app.kubernetes.io/name": req.Name, "app.kubernetes.io/phase": string(tgt.Phase), } deploy.Spec.Selector = &metav1.LabelSelector{MatchLabels:

    label} deploy.Spec.Template = corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{Labels: label}, Spec: *tgt.Spec, } return nil }); err != nil { return ctrl.Result{}, fmt.Errorf("fail to create (or update) deployment: %w", err) } } bluegreen.Status = v1.BlueGreenStatus{ RouteTo: &bluegreen.Spec.RouteTo, } if err := r.Client.Status().Update(ctx, bluegreen); err != nil { return ctrl.Result{}, fmt.Errorf("fail to update bluegreen status: %w", err) } return ctrl.Result{}, nil } BlueGreen Controller Reconcile 결과를 기록하고 종료
  18. func (r *BlueGreenReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {

    log := log.FromContext(ctx) log.Info("Reconcile Start", "name", req.Name, "namespace", req.Namespace) defer log.Info("Reconcile Finished", "name", req.Name, "namespace", req.Namespace) bluegreen := new(v1.BlueGreen) if err := r.Client.Get(ctx, req.NamespacedName, bluegreen); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } // Create or Update Service svc := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Namespace: req.Namespace, Name: req.Name}} if _, err := ctrl.CreateOrUpdate(ctx, r.Client, svc, func() error { if err := ctrl.SetControllerReference(bluegreen, svc, r.Scheme); err != nil { return fmt.Errorf("fail to set owner reference for a service: %w", err) } svc.Spec.Ports = []corev1.ServicePort{ {Name: "http", Protocol: corev1.ProtocolTCP, Port: 80}, } svc.Spec.Selector = map[string]string{ "app.kubernetes.io/managed-by": "app.demo.kakao.com", "app.kubernetes.io/name": req.Name, "app.kubernetes.io/phase": string(bluegreen.Spec.RouteTo), } return nil }); err != nil { return ctrl.Result{}, fmt.Errorf("fail to create (or update) service: %w", err) } // Create or Update BlueDeployment for _, tgt := range []struct { Phase v1.BlueOrGreen Spec *corev1.PodSpec }{ {Phase: v1.Blue, Spec: bluegreen.Spec.BlueSpec}, {Phase: v1.Green, Spec: bluegreen.Spec.GreenSpec}, } { if tgt.Spec == nil { continue } deploy := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Namespace: req.Namespace, Name: deploymentName(req.Name, tgt.Phase)}} if _, err := ctrl.CreateOrUpdate(ctx, r.Client, deploy, func() error { if err := ctrl.SetControllerReference(bluegreen, deploy, r.Scheme); err != nil { return fmt.Errorf("fail to set owner reference for a deployment: %w", err) } label := map[string]string{ "app.kubernetes.io/managed-by": "app.demo.kakao.com", "app.kubernetes.io/name": req.Name, "app.kubernetes.io/phase": string(tgt.Phase), } deploy.Spec.Selector = &metav1.LabelSelector{MatchLabels: label} deploy.Spec.Template = corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{Labels: label}, Spec: *tgt.Spec, } return nil }); err != nil { return ctrl.Result{}, fmt.Errorf("fail to create (or update) deployment: %w", err) } } bluegreen.Status = v1.BlueGreenStatus{ RouteTo: &bluegreen.Spec.RouteTo, } if err := r.Client.Status().Update(ctx, bluegreen); err != nil { return ctrl.Result{}, fmt.Errorf("fail to update bluegreen status: %w", err) } return ctrl.Result{}, nil } BlueGreen Controller Service 만들고 Deployment 만들고 종료 시작해서
  19. BlueGreen Controller 프로덕션 환경에서 사용하기에는 고려되지 않은게 많지만 1. Service

    의 Selector 는 제대로 지정되었는가? 2. Blue Spec 만 지정되었을때 
 Blue Deployment 만 생성되는가? 3. BlueGreen Spec 은 
 그대로 Deployment 에 반영되는가? 4. Deployment 가 이미 있으면 
 새롭게 생성하려고 하지 않는가? 5. Service 가 Spec 이 다를경우 
 원상태로 복구를 시도하는가? 6. 생성된 리소스들의 OwnerReference 는 
 제대로 지정되었는가? 7. BlueGreenResource 삭제시 
 Deployment / Service 도 같이 삭제되는가? 등등…. 테스트해봐야 할 내용은 벌써부터 산더미
  20. BlueGreen Controller 이제 여기에 테스트코드를 추가해봅시다! 🤓 프로덕션 환경에서 사용하기에는

    고려되지 않은게 많지만 1. Service 의 Selector 는 제대로 지정되었는가? 2. Blue Spec 만 지정되었을때 
 Blue Deployment 만 생성되는가? 3. BlueGreen Spec 은 
 그대로 Deployment 에 반영되는가? 4. Deployment 가 이미 있으면 
 새롭게 생성하려고 하지 않는가? 5. Service 가 Spec 이 다를경우 
 원상태로 복구를 시도하는가? 6. 생성된 리소스들의 OwnerReference 는 
 제대로 지정되었는가? 7. BlueGreenResource 삭제시 
 Deployment / Service 도 같이 삭제되는가? 등등…. 테스트해봐야 할 내용은 벌써부터 산더미
  21. Real Client Mock Client Client Interface Reconciler doSth() doSth() 방법.

    1 - Mock 실제 객체를 모방하는 객체를 통해 테스트를 수행 Action 수행을 검증
  22. // BlueGreenReconciler reconciles a BlueGreen object type BlueGreenReconciler struct {

    client.Client } func (r *BlueGreenReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { /* ੉ೞ ࢤۚ */ // sigs.k8s.io/controller-runtime/pkg/client type Client interface { Reader Writer /* ੉ೞ ࢤۚ */ } - Reconcile 한번에 메소드 수십번 호출 - 너무나도 작은단위의 Operation -> 프로젝트 규모가 커질수록 테스트코드 짜는게 부담됨 방법. 1 - Mock BlueGreen Reconciler Kubernetes Client CRUD
  23. // BlueGreenReconciler reconciles a BlueGreen object type BlueGreenReconciler struct {

    client.Client } func (r *BlueGreenReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { /* ੉ೞ ࢤۚ */ // pkg/bluegreen type Client interface { client.Reader CreateOrUpdateService(ctx context.Context, owner *v1.BlueGreen) error CreateOrUpdateDeployment(ctx context.Context, owner *v1.BlueGreen, phase v1.BlueOrGreen, podSpec corev1.PodSpec) error /* ੉ೞ ࢤۚ */ } bluegreen.Client 방법. 1 - Mock - Client 를 추상화 - Reconciler 에서 호출하는 메소드의 횟수를 줄임 BlueGreen Client CRUD Service CRUD Deploy BlueGreen Reconciler Kubernetes Client CRUD
  24. func (r *BlueGreenReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {

    log := log.FromContext(ctx) log.Info("Reconcile Start", "name", req.Name, "namespace", req.Namespace) defer log.Info("Reconcile Finished", "name", req.Name, "namespace", req.Namespace) bluegreen := new(v1.BlueGreen) if err := r.Client.Get(ctx, req.NamespacedName, bluegreen); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } // Create or Update Service svc := &corev1.Service{ObjectMeta: metav1.ObjectMeta{Namespace: req.Namespace, Name: req.Name}} if _, err := ctrl.CreateOrUpdate(ctx, r.Client, svc, func() error { if err := ctrl.SetControllerReference(bluegreen, svc, r.Scheme); err != nil { return fmt.Errorf("fail to set owner reference for a service: %w", err) } svc.Spec.Ports = []corev1.ServicePort{ {Name: "http", Protocol: corev1.ProtocolTCP, Port: 80}, } svc.Spec.Selector = map[string]string{ "app.kubernetes.io/managed-by": "app.demo.kakao.com", "app.kubernetes.io/name": req.Name, "app.kubernetes.io/phase": string(bluegreen.Spec.RouteTo), } return nil }); err != nil { return ctrl.Result{}, fmt.Errorf("fail to create (or update) service: %w", err) } // Create or Update BlueDeployment for _, tgt := range []struct { Phase v1.BlueOrGreen Spec *corev1.PodSpec }{ {Phase: v1.Blue, Spec: bluegreen.Spec.BlueSpec}, {Phase: v1.Green, Spec: bluegreen.Spec.GreenSpec}, } { if tgt.Spec == nil { continue } deploy := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Namespace: req.Namespace, Name: deploymentName(req.Name, tgt.Phase)}} if _, err := ctrl.CreateOrUpdate(ctx, r.Client, deploy, func() error { if err := ctrl.SetControllerReference(bluegreen, deploy, r.Scheme); err != nil { return fmt.Errorf("fail to set owner reference for a deployment: %w", err) } label := map[string]string{ "app.kubernetes.io/managed-by": "app.demo.kakao.com", "app.kubernetes.io/name": req.Name, "app.kubernetes.io/phase": string(tgt.Phase), } deploy.Spec.Selector = &metav1.LabelSelector{MatchLabels: label} deploy.Spec.Template = corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{Labels: label}, Spec: *tgt.Spec, } return nil }); err != nil { return ctrl.Result{}, fmt.Errorf("fail to create (or update) deployment: %w", err) } } bluegreen.Status = v1.BlueGreenStatus{ RouteTo: &bluegreen.Spec.RouteTo, } if err := r.Client.Status().Update(ctx, bluegreen); err != nil { return ctrl.Result{}, fmt.Errorf("fail to update bluegreen status: %w", err) } return ctrl.Result{}, nil } func (r *BlueGreenReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := log.FromContext(ctx) log.Info("Reconcile Start", "name", req.Name, "namespace", req.Namespace) defer log.Info("Reconcile Finished", "name", req.Name, "namespace", req.Namespace) bluegreen := new(v1.BlueGreen) if err := r.Client.Get(ctx, req.NamespacedName, bluegreen); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } // Create or Update Service if err := r.CreateOrUpdateService(ctx, bluegreen); err != nil { return ctrl.Result{}, err } // Create or Update BlueDeployment if spec := bluegreen.Spec.BlueSpec; spec != nil { if err := r.CreateOrUpdateDeployment(ctx, bluegreen, v1.Blue, *spec); err != nil { return ctrl.Result{}, err } } // Create or Update GreenDeployment if spec := bluegreen.Spec.GreenSpec; spec != nil { if err := r.CreateOrUpdateDeployment(ctx, bluegreen, v1.Green, *spec); err != nil { return ctrl.Result{}, err } } bluegreen.Status = v1.BlueGreenStatus{ RouteTo: &bluegreen.Spec.RouteTo, } return ctrl.Result{}, r.Client.UpdateStatus(ctx, req, bluegreen.Status) } 수십번의 메소드 호출 -> 세번의 메소드 호출 방법. 1 - Mock Deployment Service Deployment Service
  25. Reconcile 함수가 실행될때 1. CreateOrUpdateService 2. CreateOrUpdateDeployment 위 두 메소드가

    적절히 호출되었는가? 이제 신경써야할 것은 방법. 1 - Mock
  26. // INIT c := bluegreen.NewMockClient(ctrl) r := &BlueGreenReconciler{ Client: c,

    Scheme: testScheme, } // EXPECTING c.EXPECT(). Get(gomock.Any(), gomock.Eq(req.NamespacedName), gomock.Any()). SetArg(2, bg). Return(nil) c.EXPECT(). CreateOrUpdateService(gomock.Any(), &GenericMatcher[*appv1.BlueGreen]{ func(in *appv1.BlueGreen) bool { return reflect.DeepEqual(bg, *in) }, }). Return(nil) c.EXPECT(). CreateOrUpdateDeployment(gomock.Any(), &GenericMatcher[*appv1.BlueGreen]{ func(in *appv1.BlueGreen) bool { return reflect.DeepEqual(bg, *in) }, }, gomock.Eq(appv1.Blue), gomock.Eq(*bg.Spec.BlueSpec)). 방법. 1 - Mock & 가 생성되어야 하는 테스트케이스 하나의 Service 하나의 Deployment
  27. // INIT c := bluegreen.NewMockClient(ctrl) r := &BlueGreenReconciler{ Client: c,

    Scheme: testScheme, } // EXPECTING c.EXPECT(). Get(gomock.Any(), gomock.Eq(req.NamespacedName), gomock.Any()). SetArg(2, bg). Return(nil) c.EXPECT(). CreateOrUpdateService(gomock.Any(), &GenericMatcher[*appv1.BlueGreen]{ func(in *appv1.BlueGreen) bool { return reflect.DeepEqual(bg, *in) }, }). Return(nil) c.EXPECT(). CreateOrUpdateDeployment(gomock.Any(), &GenericMatcher[*appv1.BlueGreen]{ func(in *appv1.BlueGreen) bool { return reflect.DeepEqual(bg, *in) }, }, gomock.Eq(appv1.Blue), gomock.Eq(*bg.Spec.BlueSpec)). Mock 객체 초기화 방법. 1 - Mock 하나의 Service 하나의 Deployment
  28. // INIT c := bluegreen.NewMockClient(ctrl) r := &BlueGreenReconciler{ Client: c,

    Scheme: testScheme, } // EXPECTING c.EXPECT(). Get(gomock.Any(), gomock.Eq(req.NamespacedName), gomock.Any()). SetArg(2, bg). Return(nil) c.EXPECT(). CreateOrUpdateService(gomock.Any(), &GenericMatcher[*appv1.BlueGreen]{ func(in *appv1.BlueGreen) bool { return reflect.DeepEqual(bg, *in) }, }). Return(nil) c.EXPECT(). CreateOrUpdateDeployment(gomock.Any(), &GenericMatcher[*appv1.BlueGreen]{ func(in *appv1.BlueGreen) bool { return reflect.DeepEqual(bg, *in) }, }, gomock.Eq(appv1.Blue), gomock.Eq(*bg.Spec.BlueSpec)). Return(nil) c.EXPECT().UpdateStatus(gomock.Any(), &GenericMatcher[*appv1.BlueGreen]{func(bg *appv1.BlueGreen) bool { return *bg.Status.RouteTo == appv1.Blue }}, ).Return(nil) // Reconciling result, err := r.Reconcile(ctx, req) 방법. 1 - Mock 두 메소드가 호출될것을 예상 하나의 Service 하나의 Deployment
  29. c.EXPECT(). CreateOrUpdateService(gomock.Any(), &GenericMatcher[*appv1.BlueGreen]{ func(in *appv1.BlueGreen) bool { return reflect.DeepEqual(bg, *in)

    }, }). Return(nil) c.EXPECT(). CreateOrUpdateDeployment(gomock.Any(), &GenericMatcher[*appv1.BlueGreen]{ func(in *appv1.BlueGreen) bool { return reflect.DeepEqual(bg, *in) }, }, gomock.Eq(appv1.Blue), gomock.Eq(*bg.Spec.BlueSpec)). Return(nil) c.EXPECT().UpdateStatus(gomock.Any(), &GenericMatcher[*appv1.BlueGreen]{func(bg *appv1.BlueGreen) bool { return *bg.Status.RouteTo == appv1.Blue }}, ).Return(nil) // Reconciling result, err := r.Reconcile(ctx, req) // Validation assert.NoError(t, err) assert.Equal(t, reconcile.Result{Requeue: false}, result) } Reconcile 함수 실행 방법. 1 - Mock
  30. 장점: - 어떤 순서로 어떤 메소드가 어떻게 호출 
 되어야

    하는지 세밀한 검증이 가능 단점: - 메소드 호출횟수가 많아질수록 
 테스트코드가 길어지고 복잡해짐 Real Client Mock Client Client Interface Reconciler doSth() doSth() 방법. 1 - Mock
  31. 방법. 2 Mocking…? 아래 두 메소드는 여전히 미검증 상태… 1.

    CreateOrUpdateService 2. CreateOrUpdateDeployment Real Client Mock Client Client Interface Test Target doSth() doSth() 사실 문제를 덮었을뿐 호출횟수는 여전히 많음
  32. 방법. 2 - fake Real Client Fake Client Client Interface

    Test Target Fake Client! "sigs.k8s.io/controller-runtime/pkg/client/fake" In - memory - Kubernetes Community 에서 제공하는 Fake Client - In - memory KV Storage 에 단순히 manifest 를 저장하고 돌려주는 기능만 구현
  33. func TestBlueGreenReconciler_CreateOrUpdateService(t *testing.T) { t.Run("Should Create a New Service", func(t

    *testing.T) { c := fake.NewClientBuilder().WithScheme(testScheme).Build() r := &BlueGreenClient{Client: c} NN := types.NamespacedName{ Namespace: "test-namespace", Name: "test-name", } svc := new(corev1.Service) assert.Error(t, c.Get(context.TODO(), NN, svc)) err := r.CreateOrUpdateService( context.TODO(), &appv1.BlueGreen{ ObjectMeta: metav1.ObjectMeta{Namespace: NN.Namespace, Name: NN.Name}, Spec: appv1.BlueGreenSpec{RouteTo: appv1.Blue}, }) assert.NoError(t, err) assert.NoError(t, c.Get(context.TODO(), NN, svc)) assert.Equal(t, 1, len(svc.ObjectMeta.OwnerReferences)) assert.Equal(t, "app.demo.kakao.com", svc.Spec.Selector["app.kubernetes.io/managed-by"]) assert.Equal(t, "Blue", svc.Spec.Selector["app.kubernetes.io/phase"]) assert.Equal(t, NN.Name, svc.Spec.Selector["app.kubernetes.io/name"]) /* more validation here */ }) } 방법. 2 - fake CreateOrUpdateService 는 제대로 Service 를 생성하는가?
  34. func TestBlueGreenReconciler_CreateOrUpdateService(t *testing.T) { t.Run("Should Create a New Service", func(t

    *testing.T) { c := fake.NewClientBuilder().WithScheme(testScheme).Build() r := &BlueGreenClient{Client: c} NN := types.NamespacedName{ Namespace: "test-namespace", Name: "test-name", } svc := new(corev1.Service) assert.Error(t, c.Get(context.TODO(), NN, svc)) err := r.CreateOrUpdateService( context.TODO(), &appv1.BlueGreen{ ObjectMeta: metav1.ObjectMeta{Namespace: NN.Namespace, Name: NN.Name}, Spec: appv1.BlueGreenSpec{RouteTo: appv1.Blue}, }) assert.NoError(t, err) assert.NoError(t, c.Get(context.TODO(), NN, svc)) assert.Equal(t, 1, len(svc.ObjectMeta.OwnerReferences)) assert.Equal(t, "app.demo.kakao.com", svc.Spec.Selector["app.kubernetes.io/managed-by"]) assert.Equal(t, "Blue", svc.Spec.Selector["app.kubernetes.io/phase"]) assert.Equal(t, NN.Name, svc.Spec.Selector["app.kubernetes.io/name"]) /* more validation here */ }) } 초기화 방법. 2 - fake
  35. func TestBlueGreenReconciler_CreateOrUpdateService(t *testing.T) { t.Run("Should Create a New Service", func(t

    *testing.T) { c := fake.NewClientBuilder().WithScheme(testScheme).Build() r := &BlueGreenClient{Client: c} NN := types.NamespacedName{ Namespace: "test-namespace", Name: "test-name", } svc := new(corev1.Service) assert.Error(t, c.Get(context.TODO(), NN, svc)) err := r.CreateOrUpdateService( context.TODO(), &appv1.BlueGreen{ ObjectMeta: metav1.ObjectMeta{Namespace: NN.Namespace, Name: NN.Name}, Spec: appv1.BlueGreenSpec{RouteTo: appv1.Blue}, }) assert.NoError(t, err) assert.NoError(t, c.Get(context.TODO(), NN, svc)) assert.Equal(t, 1, len(svc.ObjectMeta.OwnerReferences)) assert.Equal(t, "app.demo.kakao.com", svc.Spec.Selector["app.kubernetes.io/managed-by"]) assert.Equal(t, "Blue", svc.Spec.Selector["app.kubernetes.io/phase"]) assert.Equal(t, NN.Name, svc.Spec.Selector["app.kubernetes.io/name"]) /* more validation here */ }) } 테스트를 수행할 메소드를 실행한 후 방법. 2 - fake
  36. func TestBlueGreenReconciler_CreateOrUpdateService(t *testing.T) { t.Run("Should Create a New Service", func(t

    *testing.T) { c := fake.NewClientBuilder().WithScheme(testScheme).Build() r := &BlueGreenClient{Client: c} NN := types.NamespacedName{ Namespace: "test-namespace", Name: "test-name", } svc := new(corev1.Service) assert.Error(t, c.Get(context.TODO(), NN, svc)) err := r.CreateOrUpdateService( context.TODO(), &appv1.BlueGreen{ ObjectMeta: metav1.ObjectMeta{Namespace: NN.Namespace, Name: NN.Name}, Spec: appv1.BlueGreenSpec{RouteTo: appv1.Blue}, }) assert.NoError(t, err) assert.NoError(t, c.Get(context.TODO(), NN, svc)) assert.Equal(t, 1, len(svc.ObjectMeta.OwnerReferences)) assert.Equal(t, "app.demo.kakao.com", svc.Spec.Selector["app.kubernetes.io/managed-by"]) assert.Equal(t, "Blue", svc.Spec.Selector["app.kubernetes.io/phase"]) assert.Equal(t, NN.Name, svc.Spec.Selector["app.kubernetes.io/name"]) /* more validation here */ }) } Fake Client 로부터 변경사항 수신 방법. 2 - fake
  37. func TestBlueGreenReconciler_CreateOrUpdateService(t *testing.T) { t.Run("Should Create a New Service", func(t

    *testing.T) { c := fake.NewClientBuilder().WithScheme(testScheme).Build() r := &BlueGreenClient{Client: c} NN := types.NamespacedName{ Namespace: "test-namespace", Name: "test-name", } svc := new(corev1.Service) assert.Error(t, c.Get(context.TODO(), NN, svc)) err := r.CreateOrUpdateService( context.TODO(), &appv1.BlueGreen{ ObjectMeta: metav1.ObjectMeta{Namespace: NN.Namespace, Name: NN.Name}, Spec: appv1.BlueGreenSpec{RouteTo: appv1.Blue}, }) assert.NoError(t, err) assert.NoError(t, c.Get(context.TODO(), NN, svc)) assert.Equal(t, 1, len(svc.ObjectMeta.OwnerReferences)) assert.Equal(t, "app.demo.kakao.com", svc.Spec.Selector["app.kubernetes.io/managed-by"]) assert.Equal(t, "Blue", svc.Spec.Selector["app.kubernetes.io/phase"]) assert.Equal(t, NN.Name, svc.Spec.Selector["app.kubernetes.io/name"]) /* more validation here */ }) } 여러 검증 수행 방법. 2 - fake
  38. 장점: - 실제 Kubernetes 가 작동하는것처럼 CRUD 로직 테스트 가능

    단점: - 최종형상 비교만 가능. 
 호출 순서, 타이밍 같은 세밀한 검증을 불가능 - Kubernetes 로직이 전부 구현된것은 아님 - Golang 에서만 제대로 구현됨 Real Client Fake Client Client Interface Test Target In - memory 방법. 2 - fake
  39. 방법. 3 Fake Client 는 Kubernetes CRUD 로직이 구현되어있어 Method

    의 호출 횟수와 순서에 무관하게 유효한 테스트가 가능
  40. 방법. 3 e.g. - Kubernetes Garbage Collection & Finalizer -

    Validating & Mutating Admission Webhook - Resource Versioning - Defaulting 등등…
  41. Local PC > go test . kube apiserver test cases

    Controller Testing 로컬에서 실제 kube - apiserver 프로세스를 실행 실제 manifest (e.g, BlueGreen) 를 선언하고, 리소스 (Service, Deployment) 들의 
 최종 형상을 검증 유저 스토리에 맞게 테스트를 작성하기에 적합 방법. 3 - Real
  42. Local PC > go test . kube apiserver test cases

    Controller Testing Kubebuilder 가 권장하는 테스트방식 방법. 3 - Real
  43. var _ = BeforeSuite(func() { logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) ctx, cancel =

    context.WithCancel(context.TODO()) By("bootstrapping test environment") testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true, } var err error // cfg is defined in this file globally. cfg, err = testEnv.Start() Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) err = appv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) //+kubebuilder:scaffold:scheme k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) TEST 를 시작하기 전에 방법. 3 - Real
  44. var _ = BeforeSuite(func() { logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) ctx, cancel =

    context.WithCancel(context.TODO()) By("bootstrapping test environment") testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true, } var err error // cfg is defined in this file globally. cfg, err = testEnv.Start() Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) err = appv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) //+kubebuilder:scaffold:scheme k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: scheme.Scheme, }) 테스트 환경을 시작하고 exec kube - apiserver process exec etcd process 방법. 3 - Real
  45. testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true,

    } var err error // cfg is defined in this file globally. cfg, err = testEnv.Start() Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) err = appv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) //+kubebuilder:scaffold:scheme k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: scheme.Scheme, }) Expect(err).ToNot(HaveOccurred()) err = (&BlueGreenReconciler{ Client: &bluegreen.BlueGreenClient{ Client: k8sManager.GetClient(), }, Scheme: k8sManager.GetScheme(), }).SetupWithManager(k8sManager) Expect(err).ToNot(HaveOccurred()) 임시 실행된 Kubernetes 대상 클라이언트 생성 방법. 3 - Real
  46. Expect(cfg).NotTo(BeNil()) err = appv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) //+kubebuilder:scaffold:scheme k8sClient, err = client.New(cfg,

    client.Options{Scheme: scheme.Scheme}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: scheme.Scheme, }) Expect(err).ToNot(HaveOccurred()) err = (&BlueGreenReconciler{ Client: &bluegreen.BlueGreenClient{ Client: k8sManager.GetClient(), }, Scheme: k8sManager.GetScheme(), }).SetupWithManager(k8sManager) Expect(err).ToNot(HaveOccurred()) go func() { defer GinkgoRecover() err = k8sManager.Start(ctx) Expect(err).ToNot(HaveOccurred(), "failed to run manager") }() }, 60) BlueGreen Controller 초기화 방법. 3 - Real
  47. err = appv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) //+kubebuilder:scaffold:scheme k8sClient, err = client.New(cfg, client.Options{Scheme:

    scheme.Scheme}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: scheme.Scheme, }) Expect(err).ToNot(HaveOccurred()) err = (&BlueGreenReconciler{ Client: &bluegreen.BlueGreenClient{ Client: k8sManager.GetClient(), }, Scheme: k8sManager.GetScheme(), }).SetupWithManager(k8sManager) Expect(err).ToNot(HaveOccurred()) go func() { defer GinkgoRecover() err = k8sManager.Start(ctx) Expect(err).ToNot(HaveOccurred(), "failed to run manager") }() }, 60) 별도의 고루틴에서 Bluegreen 컨트롤러 시작 방법. 3 - Real
  48. var _ = Describe("BlueGreen controller", func() { const ( timeout

    = time.Second * 10 duration = time.Second * 10 interval = time.Millisecond * 250 ) Context("When creating a New BlueGreen Resource", func() { Context("When defining Blue Spec only", func() { It("Should Create a One Service and a One Deployment", func() { Expect( k8sClient.Create(ctx, &v1.BlueGreen{ ObjectMeta: metav1.ObjectMeta{ Name: "bluegreen-test", Namespace: "default", }, Spec: v1.BlueGreenSpec{ RouteTo: v1.Blue, BlueSpec: &corev1.PodSpec{ Containers: []corev1.Container{ {Name: "blue", Image: "nginx"}, }, }, }, BlueGreen Controller 에 대한 테스트 방법. 3 - Real
  49. var _ = Describe("BlueGreen controller", func() { const ( timeout

    = time.Second * 10 duration = time.Second * 10 interval = time.Millisecond * 250 ) Context("When creating a New BlueGreen Resource", func() { Context("When defining Blue Spec only", func() { It("Should Create a One Service and a One Deployment", func() { Expect( k8sClient.Create(ctx, &v1.BlueGreen{ ObjectMeta: metav1.ObjectMeta{ Name: "bluegreen-test", Namespace: "default", }, Spec: v1.BlueGreenSpec{ RouteTo: v1.Blue, BlueSpec: &corev1.PodSpec{ Containers: []corev1.Container{ {Name: "blue", Image: "nginx"}, }, }, }, Blue Spec 만 정의한 BlueGreen Resource 가 생성될때 방법. 3 - Real
  50. var _ = Describe("BlueGreen controller", func() { const ( timeout

    = time.Second * 10 duration = time.Second * 10 interval = time.Millisecond * 250 ) Context("When creating a New BlueGreen Resource", func() { Context("When defining Blue Spec only", func() { It("Should Create a One Service and a One Deployment", func() { Expect( k8sClient.Create(ctx, &v1.BlueGreen{ ObjectMeta: metav1.ObjectMeta{ Name: "bluegreen-test", Namespace: "default", }, Spec: v1.BlueGreenSpec{ RouteTo: v1.Blue, BlueSpec: &corev1.PodSpec{ Containers: []corev1.Container{ {Name: "blue", Image: "nginx"}, }, }, }, 하나의 Service 와 하나의 Deployment 가 생성되어야한다. 방법. 3 - Real Blue Spec 만 정의한 BlueGreen Resource 가 생성될때
  51. var _ = Describe("BlueGreen controller", func() { const ( timeout

    = time.Second * 10 duration = time.Second * 10 interval = time.Millisecond * 250 ) Context("When creating a New BlueGreen Resource", func() { Context("When defining Blue Spec only", func() { It("Should Create a One Service and a One Deployment", func() { Expect( k8sClient.Create(ctx, &v1.BlueGreen{ ObjectMeta: metav1.ObjectMeta{ Name: "bluegreen-test", Namespace: "default", }, Spec: v1.BlueGreenSpec{ RouteTo: v1.Blue, BlueSpec: &corev1.PodSpec{ Containers: []corev1.Container{ {Name: "blue", Image: "nginx"}, }, }, }, 라는 유저스토리에 대한 테스트코드 방법. 3 - Real Blue Spec 만 정의한 BlueGreen Resource 가 생성될때 하나의 Service 와 하나의 Deployment 가 생성되어야한다.
  52. var _ = Describe("BlueGreen controller", func() { const ( timeout

    = time.Second * 10 duration = time.Second * 10 interval = time.Millisecond * 250 ) Context("When creating a New BlueGreen Resource", func() { Context("When defining Blue Spec only", func() { It("Should Create a One Service and a One Deployment", func() { Expect( k8sClient.Create(ctx, &v1.BlueGreen{ ObjectMeta: metav1.ObjectMeta{ Name: "bluegreen-test", Namespace: "default", }, Spec: v1.BlueGreenSpec{ RouteTo: v1.Blue, BlueSpec: &corev1.PodSpec{ Containers: []corev1.Container{ {Name: "blue", Image: "nginx"}, }, }, }, }, ), ).Should(Succeed()) svcList := &corev1.ServiceList{} Eventually(func() bool { if err := k8sClient.List(ctx, svcList); err != nil { return false 실제 Kubernetes Client 를 통해서 방법. 3 - Real 하나의 Service 와 하나의 Deployment 가 생성되어야한다. Blue Spec 만 정의한 BlueGreen Resource 가 생성
  53. Namespace: "default", }, Spec: v1.BlueGreenSpec{ RouteTo: v1.Blue, BlueSpec: &corev1.PodSpec{ Containers:

    []corev1.Container{ {Name: "blue", Image: "nginx"}, }, }, }, }, ), ).Should(Succeed()) svcList := &corev1.ServiceList{} Eventually(func() bool { if err := k8sClient.List(ctx, svcList); err != nil { return false } return len(svcList.Items) != 0 }, timeout, interval).Should(BeTrue()) Expect(len(svcList.Items)).To(Equal(1)) /* More Validation Logics here */ }) }) /* More Test Scenarios here */ }) }) Polling 방식으로 방법. 3 - Real 가 생성되었는지 검증 하나의 Service
  54. Namespace: "default", }, Spec: v1.BlueGreenSpec{ RouteTo: v1.Blue, BlueSpec: &corev1.PodSpec{ Containers:

    []corev1.Container{ {Name: "blue", Image: "nginx"}, }, }, }, }, ), ).Should(Succeed()) svcList := &corev1.ServiceList{} Eventually(func() bool { if err := k8sClient.List(ctx, svcList); err != nil { return false } return len(svcList.Items) != 0 }, timeout, interval).Should(BeTrue()) Expect(len(svcList.Items)).To(Equal(1)) /* More Validation Logics here */ }) }) /* More Test Scenarios here */ }) }) 이하생략… 방법. 3 - Real
  55. Local PC > go test . kube apiserver test cases

    Controller Testing 장점: - 실제 Kubernetes 를 대상으로 테스트 수행 - E2E 테스트 구현에 적합 단점: - 최종형상만 검증 가능. 
 호출 순서, 타이밍 같은 세밀한 검증을 불가능 - (시간적, 성능적) 테스트 비용이 큼 - 병렬로 테스트를 수행하기 까다로움 방법. 3 - Real
  56. Reconcile 함수는 Mock 을 통해 테스트 가능하게 만들면 
 1.

    Reconcile 함수의 코드 길이가 짧아지고 
 2. 수행해야하는 작업들에 대한 적절한 추상화가 이뤄지고 
 3. 그와 함께 코드 가독성이 올라가기 때문에 효과적입니다. Mock / Fake / Real Good / Bad
  57. 하지만 적절한 추상화 과정없이 Reconcile 함수를 
 Mock 으로 테스트하면

    
 1. 사소한 코드 변경만으로 거의 모든 테스트코드를 
 처음부터 다시 작성해야하고 
 2. 테스트코드의 가독성이 안좋기 때문에 효과가 떨어집니다. Mock / Fake / Real Good / Bad
  58. 매우 구체적인 하나의 목적을 가진 하나의 함수는 
 Fake Client

    로 테스트하면 
 1. 사소한 구현방법의 변화때문에 전체 테스트코드를 
 다시 작성할 필요가 없고 
 2. 일일히 어떤 메소드가 어느 타이밍에 어떻게 호출되는지 
 추적할 필요가 없기 때문에 효율적입니다. Mock / Fake / Real Good / Bad
  59. 하지만 여러 절차가 존재하는 작업의 중간과정을 검토하기에는 1. 어느 사건이

    먼저 발생했는지 검증하기가 어렵고 Kubernetes 에서 제공하는 특수한 기능을 검증해야하는 경우 2. Fake Client 는 모든 기능이 구현되어있는것은 아니기 때문에 효과가 떨어집니다. Mock / Fake / Real Good / Bad
  60. 구체적인 유저스토리에 대해서 실제 kube - apiserver 를 띄워 E2E

    테스트를 진행 하는것은 1. 실제 환경과 동일한 환경에서 
 Kubernetes 로직을 제대로 테스트 해 볼 수 있고 
 2. 별도의 문서화 작업 없이 컨트롤러의 의도된 사용법을 
 동료에게 공유할 수 있어 효과적입니다. Mock / Fake / Real Good / Bad
  61. 하지만 이를 특수한 상황에만 발생하는 
 엣지케이스 검출용으로 사용하려는경우 1.

    테스트 수행시간이 길어지고 
 2. 개별 테스트 케이스가 길어지고 복잡해지기 때문에 
 적절한 조치가 동반되어야 합니다. Mock / Fake / Real Good / Bad