-
[Kubernetes] Canary 배포 환경 구성개발/Infra 2022. 12. 30. 11:09
0. 개요
- Canary 배포란?
- 구 버전의 서비스와 새 버전의 서비스를 동시 구성, 일부 트래픽을 새 버전으로 분산하여 에러 여부 판단 및 모니터링 하는 배포방식
- 보통 구 버전은 PRODUCTION / 새 버전은 CANARY 로 서비스를 칭한다.
그림 출처: https://medium.com/@domi.stoehr/canary-deployments-on-kubernetes-without-service-mesh-425b7e4cc862 - k8s의 nginx-ingress-controller를 통해 트래픽을 분산할 수 있다.
- weight 기반: 100 - X % / X % 로 트래픽 분산 (random)
- cookie 기반: 요청의 cookie value를 확인하고 production / canary 서비스로 분산
- header 기반: 요청의 header value를 확인하고 production / canary 서비스로 분산
1. 테스트용 프로젝트 생성
- 간단하게 2개의 프로젝트를 구성한다.
- 동일한 API path를 가졌지만, 구 버전과 새 버전의 응답값이 다르게 (production / canary 구분 가능하도록) 구성했다.
@RestController public class ProductionController { @GetMapping("/test") public ResponseEntity<String> test() { return new ResponseEntity<String>("[PRODUCTION] requested.", HttpStatus.OK); } }
@RestController public class CanaryController { @GetMapping("/test") public ResponseEntity<String> test() { return new ResponseEntity<String>("[CANARY] requested.", HttpStatus.OK); } }
2. Docker build
- 해당 서비스를 k8s에 올리기 위해 Docker image를 만든다.
- Dockerfile도 간단하게 만든다..!
FROM adoptopenjdk/openjdk11 ARG JAR_FILE_PATH=./build/libs/TestProject-0.0.1-SNAPSHOT.jar COPY ${JAR_FILE_PATH} TestProject-0.0.1-SNAPSHOT.jar ENTRYPOINT ["java","-jar","TestProject-0.0.1-SNAPSHOT.jar"]
- 해당 도커파일로 이미지를 빌드한다.
- 나는 구 버전을 test-project:0.0.1, 새 버전을 test-project-0.0.2 로 빌드했다.
$ docker build -t test-project:0.0.1 . ... $ docker build -t test-project:0.0.2 . ... $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE test-project 0.0.1 ... 1 minute ago 459MB test-project 0.0.2 ... 1 minute ago 459MB
3. Kubernetes 배포
- 구 버전 / 새 버전의 서비스 총 2개의 서비스를 Kubernetes에 올리기 위해 yaml 파일을 만든다. (커맨드로도 가능하다.)
- 아래 예시는 k8s deployment 생성과 service 노출(등록) 설정이다. production / canary 서비스 각각 이미지가 다르다.
- production-service.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: svc-production spec: replicas: 1 selector: matchLabels: app: svc-production template: metadata: labels: app: svc-production spec: containers: - name: svc-production image: test-project:0.0.1 ports: - containerPort: 8080 --- apiVersion: v1 kind: Service metadata: name: svc-production spec: type: NodePort selector: app: svc-production ports: - port: 8080
- canary-service.yaml
apiVersion: apps/v1 kind: Deployment metadata: name: svc-canary spec: replicas: 1 selector: matchLabels: app: svc-canary template: metadata: labels: app: svc-canary spec: containers: - name: svc-canary image: test-project:0.0.2 ports: - containerPort: 8080 --- apiVersion: v1 kind: Service metadata: name: svc-canary spec: type: NodePort selector: app: svc-canary ports: - port: 8080
- 이제 위 2개의 파일을 가지고 서비스를 만들어보자
$ kubectl apply -f production-service.yaml deployment.apps/svc-production created service/svc-production created $ kubectl apply -f canary-service.yaml deployment.apps/svc-canary created service/svc-canary created
- 서비스 확인 (deployment, service, pod, replicaset 모두 정상적으로 생성되었다.)
$ kubectl get all NAME READY STATUS RESTARTS AGE pod/svc-canary-... 1/1 Running 0 1m pod/svc-production-... 1/1 Running 0 1m NAME TYPE CLUSTER_IP EXTERNAL-IP PORT(S) AGE service/svc-canary-... NodePort 10.xxx.xxx.xxx <none> 8080:32309/TCP 1m service/svc-production-... NodePort 10.xxx.xxx.xxx <none> 8080:31373/TCP 1m NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/svc-canary-... 1/1 1 1 1m deployment.apps/svc-production-... 1/1 1 1 1m NAME DESIRED CURRENT READY AGE replicaset.apps/svc-canary-... 1 1 1 1m replicaset.apps/svc-production-... 1 1 1 1m
- 이제 해당 두 서비스를 클러스터 외부에서 접근하기 위해 ingress를 생성한다.
- Ingress도 서비스와 마찬가지로 각각 yaml 파일로 생성한다.
- production-ingress.yaml
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: production-ingress annotations: nginx.ingress.kubernetes.io/rewrite-target: / spec: rules: - host: canary-test.com http: paths: - path: / pathType: Prefix backend: service: name: svc-production port: number: 8080
- canary-ingress.yaml 파일을 생성하기 전에! production ingress 부터 생성해보자
$ kubectl apply -f production-ingress.yaml ingress.networking.k8s.io/production-ingress created ... $ kubectl get ingress NAME CLASS HOSTS ADDRESS PORTS AGE production-ingress nginx canary-test.com 192.xxx.xxx.xxx 80 1m
- ingress를 생성한 후 kubectl get ingress 로 ingress 목록을 확인 할 때, ADDRESS가 아직 매핑이 안되있을 수 있다. (최대 n분 이상 소요 될 수 있다고 하니 기다려보자.)
- ADDRESS가 매핑이 완료되었다면 production 서비스의 API를 호출해본다.
$ curl http://canary-test.com/test [PRODUCTION] requested.
- production 서버의 API 요청/응답이 정상적으로 이루어졌다.
- 만약 정상 응답이 되지 않는다면 리눅스의 hosts 파일에 해당 host를 등록해야한다.
$ vi /etc/hosts // 맨 아랫줄에 ip 호스트 등록 ... 127.0.0.1 localhost ... 192.xxx.xxx.xxx canary-test.com
4. Canary 배포 설정
- 앞에서 production 서비스를 배포하고 ingress 도 설정을 완료했으니 이제 canary 서비스의 ingress 생성 및 설정을 해야한다.
- 그 전에 nginx-ingress의 canary 관련 annotations를 간단히 살펴보자면..
annotations type description nginx.ingress.kubernetes.io/canary String 카나리 배포 적용 여부("true", "false") nginx.ingress.kubernetes.io/canary-by-header String header key 값 nginx.ingress.kubernetes.io/canary-by-header-value String header value 값 nginx.ingress.kubernetes.io/canary-by-header-pattern String 정규표현식을 사용한 header value 확인 nginx.ingress.kubernetes.io/canary-by-cookie String cookie 기반 라우팅 설정 nginx.ingress.kubernetes.io/canary-weight String weight 기반 라우팅 nginx.ingress.kubernetes.io/canary-weight-total String 총 weight 수. (default: 100)
canary-weight-total: 1000
canary-weight: 10 일 때,
1000번 요청 중 10번 요청이 canary 서비스로 요청됨출처: https://github.com/kubernetes/ingress-nginx/blob/main/docs/user-guide/nginx-configuration/annotations.md#canary
- canary-ingress.yaml
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: canary-ingress annotations: nginx.ingress.kubernetes.io/rewrite-target: / nginx.ingress.kubernetes.io/canary: "true" # enable canary # weight 기반 라우팅 (100번 요청 중 50번) nginx.ingress.kubernetes.io/canary-weight: "50" # cookie 기반 라우팅 (cookie에 canary=always 로 설정시 canary 서버로 요청, canary=never 일 때 production) nginx.ingress.kubernetes.io/canary-by-cookie: "canary" # header 기반 라우팅 (header에 target=canary 로 설정시 canary 서버로 요청, 아니면 production) nginx.ingress.kubernetes.io/canary-by-header: "target" nginx.ingress.kubernetes.io/canary-by-header-value: "canary" spec: rules: - host: canary-test.com http: paths: - path: / pathType: Prefix backend: service: name: svc-canary port: number: 8080
- 생성 및 적용
$ kubectl apply -f production-ingress.yaml ingress.networking.k8s.io/production-ingress created ... $ kubectl get ingress NAME CLASS HOSTS ADDRESS PORTS AGE production-ingress nginx canary-test.com 192.xxx.xxx.xxx 80 10m canary-ingress nginx canary-test.com 192.xxx.xxx.xxx 80 1m
- http://canary-test.com의 동일한 호스트로 production / canary 서비스가 canary 배포 구성이 완료되었다.
5. Test
5-1. weight 기반 라우팅
$ curl http://canary-test.com/test [PRODUCTION] requested. $ curl http://canary-test.com/test [CANARY] requested. $ curl http://canary-test.com/test [PRODUCTION] requested. $ curl http://canary-test.com/test [PRODUCTION] requested. $ curl http://canary-test.com/test [CANARY] requested.
5-2. cookie 기반 라우팅
$ curl --cookie "canary=always" canary-test.com/test [CANARY] requested. $ curl --cookie "canary=always" canary-test.com/test [CANARY] requested. $ curl --cookie "canary=always" canary-test.com/test [CANARY] requested. $ curl --cookie "canary=always" canary-test.com/test [CANARY] requested. $ curl --cookie "canary=never" canary-test.com/test [PRODUCTION] requested. $ curl --cookie "canary=never" canary-test.com/test [PRODUCTION] requested. $ curl --cookie "canary=never" canary-test.com/test [PRODUCTION] requested. $ curl --cookie "canary=never" canary-test.com/test [PRODUCTION] requested.
5-3. header 기반 라우팅
$ curl -H "target:canary" canary-test.com/test [CANARY] requested. $ curl -H "target:canary" canary-test.com/test [CANARY] requested. $ curl -H "target:canary" canary-test.com/test [CANARY] requested. $ curl -H "target:canary" canary-test.com/test [CANARY] requested. $ curl -H "target:canary" canary-test.com/test [CANARY] requested.
'개발 > Infra' 카테고리의 다른 글
어떤 캐싱을 도입할까? feat. 로컬캐시, Redis, 웹캐시 (0) 2023.01.07 - Canary 배포란?