애플리케이션 업데이트 및 롤백

KubernetesBeginner
지금 연습하기

소개

이제 애플리케이션을 배포하고, 외부에 노출하고, 규모를 조정할 수 있습니다. 다음 운영 단계에서 생기는 질문은 다음과 같습니다. 전체 서비스를 중단하지 않고 애플리케이션 버전을 어떻게 교체할 수 있을까요?

Kubernetes Deployment 는 이 문제를 롤링 업데이트로 해결합니다. Pod 템플릿이 변경되면 Deployment 는 새 ReplicaSet 을 만들고, 새 Pod 를 점진적으로 시작하며, 교체 Pod 가 준비된 후에만 기존 Pod 를 제거합니다. 이 과정 내내 Service 는 동일한 안정적인 주소를 유지합니다.

이 실습에서는 NGINX 애플리케이션을 특정 버전의 이미지에서 다른 버전으로 변경하고, Deployment 리비전과 ReplicaSet 및 Pod 의 관계를 확인합니다. 그런 다음 의도적으로 잘못된 릴리스를 시도하고, 정상 상태였던 마지막 리비전으로 롤백합니다. 또한 복구된 실제 상태와 저장된 매니페스트를 일치시킵니다. 이는 이후 kubectl apply가 실패를 다시 유발하는 것을 막는 중요한 습관입니다.

시작 환경 확인

환경 시작: 이 실습에서는 완전한 Kubernetes 클러스터가 시작됩니다. 컨트롤 플레인, 노드 및 네트워크 구성 요소를 설정하는 데 일반적으로 2~3분이 걸립니다. 환경 로딩이 완료될 때까지 잠시 기다린 후 작업을 시작하세요.

이 단계에서는 클러스터 연결을 확인하고 클러스터 내부에서 사용할 HTTP 클라이언트를 준비합니다.

이 과정의 환경에는 이미 실행 중인 Kubernetes 클러스터가 있으므로 직접 클러스터를 만들 필요가 없습니다. 먼저 kubectl이 명령을 전송할 대상을 확인합니다. config current-context는 현재 활성화된 연결을 표시하고, get node는 노드 상태를 읽으며, version은 로컬 클라이언트와 연결 가능한 API 서버의 버전을 모두 출력합니다.

kubectl config current-context
kubectl get node
kubectl version

컨텍스트와 노드 이름은 labex-v135이고, 노드 상태는 Ready이며, 서버는 Kubernetes v1.35.x 를 보고합니다. 컨텍스트kubectl을 특정 클러스터, 사용자 및 기본 네임스페이스에 연결하는 kubeconfig 설정입니다.

이제 간단한 클라이언트 Pod 를 생성합니다. 나중에 이 Pod 를 사용해 Service 를 통해 애플리케이션에 접근합니다. kubectl run은 Pod 를 생성하고, --image는 BusyBox 를 선택하며, --restart=Never는 독립 실행형 Pod 로 유지합니다. -- 구분자 뒤에는 장시간 실행할 sleep 3600 명령이 이어집니다. 이후 kubectl wait는 최대 30 초 동안 Pod 가 Ready 상태가 되기를 기다립니다.

kubectl run release-client --image=busybox:1.36 --image-pull-policy=IfNotPresent \
  --restart=Never -- sleep 3600
kubectl wait --for=condition=Ready pod/release-client --timeout=30s

이렇게 하면 호출 주체와 웹 Pod 를 분리할 수 있어, 클러스터 내부에서 하나의 워크로드가 다른 워크로드를 호출하는 방식에 더 가깝게 실습할 수 있습니다.

안정적인 기준 상태 배포

이 단계에서는 변경을 적용하기 전에 정상적으로 동작하는 애플리케이션 리비전을 설정합니다.

업데이트를 연습하기 전에 정상 상태가 확인된 리비전을 먼저 구성합니다. 3 개의 복제본을 사용하는 Deployment 와 안정적인 ClusterIP Service 를 하나의 매니페스트에 작성합니다.

cd는 미리 준비된 작업 디렉터리로 이동합니다. 여기 문서 구문인 cat <<'EOF' > release-web.yaml은 닫는 EOF가 나올 때까지의 모든 내용을 파일에 기록합니다. --- 줄은 하나의 YAML 파일에 포함된 두 Kubernetes 객체를 구분합니다.

cd /home/labex/project/update-lab
cat <<'EOF' > release-web.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: release-web
  annotations:
    kubernetes.io/change-cause: "Initial release: nginx 1.26"
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
  selector:
    matchLabels:
      app: release-web
  template:
    metadata:
      labels:
        app: release-web
    spec:
      containers:
        - name: nginx
          image: nginx:1.26-alpine
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: release-web
spec:
  selector:
    app: release-web
  ports:
    - name: http
      port: 80
      targetPort: http
