AboutWritten 지나간 발자국 발자국
황현석 | 2003. 06. 23.에 작성되었습니다.

황현석 일지

클라우드 서버 구축 일지 04 - Uninstall.sh 본문

클라우드 서버 구축 일지 04 - Uninstall.sh

회고.

 

저번 글에서는 Kite의 E2E 테스트를 잡으면서 SSH gateway와 host sshd가 어떤 식으로 이어져야 하는지 정리했습니다. gateway가 22번 포트를 맡고, 기존 host sshd는 다른 포트로 이동하고, VM 계정이 아니면 다시 host sshd로 fallback되는 흐름을 실제 서버에서 확인한 이야기였습니다.

 

이번 글은 그 다음 단계의 회고입니다. stage에서 E2E가 통과했고, main에 push한 뒤 GHCR image build도 끝났습니다. 그런데 실제 사용자가 보게 될 README의 원격 uninstall.sh, ghcr-install.sh 흐름으로 기존 설치를 지우고 다시 설치해보니, stage E2E에서는 보이지 않던 Longhorn과 host sshd 경계 문제가 나왔습니다.

 

분명, Stage Branch에서 해야할 End-to-End 테스트, sshd 변경 테스트, uninstall.sh 테스트까지 모두 진행하였는데, main branch 즉 배포 브런치에서 사고가 나버린 거였습니다. 개인프로젝트라고 해도, 안일한 마음으로 배포사이클을 돌리고 그런건 아니였는데, 자괴감이 들긴 했습니다.

 

그래서 분풀이로 왜 어째서 내가 뭘 놓쳤나를 좀 자세하게 보려고 합니다.


 

처음에는 “이제 배포됐으니 README에 적힌 설치 명령만 한 번 마지막으로 돌려보자. 이미 Stage Branch에서는 모든 테스트가 끝났으니 사고가 나지 않겠지.”라고 생각했습니다. 이미 코드도 올라갔고, GHCR 빌드도 됐고, SSH gateway도 어느 정도 봤으니 끝난 줄 알았습니다. 그런데 막상 실제 서버에서 기존 설치를 지우고 다시 설치해보니, 문제는 설치 자체보다 uninstall.shghcr-install.sh 사이에 있었습니다.

 

지우는 스크립트가 끝났다고 해서 Kubernetes 안에서 모든 삭제가 즉시 끝난 것은 아니었습니다. namespace는 아직 Terminating 중일 수 있었고, Longhorn webhook 설정은 남아 있을 수 있었고, host sshd 복원 worker가 예약됐다고 해서 실제로 22번 포트가 돌아온 것도 아니었습니다. 이번에 한 일은 대단한 기능 개발이라기보다, 배포 이후 재설치라는 운영 흐름이 어디서 깨지는지 끝까지 따라간 일이었습니다.

 

즉, 무언가를 설치하는것에 문제가 있는게 아니였습니다. 무릇 개발자는 다녀간 자리가 깨끗하여야합니다. 저는 이부분을 이번에는 좀 자세하게 다뤄보자고 생각했습니다.

 

더미 데이터 생각보다 그런 부분들은 개발자들의 프로그램 설계 역량에 많이 달려있습니다. 사실 자신의 프로그램이 만든 데이터 그리고 많은 의존성이 만든 더미데이터를 모두 책임지는것은 개발자에게는 웃지못 할 책임입니다. 하지만 그걸 책임져야 시스템을 관리하는 사람이라고 할 수 있지 않을까요? 

 

이번에 Kubernetes에서도 비슷한 감각을 느꼈습니다. 내가 만든 kite namespace만 지우면 끝나는 줄 알았는데, 실제로는 내가 설치 과정에서 끌어온 Longhorn, CDI, KubeVirt, host sshd 설정까지 하나의 운영 흔적처럼 남았습니다. 그러니까 uninstall은 “내 앱 리소스 삭제”가 아니라, 내가 서버에 남긴 약속들을 어디까지 책임질 것인가의 문제에 가까웠습니다.

 

