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

Testing Kubernetes Controller

kakao
PRO
December 09, 2022

Testing Kubernetes Controller

#Kubernetes #Controller #Operator #Test

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

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

kakao
PRO

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

    View Slide

  2. Controller 란?


    BlueGreen Controller


    BlueGreen Controller w/ 테스트코드


    Wrap up

    View Slide

  3. Controller 란?


    BlueGreen Controller


    BlueGreen Controller w/ 테스트코드


    Wrap up

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  8. Controller 혹은 Operator 의 형태로


    자동화
    L4 LoadBalancer
    DNS
    Vault Secret
    fi
    rewall
    L7 LoadBalancer
    Remote Storage
    백신설치 자동화
    Component HealthCheck
    ElasticSearch
    Backup
    SnapShotter

    View Slide

  9. Autoscaler Database Cluster API

    View Slide

  10. Cluster API
    Autoscaler
    Scale
    to
    Zero
    D
    elete


    All C
    lusters
    Database
    D
    elete


    All D
    atabases

    View Slide

  11. Cluster API
    Autoscaler
    Scale
    to
    Zero
    치명적 버그 발생시


    서비스의 영구적 복구불가 상태까지 가능 😭
    D
    elete


    All C
    lusters
    Database
    D
    elete


    All D
    atabases

    View Slide

  12. Controller 란?


    BlueGreen Controller


    BlueGreen Controller w/ 테스트코드


    Wrap up

    View Slide

  13. 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
    -

    View Slide

  14. 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

    View Slide

  15. 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

    View Slide

  16. 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


    - 제약은 많지만

    고려할 요소가 적음


    - 대부분의 사용케이스에 대해

    자동화가 잘 되어있음

    View Slide

  17. Reconcile
    Observe
    Check Diff
    Action
    Control Loop
    BlueGreen Controller

    View Slide

  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),


    }



    BlueGreen Controller
    BlueGreen Resource 읽어오기

    View Slide

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

    View Slide



  20. 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


    정의되어있다면

    View Slide



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

    View Slide



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

    View Slide

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

    View Slide

  24. 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 도 같이 삭제되는가?


    등등….
    테스트해봐야 할 내용은 벌써부터 산더미

    View Slide

  25. 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 도 같이 삭제되는가?


    등등….
    테스트해봐야 할 내용은 벌써부터 산더미

    View Slide

  26. Controller 란?


    BlueGreen Controller


    BlueGreen Controller w/ 테스트코드


    Wrap up

    View Slide

  27. Real Client Mock Client
    Client Interface
    Reconciler
    doSth()
    doSth()
    방법. 1 - Mock
    실제 객체를 모방하는 객체를 통해


    테스트를 수행


    Action 수행을 검증

    View Slide

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

    View Slide

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

    View Slide

  30. 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

    View Slide

  31. Reconcile 함수가 실행될때


    1. CreateOrUpdateService


    2. CreateOrUpdateDeployment


    위 두 메소드가 적절히 호출되었는가?
    이제 신경써야할 것은
    방법. 1 - Mock

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide



  35. 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

    View Slide

  36. 장점:


    - 어떤 순서로 어떤 메소드가 어떻게 호출

    되어야 하는지 세밀한 검증이 가능


    단점:


    - 메소드 호출횟수가 많아질수록

    테스트코드가 길어지고 복잡해짐
    Real Client Mock Client
    Client Interface
    Reconciler
    doSth()
    doSth()
    방법. 1 - Mock

    View Slide

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


    1. CreateOrUpdateService


    2. CreateOrUpdateDeployment

    View Slide

  38. 방법. 2
    Mocking…?
    아래 두 메소드는 여전히 미검증 상태…


    1. CreateOrUpdateService


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

    View Slide

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

    View Slide

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

    View Slide

  41. 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

    View Slide

  42. 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

    View Slide

  43. 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

    View Slide

  44. 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

    View Slide

  45. 장점:


    - 실제 Kubernetes 가 작동하는것처럼
    CRUD 로직 테스트 가능


    단점:


    - 최종형상 비교만 가능.

    호출 순서, 타이밍 같은 세밀한 검증을 불가능


    - Kubernetes 로직이 전부 구현된것은 아님


    - Golang 에서만 제대로 구현됨
    Real Client Fake Client
    Client Interface
    Test Target
    In
    -
    memory
    방법. 2 - fake

    View Slide

  46. 방법. 3
    Fake Client 는 Kubernetes CRUD 로직이 구현되어있어


    Method 의 호출 횟수와 순서에 무관하게 유효한 테스트가 가능

    View Slide

  47. 방법. 3
    하지만…


    Kubernetes 의 모든 로직이 제대로 구현된것은 아님

    View Slide

  48. 방법. 3
    e.g.


    - Kubernetes Garbage Collection & Finalizer


    - Validating & Mutating Admission Webhook


    - Resource Versioning


    - Defaulting


    등등…

    View Slide

  49. Local PC


    > go test .
    kube


    apiserver test


    cases
    Controller
    Testing
    로컬에서 실제 kube
    -
    apiserver 프로세스를 실행


    실제 manifest (e.g, BlueGreen) 를 선언하고,


    리소스 (Service, Deployment) 들의

    최종 형상을 검증


    유저 스토리에 맞게 테스트를 작성하기에 적합
    방법. 3 - Real

    View Slide

  50. Local PC


    > go test .
    kube


    apiserver test


    cases
    Controller
    Testing
    Kubebuilder 가 권장하는 테스트방식
    방법. 3 - Real

    View Slide

  51. 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

    View Slide

  52. 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

    View Slide



  53. 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

    View Slide



  54. 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

    View Slide

  55. 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

    View Slide

  56. 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

    View Slide

  57. 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

    View Slide

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

    View Slide

  59. 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 가


    생성되어야한다.

    View Slide

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

    View Slide

  61. 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

    View Slide

  62. 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

    View Slide

  63. Local PC


    > go test .
    kube


    apiserver test


    cases
    Controller
    Testing
    장점:


    - 실제 Kubernetes 를 대상으로 테스트 수행


    - E2E 테스트 구현에 적합


    단점:


    - 최종형상만 검증 가능.

    호출 순서, 타이밍 같은 세밀한 검증을 불가능


    - (시간적, 성능적) 테스트 비용이 큼


    - 병렬로 테스트를 수행하기 까다로움
    방법. 3 - Real

    View Slide

  64. Controller 란?


    BlueGreen Controller


    BlueGreen Controller w/ 테스트코드


    Wrap up

    View Slide

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

    View Slide

  66. 정답은 없지만….

    View Slide

  67. Reconcile 함수는 Mock 을 통해 테스트 가능하게 만들면



    1. Reconcile 함수의 코드 길이가 짧아지고

    2. 수행해야하는 작업들에 대한 적절한 추상화가 이뤄지고

    3. 그와 함께 코드 가독성이 올라가기 때문에




    효과적입니다.
    Mock / Fake / Real Good / Bad

    View Slide

  68. 하지만 적절한 추상화 과정없이 Reconcile 함수를

    Mock 으로 테스트하면



    1. 사소한 코드 변경만으로 거의 모든 테스트코드를

    처음부터 다시 작성해야하고



    2. 테스트코드의 가독성이 안좋기 때문에




    효과가 떨어집니다.
    Mock / Fake / Real Good / Bad

    View Slide

  69. 매우 구체적인 하나의 목적을 가진 하나의 함수는

    Fake Client 로 테스트하면



    1. 사소한 구현방법의 변화때문에 전체 테스트코드를

    다시 작성할 필요가 없고

    2. 일일히 어떤 메소드가 어느 타이밍에 어떻게 호출되는지

    추적할 필요가 없기 때문에




    효율적입니다.
    Mock / Fake / Real Good / Bad

    View Slide

  70. 하지만 여러 절차가 존재하는 작업의 중간과정을 검토하기에는


    1. 어느 사건이 먼저 발생했는지 검증하기가 어렵고


    Kubernetes 에서 제공하는 특수한 기능을 검증해야하는 경우


    2. Fake Client 는 모든 기능이 구현되어있는것은 아니기 때문에


    효과가 떨어집니다.
    Mock / Fake / Real Good / Bad

    View Slide

  71. 구체적인 유저스토리에 대해서 실제 kube
    -
    apiserver 를 띄워


    E2E 테스트를 진행 하는것은




    1. 실제 환경과 동일한 환경에서

    Kubernetes 로직을 제대로 테스트 해 볼 수 있고



    2. 별도의 문서화 작업 없이 컨트롤러의 의도된 사용법을

    동료에게 공유할 수 있어




    효과적입니다.
    Mock / Fake / Real Good / Bad

    View Slide

  72. 하지만 이를 특수한 상황에만 발생하는

    엣지케이스 검출용으로 사용하려는경우




    1. 테스트 수행시간이 길어지고



    2. 개별 테스트 케이스가 길어지고 복잡해지기 때문에



    적절한 조치가 동반되어야 합니다.
    Mock / Fake / Real Good / Bad

    View Slide

  73. E.O.D

    View Slide