티스토리 뷰

728x90
반응형

서론

Amazon EKS는 Kubernetes를 기반으로 동작하는 PaaS 플랫폼으로 Kubernetes가 제공하는 Rolling Update 방식을 그대로 적용하여 기본 제로 다운타임 배포를 구현할 수 있다. 다만, EKS 자체의 제로 다운타임배포는 가능하지만 배포 장애에 대한 대응방안과 Managed Service 간의 배포 호환성 등의 검증은 여전히 필요하다. 예를 들어 EKS의 Ingress type으로 ALB Ingress Controller 등과 연계하여 아키텍처를 설계할때 Pod로 라우팅하는 ALB Ingress의 TargetGroup이 재생성 되는 과정에서 연결 장애가 발생할 수 있기 때문이다. 즉 EKS는 연결되어 있는 Managed Service를 포함하여 제로 다운타임을 함께 고민해야 한다. Amazon EKS에서 제로 다운타임을 구현하기 위해 다음을 검토한다.

a. terminationGracePeriodSeconds를 구성한다. (Pod 내 Graceful Shutdown 지원) EKS내 Graceful Redeploy를 구현하기 위해서는 현재 Pod에서 처리 중인 요청에 대한 처리를 완료하고 새로운 요청을 받지 않도록 해야 한다. 이때 terminationGracePeriodSeconds는 kubelet에서 pod에 SIGTERM을 보낸후에 일정시간동안 graceful shutdown이 되지 않는다면 강제로 SIGKILL을 보내서 pod를 종료하게 하는데 바로 이 Graceful Shutdown이 발생하기 까지 얼마나 대기를 시킬 것인지를 설정하는 것이다. (default 30 sec)

b. ReadinessGates를 구성한다. Deployment를 이용해서 배포할 때 maxSurge는 deployment에 설정되어 있는 기본 pod개수보다 여분의 pod가 몇개가 더 추가될 수 있는지를 설정할 수 있다. maxUnavailable는 업데이트하는 동안 몇 개의 pod가 이용 불가능하게 되어도 되는지를 설정하는데 사용된다. 이 두개의 옵션을 운영중인 서비스의 특성에 맞게 적절히 조절해 주어야지 항상 일정 개수 이상의 pod가 이용가능하게 되기 때문에 배포중 트래픽 유실이 없게 된다. 둘 다 한꺼번에 0으로 설정되면 pod가 존재하지 않는 경우가 발생하기 때문에 한꺼번에 0으로 설정할 수는 없다. 이를 기반으로 배포를 진행할 때, ReadinessGates는 배포 상태를 확인하여 무중단 배포를 실현하게 한다. 컨테이너 이미지가 손상되었거나 구성에 문제가 생겼을 경우, Pod는 새로 시작된 Pod가 Pod ReadinessGates를 통과할 때까지 대기하도록 한다. 이는 Pod가 Loadbalancer의 상태 확인에 응답하는 경우에만 발생한다. Pod가 응답하지 않거나 상태 확인이 잘못 구성된 경우 ReadinessGates 조건을 충족할 수 없으며 롤아웃을 계속할 수 없다.

c. ReadinessProbe, LivenessProbe를 구성한다. ReadinessProbe는 Pod가 정상 기동되었는지 여부를 확인하는 Probe이며, LivenessProbe는 Pod가 정상동작하는지 여부를 확인하는 Probe이다. 대체로 Readiness는 Port 기반으로 확인하고 Liveness는 httpGet으로 응답을 검증한다.

d. Hook(preStop, postStart)를 활용하여 Pod의 서비스 상태를 지속시킨다.

e. minReadySecondsPod가 배포되고, 서비스를 시작하는데 딜레이를 주는 옵션이다. 이를 통해 서비스의 준비 시간을 확보할 수 있다.


본론

Amazon EKS의 무중단 배포를 이해하기 위해서는 먼저 Pod의 LifeCycle에 대한 이해가 있어야 한다. Amazon EKS에 애플리케이션을 배포하고 신규 버전을 배포하는 과정에 대해서는 다음을 참고한다.

[② 클라우드 마스터/ⓐ AWS] - Amazon EKS 30분만에 구성하기 (CloudFormation)

LifeCycle