핵심. main에 push된 뒤의 테스트는 “설치가 되는지”만 보는 일이 아니었습니다. 실제로 지우고, 바로 다시 설치하고, 그 사이에 SSH 포트와 Longhorn namespace가 어떤 상태로 지나가는지 확인하는 일이었습니다.

Kite API

http

회원가입, 로그인, VM 생성 요청을 받는 HTTP 서버입니다. 직접 VM을 만들기보다 KiteUser, KiteVirtualMachine 같은 CRD에 사용자의 의도를 기록합니다.

Kite controller

reconcile

CRD에 적힌 desired state를 보고 namespace, quota, Secret, DataVolume, KubeVirt VM 같은 실제 Kubernetes 리소스를 맞춰주는 역할입니다.

KubeVirt, CDI, Longhorn

runtime

KubeVirt는 Kubernetes 안에서 VM을 실행하고, CDI는 VM disk image를 DataVolume/PVC로 가져오며, Longhorn은 그 PVC가 올라갈 storage backend 역할을 합니다.

kite-gateway

ssh

외부 22번 포트를 잡는 SSH 입구입니다. VM 계정이면 VM으로 보내고, 기존 host 계정이면 옮겨둔 host sshd로 인증을 넘기는 fallback 역할도 합니다.

 

배포 이후의 진짜 테스트는, 지우고 다시 설치할 때 시작됐습니다.

 

 

1. 배포 이후에 확인하고 싶었던 것

 

처음 목표는 이 정도였습니다. main에 push된 상태에서 GitHub에 올라간 원격 shell script만 사용합니다. 로컬 checkout에서 직접 파일을 실행하지 않고, README에 적힌 것처럼 GitHub raw URL에서 ghcr-install.shuninstall.sh를 받아 실행합니다. 그래야 “내 컴퓨터에 있는 최신 파일”이 아니라 “사용자가 실제로 받게 되는 main의 파일”을 테스트할 수 있습니다.

 

여기서 의문이 생겼습니다. stage에서는 E2E가 돌았는데, 왜 main에 올라간 뒤 README 방식으로 uninstall/install을 하니까 Longhorn 문제가 나왔을까. 처음에는 이게 좀 이상했습니다. 같은 코드라면 같은 결과가 나와야 할 것 같았기 때문입니다. 그런데 다시 보면 두 테스트가 보고 있던 표면이 달랐습니다.

stage E2E

runtime

현재 checkout의 스크립트와 이미지를 기준으로, Kite가 클러스터에서 실제로 뜨고 API, CRD, controller, SSH gateway가 연결되는지를 봅니다. 주로 “설치된 뒤 서비스가 일하는가”를 확인합니다.

main 원격 재설치 테스트

release

GitHub main의 raw script와 GHCR image만으로, 기존 설치를 full uninstall한 직후 다시 설치합니다. 여기서는 “사용자가 공개된 배포 경로만 써도 지웠다가 다시 살릴 수 있는가”를 봅니다.

Longhorn 문제는 후자에서 더 잘 드러났습니다. stage E2E가 잘못됐다는 뜻은 아니었습니다. 다만 stage E2E는 이미 올라간 클러스터에서 서비스의 정상 path를 강하게 봤고, main 원격 테스트는 Longhorn 설치 자체를 지우고 다시 만드는 destructive path를 봤습니다. 이 destructive path에서는 webhook configuration, namespace finalizer, host sshd restore worker 같은 설치 주변부가 제품의 일부처럼 튀어나옵니다.

 

 
1

먼저 full uninstall

Kite namespace, CRD, golden image, Longhorn host data, Longhorn 설치까지 지웁니다. host sshd도 22번으로 복원합니다.

 
2

그 다음 GHCR install

로컬 build가 아니라 GHCR image pull 기반으로 설치합니다. 사용자가 README에서 실행하는 경로와 같아야 합니다.

 
3

verify와 실제 표면 확인

