Skip to content

EKS 업그레이드 기술 및 경험

이번 글에서는 EKS 클러스터를 업그레이드하면서 겪었던 경험을 정리한다.

이전 문서에서 GitLab CI/CD와 Terraform을 통해 EKS 클러스터의 라이프사이클을 코드로 관리하는 방법을 다뤘는데, 이번 문서는 그 연장선에 있다.

코드로 관리되는 인프라에서 클러스터 버전을 올리는 작업은 사실 굉장히 단순하다. 변수 하나, 숫자 하나를 바꾸고 MR을 올리면 된다. 어렵지 않다.

문제는 그 다음이다. apply 버튼을 누르기 전까지 확인해야 할 게 생각보다 많고, 한 번 시작하면 중간에 멈출 수도 없다. 운영 환경에서 EKS 업그레이드를 몇 번 해보면서 느꼈던 부분들을 정리한다.


작업 자체는 정말 한 줄짜리 변경이다

실제로 어떻게 변경되는지부터 보자. Terraform으로 EKS 클러스터를 관리하고 있다면, 보통 이런 식의 변수 정의가 있을 것이다.

variable "cluster_version" {
  description = "EKS cluster version"
  type        = string
  default     = "1.30"
}

업그레이드는 이 값을 1.31로 바꾸는 것이 전부다. plan을 돌려보면 컨트롤 플레인 버전이 변경된다는 결과가 나오고, apply하면 AWS가 알아서 처리해준다.

- default = "1.30"
+ default = "1.31"

여기까지만 보면 너무 쉬워 보인다. 한 줄 바꾸고 MR 올리고 리뷰받고 머지하면 끝이니까. 실제로 하루 일과 중에 짬내서 할 수 있는 작업처럼 보인다.

그런데 진짜로 그렇게 하면 안 된다. 한 줄 변경 뒤에 숨어있는 고려사항들이 꽤 많다.

Standard Support 기간이 의외로 짧다

EKS는 Kubernetes 마이너 버전마다 표준 지원(Standard Support) 기간이 정해져 있다. 한 버전당 약 14개월 정도로, 새 마이너 버전이 분기마다 나오는 걸 고려하면 한 번에 두세 단계씩 밀려있어도 이상하지 않다.

표준 지원 기간이 끝나면 확장 지원(Extended Support)으로 넘어가는데, 여기서부터는 추가 비용이 발생한다. 시간당 비용이 표준 지원 대비 몇 배로 올라가서, 운영 클러스터가 여러 개라면 비용 차이가 무시할 수 없는 수준이 된다.

확장 지원 기간도 무한정은 아니다. 그 기간이 지나면 강제로 다음 버전으로 업그레이드된다. 사용자가 준비가 안 되어 있어도 AWS가 알아서 올려버린다는 뜻이다. 이게 운영 시간에 발생하면 정말 곤란해진다.

그래서 보통은 새 마이너 버전이 출시되고 어느 정도 안정화된 시점에 한 단계씩 따라가는 방식으로 운영했다. 분기마다 한 번 정도 업그레이드 일정을 잡아두고, 못해도 표준 지원이 끝나기 두세 달 전에는 다음 버전으로 넘어가는 것을 목표로 했다.

문제는 이런 일정이 다른 작업들에 자주 밀린다는 점이다. 신규 서비스 배포, 모니터링 개선, 비용 최적화, 장애 대응 같은 가시적인 작업들이 우선순위에 올라가다 보면, 클러스터 업그레이드는 "이번 달에 해야 하는데..."가 두세 번 반복되면서 미뤄진다. 그러다가 어느 순간 보면 두 단계 뒤쳐져 있는 상황이 된다.

업그레이드는 한 단계씩만 가능하기 때문에, 두 단계가 밀리면 작업이 두 번 필요하다. 그리고 업그레이드 사이에는 안정화 시간이 필요해서 한꺼번에 몰아서 하기도 어렵다. 결과적으로 계획을 미리 잡아두고 정기적으로 처리하는 게 가장 편한 방법이었다.