EOF
kubectl apply -f release-web.yaml
kubectl rollout status deployment/release-web --timeout=60s
kubectl get deployment,service,pods -l app=release-web

Deployment 는 Pod 를 관리하고, Service 는 레이블을 기준으로 Pod 를 독립적으로 선택합니다. Deployment 를 업데이트해도 Service 주소는 변경되지 않습니다.

클라이언트에서 기준 상태를 확인합니다. kubectl exec POD -- COMMAND에서 --는 kubectl 인자와 컨테이너 내부에서 실행할 명령을 구분합니다. wget -qO-는 조용히 응답을 표준 출력으로 가져오고, 파이프를 통해 응답을 head로 전달하여 앞부분만 표시합니다.

kubectl exec release-client -- wget -qO- http://release-web | head

선언적으로 새 이미지 릴리스

이 단계에서는 저장된 Pod 템플릿을 변경하고 Deployment 가 롤링 업데이트를 수행하도록 합니다.

Deployment 는 Pod 템플릿이 변경될 때 롤아웃을 시작합니다. 이미지가 이 템플릿 안에 있으므로 이미지를 변경하면 새 리비전과 새 ReplicaSet 이 생성됩니다.

저장된 매니페스트에서 이미지와 사람이 읽을 수 있는 변경 사유를 모두 업데이트합니다. 각 sed -i 's/old/new/' file 치환 명령은 파일을 직접 수정합니다. 그런 다음 grep -nE는 확장 정규식의 두 항목 (change-cause 또는 image:) 에 해당하는 줄을 줄 번호와 함께 표시하므로, 적용하기 전에 두 변경 사항을 모두 확인할 수 있습니다.

cd /home/labex/project/update-lab
sed -i 's/Initial release: nginx 1.26/Release nginx 1.27/' release-web.yaml
sed -i 's/nginx:1.26-alpine/nginx:1.27-alpine/' release-web.yaml
grep -nE 'change-cause|image:' release-web.yaml
kubectl apply -f release-web.yaml
kubectl rollout status deployment/release-web --timeout=60s

kubectl apply는 원하는 상태를 변경합니다. 이후 Deployment 컨트롤러는 업데이트된 복제본 3 개가 모두 사용 가능해질 때까지 비동기적으로 작업을 수행합니다. 필요한 Pod 만 보기 위해 -l로 애플리케이션 레이블을 선택하고, -o custom-columns로 열 제목을 객체 필드 경로에 매핑합니다.

kubectl get deployment release-web
kubectl get pods -l app=release-web \
  -o custom-columns='NAME:.metadata.name,IMAGE:.spec.containers[0].image,READY:.status.containerStatuses[0].ready'

현재 실행 중인 복제본 3 개 모두 새 이미지를 사용합니다. 롤아웃이 성공한 뒤에도 이전 이미지를 사용하는 Pod 가 잠시 Terminating 상태로 보일 수 있습니다. 이 Pod 는 더 이상 원하는 복제본이 아니며, Kubernetes 가 백그라운드에서 정상 종료 절차를 마무리하는 중입니다.

리비전, ReplicaSet, Pod 의 관계 확인

이 단계에서는 성공한 롤아웃을 구성하는 객체를 살펴보고 Service 가 계속 사용 가능한 상태였는지 확인합니다.

롤아웃은 완료되었지만 단순히“성공”이라는 결과만 보는 것보다 무엇이 변경되었는지 이해하는 것이 더 중요합니다. kubectl rollout history는 저장된 Deployment 리비전을 읽습니다. 다음 get replicasets 명령은 -l로 관련 객체만 남기고, custom-columns로 복제본 수와 이미지를 비교합니다.

kubectl rollout history deployment/release-web
kubectl get replicasets -l app=release-web \
  -o custom-columns='NAME:.metadata.name,DESIRED:.spec.replicas,CURRENT:.status.replicas,READY:.status.readyReplicas,IMAGE:.spec.template.spec.containers[0].image'

ReplicaSet 이 2 개 표시되어야 합니다. 새 ReplicaSet 은 Pod 3 개를 관리하고, 이전 ReplicaSet 은 0 개의 복제본으로 남아 있어 롤백할 때 해당 Pod 템플릿을 사용할 수 있습니다. ReplicaSet 이름에는 Pod 템플릿에서 계산된 해시가 포함되므로, 이미지가 변경되면 Pod 이름도 함께 변경됩니다.

