EKS(Elastic Kubernetes Service)는 AWS에서 제공하는 Managed Kubernetes 서비스입니다.
EKS는 "aws-loadbalancer-controller"를 제공해 AWS ALB(Application Load Balancer)를 Ingress로 사용할 수 있게끔 하고 있는데요.
ALB 기반의 Ingress로 Deployment를 배포할시 AWS와 통합된 환경을 활용해 서비스를 노출시킬 수 있다는 장점이 존재합니다.
하지만 위와 같은 환경에서 롤링 업데이트 시 일시적으로 500 에러가 발생하는 현상을 종종 발견할 수 있는데요.
이번 포스팅에서는 이 500에러의 원인이 무엇인지, 해결 방법에는 무엇이 있는지 알아보도록 하겠습니다.
1. 증상
이슈가 되는 에러를 재구현해 어떠한 환경에서 해당 이슈가 발생하는지 알아보도록 하겠습니다.
재구현에 사용된 환경의 아키텍쳐는 다음과 같습니다.
위 다이어그램에서 볼 수 있듯이 실험 환경은 Rolling update를 수행할 Deployment와 이를 내부로 노출시킬 ClusterIP, 외부로 노출시킬 ALB 기반의 Ingress로 이루어져 있습니다.
이 상태에서 Rolling update 중 500 에러가 발생하는 이슈를 재구현하기 위해 Deployment에서 Rolling update를 수행해보겠습니다.
이제 Rolling update 수행 시 관찰되는 현상을 환경별로 확인해보겠습니다.
1-1. Kubernetes
가장 먼저 Kubernetes 환경에서 Rolling update시 일어나는 현상을 살펴보겠습니다.
에러 재구현을 위해 Pod가 1개인 Nginx Deployment를 생성했습니다. 따라서 nginx Pod 1개가 Running 상태에 있는 것을 확인할 수 있습니다.
이 상태에서 Nginx Deployment에 Rolling update를 수행하겠습니다.
Rolling update 수행 후에는 아래와 같은 순서로 Pod의 생애주기가 순환하는 것을 확인할 수 있습니다.
New Pod Pending -> New Pod ContainerCreating -> New Pod Running -> Old Pod Terminating
여기서 Kubernetes에서는 Deployment가 새 Pod의 Running 상태를 확인한 후 기존 Pod를 Terminating한다는 것을 알 수 있습니다.
이는 Deployment가 Zero Downtime으로 새 버전의 Pod를 배포하기 위해 사용하는 방법으로, 현재까지는 새 버전을 배포하는 동안 아무 문제도 없어보입니다.
1-2. Client
다음으로 애플리케이션으로 접근하는 Client 입장에서 Rolling update시 볼 수 있는 현상을 확인해보겠습니다.
아래는 Nginx Deployment의 Rolling update 중 Clinet 단에서 기록한 GET 상태 코드 그래프입니다.
노란 선 : 200 OK
주황 선 : 500 Bad Gateway
그래프를 확인해보면 Rolling update 실행 후 대략 3초부터 15초까지 12초간 500 상태 코드를 반환하는 것을 볼 수 있습니다.
즉 Client 입장에서는 대략 12초 동안 서비스에 대한 Downtime이 발생했다는 것을 알 수 있습니다.
하지만 이전 Kuberntes 단에서 저희는 새 Pod를 배포할때 트래픽을 받을 준비가 되었다는 것을 확인한 후 기존 Pod를 제거하는 것으로 확인했는데요.
이론상 Zero Downtime으로 새 버전이 배포되어야 하지만 분명히 Client는 Downtime을 겪고 있습니다.
이러한 현상이 왜 발생했는지 더 알아보도록 하겠습니다.
1-3. AWS ALB
이제는 Deployment를 외부로 노출하기 위한 Ingress를 구현하는 AWS ALB에서 Rolling update이 발생하는 현상을 확인해보겠습니다.
아래는 배포 상태에 따른 ALB의 Target Group 상태입니다.
Rolling update 실행 전
배포를 실행하지 않은 기본 상태에서는 ALB가 1개의 Target으로만 트래픽을 전달하며, Target은 정상적인 Healthy 상태를 보여주고 있습니다.
Rolling update 실행 직후
다음으로 Kubernetes에서 Rolling update를 실행한 직후에는 기존 Target이 Drainging되며, 배포되는 새 버전으로 Target이 Initial되는 것을 확인할 수 있습니다.
Rolling update 진행 중
Rolling update가 진행되고 있는 중에는 새 Pod가 Healthy 상태로 트래픽을 전달받고, 기존 Pod는 계속해서 Draining 상태를 유지해 남아있는 Connection을 처리합니다.
Rolling update 완료
Rolling update가 완료되면 Draining 중이던 기존 Pod로의 모든 연결이 종료되고 새 Pod만 Healthy 상태로 남아 트래픽을 전달받습니다.
여기까지 AWS ALB단에서 확인한 Rolling update시 나타나는 현상이었습니다.
2. 원인
지금까지 Kubernetes, Client, AWS ALB 각 3가지의 측면에서 Kubernetes Rolling update시 나타나는 현상을 확인해봤습니다.
지금까지의 현상을 분석해봤을때, 시간 순서에 따른 각 요소별 동작 타임라인을 정리하면 아래와 같습니다.
위 타임라인을 봤을때, Downtime은 기존 Pod가 AWS ALB에 의하여 Draining되고 새 Pod가 Initializing되는 순간에 발생하는 것을 확인할 수 있습니다.
즉 Rolling update 중 Downtime이 발생하는 이유는 AWS ALB에 의하여 기존 Pod는 Draining 상태로 변경되어 새 트래픽을 받지 못하고, 새 Pod는 Initializing 상태로 변경되어 마찬가지로 새 트래픽을 받지 못하기 때문입니다.
이 때문에 Kubernetes에서는 Deployment가 Zero Downtime을 보장하도록 Pod 생애주기를 조절했지만, AWS ALB의 Target 등록에 지연 시간이 발생함으로서 Downtime이 발생했던 것입니다.
AWS ALB에서 Connection Draining 동작 중에는 기존 Connection은 처리되지만 새 Connection은 들어올 수 없고, Initializing 중에는 어떤 Connection도 맺지 않습니다.
3. 해결 방법
이 문제를 해결할 수 있는 방법은 Kubernetes의 preStop Hook 기능을 사용해 Terminating 시간을 지연하는 것입니다.
preStop Hook은 Pod의 Terminating 시작 시 Container에 보내는 SIGTERM 신호를 발생시키기 전에 실행하는 Hook을 말합니다.
이 preStop Hook을 이용해 AWS ALB가 충분한 Initializing 시간을 벌 수 있도록 SIGTERM의 발생 시기를 늦춤으로써 Terminating 시간을 늘리는 방법으로 문제를 해결 가능합니다.
preStop Hook은 아래와 같은 방식으로 적용할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
|
### deployment.yaml ###
...
containers:
- name: nginx
image: nginx
lifecycle:
preStop:
exec:
command: ["/bin/bash", "-c", "sleep 40"]
...
|
cs |
위와 같이 preStop Hook을 적용할시 주의해야 할 점은, Sleep time + Pod Terminating time이 terminationGracePeriodSeconds 값을 넘지 않도록 해야 한다는 것입니다.
terminationGracePeriodSeconds 값은 Pod의 Terminating이 수행되는 시간을 지정하는 속성이며, 지정된 값의 시간이 지나면 Container에 SIGKILL 신호를 발생시키기 떄문에 의도한 시간보다 빨리 Terminating될 수 있습니다.
terminationGracePeriodSeconds 속성의 기본 값은 30초로, 기본 값을 사용할시 Sleep time + Pod Terminating time이 30초를 넘는다면 이 값을 늘리도록 수정해야 합니다.
아래와 같이 해당 값을 늘릴 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
|
### deployment.yaml ###
containers:
- name: nginx
image: nginx
lifecycle:
preStop:
exec:
command: ["/bin/bash", "-c", "sleep 40"]
# SIGKILL 신호를 받기까지 60초의 유예시간이 주어집니다.
# 기본값은 30초입니다.
terminationGracePeriodSeconds: 60
|
cs |
최종적으로 이 방안을 적용할 시 아래와 같은 타임라인으로 동작하게 됩니다.
위 타임라인에서 볼 수 있듯이, preStop Hook을 수행하면 지정한 시간만큼 AWS ALB에서 새 Target을 Initializing하는 시간을 벌어주기 때문에 Downtime 없이 새 버전을 배포할 수 있습니다.
아래는 preStop Hook 적용 후 Rolling update시 Client단에서 측정한 GET 상태 코드 그래프입니다.
preStop Hook을 적용하기 전과 달리 500 에러가 거의 발생하지 않은 것을 확인할 수 있습니다.
이렇게 AWS ALB가 새 Target을 Initializing하는 시간을 벌어줌으로써 Zero Downtime에 근접한 배포를 수행할 수 있습니다.
4. 마치며
지금까지 Kubernetes 환경에서 AWS ALB Ingress를 기반으로 노출한 서비스를 Rolling update했을 시 나타나는 500 에러에 대한 증상과 원인, 그리고 해결방법을 알아봤습니다.
결론적으로 해당 이슈의 원인은 AWS ALB의 새 Target을 Initial하는데 발생하는 지연시간인 것으로 나타났습니다.
그리고 이 문제를 해결하기 위해 preStop Hook을 사용해 Initial시간을 보상하는 것으로 Downtime을 제거할 수 있다는 것을 알아봤습니다.
동일한 환경에서 Rolling update시 이와 같은 이슈를 겪는 분들께 이 포스팅이 도움이 되었으면 합니다.
'Devops' 카테고리의 다른 글
Terraform을 GitOps 방식으로 사용하기 위한 도구 선택하기(With TACOS) (0) | 2024.01.27 |
---|---|
Kubernetes 환경에서 발생하는 DNS Query Failed 이슈와 NodeLocal DNSCache를 이용한 해결 (5) | 2023.12.30 |
Kubecost로 Kubernetes 환경의 FinOps를 구현해보자 (0) | 2023.11.29 |
Clean Code를 구현하기 위해 Sonarqube로 정적 코드 분석을 해보자 (2) | 2023.10.28 |
Argo 사용해보기 (2) Standalone DEX로 Argo Workflow에서 SSO 구현하기 (0) | 2023.05.09 |