AWS 기술 블로그

롯데이커머스 사례로 본 Amazon EKS 운영 안정성 확보하기

롯데이커머스는 온라인 쇼핑몰인 롯데온을 서비스하고 있으며, 약 40여개의 마이크로서비스로 구성되어 있습니다.

롯데온은 이커머스 시장에서 지속적으로 성장함에 따라 시스템으로 유입되는 트래픽 또한 지속적으로 증가하고 있습니다. 특히 할인 행사 기간에는 트래픽이 급격하게 증가하는 추세을 보입니다. 트래픽의 변화에 따라 애플리케이션이 실행되는 Amazon EKS Pod가 확장 되거나 축소되는데 고객은 이 과정에서 예상치 못한 문제를 겪었습니다. 또한 애플리케이션 업데이트 시 이미 제거된 EKS Pod로 여전히 요청이 전달되는 문제를 겪기도 했습니다. 이번 게시글에서는 Amazon EKS 운영 중 겪을 수 있는 문제들을 롯데이커머스의 사례를  중심으로 소개하고 해결방법을 제시합니다.

Amazon EKS란?

Amazon Elastic Kubernetes Service(Amazon EKS)는 오픈소스 Kubernetes를 효과적으로 운영할 수 있는 관리형 Kubernetes 서비스입니다. Kubernetes란 컨테이너로 실행되는 애플리케이션의 배포, 확장 및 관리를 자동화할 수 있는 오픈 소스 시스템입니다. Kubernetes는 다양한 비즈니스 요구사항을 수용할 수 있을 만큼 유연하지만 그만큼 관리의 어려움도 있습니다.

오픈소스 Kubernetes와 완벽히 호환되는 Amazon EKS는 Kubernetes 컨트롤 플레인을 관리하는 역할을 합니다. Kubernetes의 컨트롤 플레인은 컨테이너 스케쥴링 및 컨테이너 가용성 관리, 클러스터에서 발생하는 데이터를 저장하는 등의 중요한 작업을 수행합니다.

고객은 컨트롤 플레인 관리를 EKS에 맡기고, 컨테이너가 실행되는 데이터 플레인만 관리하면 되므로 운영 부담을 크게 줄일 수 있습니다. 특히 Amazon EKS에서 AWS Fargate를 사용하는 경우 데이터 플레인의 인프라 관리까지 AWS에 위임하여 관리할 서버 없이 Kubernetes를 안정적으로 운영할 수 있습니다.

롯데이커머스가 Amazon EKS를 선택한 이유

롯데이커머스는 1996년도부터 온프레미스 기반의 모놀리식 아키텍처로 온라인 쇼핑몰을 운영해왔습니다. 이 때 고객은 서버 증설에 3개월 이상이 소요되어 대량의 트래픽에 대응하지 못하는 문제를 겪었습니다. 또한 물리적인 인프라까지 직접 운영해야 하고, 애플리케이션 배포에 많은 시간이 소요되어 시장 변화에 빠르게 대응하기가 어려웠습니다.

이후 새롭게 진행하는 프로젝트들에서는 점차 서비스를 분리하고, 그동안 쌓은 노하우를 바탕으로 롯데온에 Amazon EKS 기반의 마이크로서비스 아키텍처를 적용하였습니다. 아래 그림은 AWS Summit Korea 2022에서 “롯데ON, 클라우드 네이티브로 거듭나기” 세션에서 소개했던 아키텍처입니다.

그렇다면 롯데이커머스에서는 AWS의 수많은 서비스들 중 왜 Amazon EKS를 선택했을까요?

첫 번째로 AWS의 완전 관리형 서비스라는 것이었습니다. 롯데이커머스 내에 Kubernetes를 운영할 수 있는 인력이 부족했기 때문에 운영 부담을 최소화해야 했습니다. Amazon EKS를 사용하면 배포 자동화와 서비스간의 통신, 문제가 발생했을 경우의 자가 복구 등 마이크로서비스 운영에 효과적인 기능들을 제공합니다.

