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를 사용하지 않는다.
실습 전제 조건
- [Prometheus 설치]
- [Linkerd + Prometheus 통합 구성]
- [Linkerd 설치]
- [Linkerd 대시보드 구성]
- Kubernetes 1.16 이상
실습 환경
- AWS EKS 1.22
실습 절차
1. TrafficSplit 설치2. Flagger 설치
3. 테스트를 위한 Pod 배포
4. Custom 지표 설정
5. Canary 배포
6. Canary 정상 배포 테스트
7. Canary 롤백 테스트
7-1. 성공률 지표 롤백 테스트
7-2. latency 지표 롤백 테스트
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
정상 배포가 이루어지면 다음과 같은 과정이 진행된다.
- v1의 podinfo의 파드와 서비스 배포
- canary crd 배포
- podinfo-primary 파드 생성 후 기존 v1 podinfo 파드 종료
- podinfo, podinfo-primary 엔드포인트가 같은 v1 podinfo-primary 파드 ip 바인딩
- podinfo-primary, podinfo-canary 서비스 및 엔드포인트 생성
- TrafficSplit 생성 후 podinfo-canary 가중치: 0, podinfo-primary 가중치: 100
- v2 버전 배포
- v2 podinfo 파드가 생성되고 podinfo-canary 서비스 엔드포인트에 바인딩
- TrafficSplit에 podinfo-canary 가중치 점진적 증가 10->100
- 새 버전 배포 완료
- 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가 증가하고 종료된 것을 확인할 수 있다.
롤백 과정을 정리하면 다음과 같다.
'Service Mesh > Linkerd' 카테고리의 다른 글
Linkerd + Prometheus 통합 구성 (0) | 2023.04.29 |
---|---|
Linkerd 멀티클러스터 통신 구성 (0) | 2023.04.28 |
Linkerd 서킷 브레이킹 (Circuit Breaking) (1) | 2023.04.19 |
Linkerd 버전 업그레이드 (1) | 2023.04.19 |
Linkerd 트래픽 Retry, Timeout (0) | 2023.04.19 |
댓글