Kubernetes Practice 07

Deployment: ReplicaSet을 넘어선 스마트한 배포

Deployment YAML은 ReplicaSet과 비슷해 보이는데, 왜 굳이 Deployment를 쓰는지와 이미지 변경이 어떤 단계로 흘러가는지 헷갈린다.

근거 · Kubernetes 교안 p72-p83, yaml/deploy/deploy-nginx.yaml

Deployment: ReplicaSet을 넘어선 스마트한 배포 대표 이미지

1장: Deployment와 ReplicaSet: 겉모습이 같다고 속까지 같을까?

솔라가 노트북 화면의 두 파일을 번갈아 보며 미간을 찌푸렸다. 왼쪽에는 replicaset-definition.yaml, 오른쪽에는 deployment-definition.yaml이라는 이름이 붙어 있었다. 솔라의 손가락이 터치패드를 오가며 두 파일의 내용을 스크롤했지만, 보면 볼수록 고개만 갸웃거려졌다.

“언니, 잠깐 이것 좀 봐봐.”

거실 테이블에서 조용히 책을 읽고 있던 루나가 고개를 들었다. 솔라는 노트북을 통째로 들고 와 테이블 위에 올려놓았다.

“이게 ReplicaSet이고 이게 Deployment YAML 파일인데, 이상할 정도로 비슷해. replicas 개수 정하는 것도 있고, 어떤 Pod를 만들지 정의하는 template 부분은 거의 똑같잖아. 그냥 Pod를 원하는 개수만큼 유지하는 게 목적이라면, ReplicaSet만으로도 충분한 거 아니야? 굳이 Deployment를 써야 할 이유를 모르겠어. 그냥 이름만 다른, 좀 더 복잡한 버전 같아.”

솔라의 말처럼 두 파일의 spec 필드 아래 구조는 놀랍도록 닮아 있었다. 솔라가 가리키는 template 섹션은 컨테이너 이미지와 포트 번호까지 정확히 일치했다. Deployment는 그저 ReplicaSet의 다른 이름일 뿐이며 기능적 차이가 크지 않다는 솔라의 생각은 타당해 보였다.

루나는 솔라의 화면을 잠시 들여다보더니, 소파 등받이에 편안히 기댔다.

“그럴듯한 의문이네. 그럼 그 Deployment 파일을 한번 클러스터에 적용해볼까? 그리고 나서 무슨 일이 일어나는지 같이 보자.”

정의를 설명하는 대신 직접 확인해보자는 루나의 말에 솔라는 기다렸다는 듯 터미널 창을 열었다. 그리고 망설임 없이 명령어를 입력했다.

kubectl apply -f deployment-definition.yaml

deployment.apps/my-app-deployment created 라는 메시지가 터미널에 나타났다.

“자, 이제 뭐가 만들어졌는지 확인해볼 차례네. 먼저 Deployment부터.”

루나의 말에 솔라는 다음 명령어를 입력했다.

kubectl get deploy

터미널에는 방금 생성한 Deployment의 정보가 표시되었다.

NAME                READY   UP-TO-DATE   AVAILABLE   AGE
my-app-deployment   3/3     3            3           15s

“응, my-app-deployment 잘 만들어졌고, 원하는 Pod 개수 3개도 모두 준비됐네. 그럼, 이제 ReplicaSet은 어떻게 되어 있을까?”

솔라는 순간 멈칫했다. ‘ReplicaSet은 따로 만든 적이 없는데?’ 하는 표정이었다. 하지만 이내 루나의 의도를 알아차리고 명령어를 입력했다.

kubectl get rs

화면에 나타난 결과는 솔라의 예상을 완전히 뒤엎었다.

NAME                           DESIRED   CURRENT   READY   AGE
my-app-deployment-6b47d6796c   3         3         3       45s

“어?”

솔라의 입에서 짧은 탄성이 터져 나왔다.

