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

21 KiB

02 — k3s 두 노드

이 단계가 끝나면

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

전제

01 이 끝나 lab host 에서 두 게스트에 SSH 가 붙는다.

어디서 치는가

이 단계는 전부 [lab host] 에서 친다. 게스트에 로그인하지 않는다. 자세한 이유는 가이드 공통 규약 에 있고, 요점만 옮기면 이렇다.

  • 게스트에는 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]

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 가 엉뚱한 것을 고를 수 있고, 그러면 두 노드가 서로를 다른 주소로 알게 된다.

확인 — 서버가 떴고 자기 자신을 노드로 등록했는가

ssh kc-lab-1 'sudo systemctl is-active k3s; sudo kubectl get nodes'

어디를 봐야 하는가 — 유닛이 active 인가, 그리고 get nodeskc-lab-1 한 줄이 Ready 로 있는가. 설치 직후 30초 남짓은 NotReady 이거나 아예 목록이 비어 있는 것이 정상이다 — CNI 가 아직 안 올라온 시간이다. 한 번 더 친다.

이 가이드에서 kubectl 을 게스트 쪽에서 돌리는 것은 여기 한 번뿐이다. lab host 에는 아직 kubeconfig 가 없기 때문이고, 그것을 두는 것이 바로 2번이다.

이 결과가 의미하는 것active + Ready 면 API 서버가 살아 있고 kubeconfig 도 자리를 잡았다는 뜻이라 2번으로 간다. 유닛이 active 인데 get nodes 가 접속 오류를 내면 API 서버가 아직 기동 중이다. 유닛이 activating 에서 안 넘어가거나 failed 면 설치 자체가 실패한 것이니 로그를 본다.

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

2. lab host 에 kubeconfig 를 둔다

왜 여기서 하나 — 이 뒤로 kubectl 을 계속 쓴다. 지금 한 번 해두면 남은 단계에서 sshsudo 도 붙이지 않는다. 그리고 agent 노드에는 kubeconfig 가 없으므로(6번) 클러스터를 볼 자리를 먼저 정해 두는 편이 낫다.

하기[lab host]

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 자격증명이다. 비밀번호 파일과 같은 급으로 다룬다

sudocat 에만 걸리고 > 에는 걸리지 않는다. 리다이렉션은 셸이 명령보다 먼저 현재 사용자 권한으로 처리하기 때문이다. 그래서 출력 파일은 홈 아래(~/.kube/config)에 둔다.

확인 — 주소가 바뀌었고, 밖에서 붙는가

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 nodessudo 없이 도는가. 아직 노드는 한 줄뿐인 것이 정상이다 — 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.1IP Address:192.168.122.11 이 둘 다 보인다. 8번의 SSH 터널이 127.0.0.1 로 붙을 수 있는 것도 같은 이유다.

3. 토큰을 꺼낸다

하기[lab host]. 화면에 찍어 눈으로 옮기지 말고 변수로 받는다

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번과 같은 셸에서 친다

[ ${#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번의 「같은 셸」 제약도 없어진다.

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 가 클러스터에 들어왔는가, 그리고 제 주소로 들어왔는가

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 쪽 로그를 본다.

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

5. 유닛 이름이 다르다

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

확인 — 어느 노드가 무슨 이름으로, 어떤 인자로 돌고 있는가

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 widek3s 가 보고한 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 의 「agent 노드에는 kubeconfig 가 없다」에 있다.

7. k3s 가 기본으로 딸려 오는 것

따로 설치하지 않아도 이미 있다.

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

확인[lab host]. 무엇이 이미 돌고 있고, 기본 저장소가 무엇인가

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. RunningCompleted 가 섞여 있는 것이 정상이다 — 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 로 가는 경로가 없다.

[워크스테이션] $ ping -c1 192.168.122.11
1 packets transmitted, 0 received, 100% packet loss

lab host 를 거치는 터널을 뚫는다.

하기[워크스테이션]

# 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 자신을 가리키기 때문이었다. 같은 파일이라도 어느 기계에서 읽느냐에 따라 맞는 주소가 다르다.

확인

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 에서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 가 없다.정상이다 위 6번
워크스테이션에서 타임아웃 터널이 없다 위 8번
x509 오류 k3s 재설치로 CA 가 바뀌었다 2번의 복사를 다시 한다
파드가 한 노드에만 몰림 스케줄러 판단 topologySpreadConstraints 로 강제