Kubernetes Practice 06
Kubernetes ReplicaSet: Pod의 '주인'을 찾아 원하는 수를 유지하는 법
replicas: 3은 세 개를 만든다는 뜻처럼 보이지만, selector와 label이 맞지 않으면 ReplicaSet이 어떤 Pod를 자기 것으로 보는지 이해하기 어렵다.
근거 · Kubernetes 교안 p58-p71, yaml/rs/*.yaml
1장: replicas는 ‘생성’이 아닌 ‘유지’다: Pod 수량 관리의 첫 단계
솔라는 모니터에 띄워진 YAML 파일을 손가락으로 가리켰다. 파일명은 replicaset-nginx.yaml. 얼마 전까지만 해도 낯설기만 했던 암호문 같던 내용이 이제는 제법 눈에 들어왔다.
“언니, 이거 봐. replicas: 3이라고 되어 있잖아. 그럼 이건 쿠버네티스한테 ‘nginx Pod 세 개 만들어 줘’ 하고 주문하는 거랑 똑같은 거지? 주문서 보내면 배달 오고 끝나는 것처럼.”
솔라의 말에 루나는 잠시 솔라가 가리키는 화면을 들여다보았다. spec 아래 들여쓰기 된 replicas: 3이라는 한 줄. 솔라의 해석은 지극히 합리적으로 들렸다.
“그렇게 생각했구나. ‘딱 세 개 만들고 끝’. 간결하고 명확하네.”
“그치? 그럼 이 ReplicaSet이 한 번 만든 Pod는 무조건 자기 거라고 기억하고 있는 거겠네.”
솔라는 자신의 추리에 만족한 듯 의기양양하게 말했다. 루나는 대답 대신 솔라의 노트북 쪽으로 고개를 돌렸다.
“그럼 그 주문서, 한번 클러스터에 제출해 볼까? 정말 주문한 대로 세 개가 오는지, 그리고 그걸로 끝인지.”
루나의 제안에 솔라는 신나게 터미널 창을 열었다. 먼저, 실험에 사용할 replicaset-nginx.yaml 파일의 내용을 확인했다.
# replicaset-nginx.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: replicaset-nginx
spec:
replicas: 3
selector:
matchLabels:
app: my-nginx-pods
template:
metadata:
labels:
app: my-nginx-pods
spec:
containers:
- name: nginx
image: nginx
“좋아. replicas는 3이고, selector랑 template의 labels도 잘 맞춰져 있네.”
솔라는 파일을 클러스터에 적용하는 명령어를 망설임 없이 입력했다.
$ kubectl apply -f replicaset-nginx.yaml
replicaset.apps/replicaset-nginx created
“생성 완료. 이제 Pod 목록을 확인해 보면…“
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
replicaset-nginx-5z6j8 1/1 Running 0 12s
replicaset-nginx-c7k8v 1/1 Running 0 12s
replicaset-nginx-h9x2l 1/1 Running 0 12s
“봤지? 정확히 세 개. 주문한 대로 배달 완료됐어. 이제 ReplicaSet의 임무는 끝난 거 아니야?”
솔라의 목소리에는 ‘내 말이 맞았지’ 하는 확신이 묻어났다. 화면의 결과는 솔라의 처음 가설, ‘세 개 만들고 끝’을 완벽하게 증명하는 듯 보였다.
루나는 말없이 화면을 보다가 조용히 물었다.
“만약 배달 온 물건 중 하나가 갑자기 사라진다면 어떻게 될까? 예를 들어, 저기 Pod 하나가.”
“사라진다고? 음… 그럼 그냥 두 개만 남는 거겠지? 이미 배달은 끝났으니까.”
솔라는 잠시 고민하더니 대답했다. 한 번 완료된 명령을 되돌릴 이유는 없다고 생각했다. 루나는 솔라의 대답을 가만히 듣고는, 다시 모니터를 향해 턱짓했다.
“한번 직접 확인해 보자. 저기 있는 Pod 중에 아무거나 하나 골라서 지워볼래?”
솔라는 약간 의아했지만, 루나의 말대로 명령어를 입력했다. Pod 목록의 맨 위에 있던 replicaset-nginx-5z6j8를 지워보기로 했다.
$ kubectl delete pod replicaset-nginx-5z6j8
pod "replicaset-nginx-5z6j8" deleted
“자, 지웠어. 이제 두 개 남았겠…”
솔라의 말이 채 끝나기도 전에, 루나가 재빨리 말했다.
“바로 다시 확인해 봐. 이번엔 --watch 옵션을 붙여서.”
솔라는 서둘러 다음 명령어를 입력했다.
$ kubectl get pods --watch
NAME READY STATUS RESTARTS AGE
replicaset-nginx-c7k8v 1/1 Running 0 1m
replicaset-nginx-h9x2l 1/1 Running 0 1m
replicaset-nginx-5z6j8 1/1 Terminating 0 1m
replicaset-nginx-w4p5b 0/1 ContainerCreating 0 1s
replicaset-nginx-w4p5b 1/1 Running 0 3s
replicaset-nginx-5z6j8 0/0 Terminating 0 3s
...
터미널 화면에 놀라운 변화가 실시간으로 나타났다. 방금 삭제한 Pod는 Terminating 상태로 바뀌는 동시에, replicaset-nginx-w4p5b라는 이름의 새로운 Pod가 순식간에 나타나 ContainerCreating을 거쳐 Running 상태가 되었다. 잠시 후, 삭제했던 Pod는 목록에서 완전히 사라졌고, 클러스터에는 다시 세 개의 Pod가 멀쩡히 동작하고 있었다.
솔라는 눈을 동그랗게 뜨고 화면을 뚫어지라 쳐다봤다.
“어…? 내가 하나를 지웠는데… 바로 새 걸 하나 만들어 버렸네?”
“마치 세 개라는 개수를 누군가 계속 지켜보고 있다가, 부족해지자마자 채워 넣은 것 같지 않니?”
루나의 말에 솔라는 천천히 고개를 끄덕였다. 머릿속에서 기존의 생각이 와르르 무너지는 소리가 들렸다.
“replicas: 3은… ‘세 개 만들어’라는 일회성 명령이 아니었구나. 이건 ‘항상 세 개를 유지해’라는 상태에 대한 약속 같은 거였어.”
솔라는 자신이 내뱉은 말을 곱씹었다. ‘생성’이 아니라 ‘유지’. 단순한 단어 하나가 바뀌었을 뿐인데 ReplicaSet의 역할이 완전히 다르게 보이기 시작했다. 그것은 게으른 감독관이 아니라, 끊임없이 상태를 감시하고 원하는 상태로 되돌리는 부지런한 관리자였다.
“그래. 이제 replicas 필드가 단순한 ‘개수’가 아니라 ‘원하는 상태(Desired State)‘를 정의한다는 걸 알겠어. 그런데… 한 가지 이상한 점이 남아있어.”
솔라는 새로운 의문에 사로잡혔다.
“ReplicaSet은 어떻게 자기가 관리해야 할 Pod가 부족하다는 걸 알았을까? 방금 내가 지운 Pod가 ‘자기 것’이라는 건 어떻게 알았지? 만약 똑같이 생긴 다른 Pod가 클러스터에 있었다면? 뭘 보고 자기 식구인지 아닌지를 판단하는 거지?”
솔라의 시선은 다시 YAML 파일의 selector 부분으로 향했다. ‘유지’라는 개념을 이해하자, 이제 ‘누구를’ 유지할 것인지에 대한 질문이 자연스럽게 떠오른 것이다.
2장: selector는 Pod의 ‘주인’을 찾는다: 기존 Pod 채택의 비밀
솔라는 이전 실험의 흔적부터 깔끔하게 지웠다. 터미널에 명령어를 입력하자, replicaset-nginx가 사라졌다는 메시지가 떴다.
$ kubectl delete -f replicaset-nginx.yaml
replicaset.apps "replicaset-nginx" deleted
화면에는 새로운 YAML 파일 두 개가 나란히 떠 있었다. 하나는 독립적인 Pod를, 다른 하나는 새로운 ReplicaSet을 정의하는 파일. ReplicaSet이 어떤 Pod를 자기 것으로 식별하는지 알아내기 위해, 그녀는 직접 함정을 설계했다.
먼저, independent-pod.yaml 파일이다. 이 Pod는 ReplicaSet과는 아무런 관계 없이, 오직 솔라의 명령으로만 생성될 ‘외톨이’ Pod였다.
# independent-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: independent-nginx
labels:
app: test-nginx-pods-label
spec:
containers:
- name: nginx
image: nginx
“이 Pod의 이름표, 즉 label은 app: test-nginx-pods-label이야.”
솔라는 파일의 labels 부분을 콕 집어 말했다. 그리고는 옆에 띄워 둔 두 번째 파일, rs-nginx-2.yaml을 가리켰다. 여기에는 replicas: 3과 함께 중요한 단서가 담겨 있었다.
# rs-nginx-2.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: rs-nginx-2
spec:
replicas: 3
selector:
matchLabels:
app: test-nginx-pods-label
template:
metadata:
labels:
app: test-nginx-pods-label
spec:
containers:
- name: nginx
image: nginx
“그리고 이 새로운 ReplicaSet은 정확히 app: test-nginx-pods-label 이름표를 단 Pod를 찾도록 설정했어. 내가 세운 가설은 이거야. ReplicaSet은 자기가 직접 만든 Pod만 자기 식구로 생각할 거야. 그러니까 내가 먼저 저 외톨이 Pod를 만들어 둬도, ReplicaSet은 그걸 무시하고 새로 3개를 만들겠지? 결국 총 4개의 Pod가 생길 거야.”
솔라는 자신의 추리가 합리적이라고 생각했다. 자신이 낳은 자식만 돌보는 게 당연하지 않은가. 외부에서 온, 심지어 먼저 와 있던 아이를 갑자기 자기 자식이라고 하진 않을 터였다. 루나는 솔라의 가설을 조용히 듣고만 있었다.
“흥미로운 가설이네. 직접 확인해 볼까?”
솔라는 자신만만하게 터미널에 명령어를 입력하기 시작했다. 먼저 외톨이 Pod를 클러스터에 띄웠다.
$ kubectl apply -f independent-pod.yaml
pod/independent-nginx created
$ kubectl get pods --show-labels
NAME READY STATUS RESTARTS AGE LABELS
independent-nginx 1/1 Running 0 10s app=test-nginx-pods-label
“자, independent-nginx Pod 하나가 app=test-nginx-pods-label 이름표를 달고 잘 실행되고 있어.”
이제 실험의 핵심 단계. 솔라는 숨을 한번 고르고 ReplicaSet을 생성하는 명령어를 입력했다.
$ kubectl apply -f rs-nginx-2.yaml
replicaset.apps/rs-nginx-2 created
“이제 Pod 목록을 보면… 내 예상대로 Pod가 총 네 개가…”
솔라의 말꼬리가 흐려졌다. 화면에 나타난 결과는 그녀의 예상을 완전히 빗나갔다.
$ kubectl get pods --show-labels
NAME READY STATUS RESTARTS AGE LABELS
independent-nginx 1/1 Running 0 55s app=test-nginx-pods-label
rs-nginx-2-jh5f8 1/1 Running 0 15s app=test-nginx-pods-label
rs-nginx-2-qz8xw 1/1 Running 0 15s app=test-nginx-pods-label
클러스터에는 총 3개의 Pod만 있었다. 자신이 직접 만들었던 independent-nginx Pod가 여전히 그 자리에 있었고, ReplicaSet이 새로 생성한 것으로 보이는 Pod는 단 두 개뿐이었다. ReplicaSet은 Pod를 3개 만들라는 명령 대신, 이미 존재하는 1개를 포함하여 총 3개를 유지하고 있었다.
“어… 이게 어떻게 된 거지? 왜 2개만 더 만든 거야? 내 independent-nginx는…?”
솔라는 혼란스러운 표정으로 화면과 루나를 번갈아 쳐다봤다. ReplicaSet이 마치 길 잃은 아이를 입양이라도 한 것 같은 상황이었다.
루나가 조용히 입을 열었다.
“그 ReplicaSet의 속을 한번 들여다보는 건 어떨까? describe 명령어로 말이야.”
솔라는 루나의 조언에 따라 rs-nginx-2의 상세 정보를 확인했다.
$ kubectl describe replicaset rs-nginx-2
Name: rs-nginx-2
Namespace: default
Selector: app=test-nginx-pods-label
Labels: app=test-nginx-pods-label
Replicas: 3 current / 3 desired
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal SuccessfulCreate 3m replicaset-controller Created pod: rs-nginx-2-qz8xw
Normal SuccessfulCreate 3m replicaset-controller Created pod: rs-nginx-2-jh5f8
Events 섹션에 나타난 메시지가 모든 것을 설명해주고 있었다. ReplicaSet 컨트롤러는 단 2개의 Pod만 Create 했다고 기록하고 있었다. Replicas 필드 역시 3 current / 3 desired로, 원하는 상태와 현재 상태가 일치한다고 보고했다.
“분명 replicas: 3이었는데 2개만 만들었다고 나오네. 그럼 나머지 하나는…?”
“이번엔 그 ‘외톨이’였던 Pod의 정보를 확인해봐. Controlled By 필드를 유심히 봐.”
솔라는 곧바로 independent-nginx Pod의 상세 정보를 출력했다.
$ kubectl describe pod independent-nginx
Name: independent-nginx
Namespace: default
...
Labels: app=test-nginx-pods-label
...
Controlled By: ReplicaSet/rs-nginx-2
...
결정적인 증거였다. 처음에는 아무에게도 속하지 않았던 independent-nginx Pod의 Controlled By 필드에 ReplicaSet/rs-nginx-2라는 ‘주인’ 정보가 선명하게 찍혀 있었다. ReplicaSet이 이 Pod의 소유권을 가져간 것이다.
“아…!”
솔라는 낮은 탄성을 질렀다. 퍼즐 조각이 맞춰지는 순간이었다.
“ReplicaSet은 Pod가 어떻게 만들어졌는지 전혀 신경 쓰지 않는구나. 누가 만들었든, 어디서 왔든 상관없이, 오직 selector에 지정된 label과 일치하는지만 보는 거였어. 마치 이름표만 보고 자기 식구를 찾아내는 것처럼.”
솔라의 ‘자신이 직접 낳은 자식만 돌본다’는 가설은 완전히 틀렸다. ReplicaSet에게 중요한 것은 혈연이 아니라, 약속된 이름표였다. 클러스터에 흩어져 있는 Pod들 중에서 자신의 selector 조건과 일치하는 이름표를 단 Pod가 있다면, 그것들을 모두 자기 ‘관리 대상’으로 채택(adopt)하고, 부족한 수만큼만 새로 생성해서 ‘원하는 상태(Desired State)‘를 맞추는 것이었다.
“이제 selector가 Pod의 주인을 찾아내는 ‘식별표’ 역할을 한다는 걸 알겠어. 생성자가 누구인지는 전혀 중요하지 않았네.”
솔라는 고개를 끄덕이며 새로운 이해를 정리했다. 하지만 그 순간, 또 다른 질문이 꼬리를 물고 떠올랐다.
“그럼 만약에… 그렇게 입양된 Pod의 이름표를 내가 몰래 바꿔버리면 어떻게 될까? ReplicaSet은 자기가 데려온 Pod가 다른 이름표를 달게 된 걸 알아챌까? 알아채면… 그 Pod를 버리고 새 Pod를 다시 만들까?”
‘채택’의 비밀을 풀자, 이제 ‘파양’의 조건이 궁금해지기 시작했다. 소유권은 한 번 정해지면 영원한 것일까, 아니면 이름표에 따라 언제든 바뀔 수 있는 것일까.
3장: 동적인 소유권 판단: Pod의 변화에 대응하는 ReplicaSet
솔라의 터미널 화면에는 이전 실험의 결과가 그대로 남아 있었다. rs-nginx-2라는 이름의 ReplicaSet이 관리하는 세 개의 Pod 목록. 그중 하나는 원래 외톨이였지만 app=test-nginx-pods-label이라는 이름표 덕분에 가족으로 채택된 independent-nginx였다. 솔라의 손가락은 키보드 위에 멈춰 있었다. 머릿속에 맴돌던 질문을 직접 행동으로 옮길 참이었다.
‘그렇게 입양된 Pod의 이름표를 내가 몰래 바꿔버리면 어떻게 될까?’
이 질문은 더 이상 막연한 궁금증이 아니었다. 그것은 이제 화면에 구체적인 대상으로 존재했다. 바로 저 independent-nginx Pod. 솔라는 한 가지 가설을 세웠다. 한 번 가족이 되었으니, 이름표를 바꿔도 계속 가족으로 남아있지 않을까? 아니, 어쩌면 이름표가 전부라면, 이름표가 바뀌는 순간 남이 되어버리는 걸까? 두 가지 상반된 생각이 머릿속에서 부딪혔다.
“언니, 나 이거 한번 해볼래.”
솔라는 결심한 듯 루나를 보며 말했다.
“저기, independent-nginx Pod의 이름표, 즉 label을 직접 바꿔볼 거야. ReplicaSet이 이걸 어떻게 받아들이는지 궁금해.”
“이름표를 바꿔본다고? 재미있는 생각이네. 어떻게 하려고?”
루나는 호기심 어린 눈으로 솔라를 바라보았다.
“음… kubectl edit 명령어를 쓰면 되지 않을까? 실행 중인 리소스의 설정을 바로 수정할 수 있다고 했으니까.”
솔라는 곧장 명령어를 입력했다. 화면에 익숙한 YAML 형식의 텍스트 에디터가 나타났다. independent-nginx Pod의 현재 설정이었다.
$ kubectl edit pod independent-nginx
솔라는 에디터에서 metadata 아래 labels 섹션을 찾아, 값을 app: test-nginx-pods-label에서 app: rogue-pod로 수정했다. 배신자 Pod라는 의미를 담은 이름이었다. 그리고 저장 후 에디터를 종료했다.
# In-editor changes
...
metadata:
labels:
app: rogue-pod # "test-nginx-pods-label"에서 수정
...
터미널에 pod/independent-nginx edited 라는 메시지가 떴다. 솔라는 숨을 참고 다음 명령어를 입력했다. 모든 변화를 실시간으로 목격하기 위해 --watch 옵션을 붙이는 것도 잊지 않았다.
“이제… 어떻게 됐을까?”
$ kubectl get pods --watch
NAME READY STATUS RESTARTS AGE
independent-nginx 1/1 Running 0 15m
rs-nginx-2-jh5f8 1/1 Running 0 14m
rs-nginx-2-qz8xw 1/1 Running 0 14m
rs-nginx-2-x9v5c 0/1 Pending 0 0s
rs-nginx-2-x9v5c 0/1 ContainerCreating 0 0s
rs-nginx-2-x9v5c 1/1 Running 0 2s
화면을 지켜보던 솔라의 눈이 커졌다. 그녀가 이름표를 바꾼 independent-nginx는 아무런 변화 없이 그대로 Running 상태를 유지하고 있었다. 하지만 그와 동시에, rs-nginx-2-x9v5c라는 이름의 새로운 Pod가 홀연히 나타나 순식간에 Running 상태가 되었다.
“어…! 새 Pod가 생겼어! 그럼 이제 Pod가 총 네 개…?”
솔라는 Ctrl+C를 눌러 --watch 모드를 중단하고, 전체 Pod 목록을 다시 확인했다.
$ kubectl get pods --show-labels
NAME READY STATUS RESTARTS AGE LABELS
independent-nginx 1/1 Running 0 16m app=rogue-pod
rs-nginx-2-jh5f8 1/1 Running 0 15m app=test-nginx-pods-label
rs-nginx-2-qz8xw 1/1 Running 0 15m app=test-nginx-pods-label
rs-nginx-2-x9v5c 1/1 Running 0 1m app=test-nginx-pods-label
예상과 달랐다. Pod는 총 네 개가 되었지만, 그 구성이 의미심장했다. app=test-nginx-pods-label 이름표를 단 Pod는 정확히 세 개. 그리고 이름표가 app=rogue-pod로 바뀐 independent-nginx는 홀로 떨어져 나와 있었다.
“ReplicaSet이… 저 Pod를 버렸네. 이름표가 달라지자마자 자기 식구가 아니라고 판단하고, 부족해진 하나를 바로 새로 만든 거야.”
솔라는 independent-nginx Pod의 상세 정보를 확인하며 자신의 추측에 쐐기를 박았다.
$ kubectl describe pod independent-nginx
Name: independent-nginx
...
Labels: app=rogue-pod
...
Controlled By: <none>
...
Controlled By 필드가 <none>으로 바뀌어 있었다. 한때 ReplicaSet/rs-nginx-2의 관리를 받았지만, 이름표가 바뀌는 순간 그 관계는 끊어지고 다시 외톨이가 된 것이다.
루나가 조용히 말했다. “ReplicaSet에게 소유권은 영원한 게 아니었던 거네. 처음부터 끝까지, 오직 하나의 규칙만 따를 뿐이야.”
솔라는 루나의 말을 들으며 모든 조각이 맞춰지는 것을 느꼈다.
“실시간 소유권 평가… 같은 거구나. ReplicaSet은 주기적으로 클러스터를 훑어보면서 자기 selector와 일치하는 이름표를 가진 Pod가 몇 개인지 계속 세고 있는 거야. 그리고 그 수가 replicas에 적힌 숫자와 다르면, 즉시 행동에 나서는 거지. 모자라면 만들고, 많으면…(물론 이 실험에선 없었지만) 지우고.”
솔라는 자신의 깨달음을 정리하며 모니터 구석에 메모장을 열었다. 그녀는 더 이상 ReplicaSet을 ‘Pod를 생성하는 도구’로 보지 않았다. 그것은 훨씬 더 역동적이고 집요한 ‘상태 관리자’였다.
replicas: 3: “내 목표는 항상 세 개야.”selector: {app: my-app}: “이름표가app: my-app인 Pod만 내 관리 대상이야.”- 관리 루프: “계속해서 이름표를 확인하고, 개수가 3개가 아니면 맞춘다. 이름표가 바뀌면? 그건 이제 내 Pod가 아니니, 하나 새로 만들어야지!”
메모를 마친 솔라는 고개를 끄덕였다. 처음엔 그저 ‘정해진 수의 동일한 Pod가 항상 실행되도록 관리합니다’라는 교재의 한 문장이 막연하게만 들렸다. 하지만 이제 그 문장 뒤에 숨겨진 replicas와 selector, 그리고 끊임없이 작동하는 ‘실시간 소유권 평가’라는 역동적인 메커니즘을 자신의 눈으로 똑똑히 확인했다. ReplicaSet의 진짜 역할은 생성자가 아니라, 원하는 상태를 향해 끊임없이 움직이는 냉철한 조정자였다.