DevOps

EKS 클러스터 11개를 서비스 중단 없이 버전 통일하기

버전이 제각각이던 EKS 클러스터 11개를, AI를 오퍼레이터로 두고 8일 만에 v1.35로 통일했습니다.

김용현
·
# Kubernetes# Karpenter# Bottlerocket
EKS 클러스터 11개를 서비스 중단 없이 버전 통일하기
date
slug
author
status
tags(최대 3개)
summary
type
thumbnail
category
updatedAt
TL;DR
  • EKS 클러스터 11개가 v1.31~v1.34에 제각각 멈춰 있었습니다. 노드 오토스케일러인 Karpenter는 가장 낡은 곳이 v0.33이라 API 세대(v1beta1→v1)를 통째로 건너야 했습니다.
  • 8일 만에 모든 클러스터의 버전을 v1.35, Karpenter v1.9, 노드 OS Bottlerocket으로 통일했습니다.
  • AI 에이전트가 정찰·초안·검증을 맡아 PR 109건을 냈고 머지와 적용 승인은 사람이 지켰습니다.
  • 서비스 중단 없던 것은 실패 가능한 환경을 설계한 결과입니다.

출발점: 모든 다른 버전의 11개 클러스터

2년 전, 메인 클러스터를 쿠버네티스 v1.29로 올렸습니다. 착수 커밋부터 마무리 머지까지 약 6주 걸렸습니다. 당시엔 사람이 직접 호환성 매트릭스를 읽고, 변경을 만들고 검증까지 진행했습니다.
이번 업데이트 전까지 EKS 클러스터 11개의 상태입니다.
클러스터 용도
쿠버네티스 버전
Karpenter
데이터 파이프라인 (dev/prod)
v1.34 / 1.33
v1.6 / v0.33
DB 운영 (dev/prod)
v1.32 / 1.32
v1.5 / 1.5
내부 도구 (dev/prod)
v1.34 / 1.34
v1.6 / 1.6
보안 허브
v1.34
v1.6
웹 서빙 (dev/prod), 스테이징
v1.35
v1.5 ⚠️ (1.35 미지원)
개발 메인 (98노드)
v1.31
v1.0
운영 메인 (222노드, 고객 트래픽)
v1.31
v1.0
업데이트를 미루기보다 건드렸다 뭐가 터질지 몰라 버전이 밀립니다. 컨트롤 플레인을 올리는 건 간단하지만, 모든 노드를 교체해야 합니다. 노드를 교체하면 파드까지 전부 다른 노드로 옮겨갑니다. Karpenter가 새 버전을 지원하는지, CRD가 깨지지 않는지, 상태를 가진 워크로드가 재기동을 견디는지 확인해야 합니다. 11개의 클러스터에서 확인해야 했습니다. 가장 낡은 데이터 클러스터의 Karpenter는 v0.33이라, API 세대가 v1beta1에서 v1로 완전히 바뀌는 구간을 한 번에 건너야 했습니다.
이번 프로젝트로 바꾸고 싶었던 건 업그레이드 자체보다 조직에 업그레이드를 경계하지 않는 인식을 심는 것이었습니다.

원칙: 절차 생략없이 속도 유지하기

착수 전에 다음과 같은 원칙을 고려했습니다. 지속 가능한 설계로 절차를 생략하지 않고 속도감을 유지합니다.
한 번에 한 마이너만 올립니다. v1.31에서 v1.35로 건너뛰지 않습니다. 컨트롤 플레인은 v1.32, v1.33, v1.34, v1.35를 차례로 밟고 각 단계에서 노드와 애드온(coredns, kube-proxy, CNI)을 함께 맞춥니다. 문제가 생겨도 어딘지 분명해집니다.
선행 조건부터 역순으로 맞춥니다. 각 쿠버네티스 버전마다 지원되는 Karpenter 최소 버전이 있습니다(v1.34v1.6 이상, v1.35v1.9 이상). 노드를 올리기 전에 Karpenter를, Karpenter를 올리기 전에 IAM 권한과 CRD를 먼저 맞췄습니다. 순서를 어기면 새 노드가 아예 뜨지 못하는 구간이 생깁니다.
개발 클러스터를 항상 먼저 올립니다. 같은 유형의 dev에서 전체 절차를 한 번 통과시켜 패턴을 검증하고 prod에 그대로 반복했습니다. prod 클러스터는 전부 파일럿을 거친 뒤에 올라갑니다.
상태 있는 워크로드는 얼려두고 맨 마지막에 옮깁니다. 그래프 DB, 로그 분석 DB(ClickHouse), 단일 replica 도구처럼 재기동이 아픈 워크로드는 노드 교체 대상에서 먼저 제외합니다(disruption budget 0). 다만 수명이 정해진 ClickHouse 노드 13대는 옮기지 않고 그대로 뒀습니다.
매 단계를 같은 기준으로 검증합니다. 노드 버전과 시스템 컴포넌트 상태는 기본이고, 클러스터 내부 전체 파드의 상태(비정상·재시작 카운트)와 워크로드 로그(에러 레벨·5xx)까지 매 단계 같은 기준으로 확인했습니다. 체크리스트는 8일 동안 수십 번 반복했습니다.
노드 교체의 안전판은 두 겹이었습니다. 관리형 노드그룹은 새 노드가 준비된 뒤에만 기존 노드를 비우는 서지(surge) 방식입니다. Karpenter 풀은 동시에 교체 가능한 노드 수를 disruption budget으로 묶었습니다. 여기에 PodDisruptionBudget과 강제 분산(topology spread)이 겹칩니다. 어느 층에서든 새 노드가 준비되기 전에 옛 노드가 사라지는 일은 구조적으로 일어날 수 없습니다.
버전 계단과 선행 조건
버전 계단과 선행 조건