두 번째는 손쉽게 AWS의 다른 서비스들과의 통합이 가능하다는 점이었습니다. AWS IAM 서비스로 사용자를 인증하여 보안을 강화하고, Amazon ECR로 컨테이너 이미지를 효율적으로 관리할 수 있었습니다. 또한 Application Load Balancer의 리스너에 정의된 규칙에 따라 하나의 도메인으로 여러 서비스에 요청을 분배할 수 있었습니다. Pod에서 발생하는 데이터를 영속적으로 보관해야하는 경우 Amazon EBS와 Amazon EFS를 Pod에 마운트하여 데이터를 저장할 수 있었습니다. 롯데이커머스에서는 이 외에도 여러 AWS 서비스들을 통합한 아키텍처를 운영하고 있습니다.

마지막으로 마이크로 서비스로 구성된 컨테이너 기반의 애플리케이션을 효율적으로 운영하기 위해서는 자동화가 필요했습니다. 자동화가 안 된다면 운영에 상당히 많은 시간을 소비 해야 하기 때문에 생산성이 떨어지고, 사용자 실수가 발생할 수 있습니다. 이는 365일 24시간 운영해야하는 이커머스 비즈니스에 심각한 영향을 줄 수 있습니다. 또한 대규모 트래픽에도 서비스를 안정적으로 운영하기 위해 확장 가능하면서도 비용 효율적이어야 합니다. 롯데이커머스는 이와 같은 요구사항들을 충족하기 위해 Amazon EKS를 선택했습니다.

Amazon EKS Pod 축소(Scale in) 시 발생했던 문제

사전 지식 : Amazon EKS의 Auto Scaling 기능

Amazon EKS는 Pod 리소스를 관리하기 위해 몇 가지 오토스케일링 기능을 제공합니다.

먼저 Cluster Autoscaler는 Pod가 실행될 리소스가 부족한 경우 작업자 노드를 확장하는 역할을 합니다. 반대로 실행 중인 Pod에 비해 작업자 노드의 수가 많은 경우 작업자 노드를 축소합니다. 여기서 작업자 노드는 Pod가 실행되는 EC2 인스턴스를 의미합니다. 즉, Cluster Autoscaler는 EC2 인스턴스 수를 조정하기 위해서 AWS로 API 요청을 하게 되고, 결과적으로 오토스케일링 그룹의 desired 값을 조정합니다.  이는 Kubernetes 자체의 기능이 아닌 부가적인 기능이기 때문에 애드온으로 실행되는 Cluster Autoscaler가 이를 담당합니다.

Horizontal Pod Autoscaler(HPA)는 Pod의 평균 리소스 사용량에 따라 Pod 수를 조정하는 역할을 합니다. Vertical Pod Autoscaler(VPA)는 하나의 Pod가 사용할 수 있는 리소스 사용량 한도를 늘릴 수 있습니다. HPA 및 VPA는 오픈소스 Kubernetes에 내장되어 기본으로 제공되는 기능입니다.

마지막으로 karpenter는 AWS가 오픈 소스로 출시한 애드온 입니다. 오토스케일링 그룹의 설정을 변경하는 Cluster AutoScaler에 비해 EC2 API를 사용하여 더 다양한 인스턴스 유형과 빠른 확장의 장점이 있습니다.

롯데이커머스에서는 Cluster Autoscaler와 Horizontal Pod Autoscaler를 사용합니다. Amazon EKS 클러스터를 운영 중에 HPA로 인해 Pod가 축소될 때 발생했던 문제를 기술하고, 그 해결방법에 대해 설명하겠습니다.

문제 발생 원인

