Files
document-haness/docs/virtualization/tech-log-studio/lab-environment-build/setup/setup-install-k3s-server-and-agent.md
T
DongHyeonkaandClaude Opus 5 32e39e20aa fix(setup): 실험대에서 35편을 끝까지 밟고 어긋난 명령·결과 31건을 고친다
test-server 를 비우고 다시 세운 뒤 Setup 기록 35편(virtualization 9 ·
keycloak-session-store 26)을 문서에 적힌 명령 그대로 쳤다. 어긋난 자리를
기록과 SSOT 양쪽에 실측과 함께 넣었다.

막히던 것
- 04 의 인증서 경로가 live/hyeonworks.com 이라 nginx 가 [emerg] 로 안 떴다.
  실제 계보는 live/auth.hyeonworks.com 이고 「문제가 생기면」은 진단이 거꾸로였다
- 인증서가 와일드카드가 아니다. SAN 이 auth·app1·app2 셋뿐이라 그 밖의 이름은
  TLS 에서 끊기고 curl 이 exit 60 · %{http_code} 000 을 낸다. SSOT 안에서
  두 문단이 서로 어긋나 있었다
- A-7 14번 ①이 kc-lab-1 에서 여섯 줄 다 실패하는데 마지막 date 만 「차단」을 찍는다

검사가 실패할 수 없던 자리
- B-1 의 세션 키 고르기는 앞 단계가 $KEY 를 채워 둬서 루프가 한 건도 못 맞혀도
  통과한다. KEY= 로 비우고 키마다 1/0 을 찍게 바꿨다
- k3s-agent 유닛의 sed -i 는 패턴에 $HOME 이 들어 있어 아무 줄도 안 바꾼 채 성공한다

certbot
- renew --dry-run 의 종료 코드는 성공도 0, 실패도 0, 다른 사유의 실패는 1 이다.
  본문의 renew failure(s) 로만 판정할 수 있다
