K8s + ALB 제로 다운타임 (무중단 배포) 구성하는법 (w. Demo 영상)
최신 Kubernetes의 Deployment 시스템은 완벽하다. 어플리케이션만 좋다면 아무 옵션 없이 무중단 배포를 구현할 수 있다.
하지만 Kubernetes는 ALB와 같이 외부적인 컴포넌트들과 결합한다면 많은 문제들이 발생한다.
이번 포스팅에서는 K8s와 AWS ALB을 같이 사용할때 Zero Down-time 배포를 구현하고자 한다.
Demo 환경 설명
설명을 돕기 위해 아래와 같이 환경을 준비하였다.
영상의 화면 구성
먼저 1번 ALB Response
는 0.1초 마다 현재 시간을 출력한 뒤 ALB에게 요청을 보낸다. 어플리케이션은 똑바로 응답했을 경우 Hello, bar
를 출력하며 요쳥이 똑바로 처리되지 않은 경우 ALB는 502 혹은 504 오류를 보여준다.
2번 Kubectl shell
은 kubectl이 설치되어 있고 쿠버네티스 클러스터와 연결된 SSH 쉘이다. 우리는 이 쉘을 통해 Deployment를 재시작 한다.
3번 Pod list
는 쿠버네티스 클러스터에 동작중인 Pod들을 표시한다. 우리는 이중 demo-dev-bar
로 시작하는 pod만 사용한다.
4번 ALB Health Checks
는 ALB의 Health Check 상태를 각 Pod의 IP와 함께 표시한다.
어플리케이션+매니페스트
쿠버네티스 클러스터에 동작중인 어플리케이션들과 동작을 위한 매니페스트 파일들은 깃허브에서 볼 수 있다.
이 상태로 Rollout restart
를 입력하면 어떻게 될까. 놀랍게도 모든 요청을 잘 처리하는 것을 볼 수 있다.
이유는 간단하다. K8s는 Rollup 배포를 상당히 잘 수행했고 어플리케이션도 빠르게 켜져주었기 때문이다.
자 그럼 망가트려 보자.
조건 1 - 느린 어플리케이션 시작
어플리케이션이 시작되는 것을 의도적으로 느리게 만들어 보자.
이 어플리케이션을 아무런 옵션 수정없이 그대로 restart 한다면 어떻게 될까?
타이머가 15초를 가르킬때 부터 ALB에서 502에러가 표시되는 것을 알 수 있다. 이후 30초 후에 다시 돌아온다... 이유는 무엇일까?
이유는 k8s scheduler가 v1의 대체제 즉, v2가 완전히 실행되지 않았더라도, 기존 v1을 종료시켜버려 새로운 v2가 다시 정상 상태가 될때까지 아무것도 못하게 되기 때문이다.
이 문제는 k8s scheduler가 v2 pod가 켜지는 즉시 정상적인 상태라고 생각했기 때문이다.
그럼 우리는 이 문제를 해결하기 위해 k8s에게 언제가 정상적인 상태인지를 알려주는 설정을 해보자.
readinessGates - Pod가 Healthy할때까지 기다리기
k8s 기능인 readinessGates는 이 pod가 Healthy한 상태인지 Unhealthy한 상태인지를 scheduler에게 알려주는 기능이다.
(참고로 readiness fail는 지금 요청을 안받는다는 의미, liveness fail는 상태가 이미 메롱이라 아예 지워야 해야할때를 의미한다)
우리는 이 readinessGates에 AWS LBC에서 지원하는 target-health.alb.ingress.k8s.aws
를 연결해야 한다. ALB의 Health Check 결과를 k8s에게 알려준다는 의미이다.
먼저 target-health.alb.ingress.k8s.aws
에 LBC가 값을 자동으로 넣어주는 기능을 활성화하기 위해 다음과 같이 namespace label을 설정해준다.
kubectl label namespace <네임스페이스> elbv2.k8s.aws/pod-readiness-gate-inject=enabled
그 다음 deployment.yml을 다음과 같이 수정한다.
자 이제 결과를 보자.
v1을 끄기 전에 v2 하나가 Healthy가 될때까지 쭉 대기하다가 v2가 Healthy가 될때 v1하나를 끈다.
이처럼 readinessGates를 통해 사전 준비 과정이 오래 걸리는 어플리케이션의 대한 무중단 배포를 구현할 수 있다.
이 과정을 그림으로 표현하면 다음과 같다.
조건 2 - 느리고 종료후에도 남아있는 처리들
다음은 어플리케이션을 요청 처리에 상당히 느리게 만들어 보자.
import express from 'express'
import morgan from 'morgan'
const app = express()
app.use(morgan('combined'))
app.get('/health', (req, res) => {
res.send('Ok')
})
app.get('/bar/hello', (req, res) => {
setTimeout(() => {
res.send('Hello, bar') // 요청 처리까지 30초가 걸린다
}, 30 * 1000)
})
setTimeout(() => {
app.listen(8080)
}, 30 * 1000)
이제 다시 restart해보자. watch를 30초로 걸 수는 없기에 1번 터미널의 코드를 다음과 같이 수정했다.
while true; do
(curl -s https://... &)
sleep 0.1
done