모든 Pod가 재시작된다는 사실

이 부분이 사실 가장 큰 고려사항이었다. EKS 컨트롤 플레인 자체를 업그레이드하는 동안에는 워크로드에 영향이 없다. 컨트롤 플레인은 AWS가 관리하기 때문에, 사용자 입장에서는 그냥 API 서버 버전이 올라가는 정도다.

문제는 노드 그룹이다. 매니지드 노드 그룹을 컨트롤 플레인과 같이 업그레이드하면, 새 버전의 AMI를 사용하는 노드가 차례로 추가되고 기존 노드에서 Pod가 evict되어 새 노드로 옮겨간다. 이 과정에서 모든 Pod가 재시작된다.

kubectl drain이 순서대로 일어나면서 Pod들이 새 노드로 재배치되는데, Deployment의 경우 PDB와 readiness probe만 잘 설정되어 있으면 무중단으로 처리된다. 적어도 이론상으로는 그렇다.

실제로는 그렇게 깔끔하지 않은 경우가 많았다.

가장 흔한 이슈는 stateful 워크로드였다. PVC를 사용하는 StatefulSet의 경우, EBS 볼륨이 특정 AZ에 묶여있기 때문에 Pod가 다른 AZ로 못 가는 경우가 있다. 이러면 같은 AZ의 노드가 비어있어야만 재시작이 되는데, 동시에 여러 노드가 drain 대상이 되면 한동안 Pending 상태로 남아있는다.

또 다른 이슈는 startup probe 설정이 부실한 워크로드들이다. 컨테이너가 뜨자마자 readiness가 OK로 떨어지는데, 실제로는 내부 초기화가 끝나지 않아서 첫 번째 요청에서 실패하는 경우가 있다. 평소에는 한 번씩 재시작될 일이 거의 없어서 눈에 안 띄지만, 클러스터 업그레이드 시점에 모든 Pod가 동시에 재시작되면서 이런 부분이 한꺼번에 드러난다.

그리고 sidecar로 붙는 컴포넌트들. 예를 들어 서비스 메시 사이드카나 로그 수집기 같은 것들은 메인 컨테이너보다 늦게 뜨는데, 이 시간차 때문에 메인 앱이 정상 동작하기 전에 트래픽을 받는 경우가 생긴다. 이런 문제는 운영 중에는 거의 안 보이다가 업그레이드 같은 대규모 재시작 이벤트에서 갑자기 나타난다.

대응 방법은 결국 평소에 잘 챙기는 것이다.

  • 모든 Deployment에 PDB(PodDisruptionBudget)를 설정한다.
  • replica는 최소 2개 이상으로 두고, anti-affinity로 분산시킨다.
  • readiness probe는 진짜로 트래픽을 받을 수 있는 시점에 OK가 떨어지도록 만든다.
  • liveness probe는 너무 공격적으로 설정하지 않는다 (재시작 루프 방지).

이런 것들이 평소에는 부가적인 설정처럼 보이지만, 클러스터 업그레이드처럼 큰 이벤트가 있을 때 안정성을 크게 좌우한다. 매번 업그레이드 직전에 PDB부터 점검하는 게 루틴이 됐다.

Timeout이 곳곳에서 발목을 잡는다

EKS 업그레이드를 처음 해보면 의외로 자주 만나는 문제가 timeout이다. 작업 자체가 오래 걸린다는 건 알고 있어도, 그 시간 동안 어떤 도구가 어떤 timeout을 가지고 있는지는 평소에 신경 쓸 일이 거의 없기 때문이다.

EKS 컨트롤 플레인 업그레이드는 보통 10분에서 15분 정도 걸린다. 노드 그룹 업그레이드는 노드 수와 워크로드에 따라 30분에서 1시간을 넘기는 경우도 있다. 노드가 많고 PDB가 빡빡하게 걸려있으면 drain이 천천히 진행되기 때문에, 큰 클러스터에서는 더 오래 걸릴 수 있다.