롯데이커머스에서 서비스 중인 롯데온은 국내 상위권의 온라인 쇼핑몰이기 때문에 할인 행사를 진행하면 대규모의 사용자가 접속하여 서비스를 이용합니다. 이런 이유로 고객은 부하테스트를 필수로 진행합니다. 클라이언트의 요청은 Kubernetes의 ingress 오브젝트로부터 생성된 Application LoadBalancer로 전달 됩니다. Application Loadbalancer는 대상 그룹에 속한 작업자 노드로 트래픽을 분배하고, 최종적으로 Pod로 전달됩니다. 이 때 Pod들은 Kubernetes의 HPA 오브젝트를 통해 확장 및 축소됩니다.

Pod의 사용량이 증가하면 Kubernetes의 Horizontal Pod Autoscaler는 Amazon EKS 클러스터의 작업자 노드에 새로운 Pod를 추가합니다. 이를 Scale out이라고 합니다. 반대로 컴퓨팅 리소스에 여유가 생기게 되면 리소스가 낭비되지 않도록 Pod 수를 줄입니다. 이를 Scale in 이라고 하는데, 이 때 일정 시간 동안 오류를 반환하는 문제가 발생하였습니다.

이 문제의 원인은 Pod의 수명 주기 동작에 있습니다. 먼저 Kubernetes의 아키텍처를 살펴보면서 Pod가 어떻게 생성되고 삭제되는지 알아보겠습니다.

아래 그림과 같이 Kubernetes 클러스터에는 마스터 노드와 작업자 노드가 존재합니다. 마스터 노드에 포함된 API 서버는 Kubernetes 클러스터의 핵심 컴포넌트입니다. Kubernetes 내 여러 컴포넌트들 간의 통신은 대부분 API 서버를 거치게 됩니다. 작업자 노드에는 kubelet이 Agent 방식으로 노드에 설치되고 kube-proxy는 Kubernetes의 DaemonSet으로 실행됩니다. 각 작업자 노드와 API 서버 간의 통신은 kubelet이 담당합니다.

만약 Kubernetes 클러스터 관리자가 kubectl 명령어로 Pod를 생성하는 명령어를 실행하면 API 서버로 요청이 전달되고, API 서버는 Pod를 생성하는 역할을 담당하는 컨트롤러 매니저들에게 API를 전달합니다. 최종적으로는 kubelet으로 Pod 생성 명령을 전달됩니다. 명령을 수신한 kubelet은 작업자 노드에 컨테이너를 실행합니다. 유사한 방식으로 삭제 절차도 진행됩니다.

Pod의 삭제 과정을 더 깊이 있게 살펴보겠습니다. 아래 그림과 같이 kubectl 명령을 통해 API 서버가 Pod 삭제 명령을 수신할 경우 최종적으로 Kubelet에 삭제 명령이 전달됩니다. 이 후 Kubelet은 대상 Pod를 삭제하는 절차를 수행합니다.

Kubernetes에는 Service라는 리소스가 존재합니다. Service는 다수의 Pod들을 대표하는 Endpoint를 생성합니다. 이는 마치 로드밸런서와 같이 유입되는 트래픽을 분배하는 역할을 합니다. 이 때 내부적으로 작업자 노드의 iptables rule을 사용합니다. 여기서 Service 변경사항에 따라 iptables rule을 수정하는 역할은 kube-proxy가 담당합니다.

Kubernetes에서 Pod의 Scale in은 한번에 모든 Pod를 제거하지 않습니다. 설정된 비율에 따라 아래 그림과 같이 Kubernetes의 컴포넌트 중 Endpoints controller는 Service Endpoint에 지정된 Pod들 중 삭제 대상 Pod를 로드 밸런싱 대상에서 제외시킵니다.

Kubernetes 내 컴포넌트들 간에는 상호 의존성을 갖지 않고 API 서버를 통해서 통신을 합니다. 이러한 이유로 Service Endpoint에서 대상 Pod를 제거하는 요청을 API 서버에 전달하게 되고, 최종적으로 대상 작업자 노드의 kube-proxy를 통해 작업자 노드의 iptables rule을 수정하게 됩니다.

