Simple Canary deployment with Istio on Kubernetes

Istio Virtual Service, Destination Rule resources를 사용하여 Kubernetes에서 Canary Deployment를 구현하고 안전한 서버 배포 환경을 구축한다.

Jongho Jeon
Jongho’s Tech Blog
15 min readFeb 26, 2022

--

Overview

본 포스트는 Istio가 구성된 Kubernetes cluster에서 최대한 간단한 방법으로 Canary Deployment를 수행하는 방법을 설명한다. 간단히 설명하면 아래와 같다.

Namespace에 하나의 Service를 위한 Deployment를 2개 생성할 것이며, Canary를 위한 Deployment에는 replica 1개로 설정하고 1%의 ingress traffic만 흘려보낼 것이다.

새로운 버전의 container 이미지가 정상적으로 동작함을 확인한 후 Helm upgrade를 통해 stable Deployment만 1개만 남겨서 Canary deployment를 마무리한다.

아래는 본 포스트 작성에 참고한 레퍼런스들이다.

내용 이해를 위한 필요 지식

Kubernetes Basic knowledge

Helm Basic knowledge

Istio Basic knowledge

References: Canary Deployment with Istio on Kubernetes

https://istio.io/latest/docs/concepts/traffic-management/

https://kubernetes.io/ko/docs/concepts/workloads/controllers/deployment/#레이블-셀렉터-업데이트

참고: API 버전 apps/v1 에서 디플로이먼트의 레이블 셀렉터는 생성 이후에는 변경할 수 없다.

https://istio.io/latest/blog/2017/0.1-canary/

Deployment resource의 예시 설정값은 위 블로그를 참고하자

References: Further information

https://docs.harness.io/article/2xp0oyubjj-create-a-kubernetes-canary-deployment

Content

Canary Deployment Overview

Motivation of Canary Deployment

서버 배포는 할 때마다 긴장된다. 내가 설정값에 실수한 건 없는지 여러 번 체크하게 된다. — 환경변수를 빼먹었거나, kubernetes context를 잘못 설정했거나 — 배포하는 서버가 거대할 수록, 장애 여파가 클 수록(매출액 또는 유저 수), 입사기간이 짧을 수록 더 긴장된다.

Canary Deployment가 필요한 이유에 대한 개인적인 생각을 간단히 정리하면 다음과 같다.

  • 최소한의 서버 장애 스트레스로 서버를 배포할 수 있도록 → 더 민첩하게 유저에게 비즈니스 가치를 전달할 수 있도록
  • 최소한의 서버 장애 비용으로 서버를 배포할 수 있도록
  • 최소한의 QA 비용으로 서버를 배포할 수 있도록

Canary Deployment는 서버 엔지니어로서 그 동안 가장 셋업하고 싶었던 테크닉 중 하나이다.

이제 Canary Deployment를 위한 리소스들을 셋업 해보자.

Configure Service

본인은 회사 dev 환경의 서버에서 Canary Deployment PoC를 진행하였다. 따라서 예제 설정값은 레퍼런스에서 가져왔다. 기존에 이미 배포되어 있는 서버(및 배포를 위한 Helm Chart)가 있다고 가정한다.

# templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: helloworld
labels:
app: helloworld
spec:
selector:
app: helloworld
...

Service는 Pod들을 네트워크에 노출하는 데에 사용되는 리소스다

위 Service에서 주목할 점은 app=helloworld label이 등록된 pod들을 네트워크에 노출시킨다는 것이다.

Configure Deployments

# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: helloworld
spec:
replicas: 8
selector:
matchLabels:
app: helloworld
release: stable
template:
metadata:
labels:
app: helloworld
release: stable
spec:
containers:
- image: helloworld-v1
...
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: helloworld-canary
spec:
replicas: 1
selector:
matchLabels:
app: helloworld
release: stable
template:
metadata:
labels:
app: helloworld
release: canary
spec:
containers:
- image: helloworld-v2
...

