본문 바로가기
Service Mesh/Linkerd

Linkerd 자동 Canary 배포 (with Flagger, Prometheus)

by wlsdn3004 2023. 4. 29.
728x90
반응형

 

 


Canary 배포는 새로운 버전의 애플리케이션을 일부 사용자에게 먼저 배포하고, 이후에 전체 사용자에게 배포하는 전략이다. 이를 통해 새로운 버전에 대한 안정성과 성능을 검증할 수 있다.

 

Flagger는 Kubernetes에서 Canary 배포를 자동화하는 툴 중 하나이다.

서비스에 대한 트래픽을 모니터링하고, Canary 배포를 위한 새로운 버전의 Pod를 배포한다. 그 후, 일부 사용자의 트래픽을 새로운 버전의 Pod로 전환하고, 이후 일정 시간 동안 이를 모니터링한다. 만약 새로운 버전의 Pod에서 에러나 불안정한 동작이 발생하면, 자동으로 롤백을 수행한다.

또한 Prometheus를 사용하여 서비스의 지표(metric)를 수집하고, 이를 기반으로 Canary 배포를 수행한다.

 

실제 트래픽 분할은 Linkerd-SMI(Service Mesh Interface)의 TrafficSplit 기능을 사용하여 분할하게 된다.

 

linkerd와 flagger가 어떻게 연계되어 canary 배포하는지 아래 그림을 통해 참고하자.

 

 

이번 글에서는 새로운 버전의 배포가 일어나면 10초마다 10%씩 점진적으로 배포되는 실습을 다룬다.

 

실습


[참고]

본 글에서는 linkerd-viz를 통해 생성되는 prometheus를 사용하지 않는다.

 

 

실습 전제 조건

 

실습 환경

  • AWS EKS 1.22

 

1. TrafficSplit 설치


Helm을 이용하여 설치한다.

$ helm repo add l5d-smi https://linkerd.github.io/linkerd-smi
$ helm install linkerd-smi l5d-smi/linkerd-smi

 

정상으로 설치되었는지 확인한다.

$ kubectl get crd | grep -i smi
trafficsplits.split.smi-spec.io              2023-04-28T09:21:00Z

$ kubectl get po
NAME                             READY   STATUS    RESTARTS   AGE
smi-adaptor-5788b875d4-sl6rj     2/2     Running   0          45h

 

 

2. Flagger 설치


helm 차트 등록

$ helm repo add flagger https://flagger.app

 

flagger 설치 진행

$ kubectl create ns flagger-system
$ helm upgrade -i flagger flagger/flagger \
--namespace flagger-system \
--set metricsServer=http://prometheus-prometheus.monitoring:9090 \
--set meshProvider=linkerd

 

정상으로 promethus에 연결되면 아래와 같은 로그를 볼 수 있다.

 

 

3. 테스트를 위한 Pod 배포


실습을 위한 Pod 배포

apiVersion: v1
kind: Namespace
metadata:
  name: test
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: frontend
  namespace: test
data:
 nginx.conf: |-
    events {}
    http {
        server {
          listen 8080;
            location / {
                proxy_pass http://podinfo:9898;
            }
        }
    }
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
  namespace: test
  labels:
    app: frontend
spec:
  selector:
    matchLabels:
      app: frontend
  replicas: 1
  template:
    metadata:
      annotations:
        linkerd.io/inject: enabled
      labels:
        app: frontend
    spec:
      containers:
        - name: nginx
          image: nginx:alpine
          volumeMounts:
            - name: cfg
              mountPath: /etc/nginx/nginx.conf
              subPath: nginx.conf
      volumes:
        - name: cfg
          configMap:
            name: frontend
---
apiVersion: v1
kind: Service
metadata:
  name: frontend
  namespace: test
spec:
  ports:
  - name: service
    port: 8080
  selector:
    app: frontend
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: podinfo
  namespace: test
  labels:
    app: podinfo
spec:
  selector:
    matchLabels:
      app: podinfo
  template:
    metadata:
      annotations:
        linkerd.io/inject: enabled
      labels:
        app: podinfo
    spec:
      containers:
      - name: podinfod
        image: quay.io/stefanprodan/podinfo:1.7.0
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 9898
        command:
        - ./podinfo
        - --port=9898
---
apiVersion: v1
kind: Service
metadata:
  name: podinfo
  namespace: test
  labels:
    app.kubernetes.io/name: loadtester
    app.kubernetes.io/instance: flagger
spec:
  type: ClusterIP
  ports:
    - port: 9898
  selector:
    app: podinfo
---

 

정상 배포 되었는지 확인한다.