문제는 이 작업을 감싸고 있는 도구들의 timeout이 그보다 짧게 잡혀있는 경우가 많다는 점이다.

CI/CD Job Timeout

GitLab Runner Job의 기본 timeout은 1시간으로 잡혀있는 경우가 많다. 평소 실행하는 plan이나 apply 작업은 길어야 5분 안에 끝나기 때문에 1시간이면 충분해 보이지만, 클러스터 업그레이드는 얘기가 다르다.

큰 클러스터에서 컨트롤 플레인 업그레이드, Add-on 업데이트, 노드 그룹 롤링 업그레이드까지 한 파이프라인에서 다 처리하면 1시간을 훌쩍 넘기는 경우가 생긴다. 그러면 AWS 쪽에서는 작업이 멀쩡히 진행되고 있는데 GitLab Job만 timeout으로 죽는다. 파이프라인은 실패 상태로 끝나고, 콘솔에서 확인해보면 노드 그룹은 여전히 Updating 상태로 돌아가고 있다.

이런 상황이 처음 생기면 좀 당황스럽다. 작업 자체가 잘못된 건지 GitLab 설정 문제인지 바로 판단이 안 되기 때문이다. 그래서 업그레이드 관련 Job은 미리 timeout을 길게 잡아두는 것이 안전하다.

apply:
  stage: apply
  timeout: 3 hours
  script:
    - tofu apply plan.cache

GitLab의 경우 .gitlab-ci.yml의 Job 단위에서 timeout을 지정할 수 있고, 프로젝트 설정의 CI/CD timeout도 같이 확인해야 한다. Job 레벨 timeout은 프로젝트 레벨 timeout을 넘어설 수 없기 때문에, 프로젝트 기본값이 1시간으로 잡혀있으면 Job에서 3시간으로 늘려도 1시간에서 잘린다.

Helm과 그 외 재배포 작업의 Timeout

CI/CD Job timeout 외에 또 하나 신경 써야 하는 게 있다. 클러스터 위에서 동작하는 다른 자동화 작업들의 timeout이다.

Helm release를 CI/CD로 배포하는 경우, 보통 helm upgrade --wait 같은 옵션을 써서 모든 Pod가 Ready 상태가 될 때까지 기다리도록 설정한다. 평소에는 이 옵션이 빠르게 끝나는데, 클러스터 업그레이드 직후에는 다른 양상이 된다. 노드들이 새 버전으로 교체되는 동안 Pod가 한꺼번에 재배치되기 때문에, 일시적으로 Pending 상태에 머무는 Pod가 많아진다.

이때 Helm release를 새로 배포하려고 하면, --wait이 모든 Pod의 Ready를 기다리다가 timeout에 걸린다. 기본 timeout이 5분인 경우가 많은데, 노드 부족 상황에서는 5분 안에 안 풀리는 일이 흔하다.

helm upgrade myapp ./chart \
  --wait \
  --timeout 15m

특히 다음과 같은 워크로드는 평소 재배포 시간보다 클러스터 업그레이드 직후의 재배포가 훨씬 오래 걸린다.

  • StatefulSet 기반 서비스: PVC 바인딩과 AZ 제약 때문에 새 노드로 옮겨가는 데 시간이 더 걸린다.
  • 초기화가 오래 걸리는 컨테이너: JVM 기반 애플리케이션이나 무거운 캐시를 메모리에 올리는 서비스는 startup 시간이 길다.
  • DaemonSet 형태로 배포되는 모니터링/로그 수집 컴포넌트: 노드가 늘어나거나 교체되는 동안 매번 새로 떠야 한다.

ArgoCD를 쓰고 있다면 Application의 sync timeout도 비슷한 영향을 받는다. retry 정책과 timeout 값이 평소 환경에 맞춰져 있으면, 클러스터 업그레이드 직후의 불안정한 시간 동안 sync가 계속 실패하는 일이 생긴다.

