간단한 시나리오를 통해 Canary 배포 구현해보기 - Canary Deployments using Istio
*kubernetes (minikube) 1.14.0, Istio 1.2.2
Canary 배포는 예전 광산에 유독가스가 있는지 확인하기 위해 카나리아를 광산에 가지고 들어간 것에 아이디어를 얻은 배포 방법으로 Istio 의 virtualservice 와 destinationrule 을 활용하여 비교적 손쉽게(?) 구현 할 수 있습니다. 이 글에서는 간단한 시나리오를 통해 Istio 의 Canary 배포를 어떻게 구현하고 활용할 수 있는지를 확인해봅니다.
개요
Canary 배포
카나키아는 메탄, 일산화탄소에 매우 민감하며 가스에 노출되면 죽어버리게 됩니다. 그래서 예전 광산에서는 카나리아가 노래를 계속하고 있는 동안 광부들은 안전함을 느낀 채 일 할 수 있었으며, 카나리아가 죽게 되면 곧바로 탄광을 탈출함으로써 자신의 생명을 보존할 수 있었습니다.
카나리 배포는 이 처럼 광산에 유독가스가 있는지 확인하기 위해 카나리아를 광산에 가지고 들어간 것에 아이디어를 얻은 배포 방법으로 신규 버전을 배포할 때 기존 버전을 유지한 채로 앱 중 일부만 신규 버전으로 올려서 신규 버전에 버그나 이상은 없는지를 확인합니다.

준비
- Istio 를 설치합니다. 이때 모니터링을 위해 Kiali를 포함합니다.
$ kubectl create ns istio-system
$ curl -L https://git.io/getLatestIstio | ISTIO_VERSION=1.2.2 sh -
$ KIALI_USERNAME=$(echo -n 'admin' | base64)
$ KIALI_PASSPHRASE=$(echo -n 'admin' | base64)
$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
  name: kiali
  namespace: istio-system
  labels:
    app: kiali
type: Opaque
data:
  username: $KIALI_USERNAME
  passphrase: $KIALI_PASSPHRASE
EOF
$ helm template istio-1.2.2/install/kubernetes/helm/istio --name istio --namespace istio-system \
--set kiali.enabled=true \
--set gateways.istio-ingressgateway.type=NodePort \
| kubectl apply -f -
- httpbin pod 을 배포합니다. honester/httpbin:latest 는 클라이언트 트래픽 발생용 docker image 입니다.
$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: httpbin
  labels:
    app: httpbin
spec:
  containers:
  - image: docker.io/honester/httpbin:latest
    imagePullPolicy: IfNotPresent
    name: httpbin