먼저 Kubernetes의 최소 배포단위인 Pod는 다음과 같은 라이프사이클을 기반으로 배포되고 배치된다.

a. Pending : Pod가 쿠버네티스 클러스터에서 승인되었지만, 하나 이상의 컨테이너가 설정되지 않았고 실행할 준비가 되지 않았다. 여기에는 Pod가 스케줄되기 이전까지의 시간 뿐만 아니라 네트워크를 통한 컨테이너 이미지 다운로드 시간도 포함된다.

b. Running : Pod가 노드에 바인딩되었고, 모든 컨테이너가 생성되었다. 적어도 하나의 컨테이너가 아직 실행 중이거나, 시작 또는 재시작 중에 있다.

c. Succeeded : Pod에 있는 모든 컨테이너들이 성공적으로 종료되었고, 재시작되지 않을 것이다.

d. Failed : Pod에 있는 모든 컨테이너가 종료되었고, 적어도 하나 이상의 컨테이너가 실패로 종료되었다. 즉, 해당 컨테이너는 non-zero 상태로 빠져나왔거나(exited) 시스템에 의해서 종료(terminated)되었다.

e. Unknown : 어떤 이유에 의해서 파드의 상태를 얻을 수 없다. 이 단계는 일반적으로 파드가 실행되어야 하는 노드와의 통신 오류로 인해 발생한다.

이와 같이 Pod가 기동되고 배치되면 Pod는 하나의 PodStatus를 가지며, 그것은 파드가 통과했거나 통과하지 못한 조건에 대한 PodConditions 배열을 갖는다.

a. PodScheduled : 파드가 노드에 스케줄되었다. 
b. ContainersReady : 파드의 모든 컨테이너가 준비되었다. 
c. Initialized : 모든 초기화 컨테이너가 성공적으로 시작되었다. 
d. Ready : 파드는 요청을 처리할 수 있으며 일치하는 모든 서비스의 로드 밸런싱 풀에 추가되어야 한다.

노드가 죽거나 클러스터의 나머지와의 연결이 끊어지면, 쿠버네티스는 손실된 노드의 모든 Pod의 phase 를 Failed로 설정하는 정책을 적용한다.

다음으로 컨테이너이다. 전체 파드의 단계뿐 아니라, 쿠버네티스는 파드 내부의 각 컨테이너 상태를 추적한다. 컨테이너 라이프사이클 훅(hook)을 사용하여 컨테이너 라이프사이클의 특정 지점에서 실행할 이벤트를 트리거할 수 있다.

일단 스케줄러가 노드에 파드를 할당하면, kubelet은 컨테이너 런타임을 사용하여 해당 파드에 대한 컨테이너 생성을 시작한다. 표시될 수 있는 세 가지 컨테이너 상태는 Waiting, Running 그리고 Terminated 이다.

a. Waiting : Waiting 상태의 컨테이너는 시작을 완료하는데 필요한 작업(예를 들어, 컨테이너 이미지 레지스트리에서 컨테이너 이미지 가져오거나, 시크릿(Secret) 데이터를 적용하는 작업)을 계속 실행하고 있는 중이다. kubectl을 사용하여 컨테이너가 Waiting인 파드를 쿼리하면, 컨테이너가 해당 상태에 있는 이유를 요약하는 Reason 필드도 표시된다.

b. Running : Running 상태는 컨테이너가 문제없이 실행되고 있음을 나타낸다. postStart hook이 구성되어 있었다면, 이미 실행되고 완료되었다. kubectl을 사용하여 컨테이너가 Running 인 파드를 쿼리하면, 컨테이너가 Running 상태에 진입한 시기에 대한 정보도 볼 수 있다.

c. Terminated : Terminated 상태의 컨테이너는 실행을 시작한 다음 완료될 때까지 실행되었거나 어떤 이유로 실패했다. kubectl 을 사용하여 컨테이너가 Terminated 인 파드를 쿼리하면, 이유와 종료 코드 그리고 해당 컨테이너의 실행 기간에 대한 시작과 종료 시간이 표시된다. 컨테이너에 구성된 preStop 훅이 있는 경우, 컨테이너가 Terminated 상태에 들어가기 전에 실행된다.