“나는 분명 Deployment만 만들었는데, ReplicaSet이 저절로 생겼네. 이름도 이상해. 내가 만든 Deployment 이름 뒤에 알 수 없는 문자들이 붙어있고… 원하는 Pod 개수도 정확히 3개로 설정되어 있어.”

솔라는 두 개의 get 명령어 결과를 번갈아 보며 중얼거렸다. 자신이 직접 만든 것은 my-app-deployment 하나뿐이었다. 하지만 쿠버네티스 클러스터 안에는 my-app-deployment-6b47d6796c라는 이름의 ReplicaSet이 떡하니 자리 잡고 있었다. 마치 누군가가 솔라의 명령을 보고 알아서 필요한 부품을 조립해 놓은 것 같았다.

루나가 조용히 입을 열었다.

“맞아. 그게 바로 Deployment의 역할이야. 솔라 네가 직접 ReplicaSet을 만든 게 아니라, Deployment가 너를 대신해서 ReplicaSet을 만들고 관리해주는 거지.”

그제야 솔라는 모든 조각이 맞춰지는 것을 느꼈다. 겉모습이 비슷하다고 해서 둘의 역할까지 같은 것은 아니었다.

“아…! 그러니까 Deployment는 ReplicaSet을 생성하고 관리하는 상위 객체구나. 내가 직접 일하는 작업자(ReplicaSet)를 고용한 게 아니라, 작업자들을 관리하는 현장 소장(Deployment)을 고용한 셈이네. YAML 파일이 비슷했던 건, 현장 소장에게 ‘이런 기술을 가진 작업자들을 데려와줘’라고 명세서를 주는 것과 같았기 때문이고.”

솔라의 눈이 반짝였다. 이제 Deployment는 더 이상 ReplicaSet의 불필요한 복사본으로 보이지 않았다. 그것은 명백히 다른 계층에서 움직이는, 더 높은 수준의 ‘관리자’였다. 자신의 명령 하나로 하위 리소스가 자동으로 생성되는 것을 직접 목격하자, 둘의 관계가 명확하게 그려졌다.

스스로의 판단으로 혼란을 정리한 솔라의 머릿속에 새로운 질문이 떠올랐다.

“그렇구나. 그럼 굳이 현장 소장(Deployment)을 두는 이유가 분명히 있겠네. 그냥 작업자(ReplicaSet)한테 직접 일을 시키는 것보다 더 잘하는 게 있으니까. 예를 들면… 애플리케이션을 업데이트해야 할 때 같은 경우에 말이야.”

2장: 이미지 변경의 비밀: Deployment는 어떻게 Pod를 바꿀까?

솔라의 손가락이 키보드 위를 맴돌았다. 터미널에는 이전 장에서 확인했던 my-app-deployment와 그것이 생성한 ReplicaSet의 정보가 그대로 남아 있었다. ‘현장 소장(Deployment)이 작업자(ReplicaSet)를 고용한다.’ 스스로 내린 결론은 명쾌했지만, 바로 그 때문에 새로운 궁금증이 생겨났다. 현장 소장은 왜 필요할까? 그냥 작업자에게 직접 일을 시키는 것보다 무엇이 더 나을까?

솔라는 화면 속의 컨테이너 이미지 버전을 노려보았다. 현재 버전은 nginx:1.14.2였다. 만약 이 웹서버를 nginx:1.16.1 버전으로 업데이트해야 한다면? 솔라는 중얼거렸다.

“현장 소장에게 ‘이제부터는 새 기술을 가진 작업자들을 써줘’라고 말하면 되겠지. 아마 기존 작업자들(ReplicaSet)에게 새 기술(새 이미지)을 가르쳐서 일을 계속 시키지 않을까? Pod들을 멈추지 않고 이미지만 쓱 바꾸는 거지.”

이것이 솔라가 상상한 가장 효율적인 방식이었다. Deployment가 이미 실행 중인 Pod의 컨테이너 이미지를 직접 변경하거나, 아니면 최소한 기존 ReplicaSet의 설정을 바꿔서 Pod들을 업데이트할 것이라고 예상했다.