$ kubectl get po -n test
NAME                        READY   STATUS    RESTARTS   AGE
frontend-6957977dc7-hwf2b   2/2     Running   0          9s
podinfo-7bfd46f477-hf66b    2/2     Running   0          9s

$ kubectl get svc -n test
NAME       TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
frontend   ClusterIP   10.100.102.150   <none>        8080/TCP   13s
podinfo    ClusterIP   10.100.162.107   <none>        9898/TCP   13s

$ kubectl get ep -n test
NAME       ENDPOINTS           AGE
frontend   192.168.6.68:8080   18s
podinfo    192.168.8.72:9898   18s

 

 

4. Custom 지표 설정


메트릭을 수집을 위해 prometheus ServiceMonitor를 설정하여 Target을 등록한다.

$ kubectl apply -f - <<EOF
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: podinfo-canary
  namespace: test
spec:
  endpoints:
  - path: /metrics
    port: http
    interval: 5s
  selector:
    matchLabels:
      app: podinfo-canary
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: podinfo-primary
  namespace: test
spec:
  endpoints:
    - path: /metrics
      port: http
      interval: 5s
  selector:
    matchLabels:
      app: podinfo-primary
EOF

 

canary에서 참조할 custom 지표를 구성한다.

success-percent는 요청에 대한 성공 퍼센트 지표, latency는 99% 이하의 요청에 대한 지연시간 지표이다.

$ kubectl apply -f -<<EOF
apiVersion: flagger.app/v1beta1
kind: MetricTemplate
metadata:
  name: success-percent
  namespace: test
spec:
  provider:
    address: http://prometheus-prometheus.monitoring:9090
    type: prometheus
  query: |
    sum(
      rate(http_requests_total{
        namespace="{{ namespace }}",
        job="{{ target }}-canary",
        status!~"5.*"}
        [{{ interval }}]))
    /
    sum(
      rate(
        http_requests_total{
          namespace="{{ namespace }}",
          job="{{ target }}-canary"}
          [{{ interval }}])) * 100
---
apiVersion: flagger.app/v1beta1
kind: MetricTemplate
metadata:
  name: latency
  namespace: test
spec:
  provider:
    address: http://prometheus-prometheus.monitoring:9090
    type: prometheus
  query: |
    histogram_quantile(0.99,
      sum(
        rate(
          http_request_duration_seconds_bucket{
            namespace="{{ namespace }}",
            job="{{ target }}-canary"
          }[{{ interval }}]
        )
      ) by (le)
    )
EOF

 

 

5. Canary 배포


Canary crd를 배포한다.

$ kubectl apply -f - <<EOF
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
  name: podinfo
  namespace: test
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: podinfo
  service:
    port: 9898
  analysis:
    interval: 10s
    threshold: 3
    stepWeight: 10
    maxWeight: 100
    metrics:
    - name: success-percent
      templateRef:
        name: success-percent
      thresholdRange:
        min: 95
      interval: 10s
    - name: latency
      templateRef:
        name: latency
      thresholdRange:
        max: 0.5
      interval: 10s
EOF

위 설정을 간략하게 설명하면 아래와 같다.

  • 10초 동안 가중치를 10씩 100까지 늘린다.
  • metrics : 10초 동안 success-percent 지표에서 95% 이하 또는 latency 지표에서 0.5ms 이상이 되면 실패 Count에 추가되고 3번 실패하면 롤백한다.

추가로 위 canary에서 지표를 수집할 때 수집할 지표값이 없어도 실패 Count가 늘어난다.

 

 

podinfo 리소스가 어떻게 변경되었는지 확인한다.

$ kubectl get po -n test
NAME                               READY   STATUS    RESTARTS   AGE
frontend-6957977dc7-hwf2b          2/2     Running   0          3m4s
podinfo-primary-54778c9794-nhb8f   2/2     Running   0          23s

$ kubectl get svc -n test
NAME              TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
frontend          ClusterIP   10.100.102.150   <none>        8080/TCP   3m15s
podinfo           ClusterIP   10.100.162.107   <none>        9898/TCP   3m15s
podinfo-canary    ClusterIP   10.100.99.38     <none>        9898/TCP   34s
podinfo-primary   ClusterIP   10.100.163.229   <none>        9898/TCP   34s

$ kubectl get ep -n test
NAME              ENDPOINTS            AGE
frontend          192.168.6.68:8080    3m24s
podinfo           192.168.6.231:9898   3m24s
podinfo-canary    <none>               43s
podinfo-primary   192.168.6.231:9898   43s

 