이와 같은 컨테이너를 관리하는 정책을 restartPolicy라는 필드로 관리할 수 있다.사용 가능한 값은 Always, OnFailure 그리고 Never이다. 기본값은 Always이다.

restartPolicy 는 파드 내 모든 컨테이너에 적용된다. restartPolicy는 동일한 노드에서 kubelet에 의한 컨테이너 재시작만을 의미한다. 파드의 컨테이너가 종료된 후, kubelet은 5분으로 제한되는 BackOff Delay(10초, 20초, 40초, …)로 컨테이너를 재시작한다. 컨테이너가 10분 동안 아무런 문제없이 실행되면, kubelet은 해당 컨테이너의 재시작 백오프 타이머를 재설정한다.

RollingUpdate

컨테이너와 Pod의 라이프사이클에 대해 알아보았으니 이를 기반으로 Amazon EKS에서는 어떻게 Pod와 Container의 상태를 유지할 수 있는지 알아보자. 테스트는 다음과 같이 진행한다. 먼저 기존 HTTPD APP을 배포한 후 신규로 NGINX APP 이미지를 배포하고 그 과정에 서비스 다운타임이 발생하는지 여부를 검증한다.

테스트에 활용한 yaml 파일은 다음과 같다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  selector:
    matchLabels:
      run: my-nginx
  replicas: 2
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
      - name: my-nginx
        image: httpd
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: my-nginx
  labels:
    run: my-nginx