이런 timeout들은 보통 평소에는 문제가 없으니까 기본값 그대로 두게 된다. 그러다가 클러스터 업그레이드를 진행하는 그 시점에 갑자기 여러 군데서 동시에 터지면, 어디부터 봐야 할지 정신이 없어진다. 업그레이드 일정이 잡히면 관련 자동화 도구들의 timeout 설정을 한번씩 훑어보는 게 좋다.

이런 작은 설정들이 실제로 업그레이드를 시작한 다음에 발견되면, 도중에 멈춘 상태에서 수동으로 정리해야 하는 골치 아픈 상황이 된다.

미리 영향도를 파악해두는 것이 절반이다

업그레이드 자체는 한 줄 변경이지만, 그 한 줄을 푸시하기 전에 확인해야 할 게 꽤 많다. 특히 Kubernetes 자체의 API 변경 사항과 클러스터에 설치된 컴포넌트들의 호환성을 미리 점검해야 한다.

Deprecated API 확인

Kubernetes는 마이너 버전마다 일부 API를 deprecated 처리하고, 일정 버전 이후에는 완전히 제거한다. 예를 들어 policy/v1beta1 PodDisruptionBudget은 1.25에서 제거됐고, autoscaling/v2beta2도 비슷한 시기에 사라졌다.

운영 클러스터에서 사용 중인 매니페스트들이 deprecated API를 쓰고 있으면, 업그레이드 후에 갑자기 동작하지 않게 된다. 이걸 사전에 확인하는 가장 쉬운 방법은 EKS Upgrade Insights다.

aws eks list-insights \
  --filter kubernetesVersions=1.31 \
  --cluster-name $CLUSTER_NAME

AWS 콘솔에서도 EKS 클러스터 페이지의 Upgrade Insights 탭에서 같은 정보를 확인할 수 있다. 어떤 deprecated API가 사용 중이고, 어떤 리소스가 영향을 받는지 알려준다.

이것 말고도 kubent(kube-no-trouble) 같은 도구로 클러스터 안의 모든 매니페스트를 스캔해서 deprecated API 사용 여부를 검사할 수도 있다. CI 파이프라인에 이런 검사를 넣어두면 매니페스트가 머지되기 전에 잡을 수 있다.

Add-on 호환성

EKS Add-on(VPC CNI, kube-proxy, CoreDNS, EBS CSI Driver 등)도 Kubernetes 버전과 호환되는 버전이 정해져 있다. 컨트롤 플레인을 1.31로 올렸는데 Add-on이 1.30용 버전 그대로면, 이론상 한 단계 정도의 skew는 허용되지만 권장은 아니다.

특히 CoreDNS는 메이저 버전이 자주 올라가서, 컨트롤 플레인 버전과 안 맞으면 DNS 해석에 영향을 줄 수 있다. 컨트롤 플레인 업그레이드 직후에 Add-on 버전도 같이 올려주는 것이 안전하다.

# 호환되는 Add-on 버전 조회
aws eks describe-addon-versions \
  --addon-name coredns \
  --kubernetes-version 1.31 \
  --query "addons[].addonVersions[:5].{Version:addonVersion,Default:compatibilities[0].defaultVersion}" \
  --output table

Terraform에서 Add-on을 관리하고 있다면, 컨트롤 플레인 버전을 올리는 동시에 Add-on 버전도 같이 업데이트하는 변경을 한 MR에 묶어서 처리했다. plan에서 어떤 변경이 한꺼번에 일어나는지 확인할 수 있어서 리뷰하기에 편했다.

인터넷에 올라온 사례 참고

매번 새 버전으로 업그레이드할 때마다 다른 회사들이 올려둔 후기를 미리 찾아봤다. 공식 릴리즈 노트에는 안 나오는, 실제 운영하면서 겪은 이슈들이 많이 공유되어 있다.

예를 들어 어떤 버전 업그레이드 후에 특정 CNI 플러그인이 갑자기 메모리를 많이 쓴다거나, 특정 Add-on이 새 버전에서 동작이 달라졌다거나 하는 정보들이다.

