Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

Controller 란? BlueGreen Controller BlueGreen Controller w/ 테스트코드 Wrap up

Slide 3

Slide 3 text

Controller 란? BlueGreen Controller BlueGreen Controller w/ 테스트코드 Wrap up

Slide 4

Slide 4 text

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/

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

Controller 란? Reconcile Observe Check Diff Action Control Loop 현재 Pod 개수 확인 설정 Pod 개수와 비교 Pod 생성 or 삭제 - ReplicaSet Controller 의 경우 - -

Slide 7

Slide 7 text

수많은 리소스들이 컨트롤러로 관리 Deployment LoadBalancer ReplicaSet StatefulSet Service PersistentVolume PersistentVolumeClaim Endpoints Ingress NetworkPolicy HorizontalPodAutoscalers DaemonSet

Slide 8

Slide 8 text

Controller 혹은 Operator 의 형태로 자동화 L4 LoadBalancer DNS Vault Secret fi rewall L7 LoadBalancer Remote Storage 백신설치 자동화 Component HealthCheck ElasticSearch Backup SnapShotter

Slide 9

Slide 9 text

Autoscaler Database Cluster API

Slide 10

Slide 10 text

Cluster API Autoscaler Scale to Zero D elete All C lusters Database D elete All D atabases

Slide 11

Slide 11 text

Cluster API Autoscaler Scale to Zero 치명적 버그 발생시 서비스의 영구적 복구불가 상태까지 가능 😭 D elete All C lusters Database D elete All D atabases

Slide 12

Slide 12 text

Controller 란? BlueGreen Controller BlueGreen Controller w/ 테스트코드 Wrap up

Slide 13

Slide 13 text

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 -

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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 - 제약은 많지만 
 고려할 요소가 적음 - 대부분의 사용케이스에 대해 
 자동화가 잘 되어있음

Slide 17

Slide 17 text

Reconcile Observe Check Diff Action Control Loop BlueGreen Controller

Slide 18

Slide 18 text

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 읽어오기

Slide 19

Slide 19 text

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 를 생성하고

Slide 20

Slide 20 text

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 정의되어있다면

Slide 21

Slide 21 text

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 생성

Slide 22

Slide 22 text

"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 결과를 기록하고 종료

Slide 23

Slide 23 text

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 만들고 종료 시작해서

Slide 24

Slide 24 text

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 도 같이 삭제되는가? 등등…. 테스트해봐야 할 내용은 벌써부터 산더미

Slide 25

Slide 25 text

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 도 같이 삭제되는가? 등등…. 테스트해봐야 할 내용은 벌써부터 산더미

Slide 26

Slide 26 text

Controller 란? BlueGreen Controller BlueGreen Controller w/ 테스트코드 Wrap up

Slide 27

Slide 27 text

Real Client Mock Client Client Interface Reconciler doSth() doSth() 방법. 1 - Mock 실제 객체를 모방하는 객체를 통해 테스트를 수행 Action 수행을 검증

Slide 28

Slide 28 text

// 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

Slide 29

Slide 29 text

// 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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

Reconcile 함수가 실행될때 1. CreateOrUpdateService 2. CreateOrUpdateDeployment 위 두 메소드가 적절히 호출되었는가? 이제 신경써야할 것은 방법. 1 - Mock

Slide 32

Slide 32 text

// 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

Slide 33

Slide 33 text

// 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

Slide 34

Slide 34 text

// 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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

장점: - 어떤 순서로 어떤 메소드가 어떻게 호출 
 되어야 하는지 세밀한 검증이 가능 단점: - 메소드 호출횟수가 많아질수록 
 테스트코드가 길어지고 복잡해짐 Real Client Mock Client Client Interface Reconciler doSth() doSth() 방법. 1 - Mock

Slide 37

Slide 37 text

방법. 2 아래 두 메소드는 여전히 미검증 상태… 1. CreateOrUpdateService 2. CreateOrUpdateDeployment

Slide 38

Slide 38 text

방법. 2 Mocking…? 아래 두 메소드는 여전히 미검증 상태… 1. CreateOrUpdateService 2. CreateOrUpdateDeployment Real Client Mock Client Client Interface Test Target doSth() doSth() 사실 문제를 덮었을뿐 호출횟수는 여전히 많음

Slide 39

Slide 39 text