레퍼런스에는 필수값인 spec.selector 값이 누락되어 있어서 추가했다

디플로이먼트는 간단히 말해 Pod을 어떻게 — 무슨 image로부터, 몇 개의 replica로 등 — 배포할 것인지 선언적으로 업데이트할 수 있도록 제공한다.

위에서 2개의 deployment를 설정하고 있고 주목할 점은 다음과 같다

  • 서로 다른 이름으로 선언하여 동시에 배포될 수 있도록 한다
  • 2개의 deployment로부터 생성되는 pod들이 동일하게 app=helloworld라는 label을 가지도록 한다. 이를 통해 모든 pod들은 위 Service에 등록된다. (selector도 마찬가지)

이 부분을 주의하자. apps/v1 버전의 Deployment는 배포 후 spec.selector 업데이트가 불가능하다. deployment를 제거 후 다시 helm install 하거나, 새로운 이름의 deployment를 생성해야 한다.

  • pod들에 release label을 추가하여 아래에서 Service subset을 구성할 수 있도록 한다.
  • canary deployment는 stable보다 새로운 버전의 container image를 소스로 사용한다

Configure Virtual Service, Destination Rule

# templates/istio.yaml
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: helloworld
spec:
hosts:
- helloworld.com
http:
- route:
- destination:
host: helloworld
subset: stable
weight: 99
- destination:
host: helloworld
subset: canary
weight: 1
---
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: helloworld
spec:
host: helloworld
subsets:
- name: stable
labels:
release: stable
- name: canary
labels:
release: canary

Virtual Service와 Destination Rule은 레퍼런스에 나와있듯이, Istio traffic management 기능의 핵심 빌딩 블록이다.