그 결과 클라이언트의 요청이 더이상 종료된 Pod로 전송되지 않게 됩니다. 컨테이너를 삭제하는 절차와 iptables를 수정하는 절차는 아래와 같이 동시에 실행되며, 고객이 겪은 문제는 이와 같은 동시성으로 인해 발생했습니다.

Endpoint에서 Pod의 ip가 제거되기 전에 컨테이너가 삭제되어버리면 외부에서 유입되는 트래픽이 여전히 삭제된 컨테이너에게 전달되면서 502 에러가 발생하게 되는 것입니다.

문제 해결

그렇다면 이 문제는 어떻게 해결해야 할까요? 롯데이커머스의 해결책은 Pod의 컨테이너의 종료를 지연시켜 Service의 Endpoint에서 대상 Pod가 정상적으로 제외될 수 있도록 충분히 기다리는 것이었습니다. 이를 위해서는 kubelet이 Pod 내 컨테이너들을 종료시키는 절차에 대해 알아볼 필요가 있습니다. kubectl 명령어로 삭제 요청이 Kubernetes의 API 서버로 전달 되면 최종적으로 작업자 노드의 kubelet으로 전달됩니다.

kubelet은 Pod 내 컨테이너들에서 실행되는 애플리케이션을 종료하기 위해 SIGTERM 시그널을 보냅니다. 하나의 Pod 안에는 여러 개의 컨테이너가 실행될 수 있는데 각 컨테이너의 PID 1번 프로세스에게 SIGTERM 명령을 실행하는 것입니다. SIGTERM 시그널의 특징은 애플리케이션이 종료 로직을 실행할 수 있도록 일정 시간동안 기다려 준다는 것입니다.

여기서 Pod 설정에 terminationGracePeriodSeconds라는 설정값이 존재하는데 설정된 시간(기본 30초)만큼 기다린 후에도 애플리케이션이 종료되지 않을 경우 SIGKILL 시그널로 강제 종료 시킵니다. 컨테이너 내에서 실행되는 모든 프로세스들은 PID 1번 프로세스로부터 fork 된 것이기 때문에 PID 1번 프로세스가 종료된다면 컨테이너 또한 종료됩니다. 물론 30초 내에 모든 컨테이너가 종료된다면 더 기다리지 않고 Pod가 종료됩니다.

import signal
import time


class GracefulKiller:
    kill_now = False

    def __init__(self):
        signal.signal(signal.SIGTERM, self.exit_gracefully)

    def exit_gracefully(self, *args):
        self.kill_now = True


if __name__ == '__main__':
    killer = GracefulKiller()
    while not killer.kill_now:
        time.sleep(1)

    print("SIGTERM 시그널로 인해 프로그램이 종료되었습니다.")

위의 Python으로 작성된 코드의 exit_gracefully 함수와 같이 애플리케이션 코드 상에서 SIGTERM 시그널을 수신했을 때의 종료 로직을 구현할 수 있습니다. 이 부분에 Pod의 종료를 일정 시간 동안 기다리도록 코드를 구현합니다. 주의해야할 사항은 애플리케이션에서 너무 오랜 시간 동안 대기를 하게 된다면 그만큼 Pod 재시작이나 업데이트 절차가 지연됩니다.

이 경우 아래와 같이 Pod에 terminationGracePeriodSeconds를 설정하여 최대로 기다리는 시간을 설정하여 무한정 대기하는 것을 방지할 수 있습니다.

apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
containers:
  - name: my-container
    image: busybox
  terminationGracePeriodSeconds: 60

더 간단한 방법으로는 Pod 설정 중 Prestop hook 설정을 사용하는 것입니다. Prestop hook은 Pod의 수명 주기를 관리하기 위한 명령어 또는 script를 지정할 수 있습니다. 그러면 Pod 종료 시 해당 스크립트를 실행하게 되는데 여기에 sleep 명령을 넣게 되면 컨테이너가 삭제되는 시간을 잠시 늦출 수 있습니다. 이 때도 무한정 대기를 방지하기 위해 terminationGracePeriodSeconds 설정은 필요합니다.