Kite workload, DataVolume, API health, frontend, SSH gateway fallback, host sshd direct login을 확인합니다.

4

마지막 상태를 설치 완료로 남김

테스트가 끝난 뒤 서버는 22번 gateway, 2222번 host sshd 상태로 남아 있어야 합니다.

 

자동화 환경에서는 curl ... | bash를 그대로 쓰면 sudo password prompt를 받을 TTY가 없어서 실패할 수 있습니다. 그래서 테스트에서는 같은 GitHub main script를 다운로드하되, sudo env ... bash script 형태로 실행했습니다. 중요한 점은 실행한 script가 로컬 파일이 아니라 GitHub main에서 내려온 파일이었다는 점입니다.


2. 첫 번째로 걸린 것은 Longhorn 삭제였습니다

 

처음 full uninstall을 실행했을 때 Kite 리소스 자체는 지워졌습니다. golden image DataVolume과 PVC도 지워졌고, namespace와 CRD도 삭제 요청이 들어갔습니다. 그런데 longhorn-system namespace가 Terminating에서 빠져나오지 못했습니다. 겉으로는 “Longhorn이 삭제 중에 멈췄다” 정도로 보였지만, 실제로는 Kubernetes admission webhook 쪽에서 막히고 있었습니다.

 

failed calling webhook "validator.longhorn.io"
service "longhorn-admission-webhook" not found

 

여기서 webhook은 HTTP API의 일반적인 webhook과 비슷한 단어지만, Kubernetes에서는 조금 더 낮은 위치에 있습니다. 사용자가 어떤 리소스를 만들거나 지우면 요청은 먼저 Kubernetes API server로 갑니다. API server는 그 요청을 etcd에 저장하기 전에 validating webhook이나 mutating webhook을 호출할 수 있습니다. 쉽게 말하면 “이 리소스 변경을 받아도 되는지 외부 검사 서버에게 물어보는 문지기”에 가깝습니다.

 

 
1

webhook configuration

API server에게 “Longhorn 리소스 변경 전에 이 webhook을 호출하라”고 알려주는 cluster-wide 규칙입니다. namespace 안에만 묶인 설정이 아니라 클러스터에 남을 수 있습니다.

 
2

webhook backing Service

그 규칙이 실제로 호출하는 Kubernetes Service입니다. Longhorn에서는 보통 longhorn-system namespace 안의 admission webhook pod로 연결됩니다.

 
3

삭제 순서가 꼬인 상태

Service와 pod는 먼저 사라졌는데 webhook configuration만 남으면, API server는 없는 Service를 계속 호출하려고 합니다. 그러면 Longhorn 리소스를 정리하려는 요청 자체가 다시 실패합니다.

4

namespace finalization 실패

namespace 삭제의 마지막 정리 과정에서도 리소스 변경 요청이 발생할 수 있습니다. 이때 죽은 webhook 호출이 끼어들면 namespace가 Terminating에서 오래 멈출 수 있습니다.

 

이 메시지는 처음에는 좀 이상했습니다. 이미 Longhorn을 지우고 있는데 왜 Longhorn webhook이 다시 삭제를 막는가 싶었습니다. 그런데 위 구조로 보면 이유가 보입니다. webhook configuration은 클러스터 레벨 규칙이라 남아 있고, 그 규칙이 가리키는 backing Service는 namespace 안에서 이미 삭제됐습니다. API server 입장에서는 “검사를 받아야 하는데 검사 서버가 없다”가 됩니다. 그래서 삭제를 마무리하려는 요청도 실패할 수 있었습니다.

 

여기에 finalizer도 같이 얽혔습니다. finalizer는 Kubernetes 리소스가 삭제되기 전에 controller가 정리 작업을 끝낼 시간을 주는 metadata hook입니다. 정상 상황에서는 좋은 안전장치입니다. 그런데 controller나 webhook 쪽이 이미 내려간 상태에서 finalizer만 남으면, 리소스는 삭제 요청을 받았는데 마지막 정리 확인을 끝내지 못한 상태가 됩니다.

 

