18강 · 아키텍처 (컨트롤플레인)¶
영상: 18강 컨트롤플레인 안을 열어 보기 (구조 개요는 1강) · 이어서 풀 채점 세트:
18-architecture
실습 목표¶
- 컨트롤플레인 구성 요소가 어디에서 어떻게 떠 있는지 직접 찾는다.
- 스태틱 파드가 일반 파드와 무엇이 다른지 확인하고, 워커 노드에 스태틱 파드를 직접 하나 띄워 본다.
kubectl한 줄이 API 서버에서 인증 → 인가 → 어드미션 → etcd 를 거치는 과정을-v=6과--raw로 들여다본다.- 스케줄러·컨트롤러 매니저를 잠깐 멈춰서 각 구성 요소가 맡은 일을 눈으로 확인한다.
- kubectl 이 안 될 때도 쓸 수 있는 구성 요소 상태 점검 방법(
/readyz,crictl,journalctl)을 익힌다. - 원하는 상태와 실제 상태를 맞추는 선언적 모델을 확인한다.
0) 준비¶
이 페이지의 2) · 4) · 5) 는 컨트롤플레인 노드의 파일을 봅니다. 컨트롤플레인 노드(control-plane, 192.168.56.10)에 접속한 상태에서 진행하세요.
2) 의 마지막 실습은 worker-1 에도 잠깐 접속합니다.
$do 가 있는지 먼저 확인
export do="--dry-run=client -o yaml" 을 한 번 실행해 둡니다. 시험장에서도 첫 문제 전에 만들어 두면 좋습니다.
1) 컨트롤플레인은 어디에 떠 있나¶
왜 — 트러블슈팅은 "어느 부품이 멈췄나" 를 찾는 일입니다. 부품이 어디에, 어떤 형태로 떠 있는지 모르면 찾을 수가 없습니다.
NAME READY STATUS NODE
cilium-4xq7b 1/1 Running worker-1
cilium-operator-5c7d9f8b6-2kq8m 1/1 Running worker-2
cilium-r8w2p 1/1 Running control-plane
cilium-z9m3k 1/1 Running worker-2
coredns-668d6bf9bc-7xk2p 1/1 Running worker-1
coredns-668d6bf9bc-q4n8d 1/1 Running worker-2
etcd-control-plane 1/1 Running control-plane
kube-apiserver-control-plane 1/1 Running control-plane
kube-controller-manager-control-plane 1/1 Running control-plane
kube-proxy-4hq9z 1/1 Running control-plane
kube-proxy-m2n8c 1/1 Running worker-1
kube-proxy-t6v1s 1/1 Running worker-2
kube-scheduler-control-plane 1/1 Running control-plane
이름을 두 부류로 나눠 봅니다.
| 이름 모양 | 예 | 누가 만들었나 |
|---|---|---|
<구성요소>-<노드이름> |
etcd-control-plane |
노드의 kubelet 이 파일을 보고 직접 띄운 스태틱 파드 |
<이름>-<무작위 해시> |
kube-proxy-4hq9z, coredns-… |
DaemonSet · Deployment 같은 컨트롤러 |
이름 끝에 노드 이름이 붙은 네 개가 컨트롤플레인입니다. 어떤 리소스가 소유하는지도 확인할 수 있습니다.
kubectl get pod kube-scheduler-control-plane -n kube-system -o jsonpath='{.metadata.ownerReferences[0].kind}{"\n"}'
kubectl get pod -n kube-system -l k8s-app=kube-proxy -o jsonpath='{.items[0].metadata.ownerReferences[0].kind}{"\n"}'
한 번에 전부 보고 싶으면 custom-columns 로 원하는 칸만 뽑습니다. 시험에서 "어떤 값만 뽑아 적어라" 는 문제에 그대로 쓰는 기술입니다.
kubectl get pods -n kube-system \
-o custom-columns='NAME:.metadata.name,NODE:.spec.nodeName,OWNER:.metadata.ownerReferences[0].kind'
NAME NODE OWNER
cilium-4xq7b worker-1 DaemonSet
cilium-operator-5c7d9f8b6-2kq8m worker-2 ReplicaSet
coredns-668d6bf9bc-7xk2p worker-1 ReplicaSet
etcd-control-plane control-plane Node
kube-apiserver-control-plane control-plane Node
...
관찰 포인트 — OWNER 가 Node 인 것은 컨트롤러가 없는 스태틱 파드, ReplicaSet 은 Deployment 가, DaemonSet 은 노드마다 하나씩 띄운 것입니다.
| 부품 | 어디에 | 형태 | 죽으면 |
|---|---|---|---|
| kube-apiserver | control-plane | 스태틱 파드 | kubectl 전부 실패 (돌던 파드는 계속 돈다) |
| etcd | control-plane | 스태틱 파드 | API 서버도 사실상 마비 |
| kube-scheduler | control-plane | 스태틱 파드 | 새 파드가 Pending |
| kube-controller-manager | control-plane | 스태틱 파드 | 자동 복구 · 스케일 · 롤아웃이 멈춤 |
| kubelet | 모든 노드 | systemd 서비스 | 그 노드가 NotReady |
| containerd | 모든 노드 | systemd 서비스 | 그 노드에서 컨테이너 실행 불가 |
| kube-proxy | 모든 노드 | DaemonSet | 그 노드에서 Service 통신 실패 |
| CoreDNS | 아무 노드 | Deployment | 이름으로 Service 를 못 찾음 |
kube-proxy 가 안 보인다면
Cilium 을 kube-proxy 대체 모드로 설치한 클러스터에서는 kube-proxy DaemonSet 이 아예 없습니다.
kubectl get ds -n kube-system 으로 어느 구성인지 확인하세요. 이 페이지의 7) 에서 다시 봅니다.
2) 스태틱 파드 — kubelet 이 파일을 보고 띄운다¶
왜 — API 서버도 컨테이너입니다. 그런데 API 서버가 없는데 누가 API 서버를 띄울까요? kubelet 이 로컬 디렉터리의 파일만 보고 직접 띄웁니다. 그래서 컨트롤플레인 설정을 바꾸는 일은 곧 파일을 고치는 일입니다.
kubelet 은 정해진 디렉터리를 계속 지켜봅니다. 그 디렉터리가 어디인지는 kubelet 설정에 있습니다.
staticPodPath: /etc/kubernetes/manifests
etcd.yaml kube-apiserver.yaml kube-controller-manager.yaml kube-scheduler.yaml
이 파일 네 개가 컨트롤플레인 전부입니다. API 서버가 어떤 옵션으로 떠 있는지도 여기서 읽습니다. 예를 들어 서비스 IP 대역은:
같은 옵션을 kubectl 로도 읽을 수 있습니다. 파일에 접근할 수 없을 때 쓰는 두 번째 길입니다.
스태틱 파드에는 component=<이름> 레이블이 붙어 있어서 이름을 몰라도 고를 수 있습니다.
kubectl get pods -n kube-system -l tier=control-plane --show-labels
kubectl -n kube-system get pod -l component=kube-apiserver -o yaml | grep -- --secure-port
NAME READY STATUS LABELS
etcd-control-plane 1/1 Running component=etcd,tier=control-plane
kube-apiserver-control-plane 1/1 Running component=kube-apiserver,tier=control-plane
kube-controller-manager-control-plane 1/1 Running component=kube-controller-manager,tier=control-plane
kube-scheduler-control-plane 1/1 Running component=kube-scheduler,tier=control-plane
- --secure-port=6443
파일도 kubectl 도 못 쓸 때
같은 옵션이 실행 중인 프로세스 인자에도 있습니다.
거울(mirror) 파드의 표시¶
API 서버에 보이는 스태틱 파드는 kubelet 이 "이런 파드가 돌고 있다" 고 보고용으로 만든 거울입니다. 어노테이션에 표시가 있습니다.
kubectl get pod kube-scheduler-control-plane -n kube-system \
-o jsonpath='{.metadata.annotations}' | tr ',' '\n'
{"kubernetes.io/config.hash":"3f1c9e...",
"kubernetes.io/config.mirror":"3f1c9e...", ← 거울 파드라는 표시
"kubernetes.io/config.seen":"2026-09-29T05:40:12.345678901Z",
"kubernetes.io/config.source":"file"} ← 파일에서 왔다
스태틱 파드는 kubectl 로 지워지지 않는다¶
kubectl delete pod kube-scheduler-control-plane -n kube-system
kubectl get pod kube-scheduler-control-plane -n kube-system
지웠는데 곧바로 다시 보입니다. API 서버에 보이는 것은 거울 파드일 뿐이고, 진짜 컨테이너는 kubelet 이 파일을 기준으로 관리하기 때문입니다. 스태틱 파드를 없애려면 파일을 치워야 합니다.
직접 만들어 보기 — worker-1 에 스태틱 파드 하나¶
스태틱 파드는 컨트롤플레인 전용이 아닙니다. 어느 노드든 kubelet 의 감시 디렉터리에 파일을 두면 뜹니다.
먼저 control-plane 에서 매니페스트 뼈대를 뽑습니다.
kubectl run static-demo -n guide-arch --image=nginx:1.27 $do > static-demo.yaml
cat static-demo.yaml
apiVersion: v1
kind: Pod
metadata:
labels:
run: static-demo
name: static-demo # ← API 서버에는 static-demo-<노드이름> 으로 보인다
namespace: guide-arch # ← 이 네임스페이스가 있어야 거울 파드가 생긴다
spec:
containers:
- image: nginx:1.27
name: static-demo
resources: {}
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
-n 을 줬으므로 namespace: 칸이 들어 있습니다. 혹시 빠져 있으면 직접 추가하세요.
worker-1 에 접속해서(ssh worker-1 — control-plane 에서, 또는 호스트 PC 에서 ssh vboxuser@192.168.56.11) 같은 내용을 감시 디렉터리에 둡니다.
# worker-1 에서
sudo grep staticPodPath /var/lib/kubelet/config.yaml # 워커도 경로가 같은지 먼저 확인
sudo vi /etc/kubernetes/manifests/static-demo.yaml # 위 YAML 을 붙여 넣고 저장
control-plane 으로 돌아와 확인합니다.
관찰 포인트 — 스케줄러를 거치지 않았습니다. 파일이 놓인 노드가 곧 실행 노드입니다.
kubectl delete pod static-demo-worker-1 -n guide-arch 를 해 봐도 다시 생깁니다. worker-1 에서 파일을 지워야 사라집니다.
시험 팁
"노드 X 에 스태틱 파드를 만들라" 는 문제는 그 노드에 접속해서 그 노드 kubelet 의 staticPodPath 에 파일을 두는 문제입니다.
경로는 노드마다 다를 수 있으니 반드시 /var/lib/kubelet/config.yaml 에서 확인하세요. 파일을 둔 뒤 kubelet 재시작은 필요 없습니다.
3) kubectl 한 줄의 여정 — 인증 · 인가 · 어드미션¶
왜 — kubectl 은 REST 클라이언트일 뿐입니다. 모든 요청은 API 서버의 HTTPS 엔드포인트로 가고, 거기서 네 관문을 통과해야 etcd 에 저장됩니다.
에러 메시지를 보고 어느 관문에서 막혔는지 알면 원인이 바로 좁혀집니다.
sequenceDiagram
participant U as kubectl
participant A as kube-apiserver
participant E as etcd
participant C as controller-manager
participant S as kube-scheduler
participant K as kubelet (worker)
U->>A: HTTPS 요청 (POST /apis/apps/v1/...)
A->>A: ① 인증 → ② 인가 → ③ 어드미션
A->>E: ④ 저장
A-->>U: 201 Created → "created" 출력
C->>A: watch 로 발견 → ReplicaSet·Pod 생성
S->>A: watch 로 발견 → Pod 에 nodeName 기록
K->>A: watch 로 발견 → 컨테이너 실행 → status 보고
| 관문 | 하는 일 | 우리 클러스터 설정 | 막히면 |
|---|---|---|---|
| ① 인증 (Authentication) | 누구인가 — 인증서 · 토큰 | kubeconfig 의 클라이언트 인증서 | Unauthorized (401) |
| ② 인가 (Authorization) | 그 일을 해도 되는가 | --authorization-mode=Node,RBAC |
Forbidden (403) |
| ③ 어드미션 (Admission) | 요청을 고치거나(변경) 거절(검증) | --enable-admission-plugins=... + 기본 활성 플러그인 |
플러그인마다 다른 거절 메시지 |
| ④ 저장 | 형식 검사 후 etcd 에 기록 | — | BadRequest · Invalid |
-v=6 — kubectl 이 실제로 보내는 요청¶
I0929 06:12:41.482113 23145 loader.go:402] Config loaded from file: /home/vboxuser/.kube/config
I0929 06:12:41.498741 23145 round_trippers.go:560] GET https://192.168.56.10:6443/api/v1/namespaces/guide-arch/pods?limit=500 200 OK in 11 milliseconds
- 목적지는
192.168.56.10:6443— control-plane 의 API 서버입니다. - 경로가 곧 리소스입니다. 코어 그룹은
/api/v1/..., 그 밖의 그룹은/apis/<그룹>/<버전>/...입니다.
로그 형식은 버전마다 조금 다릅니다
kubectl 버전에 따라 "Response" verb="GET" url="..." status="200 OK" 같은 구조화된 형식으로 찍히기도 합니다. 보는 것은 메서드 · URL · 상태 코드 세 가지입니다.
-v=8 이면 요청·응답 본문까지 보입니다.
리소스가 어느 그룹에 속하는지는 api-resources 에 있습니다.
NAME SHORTNAMES APIVERSION NAMESPACED KIND
pods po v1 true Pod
deployments deploy apps/v1 true Deployment
leases coordination.k8s.io/v1 true Lease
--raw — kubectl 을 거치되 가공 없이¶
kubectl get --raw 는 kubeconfig 의 인증 정보를 그대로 쓰면서 경로를 직접 부릅니다.
{"kind":"Namespace","apiVersion":"v1","metadata":{"name":"guide-arch","uid":"b6d1...","resourceVersion":"48213","creationTimestamp":"2026-09-29T06:10:02Z","labels":{"kubernetes.io/metadata.name":"guide-arch"},...},"spec":{"finalizers":["kubernetes"]},"status":{"phase":"Active"}}
① 인증 → ② 인가를 따로 떼어 보기¶
kubeconfig 없이, 즉 아무 인증서도 없이 API 서버를 불러 봅니다.
{
"major": "1",
"minor": "32",
"gitVersion": "v1.32.x",
...
}
{
"kind": "Status",
"apiVersion": "v1",
"status": "Failure",
"message": "namespaces is forbidden: User \"system:anonymous\" cannot list resource \"namespaces\" in API group \"\" at the cluster scope",
"reason": "Forbidden",
"code": 403
}
관찰 포인트 — 두 번째 요청은 인증은 통과했습니다(system:anonymous 라는 익명 사용자로). 막힌 곳은 인가입니다.
/version 은 익명에게도 허용된 경로라서 통과했습니다.
kubectl 로 같은 비교를 합니다. --as 로 다른 사용자인 척 요청할 수 있습니다.
kubectl auth whoami
kubectl auth can-i list pods -n guide-arch
kubectl auth can-i list pods -n guide-arch --as=system:serviceaccount:guide-arch:default
kubectl get pods -n guide-arch --as=system:serviceaccount:guide-arch:default
ATTRIBUTE VALUE
Username kubernetes-admin
Groups [kubeadm:cluster-admins system:authenticated]
yes
no
Error from server (Forbidden): pods is forbidden: User "system:serviceaccount:guide-arch:default" cannot list resource "pods" in API group "" in the namespace "guide-arch"
③ 어드미션 — 요청을 고치고, 거절한다¶
API 서버가 어떤 인가 모드 · 어드미션 플러그인으로 떠 있는지 봅니다.
sudo grep -E -- '--(authorization-mode|enable-admission-plugins)' /etc/kubernetes/manifests/kube-apiserver.yaml
NodeRestriction 하나만 적혀 있지만 NamespaceLifecycle · ServiceAccount · LimitRanger · ResourceQuota 같은 플러그인은 적지 않아도 기본으로 켜져 있습니다.
거절(검증) 실험 — 없는 네임스페이스에 만들어 봅니다. NamespaceLifecycle 이 막습니다.
변경(mutating) 실험 — 우리가 적지 않은 필드가 저장될 때 끼워 넣어집니다.
kubectl run probe -n guide-arch --image=nginx:1.27 $do | grep -E 'serviceAccount|kube-api-access' # 보낸 것
kubectl run probe -n guide-arch --image=nginx:1.27
kubectl get pod probe -n guide-arch -o yaml | grep -E 'serviceAccountName|kube-api-access' # 저장된 것
← 보낸 YAML 에는 아무것도 없다
serviceAccountName: default ← ServiceAccount 어드미션이 채웠다
name: kube-api-access-7x2kq
- name: kube-api-access-7x2kq ← API 토큰 볼륨까지 자동으로 붙었다
④ 형식 검사 실험 — 오타가 있는 필드를 보냅니다.
cat <<'EOF' | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: typo
namespace: guide-arch
spec:
containers:
- name: app
imagee: nginx:1.27 # ← 오타: image 가 아니라 imagee
EOF
Error from server (BadRequest): error when creating "STDIN": Pod in version "v1" cannot be handled as a Pod: strict decoding error: unknown field "spec.containers[0].imagee"
| 에러 첫머리 | 막힌 관문 | 먼저 볼 것 |
|---|---|---|
Unauthorized |
① 인증 | kubeconfig · 인증서 만료 |
Forbidden: User "..." cannot ... |
② 인가 | RBAC (15강) — kubectl auth can-i |
NotFound: namespaces ..., exceeded quota, forbidden: ... |
③ 어드미션 | 네임스페이스 · ResourceQuota · LimitRange |
BadRequest ... unknown field, Invalid value |
④ 형식 검사 | YAML 필드 이름 — kubectl explain |
watch — 부품들은 묻지 않고 지켜본다¶
컨트롤러 · 스케줄러 · kubelet 은 API 서버에 "바뀐 게 있나" 를 반복해서 묻지 않습니다. watch 로 연결을 열어 두고 변경을 통지받습니다. 직접 열어 봅니다.
{"type":"ADDED","object":{"kind":"Pod","apiVersion":"v1","metadata":{"name":"probe","n
{"type":"MODIFIED","object":{"kind":"Pod","apiVersion":"v1","metadata":{"name":"probe"
{"type":"MODIFIED","object":{"kind":"Pod","apiVersion":"v1","metadata":{"name":"probe"
{"type":"DELETED","object":{"kind":"Pod","apiVersion":"v1","metadata":{"name":"probe",
관찰 포인트 — 처음 연결하면 현재 있는 것이 ADDED 로 오고, 이후엔 바뀔 때마다 MODIFIED · DELETED 가 밀려옵니다. kubectl get -w 가 바로 이것입니다. 확인했으면 터미널 1 은 Ctrl+C 로 닫습니다.
4) 스케줄러를 멈추면 무엇이 멈추나¶
왜 — "파드가 Pending 이다" 와 "파드가 아예 안 생긴다" 는 다른 부품의 문제입니다. 하나씩 멈춰 보면 그 경계가 분명해집니다.
반드시 끝까지 진행해서 파일을 되돌리세요
스케줄러가 멈춘 동안에는 새 파드가 배치되지 않습니다. 이 실습은 자기 VM 에서만 하세요.
파일을 /etc/kubernetes/manifests/ 안의 다른 이름(예: .bak)으로 바꾸면 안 됩니다 — kubelet 은 그 디렉터리의 파일을 모두 읽으려 합니다. 반드시 디렉터리 밖으로 옮기세요.
스케줄러 매니페스트를 감시 디렉터리 밖으로 옮깁니다.
sudo mv /etc/kubernetes/manifests/kube-scheduler.yaml /tmp/
kubectl get pods -n kube-system | grep scheduler # 30초쯤 뒤 사라진다
이 상태에서 Deployment 를 만듭니다.
kubectl create deployment hello -n guide-arch --image=nginx:1.27 --replicas=2
kubectl get deploy,rs,pods -n guide-arch -o wide
NAME READY UP-TO-DATE AVAILABLE
deployment.apps/hello 0/2 2 0
NAME DESIRED CURRENT READY
replicaset.apps/hello-5d9c7f8b6c 2 2 0
NAME READY STATUS NODE
pod/hello-5d9c7f8b6c-8kq2w 0/1 Pending <none>
pod/hello-5d9c7f8b6c-t7m4x 0/1 Pending <none>
- Deployment → ReplicaSet → Pod 까지는 만들어졌습니다. 이것은 컨트롤러 매니저의 일입니다.
- 파드의
NODE가<none>입니다. 노드를 정하는 스케줄러가 없기 때문입니다.
Events 가 비어 있습니다. 스케줄러가 없으니 "배치 실패" 이벤트조차 쓰는 주체가 없습니다.
스케줄러 없이 파드를 띄우는 법 — nodeName¶
스케줄러가 하는 일은 결국 파드에 spec.nodeName 을 적어 주는 것뿐입니다. 그럼 우리가 직접 적으면요?
manual.yaml:
apiVersion: v1
kind: Pod
metadata:
name: manual
namespace: guide-arch
spec:
nodeName: worker-2 # ← 스케줄러가 적었어야 할 칸을 직접 채운다
containers:
- name: web
image: nginx:1.27
NAME READY STATUS NODE
hello-5d9c7f8b6c-8kq2w 0/1 Pending <none>
hello-5d9c7f8b6c-t7m4x 0/1 Pending <none>
manual 1/1 Running worker-2 ← 스케줄러 없이 떴다
관찰 포인트 — nodeName 이 채워진 파드는 스케줄러를 건너뛰고 곧장 그 노드의 kubelet 이 받아 갑니다. 대신 Taint · 자원 검사도 건너뛰니 실무에서는 쓰지 않습니다.
이제 파일을 되돌립니다.
sudo mv /tmp/kube-scheduler.yaml /etc/kubernetes/manifests/
kubectl get pods -n guide-arch -o wide -w
스케줄러가 다시 뜨면 Pending 이던 파드에 노드가 정해지고, 그 노드의 kubelet 이 컨테이너를 띄워 Running 이 됩니다.
LAST SEEN TYPE REASON OBJECT MESSAGE
20s Normal Scheduled pod/hello-5d9c7f8b6c-8kq2w Successfully assigned guide-arch/hello-5d9c7f8b6c-8kq2w to worker-1
20s Normal Scheduled pod/hello-5d9c7f8b6c-t7m4x Successfully assigned guide-arch/hello-5d9c7f8b6c-t7m4x to worker-2
manual 파드에는 Scheduled 이벤트가 없습니다. 스케줄러를 거치지 않았으니까요.
대조 실험 — 컨트롤러 매니저를 멈추면¶
이번에는 컨트롤러 매니저를 멈춥니다. 같은 경고가 적용됩니다.
sudo mv /etc/kubernetes/manifests/kube-controller-manager.yaml /tmp/
kubectl get pods -n kube-system | grep controller-manager # 사라질 때까지 기다린다
kubectl create deployment ctl-test -n guide-arch --image=nginx:1.27 --replicas=2
kubectl get deploy,rs,pods -n guide-arch -l app=ctl-test
NAME READY UP-TO-DATE AVAILABLE
deployment.apps/ctl-test 0/2 0 0
No resources found ... ← ReplicaSet 도, 파드도 없다
관찰 포인트 — Deployment 오브젝트는 etcd 에 저장됐지만(API 서버의 일), ReplicaSet 을 만들 주체가 없습니다. 스케줄러 때와 멈춘 지점이 한 칸 앞입니다.
sudo mv /tmp/kube-controller-manager.yaml /etc/kubernetes/manifests/
kubectl get deploy,rs,pods -n guide-arch -l app=ctl-test -w # 곧 RS 와 파드가 생긴다
| 단계 | 담당 | 이 부품이 멈추면 보이는 모습 |
|---|---|---|
| 요청을 받아 etcd 에 저장 | kube-apiserver → etcd | kubectl 자체가 실패 (connection refused) |
| ReplicaSet · Pod 오브젝트 생성 | kube-controller-manager | Deployment 만 있고 RS · 파드가 없다 |
| Pod 에 노드 지정 | kube-scheduler | 파드가 Pending, NODE <none>, 이벤트 없음 |
| 실제 컨테이너 실행 | 그 노드의 kubelet + 컨테이너 런타임 | 노드 NotReady, 파드가 ContainerCreating 이나 Unknown |
5) 구성 요소 상태 점검 — 살아 있는지 어떻게 아나¶
왜 — "파드가 Running 이다" 와 "그 부품이 제대로 일한다" 는 다릅니다. API 서버는 자기 상태를 엔드포인트로 알려 주고, kubectl 이 안 되면 노드에서 직접 볼 수 있어야 합니다.
API 서버의 건강 엔드포인트¶
[+]ping ok
[+]log ok
[+]etcd ok ← API 서버가 보기에 etcd 도 정상
[+]etcd-readiness ok
[+]informer-sync ok
[+]poststarthook/start-apiserver-admission-initializer ok
...
[+]shutdown ok
readyz check passed
| 엔드포인트 | 뜻 | 쓰는 곳 |
|---|---|---|
/livez |
프로세스가 살아 있나 (죽었으면 재시작 대상) | 스태틱 파드의 livenessProbe |
/readyz |
요청을 받을 준비가 됐나 (etcd 연결 등) | readinessProbe, 로드밸런서 |
/readyz?verbose |
항목별 결과 | 사람이 볼 때 |
스케줄러 · 컨트롤러 매니저의 건강 엔드포인트¶
이 둘은 kubectl 로 부를 수 있는 경로가 없습니다. 대신 매니페스트의 livenessProbe 에 kubelet 이 어디를 찔러 보는지 적혀 있습니다.
livenessProbe:
failureThreshold: 8
httpGet:
host: 127.0.0.1
path: /healthz ← kubeadm 버전에 따라 /livez 로 보일 수도 있다
port: 10259 ← 스케줄러 (컨트롤러 매니저는 10257)
scheme: HTTPS
control-plane 에서 같은 곳을 찔러 봅니다.
kubectl get componentstatuses 는?
예전 자료에 자주 나오지만 v1.19 부터 폐기 예정(deprecated) 입니다. 지금도 결과는 나오지만 경고가 붙습니다.
Warning: v1 ComponentStatus is deprecated in v1.19+
NAME STATUS MESSAGE ERROR
scheduler Healthy ok
controller-manager Healthy ok
etcd-0 Healthy ok
/readyz?verbose 나 kubectl get pods -n kube-system 을 쓰세요.
kubectl 이 안 될 때 — 노드에서 직접¶
API 서버가 죽으면 kubectl 은 아무것도 못 합니다. 그때는 노드의 컨테이너 런타임에게 직접 묻습니다.
CONTAINER IMAGE CREATED STATE NAME ATTEMPT POD ID POD
a1b2c3d4e5f6 c2e17b8d0f4a 2 hours ago Running kube-scheduler 1 7f8e9d0c1b2a kube-scheduler-control-plane
...
sudo crictl ps -a --name kube-apiserver # -a: 죽은 컨테이너까지 (ATTEMPT 가 계속 늘면 재시작 반복 중)
sudo crictl logs <컨테이너ID> 2>&1 | tail -20 # 죽은 이유
sudo journalctl -u kubelet -n 30 --no-pager # kubelet 이 매니페스트를 못 읽었다면 여기에
관찰 포인트 — 4) 에서 매니페스트를 옮겼을 때 crictl ps 에서도 스케줄러가 사라졌을 것입니다. kubectl 과 crictl 이 같은 것을 서로 다른 층에서 보고 있습니다.
시험 팁
API 서버 매니페스트를 고친 뒤 kubectl 이 한동안 안 되는 것은 정상입니다(재시작 중). 1~2분이 지나도 안 되면
sudo crictl ps -a | grep apiserver → sudo crictl logs <ID> 순서로 봅니다. 대부분 YAML 들여쓰기나 오타입니다.
6) 노드마다 무엇이 돌고 있나¶
NAME STATUS VERSION INTERNAL-IP OS-IMAGE CONTAINER-RUNTIME
control-plane Ready v1.32.x 192.168.56.10 Ubuntu 24.04 LTS containerd://1.7.x
worker-1 Ready v1.32.x 192.168.56.11 Ubuntu 24.04 LTS containerd://1.7.x
worker-2 Ready v1.32.x 192.168.56.12 Ubuntu 24.04 LTS containerd://1.7.x
VERSION 은 그 노드 kubelet 의 버전입니다. kubelet 은 파드가 아니라 systemd 서비스로 돕니다.
kubelet 은 왜 파드가 아닐까요?
파드를 띄우는 주체가 kubelet 이기 때문입니다. 스태틱 파드인 API 서버도 kubelet 이 띄웁니다.
그래서 kubelet 이 죽으면 그 노드는 NotReady 가 되고, 컨트롤플레인 노드라면 클러스터 전체가 멈춥니다.
노드는 어떻게 "살아 있다" 고 알리나 — Lease¶
kubelet 은 약 10초마다 kube-node-lease 네임스페이스의 Lease 오브젝트를 갱신합니다. 노드 컨트롤러는 이 갱신이 일정 시간(--node-monitor-grace-period, 40~50초 — 버전에 따라 기본값이 다릅니다) 넘게 끊기면 노드의 Ready 상태를 Unknown 으로 바꿉니다. kubectl get nodes 에는 NotReady 로 보입니다.
NAME HOLDER AGE
control-plane control-plane 3d
worker-1 worker-1 3d
worker-2 worker-2 3d
GROUP: coordination.k8s.io
KIND: Lease
VERSION: v1
FIELD: spec <LeaseSpec>
...
FIELDS:
acquireTime <MicroTime>
holderIdentity <string>
leaseDurationSeconds <integer>
...
renewTime <MicroTime> ← kubelet 이 계속 갱신하는 칸
renewTime 이 10초 간격으로 바뀌는 것이 보입니다. Ctrl+C 로 나옵니다.
노드의 자세한 상태는 describe 의 Conditions 에 있습니다.
Conditions:
Type Status Reason Message
---- ------ ------ -------
MemoryPressure False KubeletHasSufficientMemory kubelet has sufficient memory available
DiskPressure False KubeletHasNoDiskPressure kubelet has no disk pressure
PIDPressure False KubeletHasSufficientPID kubelet has sufficient PID available
Ready True KubeletReady kubelet is posting ready status
Addresses:
노드 정보는 status.nodeInfo 에 모여 있습니다. jsonpath 로 필요한 것만 뽑습니다.
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.nodeInfo.osImage}{"\t"}{.status.nodeInfo.kernelVersion}{"\n"}{end}'
control-plane Ubuntu 24.04 LTS 6.8.0-xx-generic
worker-1 Ubuntu 24.04 LTS 6.8.0-xx-generic
worker-2 Ubuntu 24.04 LTS 6.8.0-xx-generic
시험 팁 — 필드 경로를 모를 때
kubectl get node worker-1 -o yaml | less 로 실제 값을 보면서 경로를 찾거나, kubectl explain node.status --recursive | less 로 구조를 봅니다.
range ... end 는 목록의 항목마다 한 줄씩 찍을 때 씁니다.
7) kube-proxy 와 CoreDNS — 노드 위의 네트워크 부품¶
왜 — "파드는 Running 인데 Service 로 접속이 안 된다" 는 1)~6) 의 부품이 아니라 이 둘의 문제일 때가 많습니다. 5강에서 깊게 다뤘고, 여기서는 어디에 무엇이 있는지만 확인합니다.
NAME DESIRED CURRENT READY NODE SELECTOR
daemonset.apps/cilium 3 3 3 kubernetes.io/os=linux
daemonset.apps/kube-proxy 3 3 3 kubernetes.io/os=linux
NAME READY UP-TO-DATE AVAILABLE
deployment.apps/cilium-operator 1/1 1 1
deployment.apps/coredns 2/2 2 2
| 부품 | 형태 | 하는 일 |
|---|---|---|
| kube-proxy | DaemonSet (노드마다) | Service IP → 파드 IP 규칙을 노드 커널(iptables)에 심는다. 트래픽을 직접 나르지 않는다 |
| Cilium | DaemonSet (노드마다) | CNI — 파드에 IP 를 주고 노드 간 파드 통신을 잇는다 |
| CoreDNS | Deployment (2개) | 이름.네임스페이스.svc.cluster.local → Service IP |
kube-proxy 의 동작 모드는 ConfigMap 에 있습니다.
파드는 DNS 서버를 어떻게 아나¶
kubectl get svc kube-dns -n kube-system
kubectl run dns-test -n guide-arch --image=busybox:1.36 --restart=Never -- sleep 3600
kubectl exec dns-test -n guide-arch -- cat /etc/resolv.conf
NAME TYPE CLUSTER-IP PORT(S)
kube-dns ClusterIP 10.96.0.10 53/UDP,53/TCP,9153/TCP
search guide-arch.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10 ← kube-dns Service 의 IP (kubelet 이 넣어 준다)
options ndots:5
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: kubernetes.default.svc.cluster.local
Address: 10.96.0.1 ← API 서버의 Service IP
깨 보기 — CoreDNS 를 0 으로¶
클러스터 전체의 이름 풀이가 멈춥니다
자기 VM 에서만 하고, 바로 되돌리세요.
kubectl scale deployment coredns -n kube-system --replicas=0
kubectl exec dns-test -n guide-arch -- nslookup kubernetes.default.svc.cluster.local
kubectl scale deployment coredns -n kube-system --replicas=2
kubectl rollout status deployment coredns -n kube-system
kubectl exec dns-test -n guide-arch -- nslookup kubernetes.default.svc.cluster.local # 다시 된다
관찰 포인트 — nameserver 10.96.0.10 은 그대로인데 답이 없습니다. Service IP 는 살아 있어도 그 뒤의 파드가 0개면 아무도 응답하지 않습니다.
8) 선언적 모델 — 원하는 상태로 되돌아온다¶
왜 — 쿠버네티스의 모든 컨트롤러는 "원하는 상태(spec)와 실제 상태(status)를 비교해서 맞춘다" 는 같은 루프입니다. 이것이 자동 복구 · 스케일 · 롤링 업데이트의 공통 원리입니다.
먼저 spec.replicas 가 무엇인지 문서로 확인합니다.
GROUP: apps
KIND: Deployment
VERSION: v1
FIELD: replicas <integer>
DESCRIPTION:
Number of desired pods. This is a pointer to distinguish between explicit
zero and not specified. Defaults to 1.
"desired" — 원하는 파드 수입니다. 4) 에서 만든 hello 를 그대로 씁니다. 터미널을 둘로 나눕니다.
지운 파드가 Terminating 이 되는 동안 새 이름의 파드가 곧바로 생깁니다.
우리는 "파드 2개" 라는 원하는 상태만 적어 두었고, 컨트롤러가 실제 상태를 계속 거기에 맞춥니다.
누가 새 파드를 만들었는지는 소유 관계를 따라가면 보입니다.
POD=$(kubectl get pods -n guide-arch -l app=hello -o jsonpath='{.items[0].metadata.name}')
kubectl get pod $POD -n guide-arch -o jsonpath='{.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"\n"}'
kubectl get rs -n guide-arch -l app=hello -o jsonpath='{.items[0].metadata.ownerReferences[0].kind}/{.items[0].metadata.ownerReferences[0].name}{"\n"}'
원하는 상태를 바꾸면 실제 상태가 따라옵니다.
kubectl scale deployment hello -n guide-arch --replicas=4
kubectl get deployment hello -n guide-arch \
-o jsonpath='spec={.spec.replicas} ready={.status.readyReplicas} generation={.metadata.generation} observed={.status.observedGeneration}{"\n"}'
spec.replicas는 우리가 적은 것,status.readyReplicas는 컨트롤러가 보고한 것입니다.generation은 spec 이 바뀔 때마다 올라가고,observedGeneration은 컨트롤러가 그 변경을 처리했음을 뜻합니다. 두 값이 같으면 컨트롤러가 따라잡은 상태입니다.
파드를 직접 만들면?
kubectl run 으로 만든 파드는 지켜보는 컨트롤러가 없습니다. 지우면 그대로 사라집니다.
컨트롤러와 싸우지 마세요
Deployment 가 관리하는 파드를 kubectl edit pod 로 고쳐도, 그 파드가 다시 만들어지면 템플릿 기준으로 되돌아갑니다.
바꾸고 싶은 것은 항상 주인(Deployment)의 spec 에서 바꿉니다. 컨트롤플레인 스태틱 파드라면 주인은 매니페스트 파일입니다.
시험에서 빠르게¶
# 어디에 무엇이 떠 있나
kubectl get pods -n kube-system -o wide
kubectl get pods -n kube-system -l tier=control-plane
kubectl get ds,deploy -n kube-system
# 스태틱 파드
sudo grep staticPodPath /var/lib/kubelet/config.yaml # 감시 디렉터리 (노드마다 확인)
ls /etc/kubernetes/manifests/
sudo grep -- --<옵션이름> /etc/kubernetes/manifests/kube-apiserver.yaml
kubectl -n kube-system get pod -l component=kube-apiserver -o yaml | grep -- --<옵션이름>
# 원하는 칸만 뽑기
kubectl get <리소스> -o custom-columns='A:.metadata.name,B:.spec.nodeName' --no-headers
kubectl get <리소스> -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{...}{"\n"}{end}'
# API 요청 · 권한
kubectl get <리소스> -v=6 # 실제 URL · 상태 코드
kubectl get --raw /api/v1/namespaces/<ns>
kubectl auth can-i <동사> <리소스> [--as=<사용자>] -n <ns>
# 상태 점검
kubectl get --raw='/readyz?verbose'
sudo crictl ps -a | grep <구성요소>
sudo crictl logs <컨테이너ID>
sudo journalctl -u kubelet -n 50 --no-pager
kubectl describe node <노드> | sed -n '/^Conditions/,/^Addresses/p'
시험 팁
- 결과를 파일로 저장하라는 문제는
--no-headers를 붙여야 머리글 줄이 섞이지 않습니다. 저장한 뒤cat으로 꼭 확인하세요. - 매니페스트를 고칠 때는 먼저
sudo cp <파일> ~/<파일>.orig로 감시 디렉터리 밖에 백업을 둡니다.
정리¶
worker-1 의 static-demo.yaml 을 아직 안 지웠다면 먼저 지웁니다(네임스페이스를 먼저 지우면 kubelet 이 거울 파드를 못 만들어 로그에 에러가 쌓입니다).
# control-plane 에서
kubectl delete namespace guide-arch
rm -f static-demo.yaml manual.yaml
ls /etc/kubernetes/manifests/ # 네 파일(etcd·apiserver·controller-manager·scheduler)이 모두 있는지 꼭 확인
kubectl get deploy coredns -n kube-system # 2/2 로 돌아와 있는지 확인
확인 질문¶
1. kube-scheduler-control-plane 을 kubectl 로 지웠는데 다시 생깁니다. 왜 그런가요?
스태틱 파드이기 때문입니다. kubelet 이 /etc/kubernetes/manifests/ 의 파일을 기준으로 띄우고,
API 서버에 보이는 것은 거울 파드일 뿐입니다. 없애려면 파일을 옮겨야 합니다.
2. 스케줄러가 멈춘 상태에서 Deployment 를 만들면 어디까지 진행되나요? 컨트롤러 매니저가 멈췄다면요?
스케줄러가 멈추면 Deployment · ReplicaSet · Pod 오브젝트까지 만들어지고 파드가 Pending 에 머뭅니다.
컨트롤러 매니저가 멈추면 Deployment 만 저장되고 ReplicaSet 과 파드가 아예 생기지 않습니다.
3. etcd 와 직접 통신하는 구성 요소는 무엇인가요?
kube-apiserver 하나뿐입니다. 다른 구성 요소는 모두 API 서버를 거칩니다(watch 포함).
4. API 서버의 실행 옵션을 확인하는 방법 세 가지는?
/etc/kubernetes/manifests/kube-apiserver.yaml 파일,
kubectl -n kube-system get pod -l component=kube-apiserver -o yaml,
ps -ef | grep kube-apiserver.
5. Error from server (Forbidden): pods is forbidden: User ... cannot list resource ... 는 어느 관문에서 막힌 것인가요?
인가(Authorization) 입니다. 누구인지는 이미 확인됐고(인증 통과), 그 사용자에게 권한이 없는 것입니다.
kubectl auth can-i list pods --as=<사용자> -n <ns> 로 재현하고 RBAC 을 봅니다.
6. 스케줄러가 없어도 파드를 특정 노드에 띄울 수 있나요?
네. spec.nodeName 을 직접 적으면 스케줄러를 건너뛰고 그 노드의 kubelet 이 바로 실행합니다.
스태틱 파드도 스케줄러와 무관하게 파일이 놓인 노드에서 뜹니다.
7. kubectl 이 connection refused 로 전혀 동작하지 않습니다. 무엇으로 상태를 보나요?
control-plane 노드에서 sudo crictl ps -a | grep apiserver 로 컨테이너 상태와 재시작 횟수를 보고,
sudo crictl logs <ID> 로 죽은 이유를, sudo journalctl -u kubelet 으로 kubelet 쪽 문제를 봅니다.
8. kubectl get nodes 의 VERSION 은 무엇의 버전이고, 노드가 NotReady 가 되는 기준은 무엇인가요?
그 노드에서 돌고 있는 kubelet 의 버전입니다. kubelet 이 kube-node-lease 의 Lease 를 일정 시간(40~50초) 동안 갱신하지 못하면
노드 컨트롤러가 그 노드를 NotReady(Unknown) 로 바꿉니다.
다음 단계 — 채점 실습¶
| 문제 | 이 페이지에서 연습한 것 |
|---|---|
| Q1 컨트롤플레인 구성 요소 | 1) 2) — 스태틱 파드 찾기 · custom-columns · --no-headers |
| Q2 API 서버는 상태를 어디에 두는가 | 2) 3) — 매니페스트 · kubectl 로 옵션 찾는 법 |
| Q3 노드마다 무엇이 돌고 있나 | 6) — get nodes -o wide · status.nodeInfo · jsonpath |
| Q4 선언적 모델 | 8) |