방법. 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 를 저장하고 돌려주는 기능만 구현

Slide 40

Slide 40 text

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 를 생성하는가?

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

장점: - 실제 Kubernetes 가 작동하는것처럼 CRUD 로직 테스트 가능 단점: - 최종형상 비교만 가능. 
 호출 순서, 타이밍 같은 세밀한 검증을 불가능 - Kubernetes 로직이 전부 구현된것은 아님 - Golang 에서만 제대로 구현됨 Real Client Fake Client Client Interface Test Target In - memory 방법. 2 - fake

Slide 46

Slide 46 text

방법. 3 Fake Client 는 Kubernetes CRUD 로직이 구현되어있어 Method 의 호출 횟수와 순서에 무관하게 유효한 테스트가 가능

Slide 47

Slide 47 text

방법. 3 하지만… Kubernetes 의 모든 로직이 제대로 구현된것은 아님

Slide 48

Slide 48 text

방법. 3 e.g. - Kubernetes Garbage Collection & Finalizer - Validating & Mutating Admission Webhook - Resource Versioning - Defaulting 등등…

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

Local PC > go test . kube apiserver test cases Controller Testing Kubebuilder 가 권장하는 테스트방식 방법. 3 - Real

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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 가 생성될때

Slide 59

Slide 59 text

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 가 생성되어야한다.

Slide 60

Slide 60 text

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 가 생성

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

Local PC > go test . kube apiserver test cases Controller Testing 장점: - 실제 Kubernetes 를 대상으로 테스트 수행 - E2E 테스트 구현에 적합 단점: - 최종형상만 검증 가능. 
 호출 순서, 타이밍 같은 세밀한 검증을 불가능 - (시간적, 성능적) 테스트 비용이 큼 - 병렬로 테스트를 수행하기 까다로움 방법. 3 - Real

Slide 64

Slide 64 text

Controller 란? BlueGreen Controller BlueGreen Controller w/ 테스트코드 Wrap up

Slide 65

Slide 65 text

그래서 언제 뭘 쓰는게 가장 좋은건가요? 🤔

Slide 66

Slide 66 text

정답은 없지만….

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

하지만 적절한 추상화 과정없이 Reconcile 함수를 
 Mock 으로 테스트하면 
 1. 사소한 코드 변경만으로 거의 모든 테스트코드를 
 처음부터 다시 작성해야하고 
 2. 테스트코드의 가독성이 안좋기 때문에 효과가 떨어집니다. Mock / Fake / Real Good / Bad

Slide 69

Slide 69 text

매우 구체적인 하나의 목적을 가진 하나의 함수는 
 Fake Client 로 테스트하면 
 1. 사소한 구현방법의 변화때문에 전체 테스트코드를 
 다시 작성할 필요가 없고 
 2. 일일히 어떤 메소드가 어느 타이밍에 어떻게 호출되는지 
 추적할 필요가 없기 때문에 효율적입니다. Mock / Fake / Real Good / Bad

Slide 70

Slide 70 text

하지만 여러 절차가 존재하는 작업의 중간과정을 검토하기에는 1. 어느 사건이 먼저 발생했는지 검증하기가 어렵고 Kubernetes 에서 제공하는 특수한 기능을 검증해야하는 경우 2. Fake Client 는 모든 기능이 구현되어있는것은 아니기 때문에 효과가 떨어집니다. Mock / Fake / Real Good / Bad

Slide 71

Slide 71 text

구체적인 유저스토리에 대해서 실제 kube - apiserver 를 띄워 E2E 테스트를 진행 하는것은 1. 실제 환경과 동일한 환경에서 
 Kubernetes 로직을 제대로 테스트 해 볼 수 있고 
 2. 별도의 문서화 작업 없이 컨트롤러의 의도된 사용법을 
 동료에게 공유할 수 있어 효과적입니다. Mock / Fake / Real Good / Bad

Slide 72

Slide 72 text

하지만 이를 특수한 상황에만 발생하는 
 엣지케이스 검출용으로 사용하려는경우 1. 테스트 수행시간이 길어지고 
 2. 개별 테스트 케이스가 길어지고 복잡해지기 때문에 
 적절한 조치가 동반되어야 합니다. Mock / Fake / Real Good / Bad

Slide 73

Slide 73 text

E.O.D