개발/Infra

[Kubernetes] Canary 배포 환경 구성

지잉지잉 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.