실행 중인 이미지와 Service 백엔드 수를 확인합니다. 두 JSONPath 표현식 모두 목록을 순회하기 위해 range를 사용합니다. 리터럴 공백과 {"\n"}을 사용해 Pod 또는 엔드포인트마다 읽기 쉬운 한 줄을 출력합니다.

kubectl get pods -l app=release-web \
  -o jsonpath='{range .items[*]}{.metadata.name}{"  "}{.spec.containers[0].image}{"\n"}{end}'
kubectl get endpointslices -l kubernetes.io/service-name=release-web \
  -o jsonpath='{range .items[*].endpoints[*]}{.addresses[0]}{" ready="}{.conditions.ready}{"\n"}{end}'
kubectl exec release-client -- wget -qO- http://release-web | head

리비전은 변경되었지만 Service 는 여전히 준비된 백엔드 3 개와 동일한 안정적인 이름을 유지합니다.

잘못된 릴리스 진단

이 단계에서는 의도적으로 잘못된 이미지 오류를 발생시키고, Pod 이벤트를 사용해 롤아웃이 완료되지 않는 원인을 확인합니다.

이제 흔히 발생하는 릴리스 실수를 재현합니다. 존재하지 않는 컨테이너 이미지 태그를 지정하는 것입니다. 이 실패를 동일한 Deployment 에서 발생시켜 롤아웃 메커니즘의 중요한 안전 특성을 확인합니다. 두 sed -i 명령은 어노테이션과 이미지 문자열을 직접 수정합니다. 예상되는 롤아웃 시간 초과는 일반적으로 실패 상태를 반환하지만, || true를 사용해 실습을 계속 진행합니다.

cd /home/labex/project/update-lab
sed -i 's/Release nginx 1.27/Broken release: missing image/' release-web.yaml
sed -i 's/nginx:1.27-alpine/nginx:does-not-exist-course/' release-web.yaml
kubectl apply -f release-web.yaml
kubectl rollout status deployment/release-web --timeout=20s || true
kubectl get deployment release-web
kubectl get pods -l app=release-web

새 Pod 가 이미지를 가져오지 못하기 때문에 롤아웃은 완료되지 않습니다. || true를 사용했으므로 예상된 시간 초과가 발생한 뒤에도 실습을 계속할 수 있습니다.

실패한 Pod 를 찾고 이벤트를 확인합니다. $(...)는 명령 출력 결과를 BAD_POD에 저장합니다. 첫 번째 파이프는 Kubernetes JSON 을 jq로 전달하고, select(...)는 잘못된 이미지를 사용하는 Pod 만 남기며, -r은 해당 Pod 이름을 일반 텍스트로 출력합니다. 두 번째 파이프의 head -n1은 이름 하나만 남깁니다. 마지막으로 sed -n '/Events:/,$p'describe 출력에서 Events:부터 끝까지 표시합니다.

BAD_POD=$(kubectl get pods -l app=release-web \
  -o json | jq -r '.items[] | select(.spec.containers[0].image == "nginx:does-not-exist-course") | .metadata.name' | head -n1)
echo "$BAD_POD"
kubectl describe pod "$BAD_POD" | sed -n '/Events:/,$p'
kubectl get replicasets -l app=release-web

ErrImagePull 또는 ImagePullBackOff를 찾아보세요. 이전의 정상 ReplicaSet 에는 여전히 사용 가능한 Pod 가 있으므로 Service 는 계속 응답할 수 있습니다.

kubectl exec release-client -- wget -qO- http://release-web | head

이는 롤아웃이 실패한 동안에도 서비스가 계속 제공되었다는 뜻이지, 새 릴리스가 정상적으로 동작한다는 증거는 아닙니다.

롤백하고 매니페스트 조정

이 단계에서는 이전의 정상 리비전으로 복원한 다음 저장된 매니페스트를 수정합니다.

실행 중인 Deployment 에는 리비전 기록이 있으므로 Kubernetes 가 이전 Pod 템플릿을 복원할 수 있습니다. rollout undo는 Deployment 컨트롤러에 이전 리비전을 다시 사용하도록 요청하고, rollout status는 복구가 완료될 때까지 기다립니다. custom-columns는 복구된 이미지와 준비된 복제본 수를 출력합니다.

kubectl rollout history deployment/release-web
kubectl rollout undo deployment/release-web
kubectl rollout status deployment/release-web --timeout=60s
kubectl get deployment release-web \
  -o custom-columns='NAME:.metadata.name,IMAGE:.spec.template.spec.containers[0].image,READY:.status.readyReplicas'

이미지는 다시 정상 상태가 되었고 복제본 3 개가 준비되었습니다. rollout undo는 이전 Pod 템플릿을 기반으로 새 리비전을 생성하며, 리비전 번호를 되돌리지는 않습니다.