spec:
  ports:
  - port: 80
    protocol: TCP
    #  type: NodePort
  selector:
    run: my-nginx
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: "ingress-app"
  annotations:
    kubernetes.io/ingress.class: alb
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/subnets: subnet-09ab6e9681ccc1d5f,subnet-03a0540fedf080b86
spec:
  rules:
   - http:
      paths:
        - path: /*
          backend:
            serviceName: "my-nginx"
            servicePort: 80

Deployment의 .spec.template.spec.containers.image의 httpd를 nginx로 변경하여 배포하는 테스트를 진행한다. 먼저 httpd app을 배포한 결과이다.

아래와 같이 pod 2개를 포함하는 Deployment가 배포되어 있다.

[root@ip-192-168-114-198 yaml]# kubectl get pods -o wide
NAME                        READY   STATUS    RESTARTS   AGE   IP                NODE                                               NOMINATED NODE   READINESS GATES
my-nginx-7dc7954b86-c5wrp   1/1     Running   0          71s   192.168.79.209    ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           <none>
my-nginx-7dc7954b86-v7jv2   1/1     Running   0          66s   192.168.145.142   ip-192-168-146-0.ap-northeast-2.compute.internal   <none>           <none>
[root@ip-192-168-114-198 yaml]#

다음으로 nginx app을 배포해 보자. 아래와 같이 Pod가 재기동되는 가운데 502 Bad Gateway와 504 Gateway Timeout이 발생하는 것을 알 수 있다. 물론 수초뒤 화면은 정상상태로 변경된다.

이는 EKS 자체적인 Rolling Update는 물론 ALB Ingress Controller와의 연결 관계인 Target Group에 대한 draining과정으로 인해 발생한다.

ALB Ingress Controller는 EKS의 Ingress로써 동작하며, Pod로의 라우팅을 위해 신규도 배치된 Pod의 Replica 수를 유지하기 위해 이전 Pod를 draining 시키고 신규 Pod를 배치한다. 이때 draining이 발생하는 시점과 신규 Pod가 배치되는 시점이 겹쳐 위와 같은 502/504 응답을 받게 되는 것이다.

정리하자면, EKS Rolling Update는 다음과 같은 과정으로 신규 Pod가 배치된다.

a. Pod는 Rolling Update 규칙에 따라 Pod를 제거하고 생성하여 하나 이상의 Eunning Pod가 유지되도록 관리한다.
b. 새로운 버전의 Pod로 교체 될 때 ALB Ingress Controller는 Target Group에 Pod를 등록하기 위해 이전 Pod를 제거한다.
c
. 이때, 새 Pod는 정상적으로 기동이 완료 될때까지 initial 상태로 남아 있게 된다.
d
. 이전 Pod의 경우 draining 상태로 변경되어 Pod를 삭제한다. 이때 initial 상태의 Pod 기동이 완료되지 않아 502/504 에러를 발생시킨다.

이와 같은 문제를 해결하기 위해 EKS는 다음과 같은 두가지 해결방식을 제공한다.

ReadinessGates

readinessGates는 파드에 대한 status.condition 필드의 현재 상태에 따라 결정된다. 만약 쿠버네티스가 status.conditions 필드에서 해당하는 조건을 찾지 못한다면, 그 조건의 상태는 기본 값인 "False"가 된다.

kind: Pod
...
spec:
  readinessGates:
    - conditionType: "www.example.com/feature-1"
status:
  conditions:
    - type: Ready                              # 내장된 PodCondition이다
      status: "False"
      lastProbeTime: null
      lastTransitionTime: 2018-01-01T00:00:00Z
    - type: "www.example.com/feature-1"        # 추가적인 PodCondition
      status: "False"
      lastProbeTime: null
      lastTransitionTime: 2018-01-01T00:00:00Z
  containerStatuses:
    - containerID: docker://abcd...
      ready: true
...

사용자 지정 조건을 사용하는 파드의 경우, 다음 두 조건이 모두 적용되는 경우에 만 해당 파드가 준비된 것으로 평가된다.

a. readinessGates에 지정된 모든 조건들이 True 이다.
b. Pod의 컨테이너가 Ready 이나 적어도 한 개의 사용자 지정 조건이 빠졌거나 False 이면, kubelet은 Pod의 조건을 ContainerReady 로 설정한다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  selector:
    matchLabels:
      run: my-nginx
  replicas: 2
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
      - name: my-nginx
        image: nginx
        ports:
        - containerPort: 80
      readinessGates:
      - conditionType: target-health.alb.ingress.k8s.aws/ingress-app_my-nginx_80

버전 v1.1.6 부터 ALB Ingress Controller는 readinessGates를 도입했다 .이 기능은 EKS가 Rolling Update를 진행하는 과정에서 예기치 않은 문제 (예 : AWS API에 대한 Timeout 등)가 발생할 경우 배포를 중지하여 항상 정상상태의 Pod가 유지될 수 있게 한다.

readinessGates는 다음과 같이 추가할 수 있다.

- conditionType: target-health.alb.ingress.k8s.aws/[INGRESS_NAME]_[SERVICE_NAME]_[SERVICE_PORT]

위와 같이 추가한 후 Deployment를 배치하면 다음과 같이 Pod의 상태에서 ReadinessGates 정보를 확인할 수 있다. 아래는 pod가 배치되는 과정이다.

[root@ip-192-168-114-198 yaml]# kubectl get pods -o wide -w
NAME                       READY   STATUS    RESTARTS   AGE   IP                NODE                                               NOMINATED NODE   READINESS GATES
my-nginx-7cdd7f9d8-442lq   1/1     Running   0          45m   192.168.109.191   ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           <none>
my-nginx-7cdd7f9d8-86xzz   1/1     Running   0          45m   192.168.143.73    ip-192-168-146-0.ap-northeast-2.compute.internal   <none>           <none>
my-nginx-6dc7bdd648-wsxs9   0/1     Pending   0          0s    <none>            <none>                                             <none>           0/1
my-nginx-6dc7bdd648-wsxs9   0/1     Pending   0          0s    <none>            ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           0/1
my-nginx-6dc7bdd648-wsxs9   0/1     ContainerCreating   0          0s    <none>            ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           0/1
my-nginx-6dc7bdd648-wsxs9   0/1     Terminating         0          1s    <none>            ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           0/1
my-nginx-75b764b685-6v2wc   0/1     Pending             0          0s    <none>            <none>                                             <none>           0/1
my-nginx-75b764b685-6v2wc   0/1     Pending             0          0s    <none>            ip-192-168-146-0.ap-northeast-2.compute.internal   <none>           0/1
my-nginx-75b764b685-6v2wc   0/1     ContainerCreating   0          0s    <none>            ip-192-168-146-0.ap-northeast-2.compute.internal   <none>           0/1
my-nginx-6dc7bdd648-wsxs9   0/1     Terminating         0          2s    <none>            ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           0/1
my-nginx-6dc7bdd648-wsxs9   0/1     Terminating         0          2s    <none>            ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           0/1
my-nginx-75b764b685-6v2wc   1/1     Running             0          5s    192.168.186.167   ip-192-168-146-0.ap-northeast-2.compute.internal   <none>           0/1
my-nginx-75b764b685-6v2wc   1/1     Running             0          21s   192.168.186.167   ip-192-168-146-0.ap-northeast-2.compute.internal   <none>           1/1
my-nginx-75b764b685-6v2wc   1/1     Running             0          21s   192.168.186.167   ip-192-168-146-0.ap-northeast-2.compute.internal   <none>           1/1
my-nginx-7cdd7f9d8-86xzz    1/1     Terminating         0          45m   192.168.143.73    ip-192-168-146-0.ap-northeast-2.compute.internal   <none>           <none>
my-nginx-75b764b685-2ncpv   0/1     Pending             0          0s    <none>            <none>                                             <none>           0/1
my-nginx-75b764b685-2ncpv   0/1     Pending             0          0s    <none>            ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           0/1
my-nginx-75b764b685-2ncpv   0/1     ContainerCreating   0          0s    <none>            ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           0/1
my-nginx-7cdd7f9d8-86xzz    0/1     Terminating         0          45m   192.168.143.73    ip-192-168-146-0.ap-northeast-2.compute.internal   <none>           <none>
my-nginx-7cdd7f9d8-86xzz    0/1     Terminating         0          45m   192.168.143.73    ip-192-168-146-0.ap-northeast-2.compute.internal   <none>           <none>
my-nginx-7cdd7f9d8-86xzz    0/1     Terminating         0          45m   192.168.143.73    ip-192-168-146-0.ap-northeast-2.compute.internal   <none>           <none>
my-nginx-75b764b685-2ncpv   1/1     Running             0          4s    192.168.107.130   ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           0/1
my-nginx-75b764b685-2ncpv   1/1     Running             0          19s   192.168.107.130   ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           1/1
my-nginx-75b764b685-2ncpv   1/1     Running             0          19s   192.168.107.130   ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           1/1
my-nginx-7cdd7f9d8-442lq    1/1     Terminating         0          45m   192.168.109.191   ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           <none>
my-nginx-7cdd7f9d8-442lq    0/1     Terminating         0          45m   192.168.109.191   ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           <none>
my-nginx-7cdd7f9d8-442lq    0/1     Terminating         0          46m   192.168.109.191   ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           <none>
my-nginx-7cdd7f9d8-442lq    0/1     Terminating         0          46m   192.168.109.191   ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           <none>
[root@ip-192-168-114-198 yaml]# kubectl get pods -o wide -w
NAME                        READY   STATUS    RESTARTS   AGE     IP                NODE                                               NOMINATED NODE   READINESS GATES
my-nginx-75b764b685-2ncpv   1/1     Running   0          3m13s   192.168.107.130   ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           1/1
my-nginx-75b764b685-6v2wc   1/1     Running   0          3m34s   192.168.186.167   ip-192-168-146-0.ap-northeast-2.compute.internal   <none>           1/1
[root@ip-192-168-114-198 yaml]#

위와 같이 READINESS GATES가 <none>에서 0/1 → 1/1로 변경되는 것을 확인할 수 있다. READY 상태가 된 이후 이전 버전의 pod를 terminating한다. 다만, 이와 같이 Readiness Gates가 등록되었음에도 Service는 여전히 502/504 에러가 발생하는 것을 볼 수 있다.

사실 Readiness Gates는 Pod의 배포 여부를 검증하는 것이지,  ALB Ingress Controller와의 Rolling 업데이트와는 별개의 동작이므로 다운타임이 발생할 수 있다.

Pod LifeCycle

다음은 lifecycle을 통해 hook을 실행하는 yaml 형식이다. 컨테이너가 lifecycle의 이벤트를 인지하고 이에 상응되는 이벤트가 실행될 때 command를 실행할 수 있다.

Gracefule Redeploy를 실현하기 위해서는 먼저 Pod가 종료되는 순서에 대한 이해가 있어야 한다.

1 - 포드가 종료 상태로 전환되고 새 트래픽 수신을 중지한다. 컨테이너는 여전히 포드 내에서 실행 중인 상태일 수 있다.

2 - REST 또는 HTTP 요청은 preStop 후크가 실행되고 포드 내부의 컨테이너로 전송된다.

3 - SIGTERM 신호가 포드로 전송되고 컨테이너가 곧 닫힐 것임을 인식한다.

4 - Kubernetes는 유예 기간 (terminationGracePeriodSeconds)을 기다린다 . 이 대기는 preStop 후크 및 SIGTERM (기본값 30 초)과 병렬이다. 따라서 Kubernetes는 이러한 작업이 완료 될 때까지 기다리지 않는다.

5 - SIGKILL 신호가 포드로 전송되고 포드가 제거된다. 컨테이너가 유예 기간 후에도 계속 실행 중이면 SIGKILL에 의해 포드가 강제로 제거되고 종료가 완료된다.

컨테이너 hook은 크게 두가지가 존재한다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
spec:
  selector:
    matchLabels:
      run: my-nginx
  replicas: 2
  minReadySeconds: 5
  template:
    metadata:
      labels:
        run: my-nginx
    spec:
      containers:
      - name: my-nginx
        image: nginx
        ports:
        - containerPort: 80
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sh", "-c", "sleep 40"]
      readinessGates:
      - conditionType: target-health.alb.ingress.k8s.aws/ingress-app_my-nginx_80
      terminationGracePeriodSeconds: 60

a. PostStart (컨테이너가 생성된 직후에 실행된다. 그러나, hook이 컨테이너 엔트리포인트에 앞서서 실행된다는 보장은 없다. 즉, 파라미터는 핸들러에 전달되지 않는다.)

b. PreStop (이 hook은 API 요청이나 활성 프로브(liveness probe) 실패, 선점, 자원 경합 등의 관리 이벤트로 인해 컨테이너가 종료되기 직전에 호출된다. 컨테이너가 이미 terminated 또는 completed 상태인 경우에는 preStop 훅 요청이 실패한다. 그것은 동기적인 동작을 의미하는, 차단(blocking)을 수행하고 있으므로, 컨테이너를 중지하기 위한 신호가 전송되기 전에 완료되어야 한다. 파라미터는 핸들러에 전달되지 않는다.)

위 yaml 파일은 preStop hook과 함께 sleep command를 적용하여 Pod가 종료되기 전 40초를 대기 시키는 hook이다. 이와 같은 방법은 ALB가 Target Group에서 이전 버전을 완전히 제거 할 시간을 가질 수 있도록 한다.

그 밖에 서비스 시작전 즉 Pod가 기동이 완료 된 이후 일정 시간 딜레이를 주거나, 강제 종료를 방지하는 옵션들이 있다.

a. minReadySeconds (Pod가 배포되고, 서비스를 시작하는데 딜레이를 주는 옵션이다. 이를 통해 서비스의 준비 시간을 확보할 수 있다.)

b. terminationGracePeriodSeconds (kubelet이 Pod를 중지하기 위해 SIGTERM을 보낸 후 일정 시간동안 서비스가 중지 되지 않을 경우 SIGKILL을 전송하게 되는데, 이때까지의 대기시간을 terminationGracePeriodSeconds으로 구성할 수 있다. PreStop보다 우선순위가 높아 PreStop Sleep을 주더라도 terminationGracePeriodSeconds 시간이 넘어 갈 경우 강제로 SIGTERM이 날라가고 서비스가 중지된다. 따라서 terminationGracePeriodSeconds는 충분히 길제 구성해야 할 수 있으며, PreStop보다 최소 길어야 한다.)

위 구성으로 다시 재기동해보도록 하자.현재 nginx image가 배포되어 있는 상태이다.

yaml 파일에 pod lifecycle을 적용한 후 다음과 같이 재기동을 수행해 보자.

실제 pod의 동작 방식은 아래와 같다.

[root@ip-192-168-114-198 yaml]# kubectl get pods -o wide -w
NAME                        READY   STATUS    RESTARTS   AGE   IP               NODE                                               NOMINATED NODE   READINESS GATES
my-nginx-57d7f69d8d-kmlx6   1/1     Running   0          39s   192.168.143.73   ip-192-168-146-0.ap-northeast-2.compute.internal   <none>           1/1
my-nginx-57d7f69d8d-qmzlk   1/1     Running   0          61s   192.168.65.159   ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           1/1
my-nginx-656b8fdb58-9d44g   0/1     Pending   0          0s    <none>           <none>                                             <none>           0/1
my-nginx-656b8fdb58-9d44g   0/1     Pending   0          0s    <none>           ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           0/1
my-nginx-656b8fdb58-9d44g   0/1     ContainerCreating   0          0s    <none>           ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           0/1
my-nginx-656b8fdb58-9d44g   0/1     Terminating         0          2s    <none>           ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           0/1
my-nginx-878597468-qnkr6    0/1     Pending             0          0s    <none>           <none>                                             <none>           0/1
my-nginx-878597468-qnkr6    0/1     Pending             0          0s    <none>           ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           0/1
my-nginx-878597468-qnkr6    0/1     ContainerCreating   0          0s    <none>           ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           0/1
my-nginx-656b8fdb58-9d44g   0/1     Terminating         0          4s    <none>           ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           0/1
my-nginx-656b8fdb58-9d44g   0/1     Terminating         0          4s    <none>           ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           0/1
my-nginx-878597468-qnkr6    1/1     Running             0          6s    192.168.109.191   ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           0/1
my-nginx-878597468-qnkr6    1/1     Running             0          21s   192.168.109.191   ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           0/1
my-nginx-878597468-qnkr6    1/1     Running             0          21s   192.168.109.191   ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           0/1
my-nginx-878597468-qnkr6    1/1     Running             0          36s   192.168.109.191   ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           1/1
my-nginx-878597468-qnkr6    1/1     Running             0          36s   192.168.109.191   ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           1/1
my-nginx-57d7f69d8d-qmzlk   1/1     Terminating         0          3m14s   192.168.65.159    ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           1/1
my-nginx-878597468-bzc9z    0/1     Pending             0          0s      <none>            <none>                                             <none>           0/1
my-nginx-878597468-bzc9z    0/1     Pending             0          0s      <none>            ip-192-168-146-0.ap-northeast-2.compute.internal   <none>           0/1
my-nginx-878597468-bzc9z    0/1     ContainerCreating   0          0s      <none>            ip-192-168-146-0.ap-northeast-2.compute.internal   <none>           0/1
my-nginx-878597468-bzc9z    1/1     Running             0          5s      192.168.186.167   ip-192-168-146-0.ap-northeast-2.compute.internal   <none>           0/1
my-nginx-878597468-bzc9z    1/1     Running             0          20s     192.168.186.167   ip-192-168-146-0.ap-northeast-2.compute.internal   <none>           1/1
my-nginx-878597468-bzc9z    1/1     Running             0          20s     192.168.186.167   ip-192-168-146-0.ap-northeast-2.compute.internal   <none>           1/1
my-nginx-57d7f69d8d-kmlx6   1/1     Terminating         0          3m12s   192.168.143.73    ip-192-168-146-0.ap-northeast-2.compute.internal   <none>           1/1
my-nginx-57d7f69d8d-qmzlk   0/1     Terminating         0          3m55s   192.168.65.159    ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           1/1
my-nginx-57d7f69d8d-qmzlk   0/1     Terminating         0          4m5s    192.168.65.159    ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           1/1
my-nginx-57d7f69d8d-qmzlk   0/1     Terminating         0          4m5s    192.168.65.159    ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           1/1
my-nginx-57d7f69d8d-kmlx6   0/1     Terminating         0          3m53s   192.168.143.73    ip-192-168-146-0.ap-northeast-2.compute.internal   <none>           1/1
my-nginx-57d7f69d8d-kmlx6   0/1     Terminating         0          3m57s   192.168.143.73    ip-192-168-146-0.ap-northeast-2.compute.internal   <none>           1/1
my-nginx-57d7f69d8d-kmlx6   0/1     Terminating         0          3m57s   192.168.143.73    ip-192-168-146-0.ap-northeast-2.compute.internal   <none>           1/1
[root@ip-192-168-114-198 yaml]# kubectl get pods -o wide -w
NAME                       READY   STATUS    RESTARTS   AGE     IP                NODE                                               NOMINATED NODE   READINESS GATES
my-nginx-878597468-bzc9z   1/1     Running   0          99s     192.168.186.167   ip-192-168-146-0.ap-northeast-2.compute.internal   <none>           1/1
my-nginx-878597468-qnkr6   1/1     Running   0          2m15s   192.168.109.191   ip-192-168-79-27.ap-northeast-2.compute.internal   <none>           1/1
[root@ip-192-168-114-198 yaml]#

위와 같이 Pod가 기동되는 시점에 중복되는 initial과 draining 시간을 없애기 위해 Terminating되는 시점을 preStop으로 강제로 연장하여 정상 상태를 확보할 수 있다. 위와 같이 첫번째 Pod의 기동이 완료되면 한번씩 번갈아 가며, 호출되고, 두대 모두 기동이 완료되면, httpd의 It Works! 페이지가 계속 나오는 것을 알 수 있다.


결론

단순히 Kubernetes가 제공하는 Rolling Update나 CI/CD 배포 방식(A/B, Blue/Green 등)을 적용하여 제로 다운타임을 구현할 수 있으나, 이는 AWS 환경 상에서 연결 되는 Managed Service까지 커버할 수는 없다. 따라서 EKS와 LB 간의 배포 프로세스에 ReadinessGates와 PreStop Hook을 적용하여 서비스 가용성을 높이고, 안정성을 도모할 수 있도록 구성해야 할 것이다.

EKS의 가용성을 위해 총 4가지 요소를 알아 보았다.

- terminationGracePeriodSeconds

- ReadinessGates

- ReadinessProbe, LivenessProbe

- Hook(preStop, postStart)

- minReadySeconds

이는 다음과 같은 조합으로 구성할 경우 최적의 아키텍처를 구성할 수 있다. 먼저, terminationGracePeriodSeconds는 서비스가 완전히 종료될 수 있도록 충분히 긴 시간으로 구성한다. 이때 PreStop Hook과 연계하여 신규 Pod가 완전히 기동되기 전까지 기존 버전의 Pod가 중지되지 않도록 Sleep을 구성하며, terminationGracePeriodSeconds를 넘지 않도록 한다. 또한 배포 장애를 사전에 차단하고 네트워크 딜레이 등의 문제를 해소하기 위해 ReadinessGates와 minReadySeconds를 구성한다.

Pod의 Graceful Shutdown은 다음과 같이 동작한다고 정리해 볼 수 있다.

1) 새 배포를 시작하면 이전 포드가 Terminating 상태로 변경된다 . 이 시점에서 서비스는 iptable 에서 포드의 IP를 제거 하고 새로운 트래픽이 들어오는 것을 방지한다.
2) 그 후 preStop 후크 및 SIGTERM 신호가 종료 포드로 전송된다. SIGTERM 메시지를 수신하는 애플리케이션은 종료 준비를 시작하고 연결을 닫으려고한다.
3) 동시에 preStop 후크가 실행되며 httpGet, command Sleep 등을 실행한다. 이후 애플리케이션의 상태를 DOWN으로 설정하고 여기서 terminateGracePeriodSeconds 값을 기다린다. 애플리케이션이 유예 기간 전에 종료되는 경우 terminateGracePeriodSeconds만큼 대기하지 않고 포드를 직접 종료한다.
여기에서 대기 프로세스 (연결 종료, 로깅 등) 전에 원하는 일부 기능을 코딩 할 수 있다. 예를 들어, 포드가 Terminate 상태에 있고 최종 요청을 처리하는 동안 일부 Spring Bean을 찾을 수 없어 BeanException이 발생g할 수 있다. 일부 빈이 조기에 종료되는 것을 방지하기 위해 Tomcat 서버를 대기 상태로 유지한다.
4) terminateGracePeriodSeconds가 지나면 포드에 대한 작업이 성공적으로 완료되고 연결이 닫힌다. SIGKILL 신호를 사용하면 이전 포드가 제거되고 새 포드 만 기동 상태가 된다.

이와 같은 문제는 AWS와 같은 CSP를 사용할 경우 충분히 발생가능한 이슈이다. 여러 컴포넌트가 연계되어 구성되는 경우 각 컴포넌트간의 관계를 진단하고 서비스 장애를 차단할 수 있는 아키텍처를 설계해야 한다는 점에 유의하여 구성해야 할 것이다.

728x90
반응형