Virtual Service는 Istio service mesh 내에서 요청이 어느 서비스로 라우팅될 지 설정할 수 있게 해준다. (마치 Virtual Memory와 Physical Memory처럼)

  • .spec.hosts: user가 접근 가능한, 이 Virtual Service에 적용될 host의 목록이다. [http://helloworld.com](http://helloworld.com) 로 들어오는 트래픽은 이 Virtual Service를 거쳐가게 된다. 이 host는 실제로 Istio service discovery 등록될 필요 없으며, 그저 가상 destination이다.
  • .specs.http: 1개의 route 하위에 2개의 destination을 선언하고 있다. traffic은 weight의 비율에 따라 해당 destination으로 향하게 된다.

여러 개의 route를 선언하는 것은 traffic의 특정 조건 — header의 값 일치 등 — 에 따라 traffic을 다르게 라우팅할 때 사용할 수 있다.

예를 들어, 레퍼런스에 나와있듯이 사내 유저를 대상으로만 traffic이 일정 비율로 분산되도록 하고, 그 외 유저는 오직 기존 서비스에만 접근되도록 설정할 수 있다.

  • destination에 host와 함께 subset이 설정되어 있다. DestinationRule에서 이 subset을 정의한다. 여기 destination의 host는 virtual service host와 달리, (Istio service discovery에) “실제 존재하는” destination이어야 한다.

DestinationRule은 트래픽이 “실제로” 향하는 도착지를 설정하는 커스텀 리소스다.

  • 특정 host에 대하여 subset들을 정의한다. 2개의 subset을 정의하는데 (pod의) release label 값을 기준으로 구분한다.

Run Canary Deployment

Canary Deployment를 위한 모든 리소스들을 정의 및 수정했다. 드디어 Canary Deployment를 실행할 때가 되었다.

helm upgrade command를 이용하여 배포하도록 하자.

helm-diff plugin을 설치하면 helm diff upgrade 커맨드를 통해 배포 전 diff를 확인할 수 있다

참고: PoC 시점에는 weight가 99:1일 경우 정상적으로 트래픽이 분산되는지 확인하기 어려우므로, 50:50으로 시작하기를 추천한다.

아래는 배포 성공 후 일부 결과이다.

deployment를 조회했을 때
pod을 조회했을 때

실제로 http 요청을 보내봤을 때, canary pod으로도 요청이 전달되는 것을 확인할 수 있었다. (관련 캡처는 첨부하지 못 헀다 😞)

트래픽 분산 확인을 위해 traffic load를 generate해보고 싶다면, 아래 링크의 loadgen.sh 파일을 참고해도 좋다.

https://github.com/istio/istio/tree/release-1.13/samples/helloworld

실제 프로덕션 배포라고 가정한다면, Canary 배포가 정상적으로 완료되었음을 확인 후 Canary pod으로 흘러들어오는 요청들을 모니터링 해야한다

변경된 코드 및 기능에 대하여 API가 예상대로 동작하는지 확인한다. 장애가 발생하더라도 오직 1%의 유저들만 장애를 겪을 것이다. Canary 배포를 하지 않았다면 모든 유저가 동시에 장애를 겪을 것이다.

  1. 새로운 버전의 서버가 안정적일 경우: Canary deployment를 제거하고 stable deployment를 새로운 버전의 image로 배포한다
  2. 새로운 버전의 서버에서 장애가 발생할 경우: Canary deployment를 제거하고 stable deployment는 기존 버전의 image로 유지한다 (bugfix한 image push 후 Canary deployment 다시 진행)

Use Helm template with values

Canary deployment 관련 설정값들을 쉽게 조절하기 위해, Helm의 template을 활용할 수 있다.

아래와 같이 사용한다.

# templates/deployment.yaml
{{- if .Values.canary.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: helloworld-canary
{{- end }}
# templates/istio.yaml
# ...
- destination:
host: helloworld
subset: stable
weight: {{ sub 100 .Values.canary.weight }}
- destination:
host: helloworld
subset: canary
weight: {{ .Values.canary.weight }}

sub는 뺼셈 연산 함수다: 100-weight 계산 결과로 evaluate된다

# values.yaml
# ...

canary:
enabled: true
weight: 1

Conclusion

Istio의 Virtual Service, Destination Rule 등의 커스텀 리소스를 사용하여 Kubernetes에서 Canary Deployment를 수행하는 방법을 알아보았다.

Helm template을 활용하여 좀더 효율적이고 편리한 배포 설정 변경도 간단히 살펴보았다.

PoC를 진행하면서 느낀 몇 가지 주의사항을 소개한다.

주의사항

  • Canary 배포를 할 경우, 당연히 pod의 개수가 1개 이상 증가한다. 이에 따라 DB Connection max size를 초과하는 등의 이슈가 발생하지 않는지 확인해볼 필요가 있다. (특히 deployment가 2개인 상태에서 rollout을 할 경우, pod이 2개 더 증가한다)
  • Practically, 여러분의 서버는 일반적으로 환경 변수들을 사용할 것이다. 새로운 버전의 서버에는 환경 변수가 추가되었을 지도 모른다. 즉, 기존 서버의 환경 변수와 Canary 배포의 환경 변수는 서로 다르게 설정할 수 있을 필요가 있다. (환경 변수 뿐만 아니라 다른 설정값들도 확인해보자)

좀더 나아가, Canary Deployment 기능을 제공하는 툴들도 많이 존재하는 듯 하다

Flagger: https://docs.flagger.app/

ArgoCD: https://argoproj.github.io/argo-rollouts/features/canary/

위와 같은 툴을 사용할 때에도 위에서 다루어본 지식이 도움이 되기를 기대해본다.

마지막으로 여담인데, Canary deployment라는 이름의 기원이 궁금하지 않은가? Canary(한국에서는 카나리아)는 새의 이름이다. 이것만 알아도 어느 정도는 유추가 가능하다 (미지의 구역에 Canary를 먼저 보내보기..)

더 궁금하면 아래 포스트에서 History 섹션을 읽어보자.

--

--