Files
keycloak-pattern/docs/guides/02-k3s/README.md

408 lines
21 KiB
Markdown

# 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 <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 가 다른 대역(예: 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` 로 강제 |