--- kind: CASE slug: an-empty-token-installed-the-agent-anyway title: 빈 토큰이 조용히 흘러갔다 — 설치 출력은 성공이었고 agent 만 5초마다 다시 죽었다 topic: build-completion-judgment topicName: 끝났다는 판정 project: virtualization status: 게시 전 sourceRevision: 9465582b5d1630eb4ae7c4e078021486919bf6b6 source: - final/document.md#185-가이드-묶음이-스스로-정한-규약 - final/document.md#188-단계-02-k3s-server-와-agent - final/document.md#184-이-부의-출처와-범위 --- # 빈 토큰이 조용히 흘러갔다 — 설치 출력은 성공이었고 agent 만 5초마다 다시 죽었다 k3s agent 설치는 오류 한 줄 없이 끝났는데 kubectl get nodes 에는 노드가 하나뿐이었다. 게스트 안에서 친 ssh 가 실패해 토큰이 빈 문자열로 넘어갔고, 설치 스크립트는 그 전까지를 성공으로 찍었다. 값을 찍지 않고 길이만 재는 한 줄을 설치 앞에 두면 잡힌다. ## 관계 - **도구가 낸 출력은 대상의 상태가 아니다 — 그 명령이 무엇을 세는지부터 가른다** 설치 스크립트의 출력을 노드가 붙었다는 뜻으로 읽은 사건이라 그 규칙의 사례가 된다. - **가까운 층부터 한 층씩 건너뛰며 확인한다 — 층마다 성공 신호가 다르다** 설치가 끝났다는 출력 대신 각 층에서 무엇이 성공 신호인지로 판정을 옮기는 절차다. - **cloud-init 이 안 도는 원인은 넷인데 증상은 「SSH 가 안 붙는다」 하나였다** 같은 구축의 한 단계 앞에서 벌어진 일이고, 거기서도 실패가 아무 오류 없이 지나갔다. - **가이드대로 다시 쳐서 이 실험대가 같은 상태로 서는가** 여기 적은 설치 명령을 다시 쳐서 같은 상태가 되는지는 확인된 적이 없다. - **단계별 구축 문서는 작성 시점이 아니라 실행 순서로 검증한다** 명령을 어느 셸에서 치는지를 단계마다 확인하라는 규칙이고, 그 확인이 빠졌을 때 나온 실패가 이것이다. ## 문제 k3s agent 설치가 끝까지 돌았고 출력에 오류가 없었다. 그런데 두 노드가 Ready 로 나와야 할 kubectl get nodes 에 두 번째 노드가 나타나지 않았다. 설치 출력의 오류 : x kubectl get nodes 의 노드 수 : 1 k3s-agent 유닛 : 5초마다 재시작 journalctl -u k3s-agent : level=fatal msg="Error: --token is required" 실패가 화면에 나오지 않아 설치는 끝난 것으로 읽힌다. ## 결론 토큰을 꺼내는 ssh 를 게스트 안에서 쳤고 그 명령이 Host key verification failed. 로 끝났다. 명령 치환으로 감싸면 오류는 stderr 로 흘러가고 변수에는 빈 문자열이 담긴다. 셸은 아무 불평도 하지 않는다. agent 는 --token '' 을 받아 level=fatal msg="Error: --token is required" 로 죽지만, 설치 스크립트는 그 전까지를 다 성공으로 찍고 끝난다. 그래서 설치 출력만 보면 성공이고, 유닛이 Restart=always 라 5초마다 조용히 재시도한다. 해결 : 게스트에 들어가지 않고 lab host 한 셸에서 ssh kc-lab-1 '...' 형태로 친다 가드 : 값을 쓰기 전에 길이로 가른다. 이 실험대의 토큰은 108자였다 히스토리에 남기지 않으려면 : --token-file 로 넘긴다. 토큰을 꺼낸 셸과 같은 셸에서 쳐야 한다는 제약도 없어진다 ## 검증 환경 호스트 : test-server, Arch Linux CPU : i5-1135G7, 논리 코어 8 RAM : 11,648MiB QEMU : 11.1.1 libvirt : 12.7.0 게스트 : Debian 12 genericcloud k3s : v1.36.4+k3s1 server 노드 : kc-lab-1, 192.168.122.11 agent 노드 : kc-lab-2, 192.168.122.12 토큰 길이 : 108자 토큰 형식 : K10<해시>::server:<비밀번호> ## 재현 조건 1. lab host 에서 게스트 세 대를 세우고 kc-lab-1 에 k3s server 를 깐다. 2. lab host 에서 ssh kc-lab-1 로 게스트에 로그인한다. 3. 게스트 프롬프트에서 TOKEN=$(ssh kc-lab-1 'sudo cat /var/lib/rancher/k3s/server/node-token') 을 친다. 4. echo "${#TOKEN} 자" 가 0 을 낸다. 5. 같은 셸에서 agent 설치 명령에 --token "$TOKEN" 을 넘긴다. 설치 출력은 오류 없이 끝난다. 6. lab host 로 돌아와 kubectl get nodes 를 친다. 노드가 하나뿐이다. 7. ssh kc-lab-2 'journalctl -u k3s-agent' 에서 --token is required 를 찾는다. ## 본문 ## 토큰이 지나는 길과 명령을 치는 셸 k3s server 는 `kc-lab-1` 에, agent 는 `kc-lab-2` 에 깔고 둘 다 lab host 에서 `ssh` 로 원격 실행한다. 가이드가 코드 블록마다 어느 기계에서 치는지를 붙여 둔 까닭이 여기에 있다 — 게스트는 libvirt NAT(`192.168.122.0/24`) 안에 있어 워크스테이션에서 직접 닿지 않고, `ssh kc-lab-1` 이라는 별칭도 lab host 의 `~/.ssh/config` 에만 있다. agent 가 server 에 붙으려면 server 가 만든 node-token 이 필요하고, 그 값을 lab host 에서 꺼내 같은 셸에서 설치 명령에 넘긴다. ```bash TOKEN=$(ssh kc-lab-1 'sudo cat /var/lib/rancher/k3s/server/node-token') echo "${#TOKEN} 자" # 값이 아니라 길이만 확인한다 ``` 비밀은 길이나 존재 여부만 확인하고 값을 찍지 않는다. 가이드가 먼저 정한 표기 규약이고, 값을 찍으면 터미널 스크롤백과 화면 공유에 그대로 남기 때문이다. ```bash ssh kc-lab-2 "curl -sfL https://get.k3s.io | sudo sh -s - agent \ --server https://192.168.122.11:6443 \ --token '$TOKEN' \ --node-ip 192.168.122.12" ``` ## 게스트 안에서 친 ssh 는 빈 문자열이 된다 토큰을 꺼내려고 게스트에 먼저 들어가면 같은 명령이 다르게 끝난다. 게스트에는 lab host 의 개인키도 `~/.ssh/config` 도 없다. ```bash [kc-lab-1] $ ssh kc-lab-1 'sudo cat /var/lib/rancher/k3s/server/node-token' Host key verification failed. ``` 여기까지는 오류 문구가 찍힌다. 문제는 이 명령을 `TOKEN=$(...)` 로 감쌌을 때다. 명령 치환은 표준 출력만 변수에 담으므로 오류는 stderr 로 흘러가고 `TOKEN` 에는 빈 문자열이 담기며, 셸은 아무 불평도 하지 않는다. 뒤이어 치는 설치 명령은 `--token ''` 을 넘긴 것과 같아진다. ## 설치 출력이 성공으로 끝나는 경로 빈 문자열을 받은 설치도 끝까지 돈다. agent 가 `--token ''` 을 받아 `level=fatal msg="Error: --token is required"` 로 죽는데, 설치 스크립트는 그 전까지를 다 성공으로 찍고 끝나므로 설치 출력만 보면 성공이다. 성공으로 찍힌 것은 내려받기와 유닛 생성과 `enable` 까지다. 유닛은 `Restart=always` 라 5초마다 조용히 재시도하고, 그래서 실패가 화면이 아니라 `journalctl -u k3s-agent` 안에서만 되풀이된다. k3s server 와 agent 를 세우는 단계가 끝났다는 판정은 lab host 에서 노드 목록을 쳐서 두 노드가 `Ready` 로 나오는 것이다. ```bash kubectl get nodes -o wide ``` ``` NAME STATUS ROLES AGE VERSION INTERNAL-IP kc-lab-1 Ready control-plane 47m v1.36.4+k3s1 192.168.122.11 kc-lab-2 Ready 21m v1.36.4+k3s1 192.168.122.12 ``` 빈 토큰으로 깔린 agent 는 이 목록에 올라오지 않는다. ## 값을 찍지 않고 길이만 잰다 토큰을 화면에 찍어 눈으로 대조하는 방법은 표기 규약이 막아 두었다. 그래서 설치 앞에 길이를 재는 한 줄을 둔다. ```bash [ ${#TOKEN} -ge 50 ] || echo "TOKEN 이 비었다 — 3번으로 돌아간다" ``` 이 실험대의 토큰은 108자였고 형식이 `K10<해시>::server:<비밀번호>` 라 k3s 판올림에 따라 자릿수가 달라진다. 그래서 이 가드는 값을 맞춰 보지 않고 길이가 `0` 이 아닌지만 본다. 길이가 `0` 으로 나오는 길은 셋이다. 가이드는 게스트 안에서 친 경우를 그중 가장 흔하다고 적어 두었고, 나머지 둘은 server 가 아직 안 떠서 토큰 파일이 없거나 새 셸을 열어 변수가 사라진 경우를 가리킨다. 가드 한 줄은 그 몇 분을 막는다 — 설치가 성공으로 끝나고, 노드 목록에서 한 줄이 빠진 것을 알아채고, `journalctl` 까지 가는 동안이다. ## 토큰을 파일로 넘기면 같은 셸 제약이 없어진다 위 가드는 `TOKEN` 이 셸 변수라 토큰을 꺼낸 셸과 같은 셸에서 쳐야 하고, 다른 창에서는 비어 있다. 토큰이 명령줄에 들어가는 것도 걸리면 파일로 넘긴다. ```bash ssh kc-lab-1 'sudo cat /var/lib/rancher/k3s/server/node-token' \ | ssh kc-lab-2 'sudo tee /tmp/token >/dev/null' ssh kc-lab-2 "curl -sfL https://get.k3s.io | sudo sh -s - agent \ --server https://192.168.122.11:6443 --token-file /tmp/token \ --node-ip 192.168.122.12; rm -f /tmp/token" ``` 토큰이 셸 히스토리에 남지 않고, 변수를 쓰지 않으니 셸이 달라도 된다. ## 확인하지 못한 것 설치 출력도 `journalctl` 출력도 `final/evidence/` 에 남기지 않았다. `level=fatal msg="Error: --token is required"` 와 `Host key verification failed.` 는 SSOT 본문에서 옮긴 것이고 명령 출력 파일이 근거가 아니다. 토큰 108자도 같다. 이 실패를 일부러 다시 만들어 본 기록도 없어서, 빈 토큰으로 설치하면 설치 출력이 성공으로 끝난다는 것은 한 번의 관측이다. 재현이 쉽지 않은 까닭은 이 부의 검증 방식에 있다 — 만드는 명령은 다시 치면 돌고 있는 실험대가 없어지므로 구축할 때 쓴 것을 옮기고 결과 상태를 확인하는 것으로 대신했다. 재현 없이 남길 수 있는 것은 `journalctl` 쪽이고, 그 출력을 `final/evidence/raw/` 에 남기면 진단 절에 증거가 붙는다. 가드 한 줄이 실제로 빈 토큰을 잡아 본 기록도 없다.