# 02 — k3s 두 노드 ## 이 단계가 끝나면 `kubectl get nodes` 에 두 노드가 `Ready` 로 나온다. ## 전제 [01](../01-vms/) 이 끝나 두 게스트에 SSH 가 붙는다. --- ## 1. server 를 깐다 (kc-lab-1) **하기** ```bash ssh kc-lab-1 curl -sfL https://get.k3s.io | sudo sh -s - server --node-ip 192.168.122.11 ``` `--node-ip` 를 준다. 게스트에 인터페이스가 여럿이면 k3s 가 엉뚱한 것을 고를 수 있고, 그러면 두 노드가 서로를 다른 주소로 알게 된다. **확인** — 서버가 떴고 자기 자신을 노드로 등록했는가 ```bash sudo systemctl is-active k3s sudo kubectl get nodes ``` **어디를 봐야 하는가** — 유닛이 `active` 인가, 그리고 `get nodes` 에 `kc-lab-1` 한 줄이 `Ready` 로 있는가. **설치 직후 30초 남짓은 `NotReady` 이거나 아예 목록이 비어 있는 것이 정상이다** — CNI 가 아직 안 올라온 시간이다. 한 번 더 친다. **이 결과가 의미하는 것** — `active` + `Ready` 면 API 서버가 살아 있고 kubeconfig 도 자리를 잡았다는 뜻이라 2번으로 간다. 유닛이 `active` 인데 `get nodes` 가 접속 오류를 내면 API 서버가 아직 기동 중이다. 유닛이 `activating` 에서 안 넘어가거나 `failed` 면 설치 자체가 실패한 것이니 로그를 본다. ```bash sudo journalctl -u k3s -n 50 --no-pager ``` ## 2. 토큰을 꺼낸다 **하기** — 화면에 찍어 눈으로 옮기지 말고 변수로 받는다 ```bash TOKEN=$(ssh kc-lab-1 'sudo cat /var/lib/rancher/k3s/server/node-token') echo "${#TOKEN} 자" # 값이 아니라 길이만 확인한다 ``` **실측** — 이 실험대에서는 56자였다. **어디를 봐야 하는가** — 찍히는 것은 **자릿수 하나뿐**이다. 값은 보지 않는다 — 화면에 띄우는 순간 터미널 스크롤백과 셸 히스토리에 남는다. **이 결과가 의미하는 것** — 두 자리 수가 나오면 토큰을 손에 쥔 것이니 3번으로 넘어간다. `0` 이면 변수가 비었다는 뜻이고 원인은 둘 중 하나다: server 가 아직 안 떠서 파일이 없거나, `sudo` 가 비대화식 SSH 에서 비밀번호를 물어 실패했거나. 어느 쪽인지는 파일부터 본다. ```bash ssh kc-lab-1 'sudo ls -l /var/lib/rancher/k3s/server/node-token' ``` ## 3. agent 를 붙인다 (kc-lab-2) **하기** — 토큰을 그대로 넘긴다 ```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" ``` > 토큰을 셸 히스토리에 남기고 싶지 않으면 파일로 넘긴다. > ```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" > ``` **확인** — agent 가 클러스터에 들어왔는가, 그리고 **제 주소로** 들어왔는가 ```bash sudo kubectl get nodes -o wide ``` **실측** ``` kc-lab-1 Ready control-plane v1.36.4+k3s1 192.168.122.11 kc-lab-2 Ready v1.36.4+k3s1 192.168.122.12 ``` **어디를 봐야 하는가** — `-o wide` 를 준 이유가 마지막 열이다. **INTERNAL-IP 두 개가 1번·3번에서 `--node-ip` 로 준 값과 같은가.** 그다음이 STATUS 두 줄 `Ready`, 그다음이 ROLES 열이다. `` 은 오류가 아니라 **역할 라벨이 없다**는 뜻이다 — agent 는 원래 그렇다. **이 결과가 의미하는 것** — 두 줄이 `Ready` 이고 IP 가 맞으면 이 단계는 끝났다. IP 가 다른 대역(예: flannel 이나 다른 인터페이스 주소)으로 잡혀 있으면 지금은 아무 증상이 없다가 **03 의 nginx upstream 과 A층의 노드 상실 실험에서** 어긋난다 — 그때 고치는 것보다 지금 재설치가 싸다. `kc-lab-2` 가 아예 안 보이면 join 이 실패한 것이니 agent 쪽 로그를 본다. ```bash ssh kc-lab-2 'sudo journalctl -u k3s-agent -n 30 --no-pager' ``` ## 4. 유닛 이름이 다르다 | 노드 | 유닛 | |---|---| | server | `k3s.service` | | agent | `k3s-agent.service` | **확인** — 어느 노드가 무슨 이름으로, 어떤 인자로 돌고 있는가 ```bash ssh kc-lab-1 'systemctl cat k3s | grep -A3 ExecStart=' ssh kc-lab-2 'systemctl cat k3s-agent | grep -A4 ExecStart=' ``` **실측** ``` 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`)과 그 뒤의 인자**. 설치 스크립트에 준 옵션이 여기 그대로 굳어 있다. 유닛 이름을 틀리면(`systemctl cat k3s` 를 agent 노드에서) `No files found` 가 나오는데, 그것 자체가 「이 노드는 agent 다」라는 답이다. **이 결과가 의미하는 것** — 3번의 `get nodes -o wide` 는 **k3s 가 보고한** IP 이고, 이 줄은 **우리가 준** IP 다. 둘이 다르면 옵션이 안 먹은 것이다. 그리고 뒤의 실험에서 노드를 멈출 때 칠 유닛 이름이 노드마다 다르다는 것을 여기서 확인해 둔다 — `systemctl stop k3s` 를 agent 노드에서 치면 아무 일도 일어나지 않고, 「주입했는데 증상이 없다」로 오독하게 된다. > 이 차이가 A-4 에서 결과를 갈랐다. server 노드를 잃으면 `kubectl` 자체가 > 불통이 되고, agent 를 잃으면 `kubectl` 은 되지만 그 위의 워크로드가 사라진다. ## 5. k3s 가 기본으로 딸려 오는 것 따로 설치하지 않아도 이미 있다. | | 무엇 | |---|---| | Traefik | 인그레스 컨트롤러. `:80` 을 듣는다 | | servicelb (klipper-lb) | LoadBalancer 타입을 호스트 포트로 매핑 | | local-path | 기본 StorageClass. **노드 로컬 디스크** | | flannel | 파드 네트워크 (VXLAN) | | kube-router | NetworkPolicy 집행 | **확인** — 무엇이 이미 돌고 있고, 기본 저장소가 무엇인가 ```bash sudo kubectl get pods -A sudo kubectl get storageclass ``` **어디를 봐야 하는가** — `get pods -A` 에서는 **NAMESPACE 열이 `kube-system` 인 줄들의 STATUS**. `Running` 과 `Completed` 가 섞여 있는 것이 정상이다 — `helm-install-traefik-*` 는 일회성 잡이라 `Completed` 로 남는다. `get storageclass` 에서는 이름 뒤의 **`(default)` 표시**가 어디 붙어 있는가. **이 결과가 의미하는 것** — 여기 뜬 것들은 우리가 안 깔았는데 있는 것이고, 뒤 단계에서 「왜 80 포트가 이미 잡혀 있지」·「왜 PVC 가 이 노드에만 묶이지」의 답이 전부 이 목록에 있다. `local-path` 에 `(default)` 가 붙어 있으면 05 의 PVC 는 StorageClass 를 안 적어도 이것으로 만들어진다. `Pending` 이나 `CrashLoopBackOff` 가 섞여 있으면 그 파드부터 `describe` 로 본다. > `local-path` 가 기본이라는 것이 A-4 에서 비용을 청구한다. PVC 가 **만들어진 > 노드에 묶여** 다른 노드로 재배치되지 않는다. ## 6. 워크스테이션에서 쓰려면 **하기** ```bash ssh kc-lab-1 'sudo cat /etc/rancher/k3s/k3s.yaml' > ~/.kube/kc-lab.yaml sed -i 's|127.0.0.1|192.168.122.11|' ~/.kube/kc-lab.yaml export KUBECONFIG=~/.kube/kc-lab.yaml ``` kubeconfig 안의 서버 주소가 `127.0.0.1` 이다. **게스트 안에서만 맞는 주소**라 밖에서 쓰려면 바꿔야 한다. **확인** — 밖에서 붙는가 ```bash grep server: ~/.kube/kc-lab.yaml kubectl get nodes ``` **어디를 봐야 하는가** — 첫 줄의 `server:` 값이 `https://192.168.122.11:6443` 인가(`127.0.0.1` 이 남아 있으면 `sed` 가 안 먹은 것이다). 그다음 `get nodes` 가 3번과 **같은 두 줄**을 내놓는가. 이제부터는 `sudo` 도 `ssh` 도 붙이지 않는다. **이 결과가 의미하는 것** — 통과하면 워크스테이션에서 곧장 클러스터를 볼 수 있고, 05 이후의 `kubectl` 은 전부 여기서 친다. `connection refused` 면 주소는 맞는데 API 서버가 안 뜬 것이고, 타임아웃이면 워크스테이션에서 `192.168.122.0/24` 로 가는 경로가 없는 것이다(호스트를 거치는 SSH 터널이나 ProxyJump 가 필요하다). `x509` 오류면 파일 안의 CA 와 서버가 지금 쓰는 CA 가 어긋난 것이다 — k3s 를 다시 깔았다면 kubeconfig 도 다시 복사해야 한다. --- ## 막히면 | 증상 | 원인 | 확인 | |---|---|---| | agent 가 `NotReady` | 토큰·주소 오타 | `journalctl -u k3s-agent -n 30` | | 노드 IP 가 예상과 다름 | `--node-ip` 없이 설치 | `kubectl get nodes -o wide` | | 밖에서 kubectl 이 안 붙음 | kubeconfig 의 `127.0.0.1` | 위 6번 | | 파드가 한 노드에만 몰림 | 스케줄러 판단 | `topologySpreadConstraints` 로 강제 |