그래서 uninstall-kite.shbuild-clear.sh에 Longhorn webhook configuration 삭제를 명시적으로 추가했습니다. 그리고 Longhorn CR finalizer를 지우는 코드도 고쳤습니다. 기존에는 kubectl get ... -A -o name 결과를 namespace 없이 patch하려고 했는데, namespaced Longhorn CR은 longhorn-system namespace를 명시해야 안정적으로 patch할 수 있었습니다.

기존 cleanup

stuck

namespace 삭제를 요청하고 finalizer patch를 시도했지만, Longhorn webhook configuration이 남아 있으면 API server가 죽은 webhook을 계속 호출할 수 있었습니다.

수정한 cleanup

explicit

Longhorn validating/mutating webhook configuration을 먼저 제거하고, namespaced CR과 cluster-scoped CR finalizer를 분리해서 정리한 뒤, Longhorn CRD 삭제도 --wait=false로 요청합니다.

이 변경 뒤에는 실제 remote uninstall에서 longhorn-webhook-validator, longhorn-webhook-mutator, Longhorn CRD들이 script 안에서 삭제됐습니다. 여기까지는 괜찮아보였습니다. 그런데 다음 문제는 SSH 복원 쪽에서 나왔습니다.

 

여기서 생각이 조금 바뀌었습니다. 처음에는 삭제 대상만 잘 지우면 된다고 봤습니다. 그런데 실제로는 삭제 대상을 감시하던 장치가 더 오래 남아서 삭제를 막을 수 있었습니다. namespace 안의 Service는 사라졌는데, cluster-wide webhook configuration은 살아 있는 상태였습니다. 이걸 보고 나니 cleanup은 리소스 목록을 지우는 작업이 아니라, 리소스를 둘러싼 규칙과 감시자까지 같이 정리하는 작업으로 보였습니다.


3. host sshd 복원 worker는 로그 파일 때문에 실패했습니다

 

Kite gateway는 외부 22번 포트를 사용합니다. 그래서 설치할 때 기존 host sshd는 2222 같은 다른 포트로 이동합니다. 반대로 uninstall할 때는 gateway를 지우기 전에 host sshd를 22번으로 되돌리는 작업을 예약해야 합니다. 그렇지 않으면 gateway를 지우는 순간 원격 SSH 세션이 끊기고, 서버 22번도 비어버릴 수 있습니다.

 

이 프로젝트에서는 manage-host-sshd.sh restore-after-port-free를 background worker로 띄워서, gateway가 22번을 내려놓으면 host sshd config를 원래대로 복원하게 했습니다. 그런데 실제 uninstall에서는 이 worker가 실패했습니다.

 

/tmp/kite-host-sshd-restore.log: Permission denied
[kite-deploy] host sshd restore worker failed

 

처음에는 root로 실행하는데 왜 /tmp 로그 파일에 못 쓰는지 이상했습니다. 그런데 Linux에는 /tmp 같은 sticky directory에서 기존 사용자 소유 regular file에 root가 redirection으로 append하는 것을 막는 보호 설정이 있을 수 있습니다. 예전에 만들어진 /tmp/kite-host-sshd-restore.log가 일반 사용자 소유로 남아 있었고, uninstall은 root로 돌면서 그 파일에 append하려다가 막혔습니다.

 

여기서 중요한 점은 restore logic이 틀린 것이 아니라, worker를 시작하기 전 redirection 단계에서 실패했다는 점이었습니다. 그래서 로그 파일을 열 수 없으면 mktemp로 고유한 새 로그 파일을 만들어 쓰도록 했습니다. 그리고 권한 검사 실패 메시지가 사용자에게 그대로 보이지 않도록 redirection 검사를 묶었습니다.

host sshd restore worker pid=2966236
log=/tmp/kite-host-sshd-restore.r7HN3o.log
...
host sshd restore worker completed

 