TrafficSplits이 생성되었고 내용을 확인해 보면 새로 생성된 podinfo-primary로 트래픽이 전달되게 구성되어 있다.

새로운 버전이 배포되면 Canary crd 규칙에 의해 flagger controller가 가중치를 점진적으로 변경한다.

$ kubectl get trafficsplits.split.smi-spec.io -n test -oyaml                                       Sat Apr 29 05:36:47 2023

apiVersion: v1
items:
- apiVersion: split.smi-spec.io/v1alpha2
  kind: TrafficSplit
  metadata:
    creationTimestamp: "2023-04-29T05:36:38Z"
    generation: 1
    name: podinfo
    namespace: test
    ownerReferences:
    - apiVersion: flagger.app/v1beta1
      blockOwnerDeletion: true
      controller: true
      kind: Canary
      name: podinfo
      uid: 378202f8-3088-4b8e-af51-bc99970e5fc7
    resourceVersion: "7159321"
    uid: 5c73655f-58bf-477e-b427-622223a4a511
  spec:
    backends:
    - service: podinfo-canary
      weight: "0"
    - service: podinfo-primary
      weight: "100"
    service: podinfo

 

flagger 로그를 보면 위 동작에 대한 내용을 확인할 수 있다.

$ kubectl logs -n flagger-system deploy/flagger flagger -f
{"level":"info","ts":"2023-04-29T05:36:38.622Z","caller":"controller/events.go:33","msg":"all the metrics providers are available!","canary":"podinfo.test"}
{"level":"info","ts":"2023-04-29T05:36:38.635Z","caller":"canary/deployment_controller.go:63","msg":"Scaling down Deployment podinfo.test","canary":"podinfo.test"}
{"level":"info","ts":"2023-04-29T05:36:38.662Z","caller":"router/kubernetes_default.go:233","msg":"Service podinfo updated","canary":"podinfo.test"}
{"level":"info","ts":"2023-04-29T05:36:38.689Z","caller":"router/smi.go:100","msg":"TrafficSplit podinfo.test created","canary":"podinfo.test"}
{"level":"info","ts":"2023-04-29T05:36:38.722Z","caller":"controller/events.go:33","msg":"Initialization done! podinfo.test","canary":"podinfo.test"}

 

 

6. Canary 정상 배포 테스트


이미지 버전을 업데이트하여 새로운 버전의 pod를 배포한다.

$ kubectl -n test set image deployment/podinfo \
  podinfod=quay.io/stefanprodan/podinfo:1.7.1

 

정상 배포가 되면 아래와 같은 flagger 로그를 확인할 수 있다.

{"level":"info","ts":"2023-04-29T19:13:28.062Z","caller":"controller/events.go:33","msg":"New revision detected! Scaling up podinfo.test","canary":"podinfo.test"}
{"level":"info","ts":"2023-04-29T19:13:38.038Z","caller":"controller/events.go:33","msg":"Starting canary analysis for podinfo.test","canary":"podinfo.test"}
{"level":"info","ts":"2023-04-29T19:13:38.059Z","caller":"controller/events.go:33","msg":"Advance podinfo.test canary weight 10","canary":"podinfo.test"}
{"level":"info","ts":"2023-04-29T19:13:48.062Z","caller":"controller/events.go:33","msg":"Advance podinfo.test canary weight 20","canary":"podinfo.test"}
{"level":"info","ts":"2023-04-29T19:13:58.075Z","caller":"controller/events.go:33","msg":"Advance podinfo.test canary weight 30","canary":"podinfo.test"}
{"level":"info","ts":"2023-04-29T19:14:08.066Z","caller":"controller/events.go:33","msg":"Advance podinfo.test canary weight 40","canary":"podinfo.test"}
{"level":"info","ts":"2023-04-29T19:14:18.063Z","caller":"controller/events.go:33","msg":"Advance podinfo.test canary weight 50","canary":"podinfo.test"}
{"level":"info","ts":"2023-04-29T19:14:28.071Z","caller":"controller/events.go:33","msg":"Advance podinfo.test canary weight 60","canary":"podinfo.test"}
{"level":"info","ts":"2023-04-29T19:14:38.067Z","caller":"controller/events.go:33","msg":"Advance podinfo.test canary weight 70","canary":"podinfo.test"}
{"level":"info","ts":"2023-04-29T19:14:48.069Z","caller":"controller/events.go:33","msg":"Advance podinfo.test canary weight 80","canary":"podinfo.test"}
{"level":"info","ts":"2023-04-29T19:14:58.065Z","caller":"controller/events.go:33","msg":"Advance podinfo.test canary weight 90","canary":"podinfo.test"}
{"level":"info","ts":"2023-04-29T19:15:08.064Z","caller":"controller/events.go:33","msg":"Advance podinfo.test canary weight 100","canary":"podinfo.test"}
{"level":"info","ts":"2023-04-29T19:15:18.049Z","caller":"controller/events.go:33","msg":"Copying podinfo.test template spec to podinfo-primary.test","canary":"podinfo.test"}
{"level":"info","ts":"2023-04-29T19:15:28.039Z","caller":"controller/events.go:33","msg":"Routing all traffic to primary","canary":"podinfo.test"}
{"level":"info","ts":"2023-04-29T19:15:38.059Z","caller":"controller/events.go:33","msg":"Promotion completed! Scaling down podinfo.test","canary":"podinfo.test"}

 

