기록 84편을 계약 에이전트로 다시 썼다. 기존 71편(kss 25 · virt 46)과, 계약에만 있고 안 쓰여 있던 새 글감 13편이다. 원장 84개를 열어 단계마다 스킬 영수증과 관문 종료 코드를 적었고 verify-pipeline-run.py 가 error 0 으로 닫는다. SSOT 결함 둘을 고쳤다. - kss 의 `약 58일` 이 반입 중 `약 59일` 로 바뀌어 있었다. 원 증거 파일이 「남은 일수: 88일 … 실제 갱신까지 약 58일」로 산수를 직접 적는다. D-4a 쪽 `약 59일` 은 강제 갱신 뒤(`VALID: 89 days`)라 맞는 값이라 그대로 뒀다. - virt §198 의 `11.6GB` 는 §178 의 원 측정 `Mem: 11648`(MiB)과 어긋나는데 원 가이드의 표기 그대로라 고치지 않고 쓰이는 자리에 대조를 적었다. 기록의 수치 오류 셋을 고쳤다 — CASE 요약의 「게스트 셋에 8240MB」(5120+3120 은 둘이다), k3s 편이 같은 것을 여섯·일곱·여덟로 세던 것, no-docker 편의 「셋을 더 든다」(§281 의 표는 네 행이고 디스크 행이 빠져 있었다). 계약을 셋 고쳤다. - kss 의 sourceRepository 리비전이 cdac9b8 이었는데 그 커밋에는 docs/guides/** 28개가 아예 없다. 9465582b 로 바꾸고, 반입한 바이트가 어느 커밋과도 같지 않다는 것을 측정값과 함께 적었다 — 반입은 커밋이 아니라 그 시점의 작업 트리에서 떠 온 것이다(kss 297/306 · virt 12/14 가 작업 트리와 같고, 200 커밋을 거슬러 전수 대조했을 때 가장 가까운 커밋도 28개가 어긋났다). - virt 계약이 「2026-09-11 재배분」이라고 적는데 SSOT 는 재배분 날짜를 적지 않고 재배분 뒤 값은 이미 2026-09-10 측정에 찍혀 있다. - kss 후보 대장이 지나친 절 아홉에 처분을 적었다(warn 9 → 0). 새 글감은 0건이고 넷은 앵커가 h3 슬러그의 접두가 아니라 중간 토막이라 검사기가 못 본 것이었다. style_profile.mjs 의 결함 둘을 고쳤다 — frontmatter 가 문장으로 세어져 (실측 398자짜리 「문장」 하나) 평균 길이를 기준 안으로 밀어 올리고 있었고, engPerSent 의 분자는 목록을 포함한 글에서, 분모는 목록을 걷어낸 글에서 세고 있었다(Question 기록에서 11.94 → 3.86). verify-pipeline.py 전 항목 PASS · error 0 · unittest 334건 OK. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
500 lines
29 KiB
Markdown
500 lines
29 KiB
Markdown
---
|
|
id: 5e629c2f-e653-4dd9-b1c9-1fe6b2bb181d
|
|
kind: SETUP
|
|
slug: install-k3s-server-and-agent
|
|
title: k3s server 와 agent 를 게스트 두 대에 깔고 lab host 에서 kubectl 로 본다
|
|
topic: lab-environment-build
|
|
topicName: 실험대 환경 구성
|
|
project: virtualization
|
|
status: 게시 전
|
|
studio: "https://hyeonworks.com/studio/documents/5e629c2f-e653-4dd9-b1c9-1fe6b2bb181d/edit"
|
|
pinnedVersions:
|
|
- name: k3s
|
|
version: v1.36.4+k3s1
|
|
- name: Debian GNU/Linux
|
|
version: 12 (bookworm)
|
|
source:
|
|
- final/document.md#188-단계-02-k3s-server-와-agent
|
|
- final/document.md#185-가이드-묶음이-스스로-정한-규약
|
|
- final/document.md#184-이-부의-출처와-범위
|
|
sourceRevision: 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 이 거절되는 것을 어느 층의 신호로 읽어야 하는지 그 기준이 정한다.
|
|
- **단계별 구축 문서는 작성 시점이 아니라 실행 순서로 검증한다**
|
|
설치 명령 두 줄이 왜 재실행으로 검증되지 않은 채 남았는지를 그 기준이 가른다.
|
|
- **가이드대로 다시 쳐서 이 실험대가 같은 상태로 서는가**
|
|
이 절차를 다시 쳐서 같은 클러스터가 서는지는 아직 재지 않았다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
## 읽기 전에 — 어디서 치는가
|
|
|
|
기본은 `[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:` 줄 하나를 고친다.
|
|
|
|
## 이 단계가 세우는 것
|
|
|
|
가이드 02 의 「이 단계가 끝나면」은 두 줄이다.
|
|
|
|
> lab host 에서 `kubectl get nodes` 를 치면 두 노드가 `Ready` 로 나온다.
|
|
> `sudo` 도 `ssh` 도 붙이지 않는다.
|
|
|
|
| 무엇 | `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.sh` 와 `k3s-agent-uninstall.sh` 를 함께 깔지만 가이드가 그 이름을 한 번도 적지 않았고 이 실험대도 부른 적이 없다. 가이드가 재설치를 말하는 곳은 두 군데인데 둘 다 되돌리는 명령이 아니라 뒤처리를 적어 두었다.
|
|
|
|
| 어디서 | 무엇을 적었나 |
|
|
|---|---|
|
|
| 노드 IP 가 다른 대역으로 잡혔을 때 | 그때 고치는 것보다 지금 재설치가 싸다 |
|
|
| CA 가 바뀌었을 때 | 2번의 kubeconfig 복사를 다시 한다 |
|
|
|
|
CA(Certificate Authority)는 인증서에 서명해 주는 쪽을 말한다. k3s 를 다시 깔면 그것이 바뀌므로 lab host 의 kubeconfig 도 같이 못 쓰게 된다. 걷어내는 명령은 여기에 적지 않는다.
|
|
|
|
## 세우기 전에 먼저 본다
|
|
|
|
**가이드 02 에도 바꾸기 전 상태를 보는 단계가 없다.** 앞 단계가 남긴 확인을 그대로 다시 치는 것이 이 단계의 전제다(inferred).
|
|
|
|
**무엇을 확인하는가** — 두 게스트가 돌고 있는지, 그리고 SSH 가 대화 없이 통과하는지.
|
|
|
|
```bash label="[lab host] 도메인 상태와 게스트 접속을 함께 본다"
|
|
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` 로 알게 한다.
|
|
|
|
```bash label="[lab host] ① server 설치 스크립트를 원격으로 돌린다"
|
|
ssh kc-lab-1 'curl -sfL https://get.k3s.io | sudo sh -s - server --node-ip 192.168.122.11'
|
|
```
|
|
|
|
```bash label="[lab host] ② 유닛이 떴고 자기 자신을 노드로 등록했는지 본다"
|
|
ssh kc-lab-1 'sudo systemctl is-active k3s; sudo kubectl get nodes'
|
|
```
|
|
|
|
**예상 결과** — 유닛이 `active` 이고 `get nodes` 에 `kc-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` 면 설치 자체가 실패한 것이니 로그를 본다.
|
|
|
|
```bash label="[lab host] server 설치가 실패했을 때 로그를 본다"
|
|
ssh kc-lab-1 'sudo journalctl -u k3s -n 50 --no-pager'
|
|
```
|
|
|
|
### 2. kubeconfig 를 lab host 로 가져온다
|
|
|
|
**목적** — lab host 에서 `sudo` 도 `ssh` 도 붙이지 않고 `kubectl` 을 치게 만든다. agent 노드에는 kubeconfig 가 없으므로 클러스터를 어디서 볼지 먼저 정해 둔다.
|
|
|
|
```bash label="[lab host] ① 받을 디렉터리를 만든다"
|
|
mkdir -p ~/.kube
|
|
```
|
|
|
|
```bash label="[lab host] ② 게스트의 kubeconfig 를 그대로 받는다"
|
|
ssh kc-lab-1 'sudo cat /etc/rancher/k3s/k3s.yaml' > ~/.kube/config
|
|
```
|
|
|
|
```bash label="[lab host] ③ 권한을 좁힌다"
|
|
chmod 600 ~/.kube/config
|
|
```
|
|
|
|
이 파일은 클러스터 admin 자격증명이라 `600` 으로 둔다. `sudo` 는 `cat` 에만 걸리고 `>` 에는 걸리지 않는다 — 리다이렉션은 셸이 명령보다 먼저 현재 사용자 권한으로 처리한다. 그래서 출력 파일은 홈 아래에 둔다.
|
|
|
|
```bash label="[lab host] ④ server 주소 한 줄을 고친다"
|
|
nano ~/.kube/config
|
|
```
|
|
|
|
`server:` 줄 하나만 고치고 나머지는 그대로 둔다.
|
|
|
|
```yaml label="고칠 줄"
|
|
server: https://192.168.122.11:6443
|
|
```
|
|
|
|
```bash label="[lab host] ⑤ 주소가 바뀌었는지 읽고 밖에서 붙어 본다"
|
|
grep server: ~/.kube/config
|
|
kubectl get nodes
|
|
```
|
|
|
|
**예상 결과**(observed)
|
|
|
|
```text
|
|
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` 로 붙을 수 있는 것도 같은 까닭이다.
|
|
|
|
```bash label="[lab host] SAN 에 두 주소가 들어 있는지 확인한다"
|
|
ssh kc-lab-1 'sudo openssl x509 -in /var/lib/rancher/k3s/server/tls/serving-kube-apiserver.crt -noout -ext subjectAltName'
|
|
```
|
|
|
|
이 실험대는 ②와 ④를 치환 한 줄로 이어 붙였다(observed).
|
|
|
|
```bash label="[lab host] 이 실험대가 실제로 친 형태"
|
|
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 에 내려놓고, 값이 아니라 길이로 비어 있지 않은지 확인한다.
|
|
|
|
```bash label="[lab host] ① 토큰을 파일로 내려놓는다"
|
|
ssh kc-lab-1 'sudo cat /var/lib/rancher/k3s/server/node-token' > node-token
|
|
```
|
|
|
|
```bash label="[lab host] ② 값이 아니라 길이만 본다"
|
|
wc -c node-token
|
|
```
|
|
|
|
**예상 결과** — 세 자리 수와 파일 이름 한 줄. 이 실험대가 변수에 담아 잰 토큰은 108자였다(observed). `K10<해시>::server:<비밀번호>` 형식이라 k3s 판올림에 따라 자릿수가 달라진다. 확인할 것은 값이 아니라 `0` 이 아니라는 사실이다.
|
|
|
|
**왜 필요한가** — 값을 화면에 찍지 않는 것은 터미널 스크롤백과 셸 히스토리에 그대로 남기 때문이다. 그리고 이 한 줄이 4번의 조용한 실패를 여기서 끊는다. 게스트 안에서 토큰을 꺼내려 하면 `Host key verification failed.` 로 끝나는데, 셸은 그것을 오류로 알려 주지 않고 빈 값을 넘긴다.
|
|
|
|
**문제가 생기면** — `wc -c` 가 `0` 을 내면 원인이 셋 가운데 하나다.
|
|
|
|
| `0` 인 까닭 | 확인 |
|
|
|---|---|
|
|
| 게스트 안에서 쳤다 — 가장 흔하다 | 프롬프트가 `kc-lab-1` 이면 `exit` 로 lab host 로 나온다 |
|
|
| server 가 아직 안 떠서 파일이 없다 | 아래 한 줄로 파일 유무부터 본다 |
|
|
| 다른 창에서 쳤다 | 같은 셸에서 ① 부터 다시 친다 |
|
|
|
|
```bash label="[lab host] 토큰 파일이 있기는 한지 본다"
|
|
ssh kc-lab-1 'sudo ls -l /var/lib/rancher/k3s/server/node-token'
|
|
```
|
|
|
|
### 4. 토큰을 agent 노드로 옮기고 거기서 설치한다
|
|
|
|
**목적** — `kc-lab-2` 를 agent 로 붙이고, 토큰이 명령줄과 히스토리에 남지 않게 파일로 넘긴다.
|
|
|
|
```bash label="[lab host] ① 토큰 파일을 agent 노드로 옮긴다"
|
|
scp node-token kc-lab-2:~/node-token
|
|
```
|
|
|
|
```bash label="[lab host] ② lab host 쪽 사본을 지운다"
|
|
rm node-token
|
|
```
|
|
|
|
```bash label="[lab host] ③ agent 노드에 들어간다"
|
|
ssh kc-lab-2
|
|
```
|
|
|
|
```bash label="[kc-lab-2] ④ 권한을 좁힌다"
|
|
chmod 600 ~/node-token
|
|
```
|
|
|
|
```bash label="[kc-lab-2] ⑤ agent 를 깐다"
|
|
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
|
|
```
|
|
|
|
```bash label="[kc-lab-2] ⑥ 설치가 끝나면 토큰 파일을 지운다"
|
|
rm ~/node-token
|
|
```
|
|
|
|
```bash label="[kc-lab-2] ⑦ lab host 로 나온다"
|
|
exit
|
|
```
|
|
|
|
**예상 결과** — 설치가 끝나면 `k3s-agent.service` 가 그 노드에 서고, lab host 의 `kubectl get nodes` 에 `kc-lab-2` 가 한 줄 더 붙는다.
|
|
|
|
**왜 필요한가** — 설치 스크립트는 `sudo` 아래 root 로 도니 홈의 `600` 파일도 읽는다. `--token` 대신 `--token-file` 을 쓰면 토큰이 명령줄에 안 들어가므로 프로세스 목록과 셸 히스토리에 남지 않고, 토큰을 꺼낸 셸과 같은 셸에서 쳐야 한다는 제약도 없어진다.
|
|
|
|
이 실험대는 두 줄로 했다(observed).
|
|
|
|
```bash label="[lab host] 이 실험대가 실제로 친 형태"
|
|
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` 대신 `scp` 와 `chmod 600` 이 그 파일을 만드는 것도 그래서 달라진다. 이 나눈 형태는 이 실험대에서 치지 않았다(unknown).
|
|
|
|
**문제가 생기면** — agent 설치는 성공했는데 노드가 안 보이면 로그에 `--token is required` 가 있는지 본다. 토큰이 빈 값이었으면 설치 스크립트는 내려받기와 유닛 생성과 활성화까지 다 성공으로 찍고 끝나고, 유닛은 `Restart=always` 라 5초마다 조용히 재시도한다.
|
|
|
|
```bash label="[lab host] agent 가 왜 못 붙었는지 본다"
|
|
ssh kc-lab-2 'sudo journalctl -u k3s-agent -n 30 --no-pager'
|
|
```
|
|
|
|
### 5. 워크스테이션에서도 쓰려면 터널을 뚫는다
|
|
|
|
**목적** — 개발 머신에서 같은 클러스터를 보게 한다. 이 단계를 건너뛰어도 클러스터는 선다.
|
|
|
|
```bash label="[워크스테이션] ① 터널을 연다. 이 창은 열어 둔다"
|
|
ssh -N -L 6443:192.168.122.11:6443 test-server
|
|
```
|
|
|
|
```bash label="[lab host] ② 게스트 원본을 lab host 로 받는다"
|
|
ssh kc-lab-1 'sudo cat /etc/rancher/k3s/k3s.yaml' > kc-lab.yaml
|
|
chmod 600 kc-lab.yaml
|
|
```
|
|
|
|
```bash label="[워크스테이션] ③ 다른 창에서 받아 오고 lab host 의 사본은 지운다"
|
|
mkdir -p ~/.kube
|
|
scp test-server:kc-lab.yaml ~/.kube/kc-lab.yaml
|
|
ssh test-server 'rm kc-lab.yaml'
|
|
```
|
|
|
|
```bash label="[워크스테이션] ④ 권한을 좁히고 이 파일을 쓰게 한다"
|
|
chmod 600 ~/.kube/kc-lab.yaml
|
|
export KUBECONFIG=~/.kube/kc-lab.yaml
|
|
```
|
|
|
|
```bash label="[워크스테이션] ⑤ 주소를 읽고 두 노드가 보이는지 본다"
|
|
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).
|
|
|
|
```bash label="[워크스테이션] 이 실험대가 실제로 친 형태"
|
|
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/config` — `600`. `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 가 클러스터에 들어왔는지, 그리고 제 주소로 들어왔는지.
|
|
|
|
```bash label="[lab host] 마지막 열까지 본다"
|
|
kubectl get nodes -o wide
|
|
```
|
|
|
|
**실측**(observed)
|
|
|
|
```text
|
|
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 로그를 본다.
|
|
|
|
### 확인 ② 우리가 준 주소가 유닛에 그렇게 적혔는가
|
|
|
|
**무엇을 확인하는가** — 설치 스크립트에 준 옵션이 유닛 파일에 굳었는지.
|
|
|
|
```bash label="[lab host] 유닛 파일의 ExecStart 를 본다"
|
|
ssh kc-lab-1 'systemctl cat k3s | grep -A3 ExecStart='
|
|
ssh kc-lab-2 'systemctl cat k3s-agent | grep -A4 ExecStart='
|
|
```
|
|
|
|
**실측**(observed)
|
|
|
|
```text
|
|
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 가 딸려 오게 한 것이 다 떴는가
|
|
|
|
**무엇을 확인하는가** — 우리가 안 깔았는데 이미 돌고 있는 것과 기본 저장소가 무엇인지.
|
|
|
|
```bash label="[lab host] 전 네임스페이스의 파드와 StorageClass 를 본다"
|
|
kubectl get pods -A
|
|
kubectl get storageclass
|
|
```
|
|
|
|
**실측**(observed)
|
|
|
|
```text
|
|
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 다. `Running` 과 `Completed` 가 섞여 있는 쪽이 정상이고, 일회성 잡은 `Completed` 로 남는다. `svclb-traefik-` 로 시작하는 줄이 둘인 것도 봐 둔다. StorageClass 에서는 이름 뒤의 `(default)` 표시가 어디 붙어 있는가.
|
|
|
|
**이 결과가 의미하는 것** — `svclb-traefik-` 두 줄은 DaemonSet 이 노드마다 하나씩 뜬 것이라, 4번의 join 이 실제로 먹었다는 또 하나의 증거가 된다. `local-path` 에 `(default)` 가 붙어 있으면 다음 단계의 PVC 는 StorageClass 를 안 적어도 그것으로 만들어진다. `Pending` 이나 `CrashLoopBackOff` 가 섞여 있으면 그 파드부터 `describe` 로 본다.
|
|
|
|
### 확인 ④ agent 노드에서 kubectl 이 거절되는 것은 정상이다
|
|
|
|
**무엇을 확인하는가** — agent 노드에서 `kubectl` 을 쳤을 때 나오는 거절이 어느 층의 신호인지.
|
|
|
|
```text
|
|
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 가 원래 가진 신원은 급이 다르다.
|
|
|
|
```text
|
|
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` |
|
|
| 어디서 치나 | 위 명령에 `sudo` 도 `ssh` 도 안 붙는다 | 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-token` 이 `0` | 게스트 안에서 쳤거나, 셸이 바뀌었다 | 프롬프트가 `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 에서 `kubectl` 이 `No 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번의 ②③도 이 실험대에서 치지 않았다. 이 실험대가 친 것은 원격 셸을 두 겹으로 겹친 한 줄이다.
|
|
- (unknown) 인증서 SAN 조회와 두 `journalctl` 은 가이드가 적어 둔 명령이고 이 실험대가 캡처한 출력이 없다. SAN 에 두 주소가 들어 있다는 것은 주소 치환과 터널이 둘 다 통한다는 사실로 뒷받침된다(inferred).
|
|
- (unknown) 원본 가이드에 되돌리는 절차가 없다. `k3s-uninstall.sh` 라는 이름이 가이드에 한 번도 안 나오고 이 실험대도 부른 적이 없다.
|
|
- (unknown) 설치 명령 두 줄은 재실행으로 검증되지 않았다. 토큰 108자도 k3s 판올림에 따라 달라진다.
|
|
|
|
<!-- body:end -->
|