수정 뒤에는 실제 uninstall에서 worker가 고유 로그 파일을 잡았고, 마지막에 host sshd restore worker completed까지 확인했습니다. 그 뒤 hy3on.site:22는 다시 host sshd로 열렸고, 2222는 닫혔습니다. 이 단계에서야 “삭제가 끝났다”라고 말할 수 있었습니다.

 

이 부분은 작아보였지만 꽤 좋은 경고였습니다. root로 실행하면 권한 문제는 거의 끝난다고 생각하기 쉽습니다. 그런데 shell redirection은 worker 로직 안으로 들어가기도 전에 실패할 수 있었습니다. 결국 restore worker의 상태는 sshd config만이 아니라, worker를 띄우는 로그 파일, 실행 사용자, /tmp 보호 정책까지 포함하고 있었습니다. 배포 스크립트에서 주변 환경도 상태라는 걸 다시 느꼈습니다.


4. 바로 재설치하니 namespace Terminating race가 나왔습니다

 

그 다음에는 다시 README install을 실행했습니다. 그런데 이번에는 Longhorn manifest apply가 실패했습니다. 이유는 longhorn-system namespace가 아직 Terminating 중이었기 때문입니다. uninstall 명령은 끝났지만, Kubernetes namespace controller가 실제 namespace deletion을 마치는 데는 시간이 더 필요했습니다.

 

namespace/longhorn-system unchanged
Warning: Detected changes to resource longhorn-system which is currently being deleted.

serviceaccounts "longhorn-service-account" is forbidden:
unable to create new content in namespace longhorn-system because it is being terminated

 

이건 uninstall이 실패한 것과는 다른 문제였습니다. 삭제 요청이 정상적으로 들어갔고, 복원 worker도 성공했습니다. 다만 바로 다음 설치가 너무 빨랐습니다. Kubernetes에서는 namespace 이름이 같아도 Terminating 중인 namespace 안에는 새 리소스를 만들 수 없습니다. 그래서 install script가 Longhorn을 apply하기 전에 longhorn-system namespace의 deletionTimestamp를 확인하도록 했습니다.

 
1

Longhorn 설치 직전 namespace 조회

longhorn-system이 없으면 바로 설치합니다. 있더라도 삭제 중이 아니면 기존처럼 apply합니다.

 
2

deletionTimestamp가 있으면 대기

이전 uninstall의 namespace deletion이 끝날 때까지 기다립니다. 기본 timeout은 300s로 잡았습니다.

3

완전히 사라진 뒤 apply

namespace가 사라진 뒤에야 Longhorn manifest를 다시 apply합니다.

 

이 수정은 install-longhorn.sh에 들어갔습니다. 재설치 직후 race를 install 쪽에서 받아주는 것이 맞아보였습니다. uninstall이 항상 namespace deletion 완료까지 기다리면 사용자는 삭제가 오래 걸린다고 느낄 수 있고, install은 어차피 같은 namespace를 다시 만들려는 순간이므로 이 조건을 가장 잘 판단할 수 있습니다.

 

여기서 배운 건 timeout을 늘리는 법이 아니라, 누가 기다림을 책임져야 하는가였습니다. uninstall이 끝났다는 말과, 클러스터가 다시 install 가능한 상태가 됐다는 말은 달랐습니다. 지우는 쪽은 삭제 요청을 넣고 복원을 예약하면 자기 역할을 거의 끝낸 것일 수 있습니다. 하지만 다시 같은 namespace를 쓰려는 install 쪽은 그 namespace가 정말 사라졌는지를 확인해야 했습니다. 기다림도 아무 데나 붙이면 되는 게 아니라, 그 상태를 가장 필요로 하는 단계가 가져가야 했습니다.


5. 중간중간 이전 수정도 실제로 검증됐습니다

 