apiVersion: v1
kind: Pod
metadata:
  name: lifecycle-demo
spec:
  containers:
  - name: lifecycle-demo-container
    image: nginx
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh","-c","sleep 1000"]
  terminationGracePeriodSeconds: 60

Blue/Green 배포 시 삭제된 Pod로 여전히 트래픽이 전달되는 문제

사전 지식 : Blue/Green 배포 전략

롯데온의 각 서비스는 배포 시 Blue/Green 배포 전략을 사용합니다. 여기서 Blue/Green 배포란 무엇일까요? 이것은 Blue라고 불리는 현재 운영 중인 Pod를 그대로 유지하면서 Green이라고 불리는 새로운 버전의 Pod를 Blue Pod의 크기와 동일하게 실행 시킨 후 한번에 전환하는 것을 의미합니다. 이후 정상동작을 확인 한 후 Blue Pod들을 제거합니다. Kubernetes 내 리소스를 사용하여 Blue/Green 배포를 하는 경우 일반적으로 Blue Deployment와 Green Deployment의 Label을 다르게 지정합니다. Green Deployment에 의해 생성된 Pod들의 정상 동작 여부가 확인되면 Service의 Label selector 값을 Green Deployment의 Label로 변경하여 트래픽을 전환 합니다.

문제 원인

Blue/Green 배포 이 후 고객은 일정 시간동안 일부 트래픽이 이미 삭제된 Pod로 보내지면서 오류가 반환되는 문제가 발생하였습니다.

고객은 새로운 버전의 애플리케이션을 배포하기 위해 먼저 Green Deployment를 생성했습니다. 이 후 Service의 Label을 변경하여 트래픽을 전환 했습니다. 고객은 정상 동작을 확인한 후 Blue Pod 삭제를 시작했습니다. Pod의 수명 주기 훅에 대한 처리가 진행되는 동안은 서비스가 문제 없이 동작했습니다. 하지만 Pod가 완전히 제거되고 난 이후에는 클라이언트에서 timeout 오류가 발생했습니다.

Pod 종료 절차가 시작되면 대상 Pod에는 이 후 들어오는 트래픽에 대해서 HTTP connection을 맺지 않지만 노드와 Pod 간의 TCP connection은 ip_conntrack에 의해 일정 시간 유지합니다. 여기서 ip_conntrack이란 ip connection tracking의 줄임말로 iptables의 상태를 추적하기 위한 모듈입니다.

노드로 새로운 패킷이 들어오면 기존에 저장하고 있던 connection과 동일 여부를 검사하고 동일한 경우 iptables rule을 스캔하지 않고 저장된 경로로 packet을 전달합니다. 즉, 저장된 정보를 기반으로 동일한 출발지로 들어온 트래픽을 동일한 목적지로 보내주는 역할을 합니다. 이로 인해 iptables rule이 갱신 되더라도 ip_conntrack에 저장된 정보가 남아있다면 이미 제거된 Pod로 packet이 전달될 수 있습니다.

아래 그림에서 보듯이 삭제된 Pod의 ip에 대해 iptable rule에는 모든 기록이 제거되었습니다. 하지만 ip_conntrack에는 아직 established 상태로 남아있습니다. 이로 인해 클라이언트의 요청이 계속해서 제거된 Pod로 유입되는 것을 확인할 수 있었고, 그 결과 클라이언트는 오류를 수신하게 됩니다.

문제 해결

한가지 해결책은 Kubernetes의 Ingress 오브젝트를 통해 Application Loadbalancer를 생성할 때 IP 모드를 사용하는 것입니다. Ingress를 통해 생성되는 Application LoadBalancer는 기본적으로 Instance 모드 입니다. instance 모드와 ip모드 간에는 큰 차이가 있습니다. instance 모드를 사용할 경우 Kubernetes Service의 NodePort  유형이 적용되어 모든 작업자 노드에 동일한 Port가 오픈되고, Application LoadBalancer의 대상 그룹으로 각 작업자 노드를 등록하고 지정한 Port로 요청이 분배됩니다.

