최근 포스트

K8s + ALB 제로 다운타임 (무중단 배포) 구성하는법 (w. Demo 영상)

K8s + ALB 제로 다운타임 (무중단 배포) 구성하는법 (w. Demo 영상)
Photo by Denys Nevozhai / Unsplash

최신 Kubernetes의 Deployment 시스템은 완벽하다. 어플리케이션만 좋다면 아무 옵션 없이 무중단 배포를 구현할 수 있다.

하지만 Kubernetes는 ALB와 같이 외부적인 컴포넌트들과 결합한다면 많은 문제들이 발생한다.

이번 포스팅에서는 K8s와 AWS ALB을 같이 사용할때 Zero Down-time 배포를 구현하고자 한다.

    Demo 환경 설명

    설명을 돕기 위해 아래와 같이 환경을 준비하였다.

    영상의 화면 구성

    먼저 1번 ALB Response 는 0.1초 마다 현재 시간을 출력한 뒤 ALB에게 요청을 보낸다. 어플리케이션은 똑바로 응답했을 경우 Hello, bar를 출력하며 요쳥이 똑바로 처리되지 않은 경우 ALB는 502 혹은 504 오류를 보여준다.

    watch -n 0.1 "date +%H:%M:%S.%3N && curl https://..."

    1번 명령어

    2번 Kubectl shell 은 kubectl이 설치되어 있고 쿠버네티스 클러스터와 연결된 SSH 쉘이다. 우리는 이 쉘을 통해 Deployment를 재시작 한다.

    3번 Pod list 는 쿠버네티스 클러스터에 동작중인 Pod들을 표시한다. 우리는 이중 demo-dev-bar 로 시작하는 pod만 사용한다.

    watch -n 0.2 "kubectl get pods -n dev"

    3번 명령어

    4번 ALB Health Checks는 ALB의 Health Check 상태를 각 Pod의 IP와 함께 표시한다.

    watch -n 1 "aws elbv2 describe-target-health --target-group-arn arn:aws:... | jq '.TargetHealthDescriptions[] | { Target: .Target.Id, Health: .TargetHealth.State }'"

    4번 명령어

    어플리케이션+매니페스트

    쿠버네티스 클러스터에 동작중인 어플리케이션들과 동작을 위한 매니페스트 파일들은 깃허브에서 볼 수 있다.

    GitHub - cloudshit/demo-application: kustomization, github actions 써보았다
    kustomization, github actions 써보았다. Contribute to cloudshit/demo-application development by creating an account on GitHub.

    매니페스트 파일들 + (Github Actions로 이미지를 올려두었다)

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: bar-deployment
      labels:
        app: bar
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: bar-app
      template:
        metadata:
          labels:
            app: bar-app
        spec:
          containers:
          - name: bar
            image: ghcr.io/cloudshit/bar:latest
            ports:
            - containerPort: 8080

    스타팅 deployment.yml

    이 상태로 Rollout restart 를 입력하면 어떻게 될까. 놀랍게도 모든 요청을 잘 처리하는 것을 볼 수 있다.

    이유는 간단하다. K8s는 Rollup 배포를 상당히 잘 수행했고 어플리케이션도 빠르게 켜져주었기 때문이다.

    자 그럼 망가트려 보자.

    조건 1 - 느린 어플리케이션 시작

    어플리케이션이 시작되는 것을 의도적으로 느리게 만들어 보자.

    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) => {
      res.send('Hello, bar')
    })
    
    setTimeout(() => {
      app.listen(8080) // 30초 후 부터 tcp Listen한다
    }, 30 * 1000)

    bar 어플리케이션의 index.mjs 코드

    이 어플리케이션을 아무런 옵션 수정없이 그대로 restart 한다면 어떻게 될까?

    0:00
    /0:47

    아무 옵션 없이

    타이머가 15초를 가르킬때 부터 ALB에서 502에러가 표시되는 것을 알 수 있다. 이후 30초 후에 다시 돌아온다... 이유는 무엇일까?

    0:00
    /0:08

    이유는 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에게 알려준다는 의미이다.

    Pod Readiness Gate - AWS Load Balancer Controller

    AWS LBC Pod Readiness Gate

    먼저 target-health.alb.ingress.k8s.aws 에 LBC가 값을 자동으로 넣어주는 기능을 활성화하기 위해 다음과 같이 namespace label을 설정해준다.

    kubectl label namespace <네임스페이스> elbv2.k8s.aws/pod-readiness-gate-inject=enabled

    그 다음 deployment.yml을 다음과 같이 수정한다.

    target-health.alb.ingress.k8s.aws/<ingress명>_<service명>_<service포트>

    이렇게 생겼다. 오타가 나지않도록 조심해야된다.

    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: bar-deployment
      labels:
        app: bar
    spec:
      replicas: 2
      selector:
        matchLabels:
          app: bar-app
      template:
        metadata:
          labels:
            app: bar-app
        spec:
          readinessGates:
            - conditionType: target-health.alb.ingress.k8s.aws/<ingress명>_<service명>_<service포트>
          containers:
          - name: bar
            image: ghcr.io/cloudshit/bar:latest
            ports:
            - containerPort: 8080

    요런 식으로...

    자 이제 결과를 보자.

    0:00
    /1:53

    readinessGates 추가

    v1을 끄기 전에 v2 하나가 Healthy가 될때까지 쭉 대기하다가 v2가 Healthy가 될때 v1하나를 끈다.

    이처럼 readinessGates를 통해 사전 준비 과정이 오래 걸리는 어플리케이션의 대한 무중단 배포를 구현할 수 있다.

    이 과정을 그림으로 표현하면 다음과 같다.

    0:00
    /0:12

    (Initial 후 Unhealty 상태 생략)

    조건 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

    유료 구독하고 더 많은 내용을 확인해 보세요!

    이미 가입하셨나요? 여기를 눌러 로그인

    구독하고 더 많은 포스트들을 즐겨보세요!

    무료 가입 후 이메일로도 포스트를 보내드려요!
    your_name@example.com
    구독하기