버전이 올라간 pod로 호출되는 걸 확인할 수 있다.

$ kubectl exec -it -n test deploy/frontend -c nginx -- /bin/sh
$ curl podinfo:9898

 

정상 배포가 이루어지면 다음과 같은 과정이 진행된다.

  1. v1의 podinfo의 파드와 서비스 배포
  2. canary crd 배포
    • podinfo-primary 파드 생성 후 기존 v1 podinfo 파드 종료
    • podinfo, podinfo-primary 엔드포인트가 같은 v1 podinfo-primary 파드 ip 바인딩
    • podinfo-primary, podinfo-canary 서비스 및 엔드포인트 생성
    • TrafficSplit 생성 후 podinfo-canary 가중치: 0, podinfo-primary 가중치: 100
  3. v2 버전 배포
    • v2 podinfo 파드가 생성되고 podinfo-canary 서비스 엔드포인트에 바인딩
    • TrafficSplit에 podinfo-canary 가중치 점진적 증가 10->100
  4. 새 버전 배포 완료
    • TrafficSplit에 podinfo-canary 가중치 100
    • v2 podinfo-primary 파드가 생성된 후 기존 v1 podinfo-primary 파드 종료
    • podinfo, podinfo-primary 엔드포인트에 v2 podinfo-primary 파드 ip 바인딩
    • podinfo-canary 엔드포인트의 연결되어 있는 파드인 v2 podinfo 파드는 종료
    • 결과는 2번 상태와 같아짐

위 Canary 배포 과정을 정리하면 아래 그림과 같다.

 

 

7. Canary 롤백 테스트


7-1. 성공률 지표 롤백 테스트

 

이미지 버전을 업데이트하여 새로운 버전의 pod를 배포한다.

$ kubectl -n test set image deployment/podinfo \
  podinfod=quay.io/stefanprodan/podinfo:1.7.1

 

처음에는 정상 호출을 하다가 40~50초 후 정상 호출과, 비정상 호출을 번갈아가면서 시도한다.

$ kubectl exec -it -n test deploy/frontend -c nginx -- /bin/sh
$ curl podinfo:9898
$ curl podinfo:9898/status/502

 

flagger의 debug 로그를 확인하면 다음과 같다.

10초 간격으로 체크하여 점진적으로 10~50 weight까지 증가했다가, request success 퍼센트가 90퍼센트 이하로 내려간 게 감지되어 10초간 3번 체크했다가 90퍼센트 이하인 게 계속 감지되어 종료되었다.

 

 

새로 배포된 pod(podinfo-canary)의 Error 퍼센트가 증가하고 종료된 것을 확인할 수 있다.

 

7-2. latency 지표 롤백 테스트

 

이미지 버전을 업데이트하여 새로운 버전의 pod를 배포한다.

$ kubectl -n test set image deployment/podinfo \
  podinfod=quay.io/stefanprodan/podinfo:1.7.2

 

처음에는 정상 요청을 하다가 30~40초 후 요청 delay 1~2초를 준다.

$ kubectl exec -it -n test deploy/frontend -c nginx -- /bin/sh
$ curl podinfo:9898
$ curl podinfo:9898/delay/1
{
  "delay": 1

$ curl podinfo:9898/delay/2
{
  "delay": 2

 

flagger의 debug 로그를 확인하면 다음과 같다.

10초 간격으로 체크하여 점진적으로 10~40 weight까지 증가했다가, latency가 0.5ms를 넘어간 게 감지되어 10초간 3번 체크했다가 latency 0.5ms 이상이 계속 감지되어 종료되었다.

 

새로 배포된 pod(podinfo-canary)의 delay가 증가하고 종료된 것을 확인할 수 있다.

 

롤백 과정을 정리하면 다음과 같다.

 

 

 

반응형

댓글