이제 한 가지 작업이 더 남아 있습니다. YAML 파일에는 여전히 잘못된 이미지가 들어 있으므로, 나중에 kubectl apply를 실행하면 Deployment 가 다시 손상될 수 있습니다. 저장된 원하는 상태를 복구된 실제 상태와 일치시킵니다. 치환 명령으로 파일을 수정하고, apply로 동기화한 다음, 마지막 history 명령으로 최종 리비전 흐름을 확인합니다.

cd /home/labex/project/update-lab
sed -i 's/Broken release: missing image/Rollback to nginx 1.27/' release-web.yaml
sed -i 's/nginx:does-not-exist-course/nginx:1.27-alpine/' release-web.yaml
kubectl apply -f release-web.yaml
kubectl rollout status deployment/release-web --timeout=60s
grep -nE 'change-cause|image:' release-web.yaml
kubectl rollout history deployment/release-web

이제 클러스터와 파일 모두 동일한 정상 릴리스를 설명합니다.

더 안전한 업데이트 예산 설정

이 단계에서는 Deployment 의 롤아웃 용량과 가용성 제한을 명시적으로 설정합니다.

Deployment 전략 설정은 향후 업데이트 중 허용되는 임시 용량을 제어합니다.

  • maxUnavailable은 롤아웃 중 사용할 수 없는 상태가 되어도 되는 원하는 복제본의 최대 수입니다.
  • maxSurge는 원하는 복제본 수를 초과하여 일시적으로 생성할 수 있는 추가 Pod 의 최대 수입니다.

이 3 개 복제본으로 구성된 소규모 애플리케이션에서는 원하는 복제본 3 개를 모두 계속 사용할 수 있도록 하고, 추가 Pod 1 개를 허용합니다.

sed 명령은 /type: RollingUpdate/라는 주소를 사용해 전략 줄을 찾습니다. a\ 동작은 다음 줄에 필요한 들여쓰기가 적용된 YAML 을 추가합니다. 적용한 뒤 JSONPath 로 두 전략 값을 추출하여 전체 객체를 확인하지 않고도 변경 내용을 검증합니다.

cd /home/labex/project/update-lab
sed -i '/type: RollingUpdate/a\    rollingUpdate:\n      maxUnavailable: 0\n      maxSurge: 1' release-web.yaml
kubectl apply -f release-web.yaml
kubectl get deployment release-web \
  -o jsonpath='maxUnavailable={.spec.strategy.rollingUpdate.maxUnavailable}{"\n"}maxSurge={.spec.strategy.rollingUpdate.maxSurge}{"\n"}'
kubectl get deployment release-web

전략 필드만 변경하면 Pod 템플릿은 바뀌지 않으므로 Pod 가 교체되지 않습니다. 향후 이미지를 업데이트할 때 Kubernetes 는 추가 Pod 1 개를 생성할 수 있으며, 사용 가능한 복제본을 의도적으로 3 개 미만으로 줄이지 않습니다.

이 설정은 가용성을 우선하지만 클러스터에 여유 용량이 필요합니다. 모든 상황에 적용되는 최적의 값은 없습니다. 규모가 큰 애플리케이션에서는 백분율을 사용하는 경우가 많으며, 실제 설정은 용량, 시작 시간, 허용 가능한 서비스 중단 수준에 따라 결정해야 합니다.

요약

애플리케이션 릴리스의 전체 운영 수명 주기를 따라가며 다음을 수행했습니다.

  • 정상적인 Deployment 와 안정적인 Service 의 기준 상태를 구성했습니다.
  • 특정 버전의 이미지를 선언적으로 변경하고 롤아웃이 완료될 때까지 기다렸습니다.
  • Deployment 리비전과 이전 및 새 ReplicaSet 의 관계를 확인했습니다.
  • 이전 릴리스가 계속 서비스를 제공하는 동안 이미지 가져오기 실패를 진단했습니다.
  • 마지막으로 정상 상태였던 Pod 템플릿으로 롤백했습니다.
  • 이후의 apply 가 잘못된 이미지를 다시 복원하지 않도록 매니페스트를 조정했습니다.
  • 향후 업데이트를 위해 가용성과 추가 Pod 수를 명시적으로 제한했습니다.

핵심은 Deployment 가 시간의 흐름에 따라 원하는 상태를 관리한다는 점입니다. 롤아웃은 단순히 이미지를 수정하는 작업이 아니라, ReplicaSet 사이를 제어된 방식으로 전환하는 과정입니다. 따라서 그 진행 상황을 관찰하고 결과를 검증하며, 필요하면 되돌릴 준비를 해야 합니다.