운영 모델: AI에게 맡긴 일과 사람이 지킨 일

notion image
진짜 실험은 업그레이드 자체보다 운영 모델이었습니다. AI 에이전트(Claude Code)를 질문에 답하는 도구가 아니라 오퍼레이터로 썼습니다.
AI는 반복과 정찰을 맡았습니다. AI는 클러스터 현황을 정찰해 버전·노드·애드온·호환성 매트릭스를 정리했습니다. 변경 PR 초안을 만들어 적용하고, 검증 루프(노드 버전·전체 파드 상태·로그 스캔)를 돌렸습니다. 8일간 세 개 저장소에 걸쳐 PR 109건이 이렇게 만들어졌습니다. 사람이 했다면 클러스터당 반나절 걸리던 일이 분 단위로 줄었습니다. 더욱 중요한 건 11개 클러스터를 도는 동안 같은 체크리스트가 단 한 번도 생략되지 않았습니다.
승인은 사람이 맡습니다. 109개의 PR이 사람의 머지 승인을 거쳤고, prod 적용(IaC apply, 수동 sync)은 별도의 승인 게이트를 한 번 더 지났습니다. GitOps와 IaC 위에서 작업했기에 게이트는 자연스럽게 PR과 apply 지점에 걸립니다. AI가 아무리 확신해도 변경이 실제 클러스터에 닿는 마지막 관문은 사람이 지켰습니다. 상태 있는 워크로드의 이동 창을 언제 열지, 인시던트에서 여러 해법 중 무엇을 택할지도 사람이 판단합니다.
무거운 결정은 AI끼리 반박시켰습니다. AI가 내린 결정에 신뢰성을 더하기 위해 DBA 상원님의 협업 프로토콜인 AI Bridge를 사용했습니다. 서로 다른 AI 모델을 파일 기반 라운드 방식으로 연결해, 한쪽이 설계하면 다른 쪽이 데이터를 들고 반박하고 그 과정을 모두 기록합니다. 무거운 결정은 전부 이 프로토콜에서 검증했고 결론을 바꾸기도 했습니다.
마지막 요소는 연속성입니다. 진행 상황을 체크포인트 문서와 장기 메모리에 계속 남겨, 세션이 끊겨도 "어디까지 했더라"에서 다시 시작하지 않게 했습니다. 8일의 작업은 여러 세션으로 쪼개져 있었지만 기록 덕에 흐름을 유지할 수 있었습니다.
*AI 협업 프로토콜(AI Bridge) 예시
notion image
노드 교체 시 게이트웨이의 종료 유예(preStop) 값을 정할 때 한쪽이 일반론에 기대 "60~90초가 필요하다"고 추정합니다.
반대쪽 검토를 거쳐 개발 환경에서 실측을 돌립니다. 측정된 실제 전파 시간은 9.5초 입니다. 설정값은 실측의 2배 마진인 20초로 확정합니다.
한 개 모델이 결론 짓는 대신 다른 AI가 서로 근거를 들고 논쟁하게 두고, 갈라지는 지점만 사람이 판정합니다.
추정끼리 적당히 합의하는 게 아니라 실측으로 판단합니다.

8일 간 업그레이드 진행 순서