이 때 노드의 iptables rule에 목적지에 대한 정보가 기록되어 있기 때문에 노드로 수신된 packet은 정상적으로 대상 Pod로 전달됩니다. 심지어 특정 노드에 대상 Pod가 존재하지 않을 경우에도 네트워크 홉은 증가할 수 있지만 목적지로 패킷이 전달 됩니다.

반면 IP 모드는 Kubernetes에 애드온으로 설치되는 AWS Loadbalancer Controller에 의해 ALB의 타겟으로 Pod의 IP가 등록됩니다. 만약 Pod가 추가로 생성되거나 제거될 경우 AWS Loadbalancer Controller가 이를 감지하여 즉시 ALB의 대상 그룹 정보를 수정합니다. ALB로 유입된 트래픽은 iptables rule을 사용하지 않고 곧바로 Pod로 전달됩니다. 하지만 대상 그룹에는 최대 1000개의 대상이 가능하기 때문에 하나의 서비스에 너무 많은 수의 Pod가 생성된다면 제한으로 인해 문제가 발생할 수 있습니다.

이 경우에는 하나의 서비스가 너무 많은 역할을 수행하고 있는 것이므로 서비스를 분리하는 것도 검토해볼 수 있습니다. IP 모드를 사용할 수 있다면 iptables 뿐만 아니라 ip_conntrack을 사용할 필요도 없기 때문에 관련된 문제를 해결할 수 있습니다. 뿐만 아니라 네트워크 홉이 줄어들기 때문에 성능 향상의 이점도 있습니다.

고객은 대상 그룹 제한에 해당되지 않을 만큼의 Pod 수를 유지하고 있어서 일부 워크로드에 IP 모드를 적용하여 문제를 해결했고, 점진적으로 전체 워크로드에 적용할 계획을 가지고 있습니다.

결론

지금까지 알아본 내용을 요약하면, 먼저 Pod가 Horizontal Pod Autoscaler에 의해 축소 될 때 이미 제거된 Pod로 트래픽이 전달되는 문제를 방지하기 위해 PreStop hook을 사용할 수 있습니다. 이는 Service의 Endpoint에서 Pod의 IP가 정상적으로 제외될 때까지 Pod의 종료를 지연시킬 수 있는 방법입니다.

Blue/Green 배포 시 ip_conntrack에 의해 이미 종료된 Pod로 요청이 전달되는 경우를 방지하기 위해 Application Load Balancer의 IP모드를 사용합니다.

롯데이커머스에서는 Amazon EKS를 운영하면서 겪었던 다양한 경험들을 바탕으로 계속해서 성장하고 있으며 2022년 AWS Summit Seoul에서도 경험을 발표한 사례가 있습니다. 이 후에도 Amazon EKS 버전 업데이트 개선과 코드형 인프라(IaC, Infrastructure as Code) 관리와 같은 개선 사항들이 있습니다. 앞으로도 AWS와 함께 협업하여 성공적인 사례를 공유할 수 있기를 기대합니다.

Byeongsik Kim

Byeongsik Kim

김병식 롯데이커머스 DevOps 파트 리더는 롯데온의 Amazon EKS 운영을 전담하고 있습니다. 오픈소스 및 자동화에 대한 관심이 많습니다.

Yongho Choi

Yongho Choi

최용호 솔루션즈 아키텍트는 개발 경험을 바탕으로 고객의 애플리케이션 현대화 여정과 함께 효율적인 아키텍처를 구성하실 수 있도록 돕고 있습니다.

JinGyu Kim

JinGyu Kim

김진규 테크니컬 어카운트 매니저는 엔터프라이즈 서포트 고객을 대상으로 온보딩 과정을 지원하고, 모범 사례를 사용하여 솔루션을 계획 및 구축하는 데 도움이 되는 지침을 제공하고자 노력하고 있습니다.