이번 main 원격 검증에서는 새로 고친 것만 확인된 것이 아니었습니다. 이전에 넣었던 수정도 실제 main install에서 같이 검증됐습니다. 예를 들어 Longhorn disk tag 재시도는 fresh Longhorn install에서 바로 드러났습니다. Longhorn node CR은 먼저 생기지만 status.diskStatus가 늦게 채워질 수 있습니다. 그래서 처음 몇 번은 disk가 Ready/Schedulable로 보이지 않았고, 몇 번 더 기다린 뒤에야 node.longhorn.io/hhs2003 patched가 나오면서 kite tag가 붙었습니다.

 

skipping Longhorn disk default-disk... because ready= schedulable= allowScheduling=true
waiting for a Ready/Schedulable Longhorn disk tagged kite
...
node.longhorn.io/hhs2003 patched
found 1 Ready/Schedulable Longhorn disk(s) tagged kite

 

GHCR pull timeout도 실제 환경에서 의미가 있었습니다. 원격 서버에서 GHCR이나 pkg-containers.githubusercontent.com DNS가 순간적으로 느릴 수 있었고, 처음에는 rollout timeout이 짧아서 Kite workload가 늦게 뜨면 실패로 보일 수 있었습니다. 그래서 KITE_ROLLOUT_TIMEOUT 기본값을 늘렸고, 이번 install에서는 20m로 실행했습니다. 실제로는 이번 마지막 install에서 GHCR pull은 빨리 끝났지만, 이 옵션은 느린 네트워크 환경에서 필요해보였습니다.

여기서 다시 느낀 점. Kubernetes install script는 “resource를 만들었다”와 “controller가 상태를 채웠다”와 “workload가 실제로 서비스를 받을 수 있다”를 같은 단계로 보면 안 됩니다. 이번 문제들은 대부분 그 사이의 간격에서 나왔습니다.


6. 최종 확인은 연결이 아니라 역할 확인이었습니다

 

마지막으로 재설치를 끝낸 뒤에는 단순히 pod가 Running인지 보는 데서 멈추지 않았습니다. Kite는 여러 서비스가 붙어서 하나의 플랫폼처럼 보이는 구조입니다. 그래서 “프로세스가 떠 있다”가 아니라 “각 서비스가 맡은 역할을 실제로 하고 있다”를 봐야 했습니다.

 

kite-api

사용자 요청을 받는 HTTP API입니다. 여기서는 health endpoint와 CRD read path를 확인했습니다.

kite-controller

CRD를 읽고 실제 Kubernetes/KubeVirt 리소스로 맞추는 reconciler입니다. 여기서는 golden image DataVolume과 PVC 상태가 주요 확인 대상이었습니다.

kite-frontend

브라우저에서 접근하는 UI입니다. 여기서는 서비스 응답과 page load가 되는지를 확인했습니다.

kite-gateway

외부 SSH 입구입니다. 여기서는 22번 포트가 gateway 역할을 하고, host 계정 fallback과 host key fingerprint가 맞는지 확인했습니다.

 

kite-api, kite-controller, kite-frontend, kite-gateway 네 pod는 모두 Running이었고, Ubuntu golden image DataVolume은 Succeeded, PVC는 Bound였습니다.

 

kite-api          1/1 Running
kite-controller   1/1 Running
kite-frontend     1/1 Running
kite-gateway      1/1 Running

ubuntu-22.04 DataVolume  Succeeded  100.0%
ubuntu-22.04 PVC         Bound

 

API health는 단순 process health만 보지 않았습니다. KiteUserKiteVirtualMachine CRD read path도 같이 봤습니다. 즉 API server가 Kubernetes API server를 통해 CRD list를 읽을 수 있는지 확인했습니다.

{
  "status": "ok",
  "checks": [
    "kiteusers.crd.etcdReadPath: ok",
    "kitevirtualmachines.crd.etcdReadPath: ok"
  ]
}

 

SSH 쪽은 더 중요했습니다. 최종 상태에서 22번은 Kite gateway가 잡고 있어야 하고, host sshd는 2222에서 직접 로그인되어야 합니다. 그리고 gateway fallback도 host 계정으로 password login을 넘길 수 있어야 합니다.

hy3on.site:22