EOF
- Kiali 를 port-forward 하고 웹브라우저에서 Kiali 콘솔(http://localhost:20001/kiali/console)을 오픈합니다.
$ kubectl -n istio-system port-forward svc/kiali 20001
- Kiali Graph 초기 화면입니다.
    - 사용중인 노드가 없으므로 빈 화면으로 조회됩니다.
 

- 
    honester/hello-server 는 테스트용 microservice (docker image) 입니다. - honester/hello-server:v1, honester/hello-server:v2
- “Hello server - v1”, “Hello server - v2” 라는 버전을 display
- uri가 “/error” 로 요청되면 503 에러를 강제로 발생
 
시나리오
기존에 서비스되고 있는 microservice v1 에서 v2 로 Canary 배포를 수행하고 Kiali를 통해 결과를 모니터링을 합니다. 모니터링 결과에 따라 롤백 및 버전업 처리를 수행합니다.
- Goal

- 1단계 : microservice v1 배포해 초기상태 구성하기
- 2단계 : microservice v1 에 버전정보 라벨링
- 3단계 : microservice v2 배포
- 4단계 : 지정된 일부 사용자를 테스트 영역으로 타켓팅하도록 subset을 구성
    - 조건은 HTTP 프로토콜의 “end-user=testers” 라는 헤더 값을 가진 사용자 대상
 
- 5단계 : 사용자 모니터링 후 오류 발생을 확인하고 microservice v1 로 롤백 처리
    - Kiali를 통해서 타겟팅된 요청을 분석
- 임의로 fault injection 하고 결과 확인
- 롤백 처리
 
- 6 단계 : 사용자 모니터링 후 문제 없음을 확인하여 v1에서 v2로 전체 라우팅 전환
    - 오류 수정 되었다고 가정하고 microservice v2를 재 배포합니다.
- 모니터링을 통해 문제 없음을 확인하고 microservice v2로 Traffic Shifting 합니다.
 
시나리오 실행
1단계 : microservice v1 배포해 초기상태 구성하기
- microservice v1 를 배포해 시나리오 초기 환경을 구성합니다.
    - label  app=hello로 pod 와 service 연계합니다.
 
- label  
- 일반적인 구성입니다.
- 현재 운영중인 환경 구성임을 가정합니다.

$ kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-server-v1
  labels:
    app: hello
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hello
  template:
    metadata:
      labels:
        app: hello
    spec:
      containers:
      - image: honester/hello-server:v1
        imagePullPolicy: IfNotPresent
        name: hello-server-v1
---
apiVersion: v1
kind: Service
metadata:
  name: svc-hello
  labels:
    app: hello
spec:
  selector:
    app: hello
  ports:
  - name: http
    protocol: TCP
    port: 8080
EOF
- microservice v1 에 트래픽을 발생시켜 정상적으로 서비스되는지 확인합니다.
$ for i in {1..5}; do kubectl exec -it httpbin -c httpbin -- curl http://svc-hello.default.svc.cluster.local:8080; sleep 0.1; done
Hello server - v1
Hello server - v1
Hello server - v1
Hello server - v1
Hello server - v1
- 트래픽 발생 후 Kiali Graph 화면입니다. 트래픽 발생 이후 화면이므로 트래픽 경로만 표시됩니다.

2단계 : Canary 배포를 처리를 위해 microservice v1 에 version=v1 라벨링
- microservice v1은 canary 배포되고 있지 않는 것으로 가정했으므로 추가로 version=v1을 라벨링 합니다.
- 이때 deployment 삭제 후 배포 해야 합니다.
- 다음 단계에서 microservice v2 가 배포될 예정이므로 service 의 spec.selector에 추가로version=v1를 지정하여 microservice v2로 트래픽이 전달되지 않도록 합니다.

$ kubectl delete deploy hello-server-v1
$ kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-server-v1
  labels:
    app: hello
    version: v1
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hello
      version: v1
  template:
    metadata:
      labels:
        app: hello
        version: v1
    spec:
      containers:
      - image: honester/hello-server:v1
        imagePullPolicy: IfNotPresent
        name: hello-server-v1
---
apiVersion: v1
kind: Service
metadata:
  name: svc-hello
  labels:
    app: hello
spec:
  selector:
    app: hello
    version: v1
  ports:
  - name: http
    protocol: TCP
    port: 8080
EOF
3단계 : microservice v2 배포
- microservice v2 를 배포 힙니다.
    - app=hello, version=v2
 

$ kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-server-v2
  labels:
    app: hello
    version: v2
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hello
      version: v2
  template:
    metadata:
      labels:
        app: hello
        version: v2
    spec:
      containers:
      - image: honester/hello-server:v2
        imagePullPolicy: IfNotPresent
        name: hello-server-v2
EOF
- Kiali Graph 화면 상단 “Display” 메뉴 에서 “Unused Nodes”를 체크하면 node v2 가 조회됩니다.
- 아직 트래픽이 발생되지 않았기 때문에 사용되지 않은 노드(점선)으로 표시됩니다.

- 현재까지 구성 환경은 다음과 같습니다.
$ kubectl  get all --show-labels
NAME                                   READY   STATUS    RESTARTS   AGE   LABELS
pod/hello-server-v1-77f9f94c77-wt97p   2/2     Running   0          19m   app=hello,pod-template-hash=77f9f94c77,version=v1
pod/hello-server-v1-77f9f94c77-x6v6b   2/2     Running   0          19m   app=hello,pod-template-hash=77f9f94c77,version=v1
pod/hello-server-v2-6c98cd6946-qxrcm   2/2     Running   0          19m   app=hello,pod-template-hash=6c98cd6946,version=v2
pod/hello-server-v2-6c98cd6946-t8txm   2/2     Running   0          7s    app=hello,pod-template-hash=6c98cd6946,version=v2
pod/httpbin                            2/2     Running   0          32m   app=httpbin
NAME                 TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE   LABELS
service/kubernetes   ClusterIP   10.96.0.1        <none>        443/TCP    16d   component=apiserver,provider=kubernetes
service/svc-hello    ClusterIP   10.109.208.234   <none>        8080/TCP   18m   app=hello
NAME                              READY   UP-TO-DATE   AVAILABLE   AGE   LABELS
deployment.apps/hello-server-v1   2/2     2            2           17m   app=hello,version=v1
deployment.apps/hello-server-v2   1/1     1            1           17m   app=hello,version=v2
NAME                                         DESIRED   CURRENT   READY   AGE   LABELS
replicaset.apps/hello-server-v1-77f9f94c77   2         2         2       17m   app=hello,pod-template-hash=77f9f94c77,version=v1
replicaset.apps/hello-server-v2-6c98cd6946   2         2         2       19m   app=hello,pod-template-hash=6c98cd6946,version=v2
- 트래픽은 여전히 microservice v1 으로 전달됩니다.
$ for i in {1..5}; do kubectl exec -it httpbin -c httpbin -- curl http://svc-hello.default.svc.cluster.local:8080; sleep 0.1; done
Hello server - v1
Hello server - v1
Hello server - v1
Hello server - v1
Hello server - v1
4단계 : 지정된 일부 사용자를 테스트 영역으로 타켓팅하도록 subset을 구성
- Istio destinationrule 로 subset 구성합니다.
    - Label version: v1,version: v2로 v1, v2 2개의 subset 을 구성합니다.
 
- Label 
- 이번 단계부터 본격적으로 Canary Deployment 를 위한 라우팅룰을 정의합니다.

$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
  name: dr-hello
spec:
  host: svc-hello.default.svc.cluster.local
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
EOF
- virtualservice 를 생성합니다.
    - 기본 요청은 subset v1 으로 라우팅 됩니다.
- HTTP 요청 중 “end-user” 라는 헤더 값이 정확히 “testers”인 경우 subset v1:v2 가 50:50 으로 라우팅되도록 합니다.
 
$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: vs-hello
spec:
  hosts:
    - svc-hello.default.svc.cluster.local
  http:
  - match:
    - headers:
        end-user:
          exact: "testers"
    route:
    - destination:
        host: svc-hello.default.svc.cluster.local
        subset: v1
      weight: 50
    - destination:
        host: svc-hello.default.svc.cluster.local
        subset: v2
      weight: 50
  - route:
    - destination:
        host: svc-hello.default.svc.cluster.local
        subset: v1
EOF
- service 의  spec.selector version=v1을 제외합니다.- 제외하지 않으면 요청 에러가 발생합니다.
- vertualservice 는 Kubernetes 의 service 를 세분화하고 추상화한 라우팅 ruleset 입니다. 그렇기 때문에 version=v1를 제외하지 않으면 subset v2 는 service 타겟 endpoints 에 포함되지 않아 에러가 발생하는 것입니다.
 
$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
  name: svc-hello
  labels:
    app: hello
spec:
  selector:
    app: hello
  ports:
  - name: http
    protocol: TCP
    port: 8080
EOF
- service svc-hello에 virtualservice 뱃지가 표시됩니다.

- 기본 요쳥은 여전히 microservice v1 으로 전달됩니다.
$ for i in {1..5}; do kubectl exec -it httpbin -c httpbin -- curl  http://svc-hello.default.svc.cluster.local:8080; sleep 0.1; done
Hello server - v1
Hello server - v1
Hello server - v1
Hello server - v1
Hello server - v1
- 트래픽이 microservice v1 으로 전달되었으므로 푸른색으로 트래픽 경로가 표시됩니다.

- 그러나 HTTP 요청 헤더에 “end-user:testers” 를 포함하면 50% 비율로 microservice v2 로 트래픽이 전달되는 것을 확인할 수 있습니다.
$ for i in {1..5}; do kubectl exec -it httpbin -c httpbin -- curl -H "end-user:testers" http://svc-hello.default.svc.cluster.local:8080; sleep 0.1; done
Hello server - v1
Hello server - v2
Hello server - v2
Hello server - v2
Hello server - v1
- microservice v2 로도 트래픽이 발생했으므로 추가로 v2로도 푸른색으로 트래픽 경로가 표시되는 것을 확인 할 수 있습니다.

5단계 : 사용자 모니터링 후 오류 발생을 확인하고 microservice v1 로 롤백 처리
- v2 에 대한 사용자 테스트 중 오류가 발생했다고 가정하고 다음과 같이 임의로 에러를 발생시키면 Kiali를 통해 오류가 모니터링 됩니다.

$ for i in {1..5}; do kubectl exec -it httpbin -c httpbin -- curl -H "end-user:testers" http://svc-hello.default.svc.cluster.local:8080/error; sleep 0.1; done
Hello server - v2 (uri=/error)
Hello server - v1 (uri=/error)
Hello server - v1 (uri=/error)
Hello server - v2 (uri=/error)
Hello server - v1 (uri=/error)
- 에러가 발생 했으므로 붉은색으로 트래픽 경로가 표시됩니다.
    - 데모 시나리오 진행 상 v1, v2 모두 붉은색 에러가 발생했지만 실제로는 v2 만 에러가 발생하게 됩니다.
 

- 전체 트래픽을 subset v1 으로 롤백합니다.
$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: vs-hello
spec:
  hosts:
    - svc-hello.default.svc.cluster.local
  http:
  - route:
    - destination:
        host: svc-hello.default.svc.cluster.local
        subset: v1
EOF
6 단계 : 사용자 모니터링 후 문제 없음을 확인하여 v1에서 v2로 전체 라우팅 전환
- 오류 수정 후 문제 없다면 버전업 선택하고 전체 트래픽을 subset v2로 전환합니다.

$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: vs-hello
spec:
  hosts:
    - svc-hello.default.svc.cluster.local
  http:
  - route:
    - destination:
        host: svc-hello.default.svc.cluster.local
        subset: v2
EOF
$ for i in {1..5}; do kubectl exec -it httpbin -c httpbin -- curl http://svc-hello.default.svc.cluster.local:8080; sleep 0.1; done
Hello server - v2
Hello server - v2
Hello server - v2
Hello server - v2
Hello server - v2
- deployment v1을 삭제합니다.
$ kubectl delete deploy hello-server-v1
- 아래와 같이 v2 로 트래픽이 전환되었습니다.

clean-up
$ kubectl delete deploy/hello-server-v1 deploy/hello-server-v2 service/svc-hello vs/vs-hello dr/dr-hello  po/httpbin
Conclusion
Istio 의 virtualservice , destinationrule 의 traffic shifting 기능을 활용하여 Canary Deployment 를 간단한 시나리오로 구현해 볼 수 있었습니다. 이를 조금만 더 응용한다면 Blug/Green, A/B Test 역시도 유사한 방식으로 구현 가능하리라 판단됩니다. 물론 production-level 에서는 이보다는 헐씬 더 복잡하고 다양한 경우가 발생할 것입니다. 뿐만 아니라 운영관점에서 Horizontal Pod Autoscaler Walkthrough 등도 추가로 고려해야 할 것입니다.
참고
- Canary Deployments using Istio
- Flagger:progressive delivery Kubernetes operator
- Official-Destination Rule
- Official-Virtual Service
Blue/Green vs Canary vs A/B Testing
- Blue-green Deployments, A/B Testing, and Canary Releases
- 
    다음과 같이 Blue/Green, Canary, A/B Testing 차이점을 이해합니다. 
- Canary

- 
    Blue/Green  
- 
    A/B Testing 

posted at 2019/07/30 09:08