옆에서 이 모습을 지켜보던 루나가 찻잔을 내려놓으며 말했다.

“그 가설을 확인해 볼 좋은 방법이 있어. 업데이트 명령을 내리는 동시에, Pod와 ReplicaSet의 변화를 실시간으로 지켜보는 거야. 한쪽 터미널에는 Pod 목록을, 다른 쪽에는 ReplicaSet 목록을 띄워놓고 시작해 봐.”

루나의 제안에 솔라는 고개를 끄덕이며 터미널 창 두 개를 나란히 띄웠다. 한쪽에는 kubectl get pods --watch를, 다른 한쪽에는 kubectl get rs를 입력할 준비를 마쳤다. 그리고 새로운 명령어를 터미널에 입력했다. Deployment의 컨테이너 이미지를 새 버전으로 교체하라는 명령이었다.

kubectl set image deployment/my-app-deployment nginx=nginx:1.16.1

deployment.apps/my-app-deployment image updated 라는 메시지가 뜨자마자, 솔라의 눈은 양옆의 터미널 창으로 향했다. 놀라운 일이 벌어지고 있었다.

Pod 목록을 지켜보던 창에서 먼저 변화가 감지되었다.

NAME                                 READY   STATUS              RESTARTS   AGE
my-app-deployment-6b47d6796c-abc      1/1     Running             0          5m
my-app-deployment-6b47d6796c-def      1/1     Running             0          5m
my-app-deployment-6b47d6796c-ghi      1/1     Running             0          5m
my-app-deployment-7567d5f5f5-jkl      0/1     ContainerCreating   0          2s
my-app-deployment-6b47d6796c-abc      1/1     Terminating         0          5m2s
my-app-deployment-7567d5f5f5-jkl      1/1     Running             0          4s
my-app-deployment-7567d5f5f5-mno      0/1     ContainerCreating   0          5s
my-app-deployment-6b47d6796c-def      1/1     Terminating         0          5m7s
...

“어? 이건… 내가 생각한 거랑 완전히 다른데!”

솔라의 목소리가 커졌다. 그녀의 예상과 달리 기존 Pod의 이미지가 바뀌는 것이 아니었다. my-app-deployment-7567d5f5f5-라는, 이전에 보지 못했던 새로운 이름의 Pod가 생성되고 있었다. 동시에, my-app-deployment-6b47d6796c-라는 익숙한 이름의 기존 Pod들은 Terminating 상태로 바뀌며 하나씩 사라졌다.

혼란스러운 마음에 다른 쪽 터미널 창을 확인하자, 그 이유가 명확해졌다.

kubectl get rs
NAME                           DESIRED   CURRENT   READY   AGE
my-app-deployment-6b47d6796c   2         2         2       6m
my-app-deployment-7567d5f5f5   2         2         2       30s

“ReplicaSet이… 하나 더 생겼어!”

그랬다. Deployment는 기존 ReplicaSet을 수정하지 않았다. 대신, 새로운 이미지(nginx:1.16.1)를 사용하는 새로운 Pod template을 가진 my-app-deployment-7567d5f5f5라는 이름의 ReplicaSet을 새로 생성했다.

그리고는 마치 교통경찰처럼 섬세하게 교체 작업을 조율하기 시작했다. 먼저 새로운 ReplicaSet의 Pod 수를 1로 늘리고, 정상적으로 실행되는 것을 확인한 뒤, 기존 ReplicaSet의 Pod 수를 1 줄였다. 이 과정을 반복하며 모든 Pod가 새 버전으로 교체될 때까지 점진적으로 트래픽을 옮겨갔다. 오래된 Pod가 먼저 사라지고 새 Pod가 생기는 것이 아니라, 새로운 Pod가 준비된 후에야 오래된 Pod가 사라지는 안전한 방식이었다.

솔라는 무릎을 탁 쳤다.