업그레이드 일정이 잡히면 며칠 전부터 관련 키워드로 검색해서 사례들을 모았다. 이게 의외로 효과가 컸다. 사전 점검 항목에 누락된 부분을 발견하기도 했고, 다른 사람들이 이미 발견한 우회 방법을 미리 알 수 있었다.

특히 EKS Upgrade Journey 같은 형태로 정리해둔 자료들이 도움이 됐다. 어떤 순서로 무엇을 점검하고, 어떤 함정이 있는지 체크리스트 형태로 정리되어 있는 것들이 많아서 그대로 따라하면서 점검할 수 있었다.

운영하면서 정착시킨 루틴

여러 번 업그레이드를 하다 보니 자연스럽게 루틴이 정착됐다. 정리하면 이렇다.

업그레이드 D-30 정도에는 다음 버전이 어떤 변경사항을 포함하고 있는지 릴리즈 노트를 훑어본다. EKS 자체 릴리즈 노트뿐만 아니라 업스트림 Kubernetes 릴리즈 노트도 같이 본다. Beta로 떠 있던 API가 v1으로 승격되거나, 알파에 있던 기능이 베타로 올라오는 시점들을 파악해둔다.

D-14 정도에는 Upgrade Insights를 돌려보고 클러스터에 deprecated API 사용이 있는지 확인한다. 있으면 해당 매니페스트를 수정하는 작업을 별도 MR로 먼저 처리한다.

D-7 정도에는 같은 버전으로 업그레이드한 다른 회사 사례들을 찾아본다. 공식 채널에 안 나오는 함정들이 보통 여기서 나온다. 발견된 이슈가 있으면 사전 점검 항목에 추가한다.

D-1에는 PDB 설정을 한번 더 점검하고, 백업이 잘 되어 있는지 확인한다. 특히 stateful 워크로드의 EBS 스냅샷이 최신인지 본다.

D-Day에는 컨트롤 플레인 업그레이드부터 시작한다. 이건 사실 가장 안전한 단계다. 워크로드에 영향이 없기 때문에 대낮에 진행해도 무방하다. 그 다음 Add-on 버전을 올리고, 마지막에 노드 그룹 롤링 업그레이드를 시작한다. 노드 업그레이드는 트래픽이 적은 시간대를 노린다.

업그레이드 도중에는 Grafana 대시보드와 alerting 채널을 같이 보면서 진행한다. 특정 서비스의 에러율이 평소보다 올라가면 일시적으로 멈추고 원인을 확인한다. 노드 그룹은 한 번에 다 올리지 않고 max_unavailable 설정으로 순차적으로 진행되기 때문에 중간에 문제가 생겨도 어느 정도 통제가 가능하다.

업그레이드 직후에는 24-48시간 정도 모니터링을 더 빡빡하게 본다. 메모리 사용량 패턴이 미묘하게 달라지거나, 특정 메트릭이 평소와 다른 양상을 보이는 경우가 가끔 있다. 이런 변화가 정상 범위인지, 새 버전에서 뭔가 바뀐 건지 확인한다.

마무리

EKS 업그레이드는 도구 측면에서는 정말 쉬워졌다. Terraform으로 관리하고 있다면 변수 하나 바꾸는 것이 전부고, GitLab CI/CD 같은 파이프라인이 있다면 그 작업도 모두 코드 리뷰를 거쳐서 진행된다.

하지만 도구가 쉬워졌다고 해서 업그레이드 자체가 가벼운 작업이 되는 건 아니다. 표준 지원 기간을 놓치지 않으려면 정기적으로 챙겨야 하고, 모든 Pod가 한꺼번에 재시작되는 이벤트라는 점에서 평소에 워크로드 설정을 잘 해둬야 하며, Terraform timeout 같은 운영 도구 설정도 미리 준비해야 한다.

그리고 가장 중요한 건 업그레이드 전에 영향도를 파악해두는 것이다.

결국 업그레이드의 난이도는 클러스터 자체의 복잡도가 아니라, 그 위에 올라간 워크로드의 안정성과 평소 운영 습관에 좌우된다.