소개
Service 는 여러 Pod 로 구성된 집합에 클라이언트가 접근할 수 있는 안정적인 단일 주소를 제공합니다. 수요가 변할 때 이 기능은 특히 유용합니다. Service 의 주소와 정체성은 그대로 유지하면서 Deployment 가 복제본을 추가하거나 제거할 수 있기 때문입니다.
이 실습에서는 각 백엔드가 자신이 처리한 Pod 의 호스트 이름을 반환합니다. 두 개의 복제본에서 네 개로 확장하고, 하나의 Service 를 통해 개별 요청을 전송하여 여러 Pod 의 응답을 확인합니다. 그런 다음 다시 두 개로 축소하고, Deployment 와 EndpointSlice 가 새로운 원하는 상태에 수렴하는 과정을 살펴봅니다.
이 실습은 수동 수평 확장을 다룹니다. HPA 를 이용한 자동 확장은 리소스 요청량, 메트릭, 제어 정책에 의존하므로, 수동 복제본 동작을 충분히 이해한 뒤에 학습하는 것이 적절합니다.
관찰 가능한 복제 애플리케이션 구축
환경 시작: 이 실습에서는 완전한 Kubernetes 클러스터가 시작됩니다. 컨트롤 플레인, 노드 및 네트워크 구성 요소를 설정하는 데 일반적으로 2~3분이 걸립니다. 환경 로딩이 완료될 때까지 잠시 기다린 후 작업을 시작하세요.
이 단계에서는 각 요청을 처리한 Pod 를 확인할 수 있도록 두 개의 백엔드를 생성합니다. 이를 통해 Service 의 트래픽 분산을 추상적인 동작이 아니라 실제 결과로 확인할 수 있습니다.
첫 번째 명령은 cd를 사용해 준비된 작업 공간으로 이동합니다. 다음 명령은 here-document를 사용합니다. cat <<'EOF' > hostname-web.yaml은 닫는 EOF가 나올 때까지 이어지는 모든 줄을 YAML 파일에 기록하며, 기존 내용이 있다면 덮어씁니다.
cd /home/labex/project/scale-lab
cat <<'EOF' > hostname-web.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: hostname-web
spec:
replicas: 2
selector:
matchLabels:
app: hostname-web
template:
metadata:
labels:
app: hostname-web
spec:
containers:
- name: web
image: busybox:1.36
imagePullPolicy: IfNotPresent
command: ["sh", "-c"]
args:
- mkdir -p /www; hostname > /www/index.html; exec httpd -f -p 8080 -h /www
ports:
- name: http
containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: hostname-web
spec:
selector:
app: hostname-web
ports:
- name: http
port: 80
targetPort: http
EOF
kubectl apply -f hostname-web.yaml
kubectl rollout status deployment/hostname-web --timeout=60s
kubectl get pods -l app=hostname-web
닫는 EOF 다음에 실행하는 kubectl apply -f는 파일에 정의된 모든 객체를 API 서버로 전송합니다. rollout status는 원하는 Pod 두 개가 준비될 때까지 최대 60 초 동안 기다립니다. -l app=hostname-web은 해당 레이블이 지정된 Pod 만 조회합니다.
컨테이너 명령의 세미콜론은 작업을 순서대로 실행합니다. /www를 생성하고, 호스트 이름을 index.html로 리디렉션한 다음, exec를 사용해 웹 서버가 컨테이너의 주 프로세스가 되도록 합니다. 두 Pod 의 이름은 서로 다르므로 각 백엔드는 서로 다른 페이지 값을 반환합니다.
기준 백엔드 상태 확인
이 단계에서는 Deployment 의 복제본 수와 Service 의 준비된 백엔드 수가 어떻게 연결되는지 확인합니다.
서로 관련된 세 가지 정보를 조회합니다. get deployment는 원하는 복제본 수와 준비된 복제본 수를 보여줍니다. -l은 애플리케이션 Pod 를 선택하고, -o wide는 IP 및 노드 열을 추가합니다. 마지막 레이블 선택기는 이 Service 를 위해 생성된 EndpointSlice 를 찾습니다.
kubectl get deployment hostname-web
kubectl get pods -l app=hostname-web -o wide
kubectl get endpointslices -l kubernetes.io/service-name=hostname-web
Deployment 에는 2/2가 표시되고 EndpointSlice 에는 두 개의 주소가 표시됩니다. 이제 하나의 장시간 실행 클라이언트 Pod 에서 Service 로 여러 번 요청을 보냅니다.
--restart=Never를 지정했기 때문에 kubectl run은 독립 실행형 Pod 를 생성합니다. -- 구분자는 kubectl 옵션의 끝을 나타내며, sleep 3600은 컨테이너가 계속 실행되도록 유지하는 명령입니다. for 반복문은 seq 1 6으로 여섯 번 반복하고, kubectl exec POD -- COMMAND는 매번 클라이언트 내부에서 wget을 실행합니다.
kubectl run load-client --image=busybox:1.36 --image-pull-policy=IfNotPresent --restart=Never -- sleep 3600
kubectl wait --for=condition=Ready pod/load-client --timeout=30s
for i in $(seq 1 6); do kubectl exec load-client -- wget -qO- http://hostname-web; done
각 응답은 Pod 이름입니다. 짧은 테스트에서는 한 이름만 또는 두 이름 모두 나타날 수 있습니다. Service 의 트래픽 분산은 엄격한 라운드 로빈 순서를 보장하지 않습니다.
선언적으로 확장하기
이 단계에서는 저장된 원하는 상태를 복제본 두 개에서 네 개로 변경합니다. 매니페스트를 수정하면 파일과 실제 객체의 상태를 일치시킬 수 있습니다.
sed -i 's/old/new/' file은 파일에서 일치하는 텍스트를 직접 교체합니다. 이어서 grep -n은 replicas:를 검색하고 해당 줄 번호를 표시하므로, 변경 사항을 적용하기 전에 빠르게 확인할 수 있습니다.
cd /home/labex/project/scale-lab
sed -i 's/replicas: 2/replicas: 4/' hostname-web.yaml
grep -n 'replicas:' hostname-web.yaml
kubectl apply -f hostname-web.yaml
kubectl rollout status deployment/hostname-web --timeout=60s
kubectl get deployment hostname-web
Deployment 에는 4/4가 표시되어야 합니다. 실제 상태가 새롭게 지정한 원하는 상태보다 부족했기 때문에 컨트롤러가 Pod 두 개를 추가로 생성했습니다.
Service 백엔드 집합의 확장 과정 확인
이 단계에서는 변경하지 않은 Service 가 새로 생성된 Pod 를 자동으로 발견하는지 확인합니다.
일반 표 형식에서는 세부 정보가 축약될 수 있으므로 EndpointSlice 명령은 JSONPath 를 사용합니다. range는 모든 엔드포인트에 대해 템플릿을 반복합니다. 각 반복에서는 첫 번째 주소, 문자 그대로의 ready, 준비 상태 값, 줄바꿈을 차례로 출력합니다. 백슬래시는 화면상 여러 줄로 나뉜 셸 명령을 하나의 명령으로 이어 줍니다.
kubectl get pods -l app=hostname-web -o wide
kubectl get endpointslices -l kubernetes.io/service-name=hostname-web \
-o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{" ready="}{.conditions.ready}{"\n"}{end}'
이제 준비된 주소가 네 개 표시됩니다. Service 는 수정하지 않았지만, app=hostname-web 레이블이 지정된 모든 준비 상태 Pod 와 계속 일치하므로 자동으로 백엔드가 추가되었습니다.
안정적으로 유지되는 Service IP 와 확장된 백엔드 집합을 비교합니다.
kubectl get service hostname-web -o wide
EndpointSlice 의 구성원은 변경되지만 ClusterIP 는 그대로 유지됩니다.
여러 Pod 에 도달하는 요청 관찰
이 단계에서는 하나의 Service 를 통해 독립적인 HTTP 요청을 보내고, 어떤 Pod 호스트 이름이 응답했는지 집계합니다.
먼저 rm -f는 기존 결과 파일이 있으면 삭제합니다. -f 옵션을 사용하면 파일이 없어도 오류가 발생하지 않습니다. 반복문은 20 개의 요청을 전송합니다. >>는 각 호스트 이름을 기존 결과 뒤에 추가하므로 이전 결과를 덮어쓰지 않습니다. 마지막으로 파이프를 통해 정렬된 줄을 uniq -c에 전달하면 연속된 중복 항목을 하나로 합치고 각 호스트 이름 앞에 개수를 표시합니다.
rm -f /tmp/hostname-responses.txt
for i in $(seq 1 20); do
kubectl exec load-client -- wget -qO- http://hostname-web >> /tmp/hostname-responses.txt
done
sort /tmp/hostname-responses.txt | uniq -c
두 개 이상의 호스트 이름이 표시되어야 합니다. 개수는 균등하지 않을 수 있으며, 짧은 실행 한 번으로 네 백엔드 모두에 요청이 전달되지 않을 수도 있습니다. Kubernetes Service 라우팅은 연결을 분산하지만, 완벽하게 균등하거나 일정한 순서의 요청을 보장하지는 않습니다.
테스트에서 실제로 도달한 고유 백엔드 수를 확인합니다. sort -u는 각 호스트 이름을 하나씩만 남기고, 파이프는 결과를 wc -l로 전달하며, wc -l은 줄 수를 계산합니다.
sort -u /tmp/hostname-responses.txt | wc -l
값이 1 보다 크다면, 안정적인 Service 주소가 여러 Pod 로 요청을 라우팅했다는 직접적인 증거입니다.
명령형 방식으로 축소하기
이 단계에서는 kubectl scale을 사용해 실행 중인 복제본 수를 네 개에서 두 개로 빠르게 조정합니다.
kubectl scale은 실행 중인 Deployment 의 원하는 복제본 수를 즉시 변경합니다. --replicas=2는 새 복제본 수를 지정하지만 YAML 파일 자체는 수정하지 않습니다.
kubectl scale deployment/hostname-web --replicas=2
kubectl rollout status deployment/hostname-web --timeout=60s
kubectl get pods -l app=hostname-web
Kubernetes 는 Pod 두 개를 종료하고 두 개를 유지합니다. Service 의 백엔드 집합도 새로운 상태에 수렴할 때까지 기다립니다.
다음의 제한된 폴링 반복문은 최대 30 번 시도합니다. 매번 준비된 엔드포인트 수를 count에 저장합니다. jq는 EndpointSlice JSON 에서 준비된 엔드포인트만 필터링한 뒤 배열의 길이를 반환합니다. [ "$count" -eq 2 ]는 숫자를 비교하는 셸 테스트이고, && break는 조건이 충족되면 반복문을 종료합니다. sleep 1은 다시 시도하기 전에 1 초간 대기합니다.
for i in $(seq 1 30); do
count=$(kubectl get endpointslices -l kubernetes.io/service-name=hostname-web -o json | jq '[.items[].endpoints[] | select(.conditions.ready == true)] | length')
[ "$count" -eq 2 ] && break
sleep 1
done
echo "Ready backends: $count"
이제 실행 중인 Deployment 는 복제본 두 개를 요청하지만, 파일에는 여전히 네 개가 지정되어 있습니다. 다음 단계에서 이 차이를 의도적으로 정리합니다.
매니페스트를 조정하고 컨트롤러의 기록 확인
이 단계에서는 저장된 매니페스트를 실행 중인 두 복제본 상태와 일치시키고, 확장 작업이 컨트롤러의 기록에 어떻게 나타나는지 확인합니다.
먼저 두 원하는 상태를 비교합니다. grep -n은 저장된 파일의 해당 줄을 표시하고, JSONPath 는 실행 중인 객체의 .spec.replicas 필드만 추출한 뒤 줄바꿈을 추가합니다.
grep -n 'replicas:' /home/labex/project/scale-lab/hostname-web.yaml
kubectl get deployment hostname-web -o jsonpath='Live replicas: {.spec.replicas}{"\n"}'
파일의 복제본 수를 네 개에서 두 개로 변경한 뒤 적용합니다.
sed -i 's/replicas: 4/replicas: 2/' /home/labex/project/scale-lab/hostname-web.yaml
kubectl apply -f /home/labex/project/scale-lab/hostname-web.yaml
실행 중인 상태가 이미 복제본 두 개이므로, 이 적용 작업으로 Pod 가 추가로 생성되지는 않아야 합니다. Deployment 이벤트를 확인합니다. 파이프는 전체 describe 출력을 sed -n으로 전달합니다. /Events:/,$p는 Events:가 포함된 줄부터 출력의 마지막 줄까지 표시하라는 의미입니다.
kubectl describe deployment hostname-web | sed -n '/Events:/,$p'
확장 및 축소 결정이 기록된 ScalingReplicaSet 메시지를 찾아보세요. 마지막으로 임시 클라이언트를 삭제합니다. --ignore-not-found를 사용하면 Pod 가 이미 사라진 경우에도 정리 명령이 성공합니다.
kubectl delete pod load-client --ignore-not-found
수동 확장은 원하는 복제본 수를 변경합니다. Deployment 컨트롤러는 이에 따라 Pod 를 생성하거나 종료하고, Service 는 준비 상태인 구성원을 자동으로 추적합니다. 매니페스트를 실행 중인 상태와 동기화해 두면 나중에 kubectl apply를 실행할 때 오래된 복제본 수가 예기치 않게 복원되는 일을 방지할 수 있습니다.
요약
Deployment 를 수동으로 복제본 두 개에서 네 개로 확장한 뒤 다시 두 개로 축소했습니다. 컨트롤러가 Pod 를 조정하는 과정을 확인하고, Service 를 변경하지 않아도 EndpointSlice 구성원이 준비된 백엔드를 따라 변경되는 모습을 관찰했습니다. 또한 하나의 Service 주소가 독립적인 요청을 여러 Pod 로 전달했다는 직접적인 증거를 수집했습니다.
기억해야 할 핵심 모델은 다음과 같습니다. 복제본 수는 원하는 상태이고, Deployment 컨트롤러는 실제 Pod 상태를 조정하며, Service 는 레이블이 일치하는 준비된 백엔드를 자동으로 따릅니다. 향후 적용 작업을 예측 가능하게 유지하려면 선언적 파일과 의도적으로 변경한 실행 중 상태를 항상 다시 일치시켜야 합니다.