notion image
1~2일차: 클러스터 9개 일괄 업그레이드
데이터·DB·내부 도구·보안 허브·웹 클러스터 9개를 v1.35로 맞췄습니다. dev에서 패턴을 검증한 뒤 prod에 그대로 반복합니다. 첫 파일럿(데이터 dev)에서 Karpenter v1.9(CRD·IAM 포함)와 컨트롤 플레인을 단계별로 올린 뒤 애드온을 정렬하고 전 파드를 검증했습니다. 이 순서를 모든 클러스터에 적용합니다. 보안 허브처럼 예민한 곳은 접근 통제 도구가 올라간 노드 풀을 교체 대상에서 제외하고 진행했습니다. 가장 낡은 데이터 클러스터는 Karpenter v0.33에서 v1.9까지 API 세대를 건너는 풀 마이그레이션을 진행했습니다.
3일차: 개발 메인 클러스터
98대 노드의 개발 메인을 v1.31에서 v1.32로 올리면서, 오래된 레거시 클러스터를 다루는 절차를 처음 정립했습니다. 클러스터가 수작업 시대의 IaC와 GitOps가 섞인 하이브리드 형태라 도구의 주체부터 확인했습니다.
6~7일차: 운영 메인, 19시간 17분
고객 트래픽을 받는 운영 메인(222대의 노드)을 v1.31에서 v1.35로 올렸습니다. 컨트롤 플레인 업그레이드 시작부터 마지막 노드 교체 완료까지 19시간 17분이 걸렸습니다. 각 버전마다 컨트롤 플레인을 올리고 곧바로 버전에 맞는 노드로 전부 교체했습니다. 4번 교체할동안 게이트웨이·인증·결제·커머스 워크로드는 트래픽을 받았습니다. 트래픽에 가장 민감한 게이트웨이 풀은 카나리 노드 한 대로 시작해 정상을 확인한 뒤에야 순차로 나머지 노드를 교체했습니다.
노드 OS 전환도 상당 부분 이번 업그레이드에서 함께 처리했습니다. 인증·게이트웨이를 포함한 여러 풀이 v1.32 에서 버전 교체와 동시에 Bottlerocket으로 넘어갔습니다. 어차피 노드를 전부 갈아야 한다면 한 번 교체할 때 두 가지 변화를 같이 적용하는 편이 총 교체 횟수를 줄입니다. 물론 앞서 파일럿 풀 하나로 검증한 뒤에 해당됩니다.
7~8일차: Bottlerocket 전환 마무리
남은 대형·특수 풀을 전환했습니다. Bottlerocket은 컨테이너 실행에 필요한 것만 남긴 불변 OS입니다. 루트 파일시스템이 읽기 전용이라 노드에 들어가 손댈 필요가 없습니다. SSH 없이 운영하는 팀의 방향과 정확히 맞물립니다. 두 곳이 까다로웠습니다. CI 러너 풀은 privileged docker-in-docker가 불변 OS 위에서 도는지를 파일럿 노드에서 빌드·실행·풀까지 실측으로 확인한 뒤 넘겼습니다. OS/데이터 볼륨이 분리되는 디스크 구조 차이는 러너 캐시 용량을 맞춰 흡수했습니다. 모니터링 풀은 같은 노드 클래스를 ClickHouse가 공유하고 있어, DB 쪽 정의를 건드리는 대신 모니터링 풀을 새 노드 클래스로 떼어내는 반대 방향을 택했습니다. 재기동이 아픈 쪽은 손대지 않는다는 원칙의 연장입니다.
8일이 지나고 클러스터 11개의 버전은 v1.35, Karpenter의 버전은 v1.9, ClickHouse 노드 13대(fade-out 예정)를 제외한 dev·prod 전체가 Bottlerocket이 됐습니다.

3번 막혀도 멀쩡한 서비스

함정 1. 남아 있던 구버전 객체가 전환을 막다
데이터 클러스터에서 Karpenter API를 v1 전용으로 전환한 직후 컨트롤러가 작동하지 않았습니다. 원인은 전환 전에 안전을 위해 노드들을 얼려뒀는데, 리소스 객체가 etcd에 구버전(v1beta1)으로 물리 저장된 채 남아 있었습니다. CRD가 v1 전용이 되는 순간, 객체들은 읽을 수 없었고, 목록 조회가 실패하자 컨트롤러의 캐시 동기화가 시작 단계에서 멈췄습니다. 해법은 쿠버네티스의 정석인 storage version migration입니다. 구버전 읽기를 임시로 열고 객체를 하나씩 써서 v1로 재저장한 뒤 닫았습니다.
다음과 같은 교훈을 얻었습니다. API 세대를 닫기 전에 저장된 모든 객체가 새 세대로 읽히는지 전수 확인해야 합니다(kubectl get --raw). 또한 안전을 위한 동결이 마이그레이션보다 먼저 오면 얼어붙은 객체가 나중에 발목을 잡습니다. 그동안 그래프 DB는 얼려진 노드 위에서 손상 없이 돌았습니다. 막힌 건 서비스가 아닌 컨트롤러였습니다.
구버전(v1beta1)을 새 세대로 재저장하는 storage version migration
구버전(v1beta1)을 새 세대로 재저장하는 storage version migration
함정 2. 선언과 현실의 불일치
선언형 컨트롤러(ACK)로 관리하는 클러스터에서 노드 그룹의 버전 필드는 올렸는데, 자동으로 채워지는 AMI 릴리스 버전 필드가 구버전을 가리켰습니다. GitOps 화면은 "정상 동기화"인데 실제 업그레이드는 두 시간째 그대로였습니다. 선언이 현실과 다르면 컨트롤러는 조용히 멈춥니다. AWS API를 직접 호출해 롤을 진행시키고, 완료 후 선언 값을 실제 값으로 맞췄습니다. 이후 클러스터부터 컨트롤 플레인이 활성화 되는 즉시 노드 그룹 업그레이드를 직접 트리거하도록 표준 절차에 넣었습니다.
함정 3. 전환 도구 자체의 결함
v1beta1에서 v1로 넘어가는 길목의 중간 버전에는 conversion webhook의 알려진 결함이 있었습니다(컨트롤러가 인증서와 어긋나는 설정을 90초마다 되쓰는 문제). 만나서 대응한 게 아니라 사전 조사에서 미리 알고 들어갔고, 해당 버전에 머무는 시간을 최소화해 다음 버전으로 곧장 탈출하는 경로를 설계에 넣었습니다.
세 개의 함정은 공통점이 있습니다. 상태 있는 워크로드는 얼려져 있었고 안전판(budget·surge·PDB)은 켜져 있었습니다. 그래서 범위 내에서 발생했습니다.