- --dry-run 은 staging 서버를 쓰는데 renewal/*.conf 의 account= 는 운영 계정을
  가리킨다. 실패한 dry-run 이 staging 계정을 하나 더 만들어 다음 실행이 계속 멎는다
- 훅을 755 로 놓고 시뮬레이션이 성공해도 Running deploy-hook command 는 안 나온다.
  certbot 2.1.0 에는 --run-deploy-hooks 도 없다
- 강제 갱신은 실제로 쳤고 서빙까지 닿았다. serial 06F3E0EF…1373 → 065547…3DF1,
  notAfter Dec 3 → Dec 16, nginx worker 2629 4712 → 4745 4754

독자가 칠 수 있는 형태로
- 안 되는 형태가 번호 붙은 단계에 앉아 있던 8곳을 뒤집고, 되는 형태를 ①로 올렸다
- 랩 안에서 공개 이름을 치는 curl 65줄에 --resolve 를 붙였다. 붙인 형태를 실제로
  쳐서 문서가 적은 값과 같은지 확인했다
- 힙독·sed -i·echo >>·&&·|| 를 편집기 + 파일 리스팅 + 분할 형태로 바꿨다
- 닫는 코드펜스가 빠져 뒤 200여 줄의 블록 종류가 뒤집혀 있던 곳을 포함해 3곳을 고쳤다

관문: check_body PASS · check_prose error 0 · check_evidence 두 프로젝트 문제 없음 ·
verify-tech-log-tree error 0 · verify-project-layout error 0 · 코드펜스 전수 0건

남은 것: B-0 주입은 keycloak-pattern 저장소의 소스를 고치고 이미지를 다시 구워야
해서 안 했다(unknown).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 19:38:12 +09:00

32 KiB

id, kind, slug, title, topic, topicName, project, status, studio, pinnedVersions, source, sourceRevision
id kind slug title topic topicName project status studio pinnedVersions source sourceRevision
5e629c2f-e653-4dd9-b1c9-1fe6b2bb181d SETUP install-k3s-server-and-agent k3s server 와 agent 를 게스트 두 대에 깔고 lab host 에서 kubectl 로 본다 lab-environment-build 실험대 환경 구성 virtualization 게시 전 https://hyeonworks.com/studio/documents/5e629c2f-e653-4dd9-b1c9-1fe6b2bb181d/edit
name version
k3s v1.36.4+k3s1
name version
Debian GNU/Linux 12 (bookworm)
final/document.md#188-단계-02-k3s-server-와-agent
final/document.md#185-가이드-묶음이-스스로-정한-규약
final/document.md#184-이-부의-출처와-범위
9465582b5d1630eb4ae7c4e078021486919bf6b6

k3s server 와 agent 를 게스트 두 대에 깔고 lab host 에서 kubectl 로 본다

kc-lab-1 에 k3s server 를 kc-lab-2 에 agent 를 깔고 lab host 에서 kubectl get nodes 로 두 노드를 보는 절차다. 설치 명령은 각각 한 줄이고, kubeconfig 를 가져오는 것과 토큰을 옮기는 것이 그 앞뒤를 채운다.

관계

  • cloud-init 시드로 게스트 세 대를 만들고 SSH 가 키로 붙게 한다 이 절차가 전제하는 앞 단계이고, 거기서 고정한 192.168.122.11 과 192.168.122.12 를 그대로 쓴다.
  • 빈 토큰이 조용히 흘러갔다 — 설치 출력은 성공이었고 agent 만 5초마다 다시 죽었다 토큰 길이를 재는 확인이 막으려는 실패를 그 기록이 처음부터 끝까지 따라간다.
  • 엣지 게스트에 nginx 를 세우고 호스트 DNAT 으로 밖에서 들어오는 길을 연다 다음 단계이고, 여기서 확인한 INTERNAL-IP 두 개가 거기서 nginx upstream 이 된다.
  • 가까운 층부터 한 층씩 건너뛰며 확인한다 — 층마다 성공 신호가 다르다 agent 에서 kubectl 이 거절되는 것을 어느 층의 신호로 읽어야 하는지 그 기준이 정한다.
  • 단계별 구축 문서는 작성 시점이 아니라 실행 순서로 검증한다 설치 명령 두 줄이 왜 재실행으로 검증되지 않은 채 남았는지를 그 기준이 가른다.
  • 가이드대로 다시 쳐서 이 실험대가 같은 상태로 서는가 이 절차를 다시 쳐서 같은 클러스터가 서는지는 아직 재지 않았다.

본문

읽기 전에 — 어디서 치는가

기본은 [lab host] 다. 게스트에 로그인해서 치지 않는다.

까닭이 셋이다. 게스트에는 lab host 의 개인키도 ~/.ssh/config 도 없어서 게스트 안에서 ssh kc-lab-1 을 치면 Host key verification failed. 로 끝난다. 그 실패를 셸 변수 대입으로 감싸면 오류는 화면으로 새고 변수에는 빈 문자열이 담기는데, 셸은 아무 불평도 하지 않는다. 그래서 토큰이 비고, agent 설치가 --token is required 로 죽는데도 설치 스크립트는 그 전까지를 다 성공으로 찍고 끝난다.

셸을 하나만 쓰면 이 문제가 통째로 없어진다. 예외가 둘이다.

번호 무엇 어디서
1 server 설치와 그 확인 [lab host] — 확인 한 번만 게스트 쪽 kubectl 로 돈다
2 · 3 kubeconfig 와 토큰 [lab host]
4 agent 설치 토큰 파일을 옮긴 뒤 [kc-lab-2] 안에서
5 워크스테이션에서 쓰기 [워크스테이션]

편집기를 여는 곳은 둘이다. 2번에서 kubeconfig 의 server: 줄 하나를 고치고, 4번에서 agent 유닛의 토큰 경로 한 줄을 고친다.

이 단계가 세우는 것

가이드 02 의 「이 단계가 끝나면」은 두 줄이다.

lab host 에서 kubectl get nodes 를 치면 두 노드가 Ready 로 나온다. sudossh 도 붙이지 않는다.

무엇 kc-lab-1 kc-lab-2
역할 server (control-plane) agent
유닛 k3s.service k3s-agent.service
--node-ip 192.168.122.11 192.168.122.12
판 번호 v1.36.4+k3s1 v1.36.4+k3s1
kubeconfig /etc/rancher/k3s/k3s.yaml 없다
ROLES 열 control-plane <none> — 라벨이 없다는 뜻이다

k3s 가 따로 설치하지 않아도 딸려 오는 것이 다섯이다. 뒤 단계에서 「왜 80 포트가 이미 잡혀 있지」와 「왜 PVC 가 이 노드에만 묶이지」의 답이 전부 이 목록에 있다.

이름 무엇
Traefik 인그레스 컨트롤러. :80 을 듣는다
servicelb (klipper-lb) LoadBalancer 타입을 호스트 포트로 매핑
local-path 기본 StorageClass. 노드 로컬 디스크
flannel 파드 네트워크 (VXLAN)
kube-router NetworkPolicy 집행

전제와 되돌리기

전제는 한 줄이다 — 앞 단계가 끝나 lab host 에서 두 게스트에 SSH 가 붙는다.

되돌리는 절차는 원본 가이드 02 에 없다(unknown). k3s 설치 스크립트는 k3s-uninstall.shk3s-agent-uninstall.sh 를 함께 깔지만 가이드가 그 이름을 한 번도 적지 않았고 이 실험대도 부른 적이 없다. 가이드가 재설치를 말하는 곳은 두 군데인데 둘 다 되돌리는 명령이 아니라 뒤처리를 적어 두었다.

어디서 무엇을 적었나
노드 IP 가 다른 대역으로 잡혔을 때 그때 고치는 것보다 지금 재설치가 싸다
CA 가 바뀌었을 때 2번의 kubeconfig 복사를 다시 한다

CA(Certificate Authority)는 인증서에 서명해 주는 쪽을 말한다. k3s 를 다시 깔면 그것이 바뀌므로 lab host 의 kubeconfig 도 같이 못 쓰게 된다. 걷어내는 명령은 여기에 적지 않는다.

세우기 전에 먼저 본다

가이드 02 에도 바꾸기 전 상태를 보는 단계가 없다. 앞 단계가 남긴 확인을 그대로 다시 치는 것이 이 단계의 전제다(inferred).

무엇을 확인하는가 — 두 게스트가 돌고 있는지, 그리고 SSH 가 대화 없이 통과하는지.

virsh list --all
ssh donghyeon@192.168.122.11 'hostname; cat /etc/os-release | head -1'

어디를 봐야 하는가 — 두 게스트가 running 인가, 그리고 SSH 가 비밀번호를 묻지 않고 호스트명을 찍는가.

이 결과가 의미하는 것ssh kc-lab-2 가 비밀번호를 물으면 3번의 토큰 전달과 4번의 설치가 전부 대화식으로 멈춘다. 여기서 걸러야 설치 도중에 멈추지 않는다.

실행 절차

1. server 를 깐다

목적kc-lab-1 을 control-plane 으로 세우고, 그 노드가 자기 주소를 192.168.122.11 로 알게 한다.

ssh kc-lab-1 'curl -sfL https://get.k3s.io | sudo sh -s - server --node-ip 192.168.122.11'
ssh kc-lab-1 'sudo systemctl is-active k3s; sudo kubectl get nodes'

예상 결과 — 유닛이 active 이고 get nodeskc-lab-1 한 줄이 Ready 로 있다. 설치 직후 30초 남짓은 NotReady 이거나 아예 목록이 비어 있는 쪽이 정상이다 — 파드 네트워크가 아직 안 올라온 시간이라 한 번 더 친다.

왜 필요한가--node-ip 를 준다. 게스트에 인터페이스가 여럿이면 k3s 가 엉뚱한 것을 고를 수 있고, 그러면 두 노드가 서로를 다른 주소로 알게 된다. 이때 Traefik 과 servicelb, local-path, flannel, kube-router 도 함께 선다. 이 가이드에서 kubectl 을 게스트 쪽에서 돌리는 일은 ② 한 번으로 끝난다. lab host 에는 아직 kubeconfig 가 없기 때문이고, 그것을 두는 일이 바로 2번이다.

문제가 생기면 — 설치 출력만으로 판정하지 않는다. 유닛이 active 인데 get nodes 가 접속 오류를 내면 API 서버가 아직 기동 중이다. 유닛이 activating 에서 안 넘어가거나 failed 면 설치 자체가 실패한 것이니 로그를 본다.

ssh kc-lab-1 'sudo journalctl -u k3s -n 50 --no-pager'

2. kubeconfig 를 lab host 로 가져온다

목적 — lab host 에서 sudossh 도 붙이지 않고 kubectl 을 치게 만든다. agent 노드에는 kubeconfig 가 없으므로 클러스터를 어디서 볼지 먼저 정해 둔다.

mkdir -p ~/.kube
ssh kc-lab-1 'sudo cat /etc/rancher/k3s/k3s.yaml' > ~/.kube/config
chmod 600 ~/.kube/config

이 파일은 클러스터 admin 자격증명이라 600 으로 둔다. sudocat 에만 걸리고 > 에는 걸리지 않는다 — 리다이렉션은 셸이 명령보다 먼저 현재 사용자 권한으로 처리한다. 그래서 출력 파일은 홈 아래에 둔다.

nano ~/.kube/config

server: 줄 하나만 고치고 나머지는 그대로 둔다.

server: https://192.168.122.11:6443
grep server: ~/.kube/config
kubectl get nodes

예상 결과(observed)

    server: https://192.168.122.11:6443
NAME       STATUS   ROLES           AGE   VERSION
kc-lab-1   Ready    control-plane   47m   v1.36.4+k3s1

아직 노드가 한 줄뿐인 쪽이 정상이다. agent 는 4번에서 붙인다.

왜 필요한가 — 세 줄이 다 필요하고 빠뜨렸을 때 깨지는 곳이 다르다.

빠뜨리면
mkdir -p ~/.kube > 는 파일을 열 뿐 경로를 만들지 않는다. cat 이 시작되기도 전에 No such file or directory 로 끝난다
주소 고치기 k3s 가 쓴 https://127.0.0.1:6443 은 게스트 안에서만 맞는 주소라 lab host 에서는 자기 자신의 6443 을 두드린다
chmod 600 이 파일은 클러스터 admin 자격증명이다. 비밀번호 파일과 같은 급으로 다룬다

주소를 고쳐도 인증서 검증이 통과하는 것은 API 서버 인증서 SAN 에 두 주소가 다 들어 있기 때문이다. 5번의 SSH 터널이 127.0.0.1 로 붙을 수 있는 것도 같은 까닭이다.

ssh kc-lab-1 'sudo openssl x509 -in /var/lib/rancher/k3s/server/tls/serving-kube-apiserver.crt -noout -ext subjectAltName'

이 실험대는 ②와 ④를 치환 한 줄로 이어 붙였다(observed).

ssh kc-lab-1 'sudo cat /etc/rancher/k3s/k3s.yaml' \
  | sed 's|127.0.0.1|192.168.122.11|' > ~/.kube/config

치환은 바꾼 줄도 나머지 줄도 보여 주지 않는다. kubeconfig 는 클러스터를 볼 때마다 다시 열게 되는 파일이라 한 번은 전체를 보는 편이 낫고, 나눈 형태는 이 실험대에서 치지 않았다(unknown).

문제가 생기면 — lab host 에서 connection refused 가 나면 ⑤ 의 grep 부터 친다. 127.0.0.1 이 그대로 보이면 ④ 에서 저장이 안 됐다. x509 오류는 파일 안의 CA 와 서버가 지금 쓰는 CA 가 어긋난 것이라, k3s 를 다시 깔았다면 이 복사도 다시 한다.

3. 토큰을 파일로 꺼내 길이만 본다

목적 — agent 가 클러스터에 들어갈 때 쓸 node-token 을 lab host 에 내려놓고, 값이 아니라 길이로 비어 있지 않은지 확인한다.

ssh kc-lab-1 'sudo cat /var/lib/rancher/k3s/server/node-token' > node-token
wc -c node-token

예상 결과 — 세 자리 수와 파일 이름 한 줄. 이 실험대가 변수에 담아 잰 토큰은 108자였다(observed). K10<해시>::server:<비밀번호> 형식이라 k3s 판올림에 따라 자릿수가 달라진다. 확인할 것은 값이 아니라 0 이 아니라는 사실이다.

왜 필요한가 — 값을 화면에 찍지 않는 것은 터미널 스크롤백과 셸 히스토리에 그대로 남기 때문이다. 그리고 이 한 줄이 4번의 조용한 실패를 여기서 끊는다. 게스트 안에서 토큰을 꺼내려 하면 Host key verification failed. 로 끝나는데, 셸은 그것을 오류로 알려 주지 않고 빈 값을 넘긴다.

문제가 생기면wc -c0 을 내면 원인이 셋 가운데 하나다.

0 인 까닭 확인
게스트 안에서 쳤다 — 가장 흔하다 프롬프트가 kc-lab-1 이면 exit 로 lab host 로 나온다
server 가 아직 안 떠서 파일이 없다 아래 한 줄로 파일 유무부터 본다
다른 창에서 쳤다 같은 셸에서 ① 부터 다시 친다
ssh kc-lab-1 'sudo ls -l /var/lib/rancher/k3s/server/node-token'

4. 토큰을 agent 노드로 옮기고 거기서 설치한다

목적kc-lab-2 를 agent 로 붙이고, 토큰이 명령줄과 히스토리에 남지 않게 파일로 넘긴다.

scp node-token kc-lab-2:~/node-token
rm node-token
ssh kc-lab-2
chmod 600 ~/node-token
curl -sfL https://get.k3s.io | sudo sh -s - agent \
  --server https://192.168.122.11:6443 \
  --token-file ~/node-token \
  --node-ip 192.168.122.12
sudo install -m 600 -o root -g root ~/node-token /etc/rancher/node-token
sudo ls -l /etc/rancher/node-token
sudo nano /etc/systemd/system/k3s-agent.service

ExecStart= 아래에서 '--token-file' 다음 줄의 경로를 /etc/rancher/node-token 으로 고친다. 고치고 나면 그 두 줄이 이렇게 보인다.

	'--token-file' \
	'/etc/rancher/node-token' \
sudo systemctl daemon-reload
sudo systemctl restart k3s-agent
rm ~/node-token

:::warning

⑥부터 ⑨ 까지를 건너뛰고 rm ~/node-token 만 치면 이 노드는 다시 못 뜬다. 설치 스크립트가 --token-file 에 준 경로를 유닛의 ExecStart 에 그대로 굽기 때문에, 파일이 사라지면 agent 가 영원히 그 파일을 기다린다. 2026-09-17 에 A-4 를 돌려 노드 전원을 뽑았다 다시 켜면서 그대로 겪었다(observed) — VM 은 떴는데 노드가 NotReady 에서 안 돌아왔다.

:::

k3s[522]: level=info msg="Waiting for file \"/home/donghyeon/node-token\" to be created"
k3s-agent.service: Scheduled restart job, restart counter is at 1.

토큰을 /etc/rancher/node-token 으로 옮기고 유닛을 그쪽으로 돌린 뒤 홈의 사본을 지우자, agent 를 완전히 세웠다 다시 켜도 active 였고 노드도 Ready 로 돌아왔다(observed).

-rw------- 1 root root 109 /etc/rancher/node-token
	'--token-file' \
	'/etc/rancher/node-token' \
exit

예상 결과 — 설치가 끝나면 k3s-agent.service 가 그 노드에 서고, lab host 의 kubectl get nodeskc-lab-2 가 한 줄 더 붙는다.

⑦ 을 편집기로 여는 까닭 — 이 실험대는 sudo sed -i "s|$HOME/node-token|/etc/rancher/node-token|" /etc/systemd/system/k3s-agent.service 로 고쳤다(observed). sed -i 는 패턴이 안 맞아도 조용히 성공하고, 이 패턴에는 $HOME 이 들어 있어 설치할 때와 다른 계정으로 치면 아무 줄도 안 바뀐 채 ⑨ 가 이어서 돈다. 그러면 지금 떠 있는 노드는 멀쩡해 보이고 다음 재부팅에서야 Waiting for file 로 멎는다. 유닛을 열면 고칠 줄이 눈에 보이고, 고쳤는지도 저장하기 전에 눈으로 본다. 이 편집기 형태는 이 실험대에서 치지 않았다(unknown).

왜 필요한가 — 설치 스크립트는 sudo 아래 root 로 도니 홈의 600 파일도 읽는다. --token 대신 --token-file 을 쓰면 토큰이 명령줄에 안 들어가므로 프로세스 목록과 셸 히스토리에 남지 않고, 토큰을 꺼낸 셸과 같은 셸에서 쳐야 한다는 제약도 없어진다.

이 실험대는 두 줄로 했다(observed).

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"

두 줄 안에 원격 셸 둘과 sudo 둘, 파이프 하나, 설치 스크립트 하나, 마지막 삭제 하나가 겹쳐 있다. 실패했을 때 어느 쪽이 실패했는지 갈리지 않아 위에서는 ① 부터 ⑪ 까지로 나눴다. 토큰이 /tmp/token 대신 자기 홈에 놓이고 sudo tee 대신 scpchmod 600 이 그 파일을 만드는 것도 그래서 달라진다. 이 나눈 형태는 이 실험대에서 치지 않았다(unknown).

문제가 생기면 — agent 설치는 성공했는데 노드가 안 보이면 로그에 --token is required 가 있는지 본다. 토큰이 빈 값이었으면 설치 스크립트는 내려받기와 유닛 생성과 활성화까지 다 성공으로 찍고 끝나고, 유닛은 Restart=always 라 5초마다 조용히 재시도한다.

ssh kc-lab-2 'sudo journalctl -u k3s-agent -n 30 --no-pager'

5. 워크스테이션에서도 쓰려면 터널을 뚫는다

목적 — 개발 머신에서 같은 클러스터를 보게 한다. 이 단계를 건너뛰어도 클러스터는 선다.

ssh -N -L 6443:192.168.122.11:6443 test-server
ssh kc-lab-1 'sudo cat /etc/rancher/k3s/k3s.yaml' > kc-lab.yaml
chmod 600 kc-lab.yaml
mkdir -p ~/.kube
scp test-server:kc-lab.yaml ~/.kube/kc-lab.yaml
ssh test-server 'rm kc-lab.yaml'
chmod 600 ~/.kube/kc-lab.yaml
export KUBECONFIG=~/.kube/kc-lab.yaml
grep server: ~/.kube/kc-lab.yaml
kubectl get nodes

예상 결과server:https://127.0.0.1:6443 이고, get nodes 가 4번과 같은 두 줄을 낸다. 터널 창을 닫으면 바로 멎는다.

왜 필요한가 — 여기서는 주소를 고치지 않는다. k3s 원본이 이미 https://127.0.0.1:6443 이고 터널 덕에 워크스테이션에서는 그 주소가 맞다. 2번에서 고쳤던 것은 lab host 에서 볼 때 127.0.0.1 이 lab host 자신을 가리키기 때문이었다. 같은 파일이라도 어느 기계에서 읽느냐에 따라 맞는 주소가 다르다.

이 실험대는 ②와 ③을 ssh 두 겹으로 겹쳐 한 줄에 넣었다(observed).

ssh test-server "ssh kc-lab-1 'sudo cat /etc/rancher/k3s/k3s.yaml'" > ~/.kube/kc-lab.yaml

따옴표가 두 겹이라 어느 기계에서 어느 명령이 도는지가 한 줄에 묻힌다. 기계마다 한 명령이 되게 나눈 형태는 이 실험대에서 치지 않았다(unknown).

문제가 생기면 — 타임아웃이면 ① 의 터널 창이 닫힌 것이고, connection refused 면 터널은 살아 있는데 반대편 API 서버가 죽은 것이다. 터널이 닫히면 kubectl 이 통째로 멎어 터널 상태와 클러스터 상태가 섞인다. 노드를 죽이고 살리는 실험은 lab host 에서 치는 편이 낫다.

구성 값

kubeconfig 사본이 사는 곳은 둘이고, 같은 파일인데 주소가 다르다.

어디에 무엇
lab host ~/.kube/config600. server:https://192.168.122.11:6443 으로 고친 사본
워크스테이션 ~/.kube/kc-lab.yaml — 원본 그대로(https://127.0.0.1:6443) + SSH 터널

유닛 이름도 노드마다 다르다.

노드 유닛
server k3s.service
agent k3s-agent.service

뒤의 실험에서 노드를 멈출 때 칠 이름이 이것이다. systemctl stop k3s 를 agent 노드에서 치면 아무 일도 일어나지 않고, 「주입했는데 증상이 없다」로 읽히게 된다. server 노드를 잃으면 kubectl 자체가 불통이 되고, agent 를 잃으면 kubectl 은 되지만 그 위의 워크로드가 사라진다.

끝났는지 판정한다

확인 ① 두 노드가 우리가 준 주소로 서 있는가

무엇을 확인하는가 — agent 가 클러스터에 들어왔는지, 그리고 제 주소로 들어왔는지.

kubectl get nodes -o wide

실측(observed)

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    <none>          21m   v1.36.4+k3s1   192.168.122.12

어디를 봐야 하는가-o wide 를 주는 까닭이 마지막 열이다. INTERNAL-IP 두 개가 1번과 4번에서 --node-ip 로 준 값과 같은가. 그다음이 STATUS 두 줄 Ready, 그다음이 ROLES 열이다. <none> 은 오류가 아니라 역할 라벨이 없다는 뜻이고, agent 는 원래 그렇다.

이 결과가 의미하는 것 — 두 줄이 Ready 이고 IP 가 맞으면 이 단계는 끝났다. IP 가 다른 대역으로 잡혀 있으면 지금은 아무 증상이 없다가 다음 단계의 nginx upstream 과 노드 상실 실험에서 어긋난다. 그때 고치는 것보다 지금 재설치가 싸다. kc-lab-2 가 아예 안 보이면 join 이 실패한 것이니 agent 로그를 본다.

확인 ② 우리가 준 주소가 유닛에 그렇게 적혔는가

무엇을 확인하는가 — 설치 스크립트에 준 옵션이 유닛 파일에 굳었는지.

ssh kc-lab-1 'systemctl cat k3s | grep -A3 ExecStart='
ssh kc-lab-2 'systemctl cat k3s-agent | grep -A4 ExecStart='

실측(observed)

ExecStart=/usr/local/bin/k3s server '--node-ip' '192.168.122.11'
ExecStart=/usr/local/bin/k3s agent  '--node-ip' '192.168.122.12'

어디를 봐야 하는가ExecStart= 줄의 부분명령(server 인가 agent 인가)과 그 뒤의 인자.

이 결과가 의미하는 것 — 확인 ① 은 k3s 가 보고한 주소이고 이 두 줄은 우리가 준 주소다. 둘이 다르면 옵션이 안 먹었다. 유닛 이름을 틀리면 No files found 가 나오는데, 그것 자체가 「이 노드는 agent 다」라는 답이 된다.

확인 ③ k3s 가 딸려 오게 한 것이 다 떴는가

무엇을 확인하는가 — 우리가 안 깔았는데 이미 돌고 있는 것과 기본 저장소가 무엇인지.

kubectl get pods -A
kubectl get storageclass

실측(observed)

NAMESPACE     NAME                                      READY   STATUS      RESTARTS      AGE
kube-system   coredns-54996dc9b4-x5bsz                  1/1     Running     0             49m
kube-system   helm-install-traefik-cnv8z                0/1     Completed   2 (48m ago)   48m
kube-system   helm-install-traefik-crd-c6vft            0/1     Completed   0             48m
kube-system   local-path-provisioner-77b9867795-5k8d2   1/1     Running     0             49m
kube-system   metrics-server-6dc596dfb8-nzwt2           1/1     Running     0             49m
kube-system   svclb-traefik-a18ee1fc-2s9rl              2/2     Running     0             48m
kube-system   svclb-traefik-a18ee1fc-xmrrt              2/2     Running     0             22m
kube-system   traefik-59b7647586-rsrbc                  1/1     Running     0             48m

NAME                   PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
local-path (default)   rancher.io/local-path   Delete          WaitForFirstConsumer   false                  49m

어디를 봐야 하는가kube-system 줄들의 STATUS 다. RunningCompleted 가 섞여 있는 쪽이 정상이고, 일회성 잡은 Completed 로 남는다. svclb-traefik- 로 시작하는 줄이 둘인 것도 봐 둔다. StorageClass 에서는 이름 뒤의 (default) 표시가 어디 붙어 있는가.

이 결과가 의미하는 것svclb-traefik- 두 줄은 DaemonSet 이 노드마다 하나씩 뜬 것이라, 4번의 join 이 실제로 먹었다는 또 하나의 증거가 된다. local-path(default) 가 붙어 있으면 다음 단계의 PVC 는 StorageClass 를 안 적어도 그것으로 만들어진다. Pending 이나 CrashLoopBackOff 가 섞여 있으면 그 파드부터 describe 로 본다.

확인 ④ agent 노드에서 kubectl 이 거절되는 것은 정상이다

무엇을 확인하는가 — agent 노드에서 kubectl 을 쳤을 때 나오는 거절이 어느 층의 신호인지.

Get "http://localhost:8080/api?timeout=32s": dial tcp [::1]:8080: connect: connection refused

어디를 봐야 하는가 — 주소 한 칸이다. localhost:8080 인가 다른 주소인가.

이 결과가 의미하는 것 — 명령 자체는 있다. 설치 스크립트가 /usr/local/bin/kubectl -> k3s 심볼릭 링크를 만든다. 없는 것은 붙을 곳을 알려 주는 kubeconfig 이고, 넷 다 못 찾으면 kubectl 은 오류를 내지 않고 하드코딩된 기본값으로 넘어간다.

어디를 찾나 kc-lab-1 kc-lab-2
$KUBECONFIG 비어 있음 비어 있음
~/.kube/config 없음 없음
/etc/rancher/k3s/k3s.yaml 있음 없음

http://localhost:8080 은 쿠버네티스 1.20 이전 API 서버가 평문으로 열던 레거시 포트인데 지금은 아무도 열지 않는다(external). 이 주소는 어디에도 적혀 있지 않으므로, 그것이 보이면 네트워크 문제가 아니라 설정을 하나도 못 찾았다는 뜻이다. 방화벽이나 k3s 를 의심하기 전에 kubeconfig 부터 본다.

:::warning

k3s.yaml 을 복사해 넣으면 agent 노드에서도 다 보이지만 복사하지 않는다. 워커 한 대가 털리면 클러스터 전체가 털리는 구성이 된다.

:::

agent 가 원래 가진 신원은 급이 다르다.

subject=O = system:nodes, CN = system:node:kc-lab-2

이 신원은 Node authorizer 와 NodeRestriction admission 이 자기 노드에 배정된 객체만 다루도록 제한하고(external), 그 자격증명은 kubelet 전용 경로(/var/lib/rancher/k3s/agent/)에 있어 kubectl 이 읽지도 않는다. 그래서 실패가 권한 없음이 아니라 설정 없음으로 나타난다.

통과 조건을 한 번에 다시 본다

무엇 명령 통과
두 노드 kubectl get nodes -o wide 둘 다 Ready · INTERNAL-IP 가 .11.12
어디서 치나 위 명령에 sudossh 도 안 붙는다 lab host 에서 그대로 돈다
server 유닛 ssh kc-lab-1 'systemctl cat k3s | grep -A3 ExecStart=' server '--node-ip' '192.168.122.11'
agent 유닛 ssh kc-lab-2 'systemctl cat k3s-agent | grep -A4 ExecStart=' agent '--node-ip' '192.168.122.12'
딸려 온 것 kubectl get pods -A svclb-traefik- 로 시작하는 줄이 둘
기본 저장소 kubectl get storageclass local-path (default)

막히면

증상 원인 확인
wc -c node-token0 게스트 안에서 쳤거나, 셸이 바뀌었다 프롬프트가 kc-lab-1 이 아닌지. 3번 표
agent 설치는 성공했는데 노드가 안 보임 토큰이 빈 값으로 넘어갔다 journalctl -u k3s-agent--token is required
agent 가 NotReady 토큰이나 주소 오타 ssh kc-lab-2 'sudo journalctl -u k3s-agent -n 30 --no-pager'
노드 IP 가 예상과 다름 --node-ip 없이 설치 kubectl get nodes -o wide
lab host 에서 kubectlNo such file mkdir -p ~/.kube 를 빠뜨렸다 2번 ①
lab host 에서 connection refused kubeconfig 의 127.0.0.1 을 안 바꿨다 grep server: ~/.kube/config
kc-lab-2 에서 localhost:8080 agent 에는 kubeconfig 가 없다. 정상이다 확인 ④
워크스테이션에서 타임아웃 터널이 없다 5번 ①
x509 오류 k3s 재설치로 CA 가 바뀌었다 2번의 복사를 다시 한다
파드가 한 노드에만 몰림 스케줄러 판단 topologySpreadConstraints 로 강제

무엇이 관측이고 무엇이 아닌가

  • (observed) v1.36.4+k3s1, 두 노드 Ready, INTERNAL-IP 두 개, 토큰 108자, ExecStart 두 줄, agent 의 localhost:8080 오류와 인증서 subject.
  • (observed) kube-system 에 뜬 파드 여덟 줄과 local-path (default) StorageClass. svclb-traefik- 두 줄이 DaemonSet 이 두 노드에 다 떴다는 증거가 된다.
  • (external) 쿠버네티스 1.20 이전의 평문 8080, Node authorizer 와 NodeRestriction 의 동작은 이 실험대에서 잰 값이 아니다.
  • (unknown) 3번과 4번의 나눈 형태는 이 실험대에서 치지 않았다. 이 실험대가 친 것은 원격 셸 둘을 파이프로 이은 두 줄이고, 나눈 형태로 같은 클러스터가 서는지는 다시 재지 않았다.
  • (unknown) 2번의 ②④ 도 이 실험대에서 치지 않았다. 이 실험대가 친 것은 치환 한 줄이고, 받아 놓고 편집기로 고친 kubeconfig 로 같은 클러스터가 보이는지는 다시 재지 않았다.
  • (unknown) 5번의 ②③도 이 실험대에서 치지 않았다. 이 실험대가 친 것은 원격 셸을 두 겹으로 겹친 한 줄이다.
  • (observed) 인증서 SAN 조회와 두 journalctl 을 2026-09-17 에 쳤다. SAN 에 두 주소가 다 들어 있다127.0.0.1192.168.122.11 이라, kubeconfig 의 주소를 어느 쪽으로 고쳐도 검증이 통과한다.
X509v3 Subject Alternative Name:
    DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc,
    DNS:kubernetes.default.svc.cluster.local, DNS:localhost, DNS:kc-lab-1,
    IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1,
    IP Address:192.168.122.11, IP Address:10.43.0.1, IP Address:192.168.122.11

192.168.122.11 이 두 번 나오는데 중복이고 동작에는 영향이 없다. 10.43.0.1 은 클러스터 안에서 API 서버를 부르는 Service 주소다.

  • (observed) journalctl -u k3s-agent 는 정상일 때도 E 로 시작하는 줄이 가득하다. 이것을 모르면 「막혔을 때 여기를 보라」는 안내대로 열었다가 멀쩡한 노드를 고장으로 읽는다. 같은 순간 kubectl get nodes 는 둘 다 Ready 였다.
E0917 07:42:16.919786 conn.go:353] "Error on socket receive" err="read tcp 127.0.0.1:10250->127.0.0.1:55428: use of closed network connection"

kubectl execlogs 로 연결했다가 끊을 때마다 한 줄씩 남는다. 봐야 할 것은 E 인지가 아니라 문구다 — 토큰이나 주소가 틀렸으면 failed to get CA certs401 Unauthorized 가 나온다.

  • (unknown) 원본 가이드에 되돌리는 절차가 없다. k3s-uninstall.sh 라는 이름이 가이드에 한 번도 안 나오고 이 실험대도 부른 적이 없다.
  • (unknown) 설치 명령 두 줄은 재실행으로 검증되지 않았다. 토큰 108자도 k3s 판올림에 따라 달라진다.