Kite gateway입니다. VM sshId가 아니면 host sshd fallback으로 password 인증을 넘깁니다. 실제 password probe가 통과했습니다.

hy3on.site:2222

기존 host sshd입니다. 직접 password login이 통과했습니다.

fingerprint

gateway가 내보내는 SSH host key fingerprint가 실제 host /etc/ssh/ssh_host_ed25519_key fingerprint와 같았습니다.

마지막 fingerprint는 이 값으로 일치했습니다.

host_fp=SHA256:/Kq/jk65Fulh/8SiQuE8POAdrYLiEI3KiDy2l1fDE2Q
gateway_fps=SHA256:/Kq/jk65Fulh/8SiQuE8POAdrYLiEI3KiDy2l1fDE2Q
fingerprint-match-ok

 

여기서도 생각보다 중요한 기준이 하나 생겼습니다. SSH 22번 포트가 열렸다는 것만으로는 충분하지 않았습니다. gateway가 SSH 입구 역할을 한다면, 사용자가 보는 host identity도 함부로 바뀌면 안 됩니다. 접속은 되는데 fingerprint가 바뀌면, 운영자 입장에서는 같은 서버인지 다른 서버인지 다시 의심해야 합니다. 그래서 이 확인은 단순 연결 테스트가 아니라, gateway가 기존 host의 정체성을 얼마나 자연스럽게 이어받는지 보는 테스트에 가까웠습니다.


7. 왜 stage에서는 됐는데 main에서 문제가 나왔나

 

마지막에 정리하고 싶었던 질문은 이거였습니다. stage에서는 E2E가 됐는데 왜 main 원격 배포 스크립트를 적용하니까 Longhorn 문제가 나왔을까. 결론부터 말하면, 서비스 자체의 E2E와 release script의 destructive 재설치 테스트는 같은 테스트가 아니었습니다.

 

1. 테스트 표면이 달랐습니다

scope

stage E2E는 Kite workload가 잘 떠서 API, CRD, controller, gateway가 이어지는지를 봤습니다. main 원격 테스트는 공개 script로 Longhorn까지 지웠다가 바로 다시 설치하는 흐름을 봤습니다. Longhorn webhook 문제는 후자에서 나오는 종류였습니다.

2. 삭제는 생성보다 상태가 더 많았습니다

state

설치는 리소스를 만드는 쪽에 가깝지만, 삭제는 finalizer, admission webhook, namespace controller, background worker가 얽힙니다. Service는 사라졌는데 webhook configuration이 남거나, namespace가 아직 Terminating 중인 상태가 생길 수 있습니다.

3. 바로 재설치하는 타이밍이 문제를 드러냈습니다

race

uninstall 명령이 끝난 직후에도 Kubernetes 내부 삭제가 끝났다고 볼 수는 없었습니다. 그래서 install 쪽에서 longhorn-system namespace가 완전히 사라질 때까지 기다리는 처리가 필요했습니다.

 

해결은 각 문제를 한 번 더 실행해도 버티는 스크립트 동작으로 넣었습니다. Longhorn을 지울 때는 validating/mutating webhook configuration을 먼저 정리하고, namespaced Longhorn CR finalizer는 namespace를 명시해서 patch하도록 했습니다. host sshd restore worker는 고정된 /tmp 로그 파일에 의존하지 않고 mktemp 로그 파일을 잡게 했습니다. 그리고 재설치 직전에는 longhorn-system namespace가 Terminating 상태인지 확인하고, 완전히 사라진 뒤 Longhorn manifest를 apply하도록 했습니다.

 

이게 최선이었냐고 하면, 현재 단계에서는 꽤 현실적인 선택이라고 봤습니다. release script는 이미 망가진 중간 상태를 만날 수 있고, 운영자는 “설치전의 상태 ”를 기대합니다. 그래서 script가 Longhorn의 webhook 잔여물, namespace Terminating, sshd 복원 로그 문제를 직접 받아주는 쪽이 맞아보였습니다.

 