숫자로 보는 결과

항목
이전(~2024년)
이후(2026년 상반기~)
쿠버네티스 버전
11개 클러스터, v1.31~1.35 혼재
전 클러스터 v1.35 통일
Karpenter
v0.33~1.6 혼재 (API 세대 격차 포함)
전 클러스터 v1.9 통일
노드 OS
AL2/AL2023 중심 혼재
ClickHouse 13대 제외 전량 Bottlerocket
소요 기간
1개 버전에 약 30 영업일
4개 버전 × 11클러스터 + OS 전환 = 8일
운영 메인 (222노드)
v1.31→v1.35 19시간 17분, 트래픽 정상
변경 규모
PR 109건, 상시 노드 약 420대 전면 교체
서비스 중단 / 상태 워크로드 손상
0건 / 0건
부수 효과도 실측했습니다. 업그레이드 완료 하루 뒤 애플리케이션 레이어에서 별개의 이슈가 생겼습니다. "인프라 작업 때문 아니냐"는 의심에 분 단위 타임라인과 측정 데이터로 반나절 안에 답할 수 있었습니다. 마지막 노드 교체와 이슈 사이에는 약 24시간의 간격이 있었고, 원인은 애플리케이션 프로세스로 확정됐습니다. 모든 단계가 기록으로 남는 운영은 사후 규명 비용을 줄입니다.

다음은 에이전트가 절차부터 직접 제안하는 단계

확인한 것은 세 가지입니다.
첫째, 속도와 안전은 비례합니다. 같은 절차의 반복, 매 단계 같은 검증, 실패해도 되는 구간을 미리 설계한 덕에 속도와 안전성을 동시에 확보했습니다.
둘째, AI는 오퍼레이터로 쓸 때 값이 큽니다. 정찰, 초안 작성, 검증 루프를 담당하지만 최종 승인은 사람이 지켰습니다. AI의 판단은 다른 AI 모델로 반박하고, 최종 판정은 측정에 맡기는 구조를 만들었습니다. 이 구조가 팀 내에 문화로 자리 잡은 점도 유의미합니다.
셋째, 업그레이드는 프로젝트보다 루틴에 가깝습니다. 정리된 정책과 가드레일 덕에 사람이 용기를 낼 수 있게 된 단계입니다. 검증된 절차, 안전판, 기록이 있으니 6주 걸리던 일을 서비스 중단 없이 8일 만에 끝낼 수 있었습니다.
다음 단계는 사람이 아닌 에이전트가 정리된 절차를 스스로 인식하고 먼저 움직이는 과정을 고민하고 있습니다. 새 버전이 나오면 에이전트가 스스로 dry-run과 호환성을 검증(Karpenter·CRD·애드온 매트릭스)합니다. 클러스터 우선순위에 따라 점진 업그레이드 계획을 세우고 사람의 승인을 기다립니다. 적용 중에는 실시간 모니터링과 이슈 대응, 롤백 준비까지 함께 진행합니다. 사람은 승인만 하면 됩니다.
노드 OS까지 통일된 이 기반 위에서 비용에 관한 이야기도 다룰 예정입니다. 같은 워크로드를 Graviton(ARM)과 SPOT으로 더 싸게 돌리기 위한 멀티아키텍처 빌드, 그리고 종료 유예를 추정이 아닌 실측으로 튜닝한 이야기를 이어가겠습니다.
 
김용현 Infra Lead 추측 대신 실측으로 계정 경계부터 클러스터까지 안전하게 인프라를 운영합니다.
 

댓글