“아하! 현장 소장(Deployment)은 기존 작업자(ReplicaSet)들을 재교육시키는 게 아니었어. 아예 새로운 기술(nginx:1.16.1)을 가진 새로운 작업팀(새 ReplicaSet)을 꾸려서 투입하고, 기존 팀은 철수시키는 거였구나! 한 명씩, 한 명씩 안전하게 교대하면서 말이야.”

Deployment가 단순히 Pod의 개수를 유지하는 것을 넘어, ‘변경’이라는 과정을 얼마나 똑똑하고 안정적으로 관리하는지 눈으로 직접 확인한 순간이었다. 기존 리소스를 직접 건드리는 위험한 방식 대신, 새로운 버전을 만들어 점진적으로 전환하는 방식. 이것이 바로 Deployment가 ReplicaSet 위에 군림하는 관리자로서 가지는 핵심적인 능력이었다.

“이 방법, 정말 똑똑한데. 기존 작업팀을 바로 해고하는 게 아니라 새 팀이 완전히 자리 잡을 때까지 남겨두는 거잖아. 그렇다는 건… 만약 새로 온 팀에 문제가 생기면, 그냥 철수시키고 다시 원래 팀을 불러오면 되는 거 아냐?”

새로운 발견에 흥분한 솔라의 머릿속에, 다음 질문이 자연스럽게 떠올랐다. 안정적인 업데이트의 이면, ‘안정적인 복구’에 대한 호기심이었다.

3장: 안정적인 변화: Deployment의 롤아웃/롤백 능력

솔라의 시선은 두 개의 터미널 창에 고정되어 있었다. 왼쪽 창에는 업데이트 후 새로 생성된 Pod들이 Running 상태로 활기차게 움직이는 모습이, 오른쪽 창에는 방금 전까지 임무를 수행하던 두 개의 ReplicaSet 목록이 나란히 표시되어 있었다. 하나는 현재 활성화된 새 버전(my-app-deployment-7567d5f5f5), 다른 하나는 Pod 개수가 0으로 줄어든 채 조용히 대기 중인 이전 버전(my-app-deployment-6b47d6796c).

이전 버전의 ReplicaSet이 즉시 사라지지 않고 남아있는 모습. 솔라는 바로 그 지점에서 멈춰 섰다. 그녀의 머릿속에서는 방금 전의 발견이 새로운 가설로 부풀어 오르고 있었다. 그녀의 혼잣말이 조용한 거실에 울렸다.

“만약 새로 배포한 nginx:1.16.1 버전에 심각한 버그가 있었다면 어떡하지? 서비스가 마비될 수도 있잖아. 하지만… 기존 작업팀(my-app-deployment-6b47d6796c)을 해고하지 않고 남겨뒀으니까, 이들을 다시 불러오면 되지 않을까? 새로 온 팀은 철수시키고.”

이 가설은 꽤 그럴듯했다. 하지만 어떻게? 솔라는 곧장 가장 직관적인 방법을 떠올렸다. ‘이전 ReplicaSet의 replicas 값을 다시 3으로 수정하고, 새 ReplicaSet의 값은 0으로 바꾸면 되지 않을까?’ 하지만 이내 고개를 저었다. 그것은 너무 수동적이고, 실수하기 쉬운 방법 같았다. 현장 소장(Deployment)이라는 똑똑한 관리자가 그렇게 허술하게 일을 처리할 리 없었다. 아마도 더 세련된 방법이 있을 터였다.

“그 질문이 바로 Deployment의 핵심 가치로 이어지는 길이야.”

솔라의 고민을 읽기라도 한 듯 루나가 입을 열었다.

“솔라 네 말대로, 새로 투입한 작업팀에 문제가 생겼을 때 이전 팀으로 되돌리는 건 아주 중요한 기능이지. Deployment는 그 과정을 기억하고 있어. 한번 현장 소장에게 작업 이력을 물어볼까?”

‘작업 이력’이라는 말에 솔라의 눈이 빛났다. 그녀는 터미널에 새로운 명령어를 입력했다.

kubectl rollout history deployment/my-app-deployment

결과는 명확했다.

REVISION  CHANGE-CAUSE
1         <none>
2         kubectl set image deployment/my-app-deployment nginx=nginx:1.16.1

“와… Deployment가 내가 한 일을 전부 기록하고 있었네. 첫 번째 버전(REVISION 1)에서 두 번째 버전(REVISION 2)으로 업데이트한 것까지. 심지어 어떤 명령어로 변경했는지도 알아.”

솔라는 감탄했다. 이것은 단순한 기록이 아니었다. Deployment가 각 변경 단계를 ‘버전’으로 인식하고 관리하고 있다는 명백한 증거였다. 롤아웃과 롤백이 복잡한 스크립트가 필요한 고급 기능일 거라는 그녀의 생각은 틀렸다. Deployment는 이미 그 모든 것을 위한 기반을 갖추고 있었다.

“그래. 현장 소장은 언제, 어떤 팀으로 교체했는지 장부에 전부 적어두고 있는 거야. 그렇다면 이제 ‘이전 작업으로 되돌려줘’라고 말하기만 하면 되지 않을까?”

루나의 말에 솔라는 망설임 없이 다음 명령어를 실행했다. 이전 버전으로 돌아가라는, 마치 시간 여행과도 같은 명령어였다.

kubectl rollout undo deployment/my-app-deployment

deployment.apps/my-app-deployment rolled back 메시지가 뜨자마자, 솔라는 Pod들의 상태를 실시간으로 보여주던 터미널 창으로 시선을 돌렸다. 경이로운 광경이 펼쳐졌다.

잠시 사라졌던 이전 버전의 Pod, my-app-deployment-6b47d6796c-로 시작하는 이름의 Pod들이 ContainerCreating 상태로 다시 나타나기 시작했다. 그리고 그들이 Running 상태가 되자, 방금 전까지 활발하게 작동하던 새 버전의 Pod, my-app-deployment-7567d5f5f5-들이 차례로 Terminating 상태로 바뀌며 사라졌다.

업데이트 때와 정확히 반대의 과정이었다. Deployment는 이전 버전의 ReplicaSet(my-app-deployment-6b47d6796c)을 다시 활성화하고, 새 버전의 ReplicaSet(my-app-deployment-7567d5f5f5)은 비활성화했다. 이 모든 것이 단 하나의 undo 명령어만으로, 한 치의 오차 없이 자동으로 진행되었다.

솔라는 완전히 이해했다는 표정으로 의자 등받이에 몸을 기댔다.

“이제야 알겠어. Deployment는 단순히 Pod의 개수를 맞추는 복제 관리자가 아니었어. 애플리케이션의 ‘버전’을 관리하는 이력 관리자이자, 안정적인 변경을 조율하는 지휘자였던 거야.”

그녀는 자신의 생각을 정리하며 말을 이었다.

“업데이트를 할 때마다 새로운 ReplicaSet을 만들어서 버전을 남기고, history 명령어로 그 이력을 보여주고, undo 명령어로 언제든 이전 버전으로 되돌릴 수 있게 해주는 것. 내가 할 일은 ‘이전 상태로 돌아가고 싶어’라고 선언하는 것뿐이고, 어떻게 돌아갈지에 대한 복잡한 과정은 Deployment가 전부 알아서 처리해 주네. 이게 바로 ‘선언적인 업데이트 관리’라는 말의 진짜 의미였구나.”

더 이상 솔라에게 Deployment는 ReplicaSet과 비슷한 무엇이 아니었다. 그것은 ReplicaSet을 작업자로 활용하여, 애플리케이션의 생명주기 전체를 안정적으로 관리하는 없어서는 안 될 핵심 컨트롤러였다. 이제 솔라는 애플리케이션을 업데이트해야 할 때, 더 이상 서비스 중단의 불안감을 느끼지 않을 자신이 있었다. 문제가 생기면 언제든 돌아갈 수 있는 안전장치가 있다는 것을, 그리고 그 모든 것을 가능하게 하는 것이 바로 Deployment라는 것을 명확히 이해했기 때문이다.