다만 더 깔끔한 방향은 있습니다. uninstall을 단순 shell 명령 묶음으로 두기보다, 먼저 cluster 상태를 진단하는 preflight/audit 단계를 두고, 삭제 plan을 출력한 뒤, 각 단계가 끝났는지 관찰하는 방식으로 나누는 겁니다.

 

조금 더 나아가면, 지금의 shell script는 “명령을 실행하는 도구”이고, 앞으로 필요한 것은 “클러스터가 어느 상태인지 판단하는 도구”에 가깝습니다. 예를 들어 Longhorn webhook configuration이 남아 있는지, backing Service가 살아 있는지, namespace deletion이 끝났는지, host sshd restore가 실제로 listen 상태까지 갔는지를 하나의 release health check로 볼 수 있어야 합니다. 그러면 stage에서 성공한 E2E와 main에서 필요한 release 재설치 테스트 사이의 빈틈이 줄어듭니다.

 

이번 테스트의 결론은 “main script로 설치가 된다”보다 조금 더 구체적입니다. 현재 main의 원격 script는 full uninstall 뒤 재설치 흐름을 버팁니다. Longhorn은 지워지고 다시 올라오며, Kite workload는 GHCR image로 뜨고, golden image는 import되고, API는 CRD read path를 통과하고, frontend는 응답하고, SSH 22번은 gateway로, 2222번은 host sshd로 역할이 분리됩니다. gateway fingerprint도 기존 host key와 같습니다. 다만 다음부터는 stage에서 E2E가 통과했다는 사실만으로 release uninstall/install까지 안전하다고 보지 않고, 이 destructive 재설치 path를 별도의 테스트 축으로 계속 가져가야겠다고 생각했습니다.


결론

 

이번에 제일 크게 바뀐 생각은, 삭제가 설치의 반대가 아니라는 점이었습니다. 처음에는 install이 리소스를 만들고, uninstall이 그 리소스를 지우면 된다고 봤습니다. 그런데 실제 Kubernetes에서는 그렇게 대칭적으로 움직이지 않았습니다. 삭제에는 finalizer가 있고, namespace controller가 있고, admission webhook이 있고, host sshd 복원처럼 Kubernetes 바깥의 상태도 같이 끼어 있었습니다.

 

그러니까 이번 문제는 단순히 Longhorn cleanup 버그 하나를 잡은 일이 아니었습니다. 제가 갖고 있던 테스트의 모델이 조금 바뀐 일이었습니다. stage E2E는 “원하는 상태가 만들어진 뒤 서비스가 도는가”를 확인했습니다. 하지만 release 재설치 테스트는 “원하는 상태를 지우고 다시 적는 동안, 시스템이 중간 상태를 어떻게 지나가는가”를 봐야 했습니다. 둘은 비슷해 보이지만 실제로는 전혀 다른 질문이었습니다.

 

여기서 얻은 깨달음은, 배포 스크립트도 어느 순간부터 작은 controller처럼 행동해야 한다는 것이었습니다. 그냥 명령을 순서대로 실행하는 shell script로는 부족했습니다. 지금 namespace가 정말 사라졌는지, webhook configuration은 남아 있는지, backing Service는 살아 있는지, host sshd가 실제로 listen 중인지 관찰해야 했습니다. 그리고 관찰한 상태에 따라 기다리거나, 정리하거나, 실패 이유를 말해야 했습니다.

 

결국 테스트는 “성공했다”는 도장을 찍는 도구라기보다, 내가 시스템을 어떤 식으로 오해하고 있었는지 드러내는 도구에 가까웠습니다. 이번에는 제가 Kubernetes 삭제를 너무 즉시적인 동작으로 보고 있었다는 것을 알게 됐습니다. 앞으로 Kite의 release 테스트는 서비스가 잘 도는지만 보지 않고, 삭제 중간 상태와 재설치 경계까지 계속 봐야 할 것 같습니다. 그래야 “플랫폼이 뜬다”와 “운영자가 다시 살릴 수 있다”를 같은 말로 착각하지 않을 수 있습니다.