Application 서비스를 운영하면 버그 패치나 기능 추가, 성능 개선 등으로 새 버전으로 업데이트하는 경우가 빈번하게 발생한다. 특히나 MSA 같은 환경에서는 개별 서비스들의 독립적인 배포가 잦기에 수많은 업데이트와 배포를 거치게 되며 좀 더 효율적인 배포 전략을 세워야한다.
이러한 업데이트에서는 '사용자 경험의 연속성'이 중요한 요소가 되는데, 업데이트 과정에 있어서 사용자가 백엔드 단에서 업데이트가 벌어지고 있다는 등의 사실을 인지하지 못한 채로 서비스를 끊임없이 이용할 수 있어야하기 때문이다.
이러한 요구를 충족시키기 위해 이른바 '무중단' 업데이트가 중요시 되는데, 이번에는 대표적인 Application 무중단 배포 전략 3가지를 간단한 실습과 함께 알아보도록 한다.
Rolling Update
기존 버전의 서비스 인스턴스를 하나씩 새 버전으로 순차적으로 전환하는 방식
기존 버전을 하나씩 내리고, 새로운 버전을 하나씩 올리는 과정을 반복하며 배포하게 된다.
장점
- 리소스 효율 : 서비스 인스턴스를 하나씩 전환하기에 리소스 사용량이 급격하게 증가하지는 않는다.
- 자동 지원 : Rolling Update는 Kubernetes의 Deployment가 기본적으로 적용하고 있는 배포전략이다.
단점
- 긴 전환 시간 : 하나씩 새 버전으로 전환하기에, 모든 인스턴스가 전환되기까지 긴 시간이 소요된다.
- 트래픽 조절 어려움 : 새 버전의 App이 일부 배포되면, 사용자가 어떤 버전에 접속할 지 통제가 불가능
- 어려운 롤백 : 새 버전에 문제가 발생한 경우, 이전 버전으로의 즉각적인 롤백이 어려움
구현
간단하게 아래와 같이 쿠버네티스의 deployment와 service를 통해 Rolling Update를 구현해볼 수 있다.
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
labels:
app: myapp
spec:
replicas: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # 지정된 replicas 수 이상으로 생성할 수 있는 최대 파드 수
maxUnavailable: 1 # 업데이트 중 사용할 수 없는 최대 파드 수
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myapp:v1
ports:
- containerPort: 8080
---
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: myapp-service
spec:
selector:
app: myapp
ports:
- port: 80
targetPort: 8080
type: ClusterIP
deployment에 5개의 replicas를 구성해 service와 연결해 구 버전 App의 운영상태를 만들 수 있다.
여기서 새 버전의 이미지로 교체하면 자동으로 rollout을 진행한다.
kubectl set image deployment/myapp myapp=myapp:v2
kubectl rollout status deployment/myapp
Blue/Green
두 개의 동일한 환경(Blue: 현재 버전, Green: 새 버전)을 준비하고, 트래픽을 한 번에 전환하는 방식
Green 환경에서의 테스트와 검증이 끝나면 트래픽을 Blue에서 Green으로 전환하게 된다.
장점
- 빠른 롤백 : 기존 Blue 환경을 유지하기에, Green 환경에 문제가 발생하면 빠르게 Blue로 롤백이 가능하다.
- 배포 테스트 가능 : Green 환경을 배포하고, 실제 트래픽을 전환하기 전에 검증과정을 거칠 수 있다.
- 트래픽 제어 가능 : Green 환경이 성공적으로 동작하는 지 확인 후에 전환할 수 있다.
단점
- 리소스 사용량 : 기존 운영환경(Blue)과 동일한 스펙의 환경을 동시에 띄워야하기에, 리소스 사용량이 2배가 된다.
구현
Blue/Green도 아래와 같이 쿠버네티스로 실습해볼 수 있다.
# blue-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-blue
labels:
app: myapp
version: blue
spec:
replicas: 3
selector:
matchLabels:
app: myapp
version: blue
template:
metadata:
labels:
app: myapp
version: blue
spec:
containers:
- name: myapp
image: myapp:v1
ports:
- containerPort: 8080
---
# green-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-green
labels:
app: myapp
version: green
spec:
replicas: 3
selector:
matchLabels:
app: myapp
version: green
template:
metadata:
labels:
app: myapp
version: green
spec:
containers:
- name: myapp
image: myapp:v2
ports:
- containerPort: 8080
---
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: myapp-service
spec:
selector:
app: myapp
version: blue # 초기에는 blue 버전으로 트래픽 전송
ports:
- port: 80
targetPort: 8080
type: ClusterIP
위와 같이 blue deployment와 green deployment를 구성하고, 초기에는 blue에 Service를 연결한다.
이제 이 상황에서 green deployment에 대한 테스트를 마쳐, blue에서 green으로 트래픽을 전환하는 과정을 거치게 된다.
kubectl patch service myapp-service -p '{"spec":{"selector":{"version":"green"}}}'
이렇게 되면 Service에서 기존 blue로 향하던 트래픽이 green으로 한번에 전환되는 것을 볼 수 있다.
Canary
새 버전을 소수나 일부 사용자들에게 먼저 배포하고, 이상이 없음을 확인하면 점진적으로 새 버전의 배포를 확대하는 방식
탄광 노동자들에게 위험을 미리 감지하고 알려주는 역할의 '카나리아'에서 그 이름과 방식을 따온 것이다.
장점
- 빠른 롤백 : Canary로의 트래픽을 유동적으로 조절하면서 빠르게 복구 가능
- 실제 사용자를 대상으로 안전한 테스트 수행 가능 : 일부 사용자만 새 버전을 사용하기에 실제 환경에서 테스트를 수행할 수 있다.
- 배포의 안정성 : 점진적으로 트래픽 비율을 조절하면서 실제 서비스의 영향을 최소화할 수 있음
단점
- 구성의 복잡성 : Ingress나 서비스 라우팅을 조절해야하므로 구성이 복잡할 수 있음
- 배포 시간 증가 : 비율을 점차 늘리면서 안정성을 확인하는 과정에서 배포 시간이 증가하게 됨
구현
아래와 같이 쿠버네티스에서 Canary를 구현해볼 수 있다.
구 버전의 Deployment와 Service를 각각 생성해 기존 운영 환경을 만든다.
# stable-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-stable
labels:
app: myapp
version: stable
spec:
replicas: 9
selector:
matchLabels:
app: myapp
version: stable
template:
metadata:
labels:
app: myapp
version: stable
spec:
containers:
- name: myapp
image: myapp:v1
ports:
- containerPort: 8080
---
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: myapp-service
spec:
selector:
app: myapp
ports:
- port: 80
targetPort: 8080
type: ClusterIP
그리고 새 버전의 Deployment를 생성해서 앞서 만든 Service와 연결하면, 이 Service를 통해 구 버전과 신 버전에 모두 트래픽이 연결된다.
# canary-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-canary
labels:
app: myapp
version: canary
spec:
replicas: 1 # 전체 트래픽의 10%를 수신
selector:
matchLabels:
app: myapp
version: canary
template:
metadata:
labels:
app: myapp
version: canary
spec:
containers:
- name: myapp
image: myapp:v2
ports:
- containerPort: 8080
이렇게 적용하면 myapp-service로는 구 버전과 신 버전이 모두 연결되며, 구 버전의 Deployment와 신 버전의 Deployment의 replicas의 수에 따라 트래픽의 비율이 분산되는 것으로 볼 수 있다.
이제 이 상황에서 테스트를 진행하며, Canary 버전에 문제가 없는 경우에 비율을 조정하며 점진적으로 새 버전으로 완전한 전환을 이루게 된다.
# 7:3
kubectl scale deployment myapp-stable --replicas=7
kubectl scale deployment myapp-canary --replicas=3
# 완전 전환
kubectl scale deployment myapp-stable --replicas=0
kubectl scale deployment myapp-canary --replicas=10
# 완전 전환의 후처리
kubectl delete deployment myapp-stable
kubectl apply -f stable-deployment.yaml # 이미지 버전을 v2로 업데이트한 후
kubectl delete deployment myapp-canary
또한, Canary는 쿠버네티스에서 Ingress를 통해 구현할 수 있다. Nginx Ingress Controller를 이용해서 구현해보도록 한다.
nginx ingress controller를 설치한 환경에서 아래와 같이 예제를 배포해본다.
# stable-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-stable
labels:
app: myapp
version: stable
spec:
replicas: 3
selector:
matchLabels:
app: myapp
version: stable
template:
metadata:
labels:
app: myapp
version: stable
spec:
containers:
- name: myapp
image: myapp:v1
ports:
- containerPort: 8080
---
# stable-service.yaml
apiVersion: v1
kind: Service
metadata:
name: myapp-stable
spec:
selector:
app: myapp
version: stable
ports:
- port: 80
targetPort: 8080
---
# main-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-main-ingress
spec:
ingressClassName: nginx
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-stable
port:
number: 80
다음으로 Canary 버전을 배포한다. 여기에서 Canary Ingress를 구 버전으로의 ingress와 동일한 host로 전달되도록 하며 Canary를 구현한다.
# canary-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-canary
labels:
app: myapp
version: canary
spec:
replicas: 3
selector:
matchLabels:
app: myapp
version: canary
template:
metadata:
labels:
app: myapp
version: canary
spec:
containers:
- name: myapp
image: myapp:v2
ports:
- containerPort: 8080
---
# canary-service.yaml
apiVersion: v1
kind: Service
metadata:
name: myapp-canary
spec:
selector:
app: myapp
version: canary
ports:
- port: 80
targetPort: 8080
---
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-ingress
annotations:
kubernetes.io/ingress.class: "nginx"
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "20" # canary로의 가중치 (현재 20%로 설정)
spec:
ingressClassName: nginx
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-canary
port:
number: 80
---
이렇게 되면 2개의 ingress로 구버전(8), 신버전(2) 비율만큼의 트래픽 전달이 구현된다.
ingress를 통한 Canary의 트래픽 비율 조절은 ingress의 annotation에 작성된 `canary-weight` 값을 수정함으로써 가능하다.
kubectl patch ingress myapp-ingress -p \
'{"metadata":{"annotations":{"nginx.ingress.kubernetes.io/canary-weight":"50"}}}'
비율 뿐만 아니라 쿠키, 헤더값을 기반으로도 canary로 트래픽 전달을 구현할 수 있다.
이에 사용되는 annotation값은 Nginx Ingress Controller의 Docs를 참고
이렇게 Application의 무중단 배포 전략 3가지를 알아보았다. 배포 전략에 완벽한 정답은 없다.
배포되는 Application의 특성과 리소스의 상황 등의 요소들을 고려해서 적절한 전략을 선택하는 것이 중요하게 작용할 것이다.