# 02 — k3s 두 노드 ## 이 단계가 끝나면 lab host 에서 `kubectl get nodes` 를 치면 두 노드가 `Ready` 로 나온다. `sudo` 도 `ssh` 도 붙이지 않는다. ## 전제 [01](../01-vms/) 이 끝나 lab host 에서 두 게스트에 SSH 가 붙는다. ## 어디서 치는가 **이 단계는 전부 `[lab host]` 에서 친다.** 게스트에 로그인하지 않는다. 자세한 이유는 [가이드 공통 규약](../README.md#어느-기계에서-치는가) 에 있고, 요점만 옮기면 이렇다. - 게스트에는 lab host 의 개인키도 `~/.ssh/config` 도 없다. 게스트 안에서 `ssh kc-lab-1` 을 치면 `Host key verification failed` 로 끝난다. - 그 실패를 `TOKEN=$(...)` 로 감싸면 **오류는 화면으로 새고 변수는 빈 채로** 남는다. 셸은 불평하지 않는다. - 그래서 3번의 토큰이 비고, 4번의 agent 설치가 `--token is required` 로 죽는다. 설치 스크립트는 그 전까지를 다 성공으로 찍고 끝나기 때문에 **설치 출력만 보면 성공으로 읽힌다.** 셸을 하나만 쓰면 이 문제가 통째로 없어진다. --- ## 1. server 를 깐다 (kc-lab-1) **하기** — `[lab host]` ```bash ssh kc-lab-1 'curl -sfL https://get.k3s.io | sudo sh -s - server --node-ip 192.168.122.11' ``` 게스트에 들어가지 않고 원격 실행한다. cloud-init 이 `NOPASSWD:ALL` 을 넣어 두었으므로 비대화식 `sudo` 가 멈추지 않는다. `--node-ip` 를 준다. 게스트에 인터페이스가 여럿이면 k3s 가 엉뚱한 것을 고를 수 있고, 그러면 두 노드가 서로를 다른 주소로 알게 된다. **확인** — 서버가 떴고 자기 자신을 노드로 등록했는가 ```bash ssh kc-lab-1 'sudo systemctl is-active k3s; sudo kubectl get nodes' ``` **어디를 봐야 하는가** — 유닛이 `active` 인가, 그리고 `get nodes` 에 `kc-lab-1` 한 줄이 `Ready` 로 있는가. **설치 직후 30초 남짓은 `NotReady` 이거나 아예 목록이 비어 있는 것이 정상이다** — CNI 가 아직 안 올라온 시간이다. 한 번 더 친다. 이 가이드에서 `kubectl` 을 게스트 쪽에서 돌리는 것은 여기 한 번뿐이다. lab host 에는 아직 kubeconfig 가 없기 때문이고, 그것을 두는 것이 바로 2번이다. **이 결과가 의미하는 것** — `active` + `Ready` 면 API 서버가 살아 있고 kubeconfig 도 자리를 잡았다는 뜻이라 2번으로 간다. 유닛이 `active` 인데 `get nodes` 가 접속 오류를 내면 API 서버가 아직 기동 중이다. 유닛이 `activating` 에서 안 넘어가거나 `failed` 면 설치 자체가 실패한 것이니 로그를 본다. ```bash ssh kc-lab-1 'sudo journalctl -u k3s -n 50 --no-pager' ``` ## 2. lab host 에 kubeconfig 를 둔다 **왜 여기서 하나** — 이 뒤로 `kubectl` 을 계속 쓴다. 지금 한 번 해두면 남은 단계에서 `ssh` 도 `sudo` 도 붙이지 않는다. **그리고 agent 노드에는 kubeconfig 가 없으므로**(6번) 클러스터를 볼 자리를 먼저 정해 두는 편이 낫다. **하기** — `[lab host]` ```bash mkdir -p ~/.kube ssh kc-lab-1 'sudo cat /etc/rancher/k3s/k3s.yaml' \ | sed 's|127.0.0.1|192.168.122.11|' > ~/.kube/config chmod 600 ~/.kube/config ``` 세 줄 다 필요하다. | 줄 | 빠뜨리면 | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `mkdir -p ~/.kube` | `>` 는 파일을 열 뿐 경로를 만들지 않는다. `cat` 이 시작되기도 전에 `No such file or directory` 로 끝난다 | | `sed` | k3s 가 쓴 주소는`https://127.0.0.1:6443` 이다. **게스트 안에서만 맞는 주소**라 lab host 에서는 자기 자신의 6443 을 두드리게 된다 | | `chmod 600` | 이 파일은 클러스터 admin 자격증명이다. 비밀번호 파일과 같은 급으로 다룬다 | > `sudo` 는 `cat` 에만 걸리고 `>` 에는 걸리지 않는다. 리다이렉션은 셸이 > **명령보다 먼저** 현재 사용자 권한으로 처리하기 때문이다. 그래서 출력 > 파일은 홈 아래(`~/.kube/config`)에 둔다. **확인** — 주소가 바뀌었고, 밖에서 붙는가 ```bash grep server: ~/.kube/config kubectl get nodes ``` **실측** ``` server: https://192.168.122.11:6443 NAME STATUS ROLES AGE VERSION kc-lab-1 Ready control-plane 47m v1.36.4+k3s1 ``` **어디를 봐야 하는가** — `server:` 값에 `127.0.0.1` 이 남아 있으면 `sed` 가 안 먹은 것이다. 그다음 `get nodes` 가 **`sudo` 없이** 도는가. 아직 노드는 한 줄뿐인 것이 정상이다 — agent 는 4번에서 붙인다. **이 결과가 의미하는 것** — 통과하면 이 뒤의 `kubectl` 은 전부 lab host 에서 친다. `x509` 오류가 나면 파일 안의 CA 와 서버가 지금 쓰는 CA 가 어긋난 것이다 — k3s 를 다시 깔았다면 이 복사도 다시 해야 한다. `connection refused` 면 주소는 맞는데 API 서버가 아직 안 뜬 것이다. > **인증서 SAN 에 두 주소가 다 들어 있어서** 이 `sed` 가 통한다. 확인하려면 > `ssh kc-lab-1 'sudo openssl x509 -in /var/lib/rancher/k3s/server/tls/serving-kube-apiserver.crt -noout -ext subjectAltName'` > 을 친다. `IP Address:127.0.0.1` 과 `IP Address:192.168.122.11` 이 둘 다 > 보인다. 8번의 SSH 터널이 `127.0.0.1` 로 붙을 수 있는 것도 같은 이유다. ## 3. 토큰을 꺼낸다 **하기** — `[lab host]`. 화면에 찍어 눈으로 옮기지 말고 변수로 받는다 ```bash TOKEN=$(ssh kc-lab-1 'sudo cat /var/lib/rancher/k3s/server/node-token') echo "${#TOKEN} 자" # 값이 아니라 길이만 확인한다 ``` **실측** — 이 실험대에서는 108자였다. (`K10<해시>::server:<비밀번호>` 형식이라 k3s 판올림에 따라 자릿수가 달라진다. **중요한 것은 값이 아니라 `0` 이 아니라는 것이다.**) **어디를 봐야 하는가** — 찍히는 것은 **자릿수 하나뿐**이다. 값은 보지 않는다 — 화면에 띄우는 순간 터미널 스크롤백과 셸 히스토리에 남는다. **이 결과가 의미하는 것** — 세 자리 수가 나오면 토큰을 손에 쥔 것이니 4번으로 넘어간다. `0` 이면 변수가 비었다는 뜻이고 원인은 셋 중 하나다. | `0` 인 이유 | 확인 | | ------------------------------------------ | ---------------------------------------------------------------------- | | **게스트 안에서 쳤다** (가장 흔하다) | 프롬프트가`kc-lab-1` 이면 `exit` 로 lab host 로 나온다 | | server 가 아직 안 떠서 파일이 없다 | `ssh kc-lab-1 'sudo ls -l /var/lib/rancher/k3s/server/node-token'` | | 새 셸을 열어 변수가 사라졌다 | `echo "${#TOKEN} 자"` 를 **4번을 칠 바로 그 셸에서** 다시 친다 | > **4번을 3번과 같은 셸에서 친다.** 변수는 셸 밖으로 나가지 않는다. 창을 > 새로 열거나 `ssh` 로 어딘가 들어갔다 나오면 `TOKEN` 은 없다. ## 4. agent 를 붙인다 (kc-lab-2) **하기** — `[lab host]`. 3번과 **같은 셸**에서 친다 ```bash [ ${#TOKEN} -ge 50 ] || echo "TOKEN 이 비었다 — 3번으로 돌아간다" 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" ``` 첫 줄의 가드를 빼지 않는다. `$TOKEN` 이 비면 게스트에는 `--token '' --node-ip ...` 가 전달되고, agent 는 기동 즉시 `level=fatal msg="Error: --token is required"` 로 죽는다. **그런데 설치 스크립트는 내려받기·유닛 생성·`enable` 까지 다 성공으로 찍고 끝나고**, 유닛은 `Restart=always` 라 5초마다 조용히 재시도한다. 가드 한 줄이 그 몇 분을 막는다. > 토큰을 셸 히스토리에 남기고 싶지 않으면 파일로 넘긴다. 이러면 변수를 쓰지 > 않으므로 3번의 「같은 셸」 제약도 없어진다. > > ```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 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 ``` **어디를 봐야 하는가** — `-o wide` 를 준 이유가 마지막 열이다. **INTERNAL-IP 두 개가 1번·4번에서 `--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' ``` ## 5. 유닛 이름이 다르다 | 노드 | 유닛 | | ------ | --------------------- | | 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 다」라는 답이다. **이 결과가 의미하는 것** — 4번의 `get nodes -o wide` 는 **k3s 가 보고한** IP 이고, 이 줄은 **우리가 준** IP 다. 둘이 다르면 옵션이 안 먹은 것이다. 그리고 뒤의 실험에서 노드를 멈출 때 칠 유닛 이름이 노드마다 다르다는 것을 여기서 확인해 둔다 — `systemctl stop k3s` 를 agent 노드에서 치면 아무 일도 일어나지 않고, 「주입했는데 증상이 없다」로 오독하게 된다. > 이 차이가 A-4 에서 결과를 갈랐다. server 노드를 잃으면 `kubectl` 자체가 > 불통이 되고, agent 를 잃으면 `kubectl` 은 되지만 그 위의 워크로드가 사라진다. ## 6. agent 노드에서는 `kubectl` 이 안 된다 `kc-lab-2` 에 들어가 `sudo kubectl get pods -A` 를 치면 이렇게 끝난다. ``` Get "http://localhost:8080/api?timeout=32s": dial tcp [::1]:8080: connect: connection refused ``` **`kubectl` 명령 자체는 있다.** 설치 스크립트가 `/usr/local/bin/kubectl -> k3s` 심볼릭 링크를 만들기 때문이다. 없는 것은 **붙을 곳을 알려 주는 파일**, 곧 kubeconfig 다. | 어디를 찾나 | kc-lab-1 | kc-lab-2 | | ----------------------------- | -------------- | -------------- | | `$KUBECONFIG` | (비어 있음) | (비어 있음) | | `~/.kube/config` | 없음 | 없음 | | `/etc/rancher/k3s/k3s.yaml` | **있음** | **없음** | 넷 다 못 찾으면 kubectl 은 오류를 내지 않고 하드코딩된 기본값 `http://localhost:8080` 으로 넘어간다. 쿠버네티스 1.20 이전 API 서버가 평문으로 열던 레거시 포트인데 지금은 아무도 열지 않는다. > **`localhost:8080` 이 보이면 네트워크 문제가 아니라 「설정을 하나도 못 > 찾았다」는 뜻이다.** 이 주소는 어디에도 적혀 있지 않다. 방화벽이나 k3s 를 > 의심하기 전에 kubeconfig 부터 본다. **워커라서 파드가 안 보이는 것이 아니다.** kubectl 은 그냥 HTTP 클라이언트라 어디서 실행하든 상관없다. `k3s.yaml` 을 kc-lab-2 로 복사해 넣으면 거기서도 전부 보인다. **그래도 복사하지 않는다** — 워커 한 대가 털리면 클러스터 전체가 털리는 구성이 된다. agent 가 가진 자격증명은 급이 다르다. ``` subject=O = system:nodes, CN = system:node:kc-lab-2 ``` 이 신원은 Node authorizer 와 NodeRestriction admission 이 **자기 노드에 배정된 객체만** 다루도록 제한한다. 게다가 그 자격증명은 kubelet 전용 경로 (`/var/lib/rancher/k3s/agent/`)에 있어 kubectl 이 읽지도 않는다. 그래서 실패가 「권한 없음(403)」이 아니라 「설정 없음(`localhost:8080`)」으로 나타난다. **그래서 클러스터는 2번에서 만든 lab host 의 kubeconfig 로 본다.** 개념 설명은 [`session-lab-concepts.md`](../../session-lab-concepts.md) 의 「agent 노드에는 kubeconfig 가 없다」에 있다. ## 7. k3s 가 기본으로 딸려 오는 것 따로 설치하지 않아도 이미 있다. | | 무엇 | | ---------------------- | -------------------------------------------- | | Traefik | 인그레스 컨트롤러.`:80` 을 듣는다 | | servicelb (klipper-lb) | LoadBalancer 타입을 호스트 포트로 매핑 | | local-path | 기본 StorageClass.**노드 로컬 디스크** | | flannel | 파드 네트워크 (VXLAN) | | kube-router | NetworkPolicy 집행 | **확인** — `[lab host]`. 무엇이 이미 돌고 있고, 기본 저장소가 무엇인가 ```bash kubectl get pods -A kubectl get storageclass ``` **실측** ``` 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 ``` **어디를 봐야 하는가** — `get pods -A` 에서는 **NAMESPACE 열이 `kube-system` 인 줄들의 STATUS**. `Running` 과 `Completed` 가 섞여 있는 것이 정상이다 — `helm-install-traefik-*` 는 일회성 잡이라 `Completed` 로 남는다. `svclb-traefik-*` 가 **두 줄**인 것도 봐 둔다. DaemonSet 이라 노드마다 하나씩 뜨는 것이고, 이 두 줄이 4번의 join 이 실제로 먹었다는 또 하나의 증거다. `get storageclass` 에서는 이름 뒤의 **`(default)` 표시**가 어디 붙어 있는가. **이 결과가 의미하는 것** — 여기 뜬 것들은 우리가 안 깔았는데 있는 것이고, 뒤 단계에서 「왜 80 포트가 이미 잡혀 있지」·「왜 PVC 가 이 노드에만 묶이지」의 답이 전부 이 목록에 있다. `local-path` 에 `(default)` 가 붙어 있으면 05 의 PVC 는 StorageClass 를 안 적어도 이것으로 만들어진다. `Pending` 이나 `CrashLoopBackOff` 가 섞여 있으면 그 파드부터 `describe` 로 본다. > `local-path` 가 기본이라는 것이 A-4 에서 비용을 청구한다. PVC 가 **만들어진 > 노드에 묶여** 다른 노드로 재배치되지 않는다. ## 8. 워크스테이션에서 쓰려면 **2번의 kubeconfig 를 그대로 가져와도 안 된다.** 게스트는 libvirt NAT 안에 있어서 워크스테이션에서 `192.168.122.11` 로 가는 경로가 없다. ```bash [워크스테이션] $ ping -c1 192.168.122.11 1 packets transmitted, 0 received, 100% packet loss ``` lab host 를 거치는 터널을 뚫는다. **하기** — `[워크스테이션]` ```bash # 1) 터널. 이 창은 열어 둔다 ssh -N -L 6443:192.168.122.11:6443 test-server # 2) 다른 창에서 — 게스트 원본을 그대로 가져온다. sed 가 필요 없다 mkdir -p ~/.kube ssh test-server "ssh kc-lab-1 'sudo cat /etc/rancher/k3s/k3s.yaml'" > ~/.kube/kc-lab.yaml chmod 600 ~/.kube/kc-lab.yaml export KUBECONFIG=~/.kube/kc-lab.yaml ``` **2번과 달리 `sed` 를 치지 않는다.** k3s 원본이 이미 `https://127.0.0.1:6443` 이고, 터널 덕에 워크스테이션에서는 그 주소가 맞기 때문이다. 2번이 `192.168.122.11` 로 바꿨던 것은 lab host 에서 볼 때 `127.0.0.1` 이 lab host 자신을 가리키기 때문이었다. **같은 파일이라도 어느 기계에서 읽느냐에 따라 맞는 주소가 다르다.** **확인** ```bash grep server: ~/.kube/kc-lab.yaml kubectl get nodes ``` **어디를 봐야 하는가** — `server:` 가 `https://127.0.0.1:6443` 인가. 그다음 `get nodes` 가 4번과 **같은 두 줄**을 내놓는가. **이 결과가 의미하는 것** — 터널 덕에 워크스테이션의 6443 이 `kc-lab-1` 의 6443 이다. API 서버 인증서 SAN 에 `127.0.0.1` 이 들어 있어서(2번의 각주) 인증서 검증도 통과한다. 타임아웃이면 1)의 터널 창이 닫힌 것이고, `connection refused` 면 터널은 살아 있는데 반대편 API 서버가 죽은 것이다. > **터널이 닫히면 `kubectl` 이 통째로 멎는다.** 이 실험대의 A층 실험은 노드를 > 죽이고 살리는 것이 목적이라, 터널 상태와 클러스터 상태가 섞여 오독하기 > 쉽다. **A층 실험은 lab host 에서 치는 것을 권한다.** --- ## 막히면 | 증상 | 원인 | 확인 | | -------------------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------- | | `echo "${#TOKEN} 자"` 가 `0` | 게스트 안에서 쳤거나, 셸이 바뀌었다 | 프롬프트가`kc-lab-1` 이 아닌지. 3번 표 | | agent 설치는 성공했는데 노드가 안 보임 | `--token` 이 빈 문자열 | `ssh kc-lab-2 'sudo journalctl -u k3s-agent -n 30 --no-pager'` 에 `--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 가 없다.**정상이다** | 위 6번 | | 워크스테이션에서 타임아웃 | 터널이 없다 | 위 8번 | | `x509` 오류 | k3s 재설치로 CA 가 바뀌었다 | 2번의 복사를 다시 한다 | | 파드가 한 노드에만 몰림 | 스케줄러 판단 | `topologySpreadConstraints` 로 강제 |