본문으로 건너뛰기
인프라 · infra/kubernetes/02 전반 ① — 클러스터 운영

← 스터디 홈

Kubernetes Study · 02 전반 ①

쿠버네티스 전반 ① — 클러스터 운영

입문에서 쿠버네티스가 무엇이고 왜 필요한지 봤다면, 전반에서는 어떻게 쓰고 안에서 어떻게 도는지를 봐요. 학습 내용을 빠짐없이 담되 배우기 좋은 순서로 다시 묶었고, 입문에서 다룬 내용도 복습 삼아 다시 짚어요. 학습 자료에 없지만 꼭 알아야 하는 내용과, 학습 자료가 나온 뒤 바뀐 최신 내용도 함께 넣었어요. 전반은 세 노트로 나눴어요 — 이 노트(①)는 1~6부 클러스터 구조 · 리소스 · 스케줄링 · 모니터링 · 앱 라이프사이클 · 유지보수, 이어서 ② 보안 · 스토리지 · 네트워크, ③ 설치 · 도구 · 트러블슈팅이에요.

전반 ① · 1~6부 클러스터 운영 · 2026.10.30 시작 · 2026.11.02 업데이트

그림의 색 = 역할 (입문과 같아요)
컨테이너 · 앱 이미지 · 레이어 레지스트리 가상 머신 OS · 커널 · 런타임 하드웨어 · 서버 · 사용자 원하는 상태 · 컨트롤 플레인 노드 새 버전 · 변경 정상 장애
입문 노트와 같이 보기 입문에서 처음 설명한 개념은 입문 2부 01처럼 링크를 걸어 뒀어요. 기억이 흐릿하면 링크로 가서 그림을 한 번 돌려 보고 돌아오면 돼요.
1부클러스터 아키텍처전체 구조컨테이너 런타임etcdAPI 서버컨트롤러스케줄러kubelet · kube-proxy

01. 클러스터 한눈에 보기

입문 2부 01에서 클러스터를 마스터 노드(결정)와 워커 노드(실행)로 나눠 봤어요. 전반에서는 마스터 노드를 정식 이름인 컨트롤 플레인(control plane)으로 부르고, 그 안의 부품 하나하나가 정확히 무슨 일을 하는지 들어가 봐요.

학습 자료는 화물선 비유를 써요. 컨트롤 플레인은 항구의 관제 본부, 워커 노드는 컨테이너를 싣고 다니는 화물선이에요. 본부는 어느 배에 어떤 화물을 실을지 정하고 기록하고 감시하며, 배는 실제로 화물을 싣고 다녀요.

1. 컨트롤 플레인과 워커 노드

구성 요소사는 곳하는 일화물선 비유
kube-apiserver컨트롤 플레인모든 요청을 받는 정문. 다른 부품은 전부 API 서버를 통해서만 이야기해요접수 창구
etcd컨트롤 플레인클러스터의 모든 상태를 저장하는 키-값 저장소화물 장부
kube-scheduler컨트롤 플레인새 Pod를 어느 노드에 둘지 결정만 해요배정 담당
kube-controller-manager컨트롤 플레인원하는 상태와 현재 상태를 계속 맞추는 컨트롤러 묶음현장 감독관들
cloud-controller-manager컨트롤 플레인 (클라우드에서만)로드밸런서 · 디스크처럼 클라우드 자원을 만들고 연결외부 업체 담당
kubelet모든 노드자기 노드에 배정된 Pod를 실행하고 상태를 보고선장
kube-proxy모든 노드Service로 들어온 요청이 Pod에 닿도록 노드의 네트워크 규칙을 관리배끼리의 통신 담당
컨테이너 런타임모든 노드실제로 이미지를 받아 컨테이너를 실행 (보통 containerd)크레인

직접 설치한 클러스터(kubeadm)에서는 컨트롤 플레인 부품들도 kube-system 네임스페이스에서 Pod로 돌아요. 그래서 kubectl로 볼 수 있어요.

kubeadm으로 만든 클러스터의 kube-system
$ kubectl get pods -n kube-system
NAME                                   READY   STATUS    RESTARTS   AGE
coredns-668d6bf9bc-4tq7z               1/1     Running   0          3d
coredns-668d6bf9bc-x9vbn               1/1     Running   0          3d
etcd-controlplane                      1/1     Running   0          3d
kube-apiserver-controlplane            1/1     Running   0          3d
kube-controller-manager-controlplane   1/1     Running   0          3d
kube-proxy-7xkqp                       1/1     Running   0          3d
kube-proxy-m2d8s                       1/1     Running   0          3d
kube-scheduler-controlplane            1/1     Running   0          3d
GKE에서는 왜 안 보일까 입문에서 쓴 GKE 같은 관리형 서비스는 컨트롤 플레인을 구글이 따로 운영해요(입문 2부 03). 그래서 kube-system에 etcd나 kube-apiserver Pod가 보이지 않아요. 이번 실습은 컨트롤 플레인까지 직접 보이는 kubeadm 클러스터를 기준으로 해요.

2. kubectl apply 한 번이 지나가는 길

부품 이름만 외우면 금방 잊어요. 대신 Deployment 하나를 만들 때 각 부품이 차례로 무슨 일을 하는지 따라가 보면 역할이 한 번에 잡혀요. 그림에서 모든 화살표가 API 서버를 거친다는 점을 눈여겨보세요.

kubectl apply → API 서버 → etcd → 컨트롤러 → 스케줄러 → kubelet

이 순서는 kubectl get events에도 그대로 남아요. 무언가 안 될 때 어느 단계에서 멈췄는지 보는 첫 번째 단서예요.

이벤트로 보는 순서 (일부 열 생략)
$ kubectl get events --sort-by=.metadata.creationTimestamp
REASON              OBJECT                      MESSAGE
ScalingReplicaSet   deployment/web              Scaled up replica set web-7d4b9c to 3
SuccessfulCreate    replicaset/web-7d4b9c       Created pod: web-7d4b9c-x2kfp
Scheduled           pod/web-7d4b9c-x2kfp        Successfully assigned default/web-7d4b9c-x2kfp to node01
Pulling             pod/web-7d4b9c-x2kfp        Pulling image "nginx:1.27"
Created             pod/web-7d4b9c-x2kfp        Created container: nginx
Started             pod/web-7d4b9c-x2kfp        Started container nginx
이벤트남긴 부품여기서 멈추면 의심할 곳
ScalingReplicaSet · SuccessfulCreate컨트롤러 매니저Pod가 아예 안 생김 → ResourceQuota, 어드미션 거절 (2부 · 7부)
Scheduled스케줄러Pending에 머묾 → 자원 부족, taint, affinity (3부)
Pulling · Created · Startedkubelet이미지 이름 · 권한, 컨테이너 시작 실패 (16부)
핵심 — 부품끼리는 서로 직접 말하지 않는다 스케줄러가 kubelet에게 “이거 실행해”라고 전화하지 않아요. 스케줄러는 API 서버에 “이 Pod는 node01”이라고 기록만 하고, node01의 kubelet은 API 서버를 지켜보다가(watch) 자기 몫을 발견해요. 모든 부품이 API 서버를 지켜보고, 자기 일을 하고, 결과를 API 서버에 기록하는 구조라서, 부품 하나가 잠깐 멈춰도 다시 살아나면 밀린 일을 이어서 할 수 있어요.

02. 컨테이너 런타임

입문 1부에서는 docker run으로 컨테이너를 띄웠어요. 그런데 요즘 쿠버네티스 노드에는 Docker가 깔려 있지 않은 경우가 대부분이에요. 대신 containerd라는 런타임이 컨테이너를 띄워요. 왜 그렇게 됐는지 역사를 따라가 보면 CRI · OCI 같은 용어가 자연스럽게 정리돼요.

1. Docker · containerd · CRI의 관계

Docker만 되던 시절 → CRI 등장 → dockershim → containerd 직접 연결
용어정체한 줄 정리
OCI규격 (Open Container Initiative)이미지는 이렇게 생겨야 하고(image spec), 컨테이너는 이렇게 실행해야 한다(runtime spec)는 업계 표준
CRI규격 (Container Runtime Interface)kubelet이 런타임에게 “Pod 만들어 · 이미지 받아 · 컨테이너 시작해”라고 말하는 약속된 방법
containerd런타임 (CNCF 졸업 프로젝트)원래 Docker 안에 있던 부품. 독립해서 CRI를 지원하고, 지금 가장 널리 쓰여요
CRI-O런타임쿠버네티스 전용으로 만든 가벼운 런타임 (OpenShift 등에서 사용)
runc저수준 런타임실제로 리눅스 네임스페이스 · cgroup을 걸어 프로세스를 띄우는 가장 아래 단계 (입문 1부 03)

2. dockershim이 빠진 이유

Docker는 CRI가 생기기 전부터 있던 도구라 CRI를 따르지 않았어요. 그래서 쿠버네티스는 kubelet 안에 Docker용 통역기인 dockershim을 따로 넣어 관리해 왔어요. 하지만 Docker 안에는 이미 containerd가 들어 있으니 kubelet → dockershim → Docker → containerd로 한 바퀴 돌아갈 이유가 없었죠. 결국 1.24(2022년)에서 dockershim이 제거됐어요.

“Docker를 못 쓴다”는 오해 바뀐 건 노드에서 컨테이너를 띄우는 프로그램뿐이에요. docker build로 만든 이미지는 OCI 규격이라 containerd에서 그대로 돌아요. 개발자 PC에서 Docker로 이미지를 만들고 레지스트리에 올리는 흐름은 그대로예요. 다만 노드에 들어가서 docker ps를 치면 “명령을 찾을 수 없음”이 나올 수 있어요. 그때 쓰는 도구가 다음의 crictl이에요.

3. ctr · nerdctl · crictl

Docker 없이 노드의 컨테이너를 볼 때 쓰는 명령줄 도구가 세 가지 있어요. 이름이 비슷해서 헷갈리지만 누구를 위한 도구인지로 구분하면 쉬워요.

도구만든 곳대상언제 쓰나
ctrcontainerdcontainerd 자체containerd 디버깅용. 불편하고 기능이 적어요
nerdctlcontainerd 커뮤니티containerdDocker와 거의 같은 명령(run · ps · build)을 containerd로 쓰고 싶을 때
crictl쿠버네티스 (cri-tools)CRI를 지원하는 모든 런타임쿠버네티스 노드 디버깅. Pod 단위로 보여 줘요
노드에서 crictl로 보기
$ crictl pods                     # 이 노드의 Pod 목록
POD ID          CREATED       STATE   NAME                        NAMESPACE
3f2a1c9e8b7d4   2 hours ago   Ready   web-7d4b9c-x2kfp            default
$ crictl ps                       # 실행 중인 컨테이너
$ crictl images                   # 받아 둔 이미지
$ crictl logs 9b1e0c7a2f3d        # 컨테이너 로그 (컨테이너 ID)
crictl은 보는 용도 crictl로 컨테이너를 직접 만들 수도 있지만, kubelet이 모르는 컨테이너는 결국 kubelet이 정리해 버려요. 만들기 · 지우기는 kubectl로, crictl은 API 서버가 죽어서 kubectl이 안 될 때 노드에서 상황을 보는 용도로 써요 (16부 컨트롤 플레인 장애).

03. etcd

etcd는 클러스터의 기억이에요. 노드 목록, Pod, Deployment, ConfigMap, Secret, 권한 설정까지 kubectl get으로 보는 모든 것이 etcd에 저장돼 있어요. 입문에서 말한 “원하는 상태”가 실제로 적혀 있는 곳이죠.

1. 클러스터 상태를 담는 키-값 저장소

엑셀처럼 행과 열이 있는 표(관계형 DB)와 달리, 키-값 저장소는 이름표(키) 하나에 내용(값) 하나를 붙여 두는 단순한 구조예요. 쿠버네티스는 /registry/종류/네임스페이스/이름 모양의 키를 써요. Dart로 치면 거대한 Map<String, Object> 하나라고 보면 돼요.

etcd — 키-값 저장소, 그리고 API 서버만 들어갈 수 있는 금고
알아 둘 것내용
포트2379 클라이언트(API 서버)용 · 2380 etcd끼리(복제)용
누가 접근하나API 서버만. 다른 부품은 API 서버를 거쳐요
여러 대로 운영Raft라는 합의 방식으로 3대 · 5대처럼 홀수로 복제 → 12부 etcd HA
백업etcd를 잃으면 클러스터가 모든 기억을 잃어요 → 6부 백업과 복구

2. etcdctl로 들여다보기

kubeadm 클러스터에서 etcd는 kube-system의 Pod로 돌아요. 그 안의 etcdctl로 어떤 키가 있는지 볼 수 있어요. etcd는 통신을 TLS 인증서로 보호하기 때문에 인증서 세 개를 함께 넘겨야 해요(인증서는 7부 04).

Pod 키만 골라 보기
$ kubectl exec -n kube-system etcd-controlplane -- etcdctl \
    --cacert /etc/kubernetes/pki/etcd/ca.crt \
    --cert /etc/kubernetes/pki/etcd/server.crt \
    --key /etc/kubernetes/pki/etcd/server.key \
    get /registry/pods --prefix --keys-only
/registry/pods/default/web-7d4b9c-x2kfp
/registry/pods/kube-system/coredns-668d6bf9bc-4tq7z
/registry/pods/kube-system/etcd-controlplane
/registry/pods/kube-system/kube-apiserver-controlplane
학습 자료의 ETCDCTL_API=3 예전 etcdctl은 기본이 v2 API라서 export ETCDCTL_API=3을 먼저 해야 했어요. etcd 3.4부터는 v3가 기본이라 지금은 생략해도 돼요. 학습 자료에 나오면 “예전 버전 습관”으로 이해하면 돼요.
값은 읽기 어렵고, etcd를 직접 고치면 안 된다 키 대신 값을 꺼내면 사람이 읽기 힘든 바이너리(protobuf)가 나와요. 그리고 Secret도 기본 설정에서는 암호화되지 않은 채 저장돼요(5부 04에서 암호화). etcd 값을 직접 고치는 건 API 서버의 검사를 건너뛰는 일이라, 조회 · 백업 · 복구 말고는 쓰지 않아요.

04. kube-apiserver

API 서버는 클러스터의 정문이에요. kubectl, 대시보드, 컨트롤러, 스케줄러, 각 노드의 kubelet까지 모두 API 서버와만 이야기해요. 정체는 HTTPS(기본 포트 6443)로 요청을 받는 REST API 서버예요. kubectl 명령도 결국 HTTP 요청으로 바뀌어 전달돼요.

kubectl이 보내는 실제 요청 보기 (-v=6)
$ kubectl get pods -v=6
... GET https://192.168.0.10:6443/api/v1/namespaces/default/pods?limit=500 200 OK in 12 milliseconds
NAME                 READY   STATUS    RESTARTS   AGE
web-7d4b9c-x2kfp     1/1     Running   0          2h

1. 요청 처리 순서: 인증 → 인가 → 어드미션 → 저장

요청 하나가 들어오면 API 서버는 정해진 관문을 차례로 통과시켜요. 앞 관문에서 막히면 뒤로 가지 않아요. 이 순서는 7부 보안 전체의 뼈대라서 지금 확실히 잡아 두면 좋아요.

API 서버의 관문 — 누구세요? 해도 되나요? 규칙에 맞나요?
관문묻는 것실패하면자세히
인증 (Authentication)누구세요? — 인증서 · 토큰 확인401 Unauthorized7부 02 · 04
인가 (Authorization)그 사람이 이걸 해도 되나요? — 보통 RBAC403 Forbidden7부 07
어드미션 (Admission)규칙에 맞나요? 기본값을 채울까요?요청 거절 (메시지는 플러그인마다 다름)7부 09
저장etcd에 쓰고 응답etcd 장애면 500 계열1부 03
인가에서 막혔을 때의 메시지
$ kubectl get pods --as dev-user
Error from server (Forbidden): pods is forbidden: User "dev-user" cannot list resource "pods" in API group "" in the namespace "default"

2. 설정 파일은 어디에 있나

API 서버는 실행할 때 붙이는 옵션(플래그)으로 설정해요. 설치 방법에 따라 설정 파일 위치가 달라요.

설치 방법어떻게 도나설정 위치
kubeadmStatic Pod (3부 07)/etc/kubernetes/manifests/kube-apiserver.yaml
직접 설치 (“the hard way”)systemd 서비스/etc/systemd/system/kube-apiserver.service
어느 쪽이든실행 중인 프로세스 확인ps -aux | grep kube-apiserver
/etc/kubernetes/manifests/kube-apiserver.yaml (일부)
spec:
  containers:
  - command:
    - kube-apiserver
    - --advertise-address=192.168.0.10
    - --etcd-servers=https://127.0.0.1:2379          # etcd 주소 (1부 03)
    - --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt
    - --client-ca-file=/etc/kubernetes/pki/ca.crt     # 인증서 인증 (7부 04)
    - --authorization-mode=Node,RBAC                  # 인가 방식 (7부 07)
    - --enable-admission-plugins=NodeRestriction      # 어드미션 (7부 09)
    - --service-cluster-ip-range=10.96.0.0/12         # Service IP 대역 (11부 04)
    image: registry.k8s.io/kube-apiserver:v1.35.2
이 파일을 고칠 때 조심할 점 Static Pod라서 파일을 저장하면 kubelet이 알아서 API 서버를 다시 띄워요. 그런데 오타가 있으면 API 서버가 뜨지 못하고, 그러면 kubectl 자체가 안 돼요. 고치기 전에 원본을 복사해 두고, 안 뜨면 노드에서 crictl ps -a와 crictl logs로 원인을 봐요.

05. kube-controller-manager

입문 1부 05에서 쿠버네티스의 핵심은 원하는 상태를 선언하면 현재 상태를 맞춰 주는 것이라고 했어요. 그 “맞춰 주는 일”을 실제로 하는 게 컨트롤러예요. 컨트롤러는 종류별로 수십 개가 있는데, 이걸 한 프로그램으로 묶어 실행하는 게 kube-controller-manager예요.

1. 컨트롤러 = 제어 루프

모든 컨트롤러는 같은 일을 끝없이 반복해요. 지켜보고(watch) → 비교하고 → 차이가 있으면 고친다. 에어컨 온도 조절기처럼 “설정 24도, 지금 27도 → 냉방”을 계속 도는 거예요.

컨트롤러지켜보는 것고치는 방법
DeploymentDeployment의 이미지 · 개수새 ReplicaSet을 만들고 옛 것을 줄임 (5부 01)
ReplicaSetreplicas와 실제 Pod 수모자라면 Pod 생성, 남으면 삭제
Node (노드 수명 주기)노드의 상태 보고보고가 끊기면 NotReady 표시 · taint로 Pod 내보내기
Job · CronJob작업 완료 여부 · 일정Pod를 만들어 작업 실행 (5부 08)
Namespace지워지는 네임스페이스안의 리소스를 모두 정리
ServiceAccount새 네임스페이스default ServiceAccount 생성 (7부 08)
EndpointSliceService의 셀렉터와 PodService가 보낼 Pod 주소 목록 갱신 (2부 03)

어떤 컨트롤러를 켤지는 --controllers 플래그로 정해요. 기본값 *는 “기본으로 켜지는 것 전부”라는 뜻이에요.

/etc/kubernetes/manifests/kube-controller-manager.yaml (일부)
    - kube-controller-manager
    - --controllers=*,bootstrapsigner,tokencleaner
    - --kubeconfig=/etc/kubernetes/controller-manager.conf
    - --leader-elect=true            # 여러 대일 때 한 대만 일하게 (12부 02)

2. 노드 컨트롤러와 ReplicaSet 컨트롤러

두 컨트롤러가 함께 일하는 대표 장면이 노드가 죽었을 때예요. 입문 2부 01에서 “일정 시간 뒤 다른 노드에 다시 생긴다”고만 했던 부분을 단계별로 쪼개 봐요.

노드가 멈추면 — 노드 컨트롤러가 알리고, ReplicaSet 컨트롤러가 채운다
멈춘 노드와 Pod에 붙은 기본 toleration
$ kubectl get nodes
NAME           STATUS     ROLES           AGE   VERSION
controlplane   Ready      control-plane   3d    v1.35.2
node01         Ready      <none>          3d    v1.35.2
node02         NotReady   <none>          3d    v1.35.2

$ kubectl describe node node02 | grep -A1 Taints
Taints:             node.kubernetes.io/unreachable:NoExecute
                    node.kubernetes.io/unreachable:NoSchedule

$ kubectl get pod web-7d4b9c-x2kfp -o yaml     # tolerations 부분만
  tolerations:
  - effect: NoExecute
    key: node.kubernetes.io/not-ready
    operator: Exists
    tolerationSeconds: 300
  - effect: NoExecute
    key: node.kubernetes.io/unreachable
    operator: Exists
    tolerationSeconds: 300
“5분”은 어디서 나왔나 Pod를 만들 때 어드미션 단계에서 not-ready · unreachable taint를 300초 동안 참는다는 toleration이 자동으로 붙어요. 그래서 노드가 NotReady가 된 뒤 약 5분이 지나야 Pod가 내보내져요. 잠깐의 네트워크 끊김에 Pod를 성급히 옮기지 않으려는 장치예요. Pod마다 이 시간을 바꿀 수도 있어요(taint와 toleration은 3부 03).

06. kube-scheduler

스케줄러는 새 Pod를 어느 노드에 둘지 결정만 해요. 실제로 Pod를 띄우는 건 그 노드의 kubelet이에요. 화물을 어느 배에 실을지 정하는 배정 담당이 직접 크레인을 몰지 않는 것과 같아요.

1. 어느 노드에 둘지만 정한다

스케줄러는 API 서버를 지켜보다가 spec.nodeName이 비어 있는 Pod를 발견하면 노드를 골라서 그 칸을 채워요(바인딩). 칸이 채워지는 순간 해당 노드의 kubelet이 자기 몫이라는 걸 알아채요. 그래서 스케줄러가 멈추면 새 Pod는 전부 Pending에 머물러요. 반대로 YAML에 nodeName을 직접 써 두면 스케줄러를 거치지 않아요(3부 01).

2. 필터링과 점수 매기기

노드를 고르는 과정은 크게 두 단계예요. 필터링으로 “아예 안 되는 노드”를 빼고, 남은 노드에 점수를 매겨 가장 높은 곳에 둬요.

필터링 → 점수 매기기 → 바인딩
필터링에서 걸리는 이유예시자세히
자원 부족요청한 CPU · 메모리를 줄 수 없음3부 06
taint를 견디지 못함GPU 전용 노드 · 컨트롤 플레인 노드3부 03
노드 조건 불일치nodeSelector · Node Affinity3부 04
다른 Pod와의 관계Pod Anti-Affinity · Topology Spread3부 05
맞는 노드가 하나도 없을 때
$ kubectl describe pod big-job | tail -3
Events:
  Type     Reason            Message
  Warning  FailedScheduling  0/4 nodes are available: 1 node(s) had untolerated taint {gpu: true}, 3 Insufficient cpu.
점수는 여러 플러그인의 합 그림은 “배치 후 남는 자원이 많을수록 높은 점수”(기본 전략) 하나만 보여 줬지만, 실제로는 이미지가 이미 있는 노드, Pod를 고르게 퍼뜨리는 정도 등 여러 플러그인 점수를 더해요. 이 플러그인 조합을 바꾸는 방법이 3부 09의 스케줄러 프로필이에요.

07. kubelet과 kube-proxy

지금까지는 컨트롤 플레인 이야기였어요. 이제 모든 노드에서 도는 두 부품을 봐요. 입문 2부 02에서 워커 노드를 확대해 봤던 그림의 주인공들이에요.

1. kubelet: 노드의 관리인

kubelet은 배의 선장이에요. 노드를 클러스터에 등록하고, 자기 노드에 배정된 Pod를 발견하면 컨테이너 런타임에게 CRI로 실행을 시키고, 헬스 체크(5부 06)를 돌리고, 결과를 API 서버에 보고해요.

kubelet은 Pod를 띄우고, kube-proxy는 길을 깐다
kubelet만은 Pod가 아니다 kubeadm은 컨트롤 플레인 부품을 Pod로 띄워 주지만, kubelet은 직접 설치해야 해요. Pod를 띄우는 주체가 kubelet이라 kubelet 자신은 Pod로 띄울 수 없거든요. 그래서 모든 노드(컨트롤 플레인 노드 포함)에 systemd 서비스로 깔려 있어요.
kubelet 확인하기
$ systemctl status kubelet          # 서비스 상태
● kubelet.service - kubelet: The Kubernetes Node Agent
     Active: active (running) since ...
$ ps -aux | grep kubelet            # 실행 옵션
... /usr/bin/kubelet --kubeconfig=/etc/kubernetes/kubelet.conf --config=/var/lib/kubelet/config.yaml ...
$ grep staticPodPath /var/lib/kubelet/config.yaml
staticPodPath: /etc/kubernetes/manifests

마지막 줄의 staticPodPath가 바로 1부 04에서 본 /etc/kubernetes/manifests예요. kubelet은 이 폴더의 YAML을 API 서버 없이도 직접 Pod로 띄워요. 컨트롤 플레인이 Pod로 뜰 수 있는 비밀이 이거예요(3부 07 Static Pod).

2. kube-proxy: Service 규칙 만들기

Service의 IP(ClusterIP)는 어느 기계에도 실제로 붙어 있지 않은 가상 주소예요. 그런데도 그 주소로 요청을 보내면 Pod에 닿는 건, 모든 노드의 kube-proxy가 “이 주소로 오는 건 저 Pod들로 보내”라는 규칙을 노드에 적어 두기 때문이에요. kube-proxy는 kubeadm에서 DaemonSet으로 노드마다 하나씩 떠요.

모드방식상태
iptables리눅스 iptables 규칙으로 전달리눅스의 오랜 기본값
ipvs리눅스 IPVS(커널 로드밸런서)1.35에서 지원 중단 예고(deprecated)
nftablesiptables의 후속인 nftables1.33에서 정식(GA) — 새 클러스터의 다음 기본 후보
kube-proxy 모드와 실제 규칙 보기
$ kubectl get daemonset kube-proxy -n kube-system
NAME         DESIRED   CURRENT   READY   NODE SELECTOR            AGE
kube-proxy   3         3         3       kubernetes.io/os=linux   3d
$ kubectl get configmap kube-proxy -n kube-system -o yaml | grep "mode:"
    mode: ""                        # 비어 있으면 iptables
$ iptables -t nat -S | grep web-svc  # 노드에서
-A KUBE-SERVICES -d 10.96.120.15/32 -p tcp -m comment --comment "default/web-svc cluster IP" -m tcp --dport 80 -j KUBE-SVC-W5RQ3PZ2LJ6YT4AB
kube-proxy가 없는 클러스터도 있다 Cilium 같은 일부 CNI 플러그인은 eBPF라는 커널 기술로 kube-proxy의 일을 대신해요. 이런 클러스터에서는 kube-proxy DaemonSet이 아예 없어요. Service가 실제로 어떻게 연결되는지는 11부 04 Service 네트워킹에서 더 깊게 봐요.

3. 1부 셀프 체크

  1. 컨트롤 플레인의 네 부품과 각자 하는 일을 한 줄씩 말해 보면?
    API 서버 — 모든 요청의 정문. etcd — 클러스터 상태 저장. 스케줄러 — 새 Pod를 둘 노드 결정. 컨트롤러 매니저 — 원하는 상태와 현재 상태를 맞추는 제어 루프 묶음.
  2. 스케줄러는 kubelet에게 어떻게 “이 Pod를 실행해”라고 알리나요?
    직접 알리지 않아요. 스케줄러는 API 서버에 Pod의 nodeName을 기록할 뿐이고, 그 노드의 kubelet이 API 서버를 지켜보다가(watch) 자기 몫을 발견해요.
  3. dockershim이 제거된 뒤에도 Docker로 만든 이미지를 쓸 수 있는 이유는?
    Docker 이미지는 OCI 이미지 규격을 따르기 때문이에요. 바뀐 건 노드에서 컨테이너를 띄우는 프로그램(Docker → containerd)뿐이에요.
  4. kubectl이 안 될 때 노드에서 컨테이너 상태를 볼 수 있는 도구는?
    crictl이에요. crictl ps -a, crictl logs로 API 서버 컨테이너가 왜 죽었는지 볼 수 있어요.
  5. etcd에 직접 접근하는 부품은 무엇이고, etcd를 잃으면 어떻게 되나요?
    API 서버만 접근해요. etcd를 잃으면 클러스터가 모든 리소스 정의를 잊어버리기 때문에 정기적인 etcd 백업이 필수예요(6부).
  6. API 서버가 요청을 처리하는 순서와, 각 단계에서 막혔을 때의 응답은?
    인증(401) → 인가(403) → 어드미션(거절) → etcd 저장 → 응답.
  7. 노드가 NotReady가 된 뒤 Pod가 다른 노드로 옮겨지기까지 약 5분이 걸리는 이유는?
    Pod에 unreachable · not-ready taint를 300초 동안 참는 toleration이 자동으로 붙기 때문이에요. 그 뒤 Pod가 내보내지고, ReplicaSet 컨트롤러가 다른 노드에 새로 만들어요.
  8. kubelet이 Static Pod가 아니라 systemd 서비스로 도는 이유는?
    Pod를 띄우는 주체가 kubelet이라, kubelet 자신은 Pod로 띄울 수 없기 때문이에요. 그래서 모든 노드에 직접 설치해요.
2부리소스 기본기 다시 보기Pod와 YAMLReplicaSet · DeploymentServiceNamespace명령형과 선언형

01. Pod와 YAML

1부에서 클러스터의 속을 봤으니, 이제 그 위에 올라가는 리소스를 다시 정리해요. 입문에서 다 본 내용이지만, 이번에는 “YAML 한 칸이 클러스터 안에서 무엇이 되는지”와 “명령어로 빠르게 다루는 법”에 초점을 맞춰요. 전반 실습과 이후 모든 부의 바탕이 되는 부분이에요.

1. YAML 네 가지 필드 복습

입문 2부 05에서 본 것처럼 모든 리소스 YAML은 네 칸으로 시작해요. 우리가 쓰는 건 위 세 칸 반(apiVersion · kind · metadata · spec)이고, status는 클러스터가 채워요.

YAML 한 칸 한 칸이 클러스터에서 무엇이 되나

apiVersion을 외울 필요는 없어요. 헷갈릴 때는 kubectl api-resources로 바로 확인해요.

자주 쓰는 리소스의 apiVersion · 줄임말
$ kubectl api-resources
NAME                SHORTNAMES   APIVERSION   NAMESPACED   KIND
pods                po           v1           true         Pod
services            svc          v1           true         Service
namespaces          ns           v1           false        Namespace
resourcequotas      quota        v1           true         ResourceQuota
deployments         deploy       apps/v1      true         Deployment
replicasets         rs           apps/v1      true         ReplicaSet
jobs                             batch/v1     true         Job
ingresses           ing          networking.k8s.io/v1   true   Ingress
v1과 apps/v1의 차이 / 앞이 API 그룹이에요. Pod · Service처럼 처음부터 있던 리소스는 그룹 이름이 없는 core 그룹이라 v1만 쓰고, 나중에 생긴 리소스는 apps/v1 · batch/v1처럼 그룹이 붙어요. API 그룹은 7부 06에서 RBAC와 함께 다시 봐요.

2. kubectl explain으로 필드 찾기

“resources 아래에 뭐가 들어가더라?” 같은 건 문서를 찾지 않아도 돼요. kubectl explain은 클러스터가 알고 있는 정확한 필드 이름과 설명을 바로 보여 줘요. 점(.)으로 한 단계씩 들어가요.

점으로 한 단계씩 들어가기
$ kubectl explain deployment.spec.strategy
GROUP:      apps
KIND:       Deployment
VERSION:    v1

FIELD: strategy <DeploymentStrategy>

FIELDS:
  rollingUpdate   <RollingUpdateDeployment>
    Rolling update config params. Present only if DeploymentStrategyType = RollingUpdate.
  type            <string>
  enum: Recreate, RollingUpdate
    Type of deployment. Can be "Recreate" or "RollingUpdate". Default is RollingUpdate.

$ kubectl explain pod.spec.containers --recursive | less   # 아래 필드를 트리로 한 번에
옵션보여 주는 것언제
kubectl explain pod.spec바로 아래 필드와 설명필드 이름 · 타입이 헷갈릴 때
--recursive아래 모든 필드를 이름만 트리로어디쯤 있는지 위치를 찾을 때
kubectl explain <리소스>그룹 · 버전 · 종류apiVersion을 확인할 때

02. ReplicaSet과 Deployment

Pod 하나만 띄우면 그 Pod가 죽었을 때 끝이에요(입문 4부 02). 그래서 실제로는 Deployment를 만들고, Deployment가 ReplicaSet을, ReplicaSet이 Pod를 관리하는 세 층 구조로 운영해요. 이번에는 이 세 층이 서로를 어떻게 알아보는지를 봐요.

1. 개수 지키기 — ReplicaSet

ReplicaSet의 핵심은 두 칸이에요. replicas(몇 개)와 selector(어떤 Pod를 내 것으로 셀지). ReplicaSet은 Pod를 이름으로 세지 않고 라벨로 세요.

replicaset.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: web-rs
spec:
  replicas: 3
  selector:                 # 이 라벨이 붙은 Pod를 “내 것”으로 센다
    matchLabels:
      app: web
  template:                 # 모자랄 때 이 모양으로 새로 만든다
    metadata:
      labels:
        app: web            # ← selector와 반드시 맞아야 함
    spec:
      containers:
      - name: nginx
        image: nginx:1.27
selector와 template 라벨이 다르면 ReplicaSet이 자기가 만든 Pod를 세지 못해 끝없이 만들게 되니, API 서버가 처음부터 거절해요. selector does not match template labels 오류가 나오면 두 라벨부터 비교하세요. 학습 자료에 나오는 ReplicationController는 ReplicaSet 이전의 옛 버전이라, 새로 쓸 일은 없어요.
개수 바꾸기 — 세 가지 방법
$ kubectl scale rs web-rs --replicas=5          # 명령으로 (파일은 그대로 3)
$ kubectl edit rs web-rs                        # 실행 중인 객체를 바로 편집
$ kubectl apply -f replicaset.yaml              # 파일의 replicas를 고친 뒤 적용 (권장)

2. Deployment가 ReplicaSet을 다루는 방식

Deployment → ReplicaSet → Pod — 주인과 이름의 연결
이름규칙예
Deployment내가 지은 이름web
ReplicaSetDeployment 이름 + Pod 템플릿의 해시web-7d4b9c
PodReplicaSet 이름 + 무작위 5글자web-7d4b9c-x2kfp

Pod 이름만 봐도 어느 ReplicaSet · Deployment에서 왔는지 알 수 있어요. 템플릿이 바뀌면 해시가 바뀌니 새 ReplicaSet이 생기는 거고요. 롤링 업데이트와 롤백의 자세한 동작은 5부 01에서 다시 봐요.

세 층을 한 번에 보기
$ kubectl create deployment web --image=nginx:1.27 --replicas=3
$ kubectl get deploy,rs,pods -l app=web
NAME                  READY   UP-TO-DATE   AVAILABLE
deployment.apps/web   3/3     3            3

NAME                             DESIRED   CURRENT   READY
replicaset.apps/web-7d4b9c       3         3         3

NAME                   READY   STATUS
pod/web-7d4b9c-x2kfp   1/1     Running
pod/web-7d4b9c-5mq8r   1/1     Running
pod/web-7d4b9c-t7wzd   1/1     Running

03. Service

Pod는 다시 만들어질 때마다 IP가 바뀌어요. 그래서 Pod 앞에 바뀌지 않는 주소를 하나 세워 두는 게 Service예요(입문 5부 01). 1부 07에서 이 주소가 kube-proxy의 규칙으로만 존재한다는 것도 봤어요.

1. ClusterIP · NodePort · LoadBalancer 복습

세 종류는 바깥으로 얼마나 열리느냐의 차이예요(입문 5부 02). 실습에서 가장 많이 헷갈리는 건 NodePort의 포트 세 개예요. 요청이 들어오는 방향 그대로 따라가면 쉬워요.

nodePort → port → targetPort — 요청이 지나가는 세 개의 문
nodeport-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: web-svc
spec:
  type: NodePort          # ClusterIP(기본) · NodePort · LoadBalancer
  selector:
    app: web              # 이 라벨의 Pod로 보낸다
  ports:
  - port: 80              # Service의 포트 (클러스터 안에서 부를 때)
    targetPort: 8080      # Pod 컨테이너가 실제로 듣는 포트 (생략하면 port와 같음)
    nodePort: 30080       # 노드에 열리는 포트 (30000-32767, 생략하면 자동)
종류누가 접근하나포트주로
ClusterIP (기본)클러스터 안의 Podport → targetPort서비스끼리 내부 통신
NodePort+ 노드 IP로 밖에서nodePort → port → targetPort실습 · 간단한 외부 노출
LoadBalancer+ 클라우드 로드밸런서 IP로NodePort 위에 LB가 추가클라우드 운영 환경

2. 셀렉터와 EndpointSlice

Service는 Pod를 직접 기억하지 않아요. 셀렉터만 들고 있고, 1부 05의 EndpointSlice 컨트롤러가 셀렉터에 맞고 준비된(Ready) Pod의 IP를 모아 EndpointSlice라는 목록을 계속 갱신해요. kube-proxy가 규칙을 만들 때 보는 게 이 목록이에요.

Service는 셀렉터만 갖고, 실제 주소 목록은 EndpointSlice에
Service가 안 될 때 가장 먼저 보는 곳
$ kubectl get endpointslices -l kubernetes.io/service-name=web-svc
NAME            ADDRESSTYPE   PORTS   ENDPOINTS
web-svc-8xq2m   IPv4          8080    10.244.1.5,10.244.2.7
$ kubectl describe svc web-svc | grep -E "Selector|Endpoints"
Selector:          app=web
Endpoints:         10.244.1.5:8080,10.244.2.7:8080
Endpoints와 EndpointSlice 학습 자료에는 Endpoints라는 리소스가 나와요. 예전 방식으로, 큰 클러스터에서 목록 하나가 너무 커지는 문제가 있어 여러 조각으로 나눈 EndpointSlice로 바뀌었고, Endpoints API는 1.33부터 지원 중단 예고(deprecated) 상태예요. kubectl describe svc의 Endpoints 줄은 계속 보여서 확인용으로는 그대로 쓸 수 있어요.

04. Namespace

Namespace는 한 클러스터를 이름 · 권한 · 자원 한도가 따로인 칸으로 나누는 방법이에요(입문 7부 01). 팀별 · 환경별(dev · prod)로 나누는 게 일반적이에요.

1. 칸 나누기와 DNS 이름

같은 이름도 칸이 다르면 괜찮다 — 그리고 칸을 넘을 때의 주소
기본 Namespace용도
default-n을 안 쓰면 들어가는 곳
kube-system컨트롤 플레인 · CoreDNS · kube-proxy 등 시스템 부품 (1부 01)
kube-public인증 없이도 읽을 수 있는 공개 정보 (거의 쓰지 않음)
kube-node-lease노드 하트비트용 Lease 객체 (1부 05)
Namespace 다루기
$ kubectl create namespace dev
$ kubectl get pods -n dev                                 # 한 번만 dev에서
$ kubectl get pods -A                                     # 모든 Namespace (--all-namespaces)
$ kubectl config set-context --current --namespace=dev    # 앞으로 기본값을 dev로
$ kubectl api-resources --namespaced=false                # Namespace에 속하지 않는 리소스 (노드 · PV 등)

2. ResourceQuota

칸마다 쓸 수 있는 총량을 정하는 게 ResourceQuota예요. 한 팀이 클러스터 자원을 다 써 버리는 걸 막아요.

dev-quota.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: dev-quota
  namespace: dev
spec:
  hard:
    pods: "10"              # Pod 최대 10개
    requests.cpu: "4"       # 모든 Pod의 CPU 요청 합계 최대 4
    requests.memory: 8Gi
    limits.cpu: "8"
    limits.memory: 16Gi
쿼터를 걸면 requests를 꼭 써야 한다 requests.cpu처럼 자원 쿼터가 걸린 Namespace에서는 Pod에 requests · limits가 없으면 생성이 거절돼요. 매번 쓰기 번거로우면 기본값을 채워 주는 LimitRange를 같이 걸어요(3부 06). 또 Deployment로 만들 때 쿼터에 걸리면 kubectl apply는 성공해 보이고, 실제 거절은 ReplicaSet의 이벤트(FailedCreate)에 남아요.
쿼터 확인과 거절 메시지
$ kubectl describe quota dev-quota -n dev
Resource         Used  Hard
--------         ----  ----
pods             3     10
requests.cpu     1500m 4
$ kubectl describe rs web-7d4b9c -n dev | grep FailedCreate
Warning  FailedCreate  ...  Error creating: pods "web-7d4b9c-q8w2z" is forbidden: exceeded quota: dev-quota, requested: pods=1, used: pods=10, limited: pods=10

05. 명령형과 선언형

입문 1부 05에서 “이걸 해라”(명령형)와 “이런 상태여야 한다”(선언형)를 봤어요. 실제로는 둘을 섞어 써요. 빠르게 만들 땐 명령형, 오래 관리할 땐 선언형 파일이 원칙이에요. 특히 시험처럼 시간이 정해진 상황에서는 명령형으로 YAML 뼈대를 뽑아 고치는 게 가장 빨라요.

1. kubectl run · create로 빠르게 만들기

만들 것명령
Podkubectl run nginx --image=nginx --port=80 --labels=app=web
Pod + 같은 이름 Servicekubectl run httpd --image=httpd:alpine --port=80 --expose
Deploymentkubectl create deployment web --image=nginx --replicas=3
Service (Pod 라벨을 셀렉터로)kubectl expose deployment web --port=80 --target-port=8080 --name=web-svc
NodePort (포트 지정)kubectl create service nodeport web --tcp=80:8080 --node-port=30080
Namespacekubectl create namespace dev
개수 바꾸기kubectl scale deployment web --replicas=5
이미지 바꾸기kubectl set image deployment/web nginx=nginx:1.28
expose와 create service의 차이 kubectl expose는 대상(Pod · Deployment)의 라벨을 그대로 셀렉터로 가져와요. kubectl create service는 셀렉터를 app=<이름>으로 가정해서, 라벨이 다르면 EndpointSlice가 비어요(2부 03). 대신 --node-port를 지정할 수 있어요. 보통은 expose로 뽑고 nodePort만 YAML에서 고쳐요.

2. --dry-run=client -o yaml로 YAML 뽑기

YAML을 처음부터 손으로 쓰면 들여쓰기 하나로 시간이 다 가요. 명령형 명령에 --dry-run=client -o yaml을 붙이면 실제로 만들지 않고 YAML만 출력해요. 이걸 파일로 저장해서 필요한 칸만 고치는 게 가장 빠른 방법이에요.

뼈대 뽑기 → 고치기 → 적용
$ kubectl create deployment web --image=nginx --replicas=3 \
    --dry-run=client -o yaml > web.yaml
$ vi web.yaml                      # resources · probe 등 필요한 칸 추가
$ kubectl apply -f web.yaml

$ kubectl get deploy web -o yaml > web-live.yaml   # 이미 있는 객체에서 YAML 뽑기 (status 등은 지우고 쓰기)
방식명령특징
명령형 명령run · create · expose · scale · set빠르지만 기록이 남지 않음
명령형 파일create -f · replace -f · delete -f이미 있으면 create는 실패, replace는 통째로 교체
선언형 파일apply -f있으면 바뀐 부분만 반영, 없으면 생성 — 파일을 Git으로 관리

3. apply는 무엇과 비교하나

kubectl apply는 똑똑하게 “바뀐 것만” 반영하는데, 그러려면 비교할 대상이 필요해요. apply는 세 가지를 비교해요. 내 파일, 실제 객체, 그리고 지난번 apply한 내용이에요.

로컬 파일 · 지난번 apply · 실제 객체 — 세 군데를 비교한다
지난번 apply 내용은 어노테이션에 저장된다
$ kubectl get deploy web -o yaml | grep -A1 last-applied
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"apps/v1","kind":"Deployment","metadata":{"labels":{"app":"web"},"name":"web",...
서버 쪽 apply (Server-Side Apply) 위 방식은 kubectl이 내 PC에서 비교하는 클라이언트 쪽 apply예요. 요즘은 kubectl apply --server-side로 API 서버가 비교하게 할 수도 있어요. 이때는 어노테이션 대신 “어느 필드를 누가 관리하는지”를 managedFields에 기록해서, 여러 도구가 같은 객체를 고칠 때 충돌을 알려 줘요. Helm · 컨트롤러가 많이 쓰는 방식이라 13부에서 다시 만나요.
명령형과 선언형을 섞을 때 파일에 replicas: 3이 있는데 kubectl scale로 5로 늘리면, 다음 apply 때 다시 3으로 돌아가요(그림 마지막 단계). HPA처럼 다른 무언가가 개수를 관리한다면 파일에서 replicas 칸을 아예 빼는 게 좋아요(5부 07).

4. 2부 셀프 체크

  1. YAML의 네 칸 중 내가 쓰지 않는 칸과, 그 칸을 채우는 쪽은?
    status예요. 클러스터(주로 kubelet과 컨트롤러)가 현재 상태를 채워요. 우리는 spec에 원하는 상태를 써요.
  2. Deployment의 apiVersion이 기억나지 않을 때 확인하는 명령 두 가지는?
    kubectl api-resources (APIVERSION 열) 또는 kubectl explain deployment (GROUP · VERSION).
  3. ReplicaSet은 자기 Pod를 무엇으로 세나요? 그리고 그게 틀리면?
    라벨 셀렉터로 세요. selector와 template의 라벨이 다르면 API 서버가 생성을 거절해요.
  4. Pod 이름 web-7d4b9c-x2kfp에서 각 부분의 뜻은?
    web = Deployment 이름, 7d4b9c = Pod 템플릿 해시(ReplicaSet 이름의 끝), x2kfp = 무작위 글자.
  5. NodePort Service에서 nodePort · port · targetPort는 각각 어디의 포트인가요?
    nodePort = 노드에 열리는 포트(30000-32767), port = Service의 포트, targetPort = Pod 컨테이너가 듣는 포트. 요청은 이 순서로 지나가요.
  6. Service에 요청했는데 아무 응답이 없을 때 가장 먼저 볼 것은?
    EndpointSlice(또는 describe svc의 Endpoints)가 비어 있지 않은지 봐요. 비어 있으면 셀렉터와 Pod 라벨이 다르거나, Pod가 Ready가 아닌 거예요.
  7. dev Namespace의 Pod가 prod Namespace의 Service db를 부를 때 주소는?
    db.prod 또는 전체 이름 db.prod.svc.cluster.local. 같은 Namespace 안에서는 db만 써도 돼요.
  8. YAML을 빠르게 만드는 방법은?
    명령형 명령에 --dry-run=client -o yaml > 파일을 붙여 뼈대를 뽑고, 필요한 칸만 고쳐서 kubectl apply -f해요.
  9. kubectl apply가 파일에서 지운 필드를 실제 객체에서도 지울 수 있는 이유는?
    지난번 apply한 내용을 last-applied-configuration 어노테이션에 저장해 두고, 이번 파일과 비교해서 “내가 넣었다가 뺀 필드”를 알아내기 때문이에요.
3부스케줄링원리라벨Taint · TolerationAffinity퍼뜨리기자원DaemonSet · Static Pod우선순위여러 스케줄러

01. 스케줄링 원리

1부 06에서 스케줄러는 필터링 → 점수 매기기 → 바인딩으로 노드를 고른다고 했어요. 3부는 그 과정에 우리가 끼어드는 방법을 모은 부예요. “이 Pod는 GPU 노드에만”, “이 노드에는 아무나 오지 마”, “같은 앱은 노드마다 흩어 놔” 같은 요구를 YAML로 어떻게 표현하는지 봐요.

1. 스케줄러 없이 직접 배치: nodeName

가장 직접적인 방법은 스케줄러를 아예 건너뛰는 거예요. Pod를 만들 때 spec.nodeName에 노드 이름을 써 두면, 그 노드의 kubelet이 바로 가져가요. 스케줄러가 없는 클러스터에서 Pod가 Pending에 머무를 때 쓸 수 있는 방법이에요.

스케줄러가 없으면 Pending — nodeName과 Binding으로 직접 배치
nodeName — 만들 때만 쓸 수 있다
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  nodeName: node02          # 스케줄러를 건너뛰고 바로 node02로
  containers:
  - name: nginx
    image: nginx
이미 있는 Pod의 nodeName은 못 바꾼다 Pod의 nodeName은 만든 뒤에는 고칠 수 없어요. 이미 Pending인 Pod를 배정하려면 스케줄러가 하는 것처럼 Binding 객체를 API로 보내거나, 보통은 그냥 Pod를 지우고 nodeName을 넣어 다시 만들어요(kubectl replace --force -f).

2. 스케줄러가 노드를 고르는 과정

스케줄러를 거칠 때 우리가 쓸 수 있는 손잡이는 아래와 같아요. 이번 부의 지도라고 보면 돼요.

손잡이누가 정하나뜻절
nodeSelector · Node AffinityPod“이런 라벨의 노드로 가고 싶다”04
Taint · Toleration노드 (+ Pod)“이 노드는 허락받은 Pod만 받는다”03
Pod Affinity · Topology SpreadPod“다른 Pod와 붙어서 / 떨어져서 가고 싶다”05
requestsPod“이만큼 자리가 있는 노드여야 한다”06
PriorityClassPod“자리가 없으면 덜 중요한 Pod를 밀어내라”08
schedulerName · 프로필Pod · 관리자“다른 스케줄러 · 다른 규칙으로 골라라”09

02. Labels와 Selectors

라벨은 쿠버네티스의 거의 모든 “고르기”의 바탕이에요(입문 3부 02). Service가 Pod를, ReplicaSet이 Pod를, Pod가 노드를 고를 때 모두 라벨을 써요.

1. 라벨로 묶고 고르기

라벨은 여러 개, 셀렉터는 조건 — 고르는 방법이 달라지면 묶음도 달라진다
명령과 YAML의 셀렉터
$ kubectl get pods -l app=web                      # 같다
$ kubectl get pods -l app=web,env=prod             # AND
$ kubectl get pods -l 'env in (dev,stg)'           # 집합
$ kubectl get pods -l '!canary'                     # 라벨이 없는 것
$ kubectl get pods --show-labels                    # 라벨 같이 보기
$ kubectl label pod web-1 tier=front               # 붙이기 ( 끝에 tier- 이면 떼기)
selector:
  matchLabels:              # 같다 (여러 개면 AND)
    app: web
  matchExpressions:         # 조건식: In · NotIn · Exists · DoesNotExist
  - key: env
    operator: In
    values: [dev, stg]

2. 어노테이션은 무엇이 다른가

어노테이션도 metadata에 붙는 키-값이지만, 고르는 데 쓸 수 없어요. 사람이나 도구가 읽을 부가 정보를 적는 칸이에요. 2부 05의 last-applied-configuration도 어노테이션이었죠.

라벨 (labels)어노테이션 (annotations)
셀렉터로 고르기됨안 됨
값짧은 이름 (63자 이내 규칙)긴 글 · JSON도 가능
예app=web · env=prod · tier=front빌드 번호 · 담당자 연락처 · 도구 설정(Ingress 컨트롤러 옵션 등)

03. Taints와 Tolerations

Taint(얼룩)는 노드에 붙이는 “접근 금지” 표시이고, Toleration(용인)은 Pod가 들고 다니는 “그 표시는 참을 수 있어요” 허가증이에요. 노드가 Pod를 밀어내는 방향이라는 점이 핵심이에요. 학습 자료의 비유로는 사람(노드)에게 벌레 퇴치 스프레이(taint)를 뿌리면, 그 냄새를 견디는 벌레(toleration이 있는 Pod)만 앉을 수 있어요.

1. 노드가 Pod를 밀어내는 방법

Taint는 밀어내고, Toleration은 허락할 뿐 — 끌어당기지 않는다
taint 붙이기 · 떼기 · 보기
$ kubectl taint nodes node01 app=blue:NoSchedule        # 키=값:효과
$ kubectl taint nodes node01 app=blue:NoSchedule-       # 끝에 - 면 떼기
$ kubectl describe node node01 | grep Taints
Taints:             app=blue:NoSchedule
Pod에 toleration 넣기
spec:
  tolerations:
  - key: app
    operator: Equal         # Equal(값까지 같아야) · Exists(키만 있으면)
    value: blue
    effect: NoSchedule
  containers:
  - name: nginx
    image: nginx

2. NoSchedule · PreferNoSchedule · NoExecute

효과새 Pod이미 돌던 Pod
NoScheduletoleration 없으면 배치 안 함그대로 둠
PreferNoSchedule가능하면 피함 (자리가 없으면 배치)그대로 둠
NoExecute배치 안 함toleration 없으면 쫓아냄 (tolerationSeconds만큼 기다렸다가)

1부 05에서 본 node.kubernetes.io/unreachable:NoExecute가 바로 이 NoExecute예요. Pod에 자동으로 붙은 tolerationSeconds: 300 덕분에 5분을 기다린 거고요.

3. 컨트롤 플레인 노드에 Pod가 안 뜨는 이유

kubeadm은 컨트롤 플레인 노드에 node-role.kubernetes.io/control-plane:NoSchedule taint를 붙여 둬요. 그래서 우리 앱은 워커 노드에만 가요. 실습 환경처럼 노드가 하나뿐이라 컨트롤 플레인에도 Pod를 띄워야 하면 이 taint를 떼요.

$ kubectl describe node controlplane | grep Taints
Taints:             node-role.kubernetes.io/control-plane:NoSchedule
$ kubectl taint nodes controlplane node-role.kubernetes.io/control-plane:NoSchedule-
Taint는 “여기로 와”가 아니다 toleration은 그 노드에 갈 수 있다는 허락일 뿐이에요. toleration이 있는 Pod도 스케줄러 점수에 따라 다른 노드로 갈 수 있어요. “파란 Pod는 반드시 파란 노드로”를 만들려면 다음 절의 Node Affinity를 같이 써야 해요.

04. Node Selector와 Node Affinity

Taint가 노드 쪽에서 밀어내는 방법이라면, 이번에는 Pod 쪽에서 노드를 고르는 방법이에요. 노드에 라벨을 붙이고, Pod는 그 라벨로 갈 곳을 정해요.

1. nodeSelector

$ kubectl label nodes node01 size=large            # 노드에 라벨 붙이기
$ kubectl get nodes -L size                          # 라벨을 열로 보기
spec:
  nodeSelector:
    size: large             # 이 라벨이 있는 노드에만

간단하지만 “large 또는 medium”, “small만 아니면” 같은 조건은 못 써요. 그래서 나온 게 Node Affinity예요.

2. required와 preferred

nodeSelector → required Affinity → preferred Affinity
node affinity
spec:
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:     # 반드시
        nodeSelectorTerms:
        - matchExpressions:
          - key: size
            operator: In          # In · NotIn · Exists · DoesNotExist · Gt · Lt
            values: [large, medium]
      preferredDuringSchedulingIgnoredDuringExecution:    # 가능하면
      - weight: 80                # 1~100, 점수에 더해짐
        preference:
          matchExpressions:
          - key: disk
            operator: In
            values: [ssd]
이름의 앞 절반뜻뒤 절반 IgnoredDuringExecution
requiredDuringScheduling…맞는 노드가 없으면 Pending이미 실행 중인 Pod는 노드 라벨이 바뀌어도 그대로 둔다
preferredDuringScheduling…맞는 노드에 점수를 더 줄 뿐, 없으면 다른 곳에 배치

3. Taint와 Affinity를 함께 쓰는 이유

“파란 팀 노드에는 파란 Pod만, 파란 Pod는 파란 노드에만”을 만들고 싶다고 해 봐요. 하나씩만 쓰면 빈틈이 생겨요.

방법막아 주는 것남는 빈틈
Taint · Toleration만다른 Pod가 파란 노드에 오는 것파란 Pod가 다른 노드로 갈 수 있음
Node Affinity만파란 Pod가 다른 노드로 가는 것다른 Pod가 파란 노드에 올 수 있음
둘 다양쪽 다없음 — 전용 노드(GPU · 팀 전용)를 만들 때의 정석

05. Pod끼리의 배치

지금까지는 “어느 노드로”였다면, 이번에는 “다른 Pod 옆으로 / 다른 Pod와 떨어져서”예요. 학습 자료에는 없지만 실제 운영에서 아주 자주 쓰는 설정이라 넣었어요. 레플리카 3개가 한 노드에 몰려 있으면 그 노드 하나만 죽어도 서비스가 멈추니까요.

1. Pod Affinity · Anti-Affinity

한 노드에 몰리지 않게 — Anti-Affinity와 Topology Spread
같은 앱끼리 노드를 나눠 쓰기 (Anti-Affinity)
spec:
  affinity:
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchLabels:
            app: web               # app=web Pod가 있는
        topologyKey: kubernetes.io/hostname   # “같은 노드”에는 가지 마

topologyKey는 “같은 곳”의 기준이에요. kubernetes.io/hostname이면 노드 단위, topology.kubernetes.io/zone이면 가용 영역(데이터센터) 단위예요. 반대로 캐시 Pod를 웹 Pod 옆에 두고 싶으면 podAffinity를 써요.

2. Topology Spread Constraints

Anti-Affinity의 required는 “한 노드에 하나만”이라 노드보다 레플리카가 많으면 Pending이 생겨요. Topology Spread는 “영역 사이 개수 차이가 maxSkew 이하”라는 더 유연한 규칙이라, 요즘은 고르게 퍼뜨릴 때 이쪽을 더 많이 써요.

spec:
  topologySpreadConstraints:
  - maxSkew: 1                               # 영역 사이 개수 차이 최대 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: DoNotSchedule         # 못 지키면 Pending (ScheduleAnyway면 최대한만)
    labelSelector:
      matchLabels:
        app: web

06. Resource Requirements

입문 3부 06에서 requests(최소 보장)와 limits(최대 한도)를 봤어요. 전반에서는 이게 스케줄링 · 실행 · 축출 세 단계에서 각각 어떻게 쓰이는지 정리해요.

1. requests와 limits

requests는 자리 예약, limits는 천장 — CPU는 느려지고 메모리는 죽는다
resources:
  requests:
    cpu: 250m          # 0.25 코어 (1 = vCPU 1개, 1m = 0.001)
    memory: 256Mi      # Mi = 1024², M = 1000² — 보통 Mi · Gi를 씀
  limits:
    cpu: 500m
    memory: 512Mi
단계쓰이는 값일어나는 일
스케줄링requests노드의 Allocatable에서 이미 예약된 requests 합을 빼고 남는 자리를 봐요. 실제 사용량은 안 봐요
실행 중 · CPUlimits.cpu넘으면 느려짐(throttling). 죽지는 않아요
실행 중 · 메모리limits.memory넘으면 컨테이너가 OOMKilled(exit 137) — 입문 1부 03의 cgroup
노드가 부족할 때QoS 등급kubelet이 Pod를 축출(eviction) — 아래 3번

2. LimitRange

Pod마다 requests · limits를 쓰는 걸 잊기 쉬워요. LimitRange는 Namespace 단위로 기본값과 최소 · 최대를 정해서, 안 쓴 Pod에는 기본값을 채워 주고 범위를 벗어난 Pod는 거절해요. 2부 04의 ResourceQuota가 “칸 전체 합계”라면 LimitRange는 “컨테이너 하나당”이에요.

apiVersion: v1
kind: LimitRange
metadata:
  name: cpu-mem-defaults
  namespace: dev
spec:
  limits:
  - type: Container
    defaultRequest:        # requests를 안 쓰면 이 값
      cpu: 100m
      memory: 128Mi
    default:               # limits를 안 쓰면 이 값
      cpu: 500m
      memory: 512Mi
    max:                   # 이보다 크게 쓰면 거절
      cpu: "2"
      memory: 2Gi
만들 때만 적용된다 LimitRange는 어드미션 단계(1부 04)에서 동작해서, 새로 만드는 Pod에만 적용돼요. 이미 돌던 Pod는 바뀌지 않아요.

3. QoS 클래스와 축출 순서

requests · limits를 어떻게 썼느냐에 따라 Pod는 자동으로 QoS 클래스를 받아요. 노드 메모리가 모자라면 kubelet은 이 등급과 사용량을 보고 덜 보호된 Pod부터 축출해요.

QoS 클래스조건메모리가 부족할 때
Guaranteed모든 컨테이너가 CPU · 메모리 모두 requests = limits가장 늦게 쫓겨남
Burstablerequests나 limits가 일부만 있음requests보다 많이 쓰는 Pod부터
BestEffortrequests · limits가 전혀 없음가장 먼저 쫓겨남
$ kubectl get pod web-1 -o jsonpath='{.status.qosClass}'
Burstable

07. DaemonSet과 Static Pod

지금까지는 “스케줄러가 어디에 둘지 고르는” Pod였어요. 이번에는 노드마다 하나씩 있어야 하는 Pod를 띄우는 두 방법이에요. 둘 다 노드에 붙어 있다는 점은 같지만, 만드는 주체가 달라요.

1. DaemonSet 복습

DaemonSet은 모든 노드(또는 고른 노드)에 Pod를 하나씩 띄워요(입문 4부 04). 로그 수집기, 모니터링 에이전트, 1부 07의 kube-proxy, CNI 플러그인이 대표적이에요. 노드가 추가되면 자동으로 하나가 더 생겨요.

$ kubectl get daemonsets -A
NAMESPACE     NAME         DESIRED   CURRENT   READY   NODE SELECTOR
kube-system   kube-proxy   3         3         3       kubernetes.io/os=linux
$ kubectl create deployment log-agent --image=fluent/fluent-bit --dry-run=client -o yaml > ds.yaml
# kind를 DaemonSet으로, replicas · strategy 줄을 지우면 DaemonSet YAML 완성
DaemonSet Pod도 스케줄러가 배치한다 예전에는 DaemonSet 컨트롤러가 직접 노드를 정했지만, 지금은 Pod마다 “이 노드에만” Node Affinity를 붙여서 기본 스케줄러가 배치해요. 그래서 taint가 있는 노드에 DaemonSet을 띄우려면 toleration이 필요해요(노드 장애 관련 toleration은 자동으로 붙어요).

2. kubelet이 직접 띄우는 Static Pod

Static Pod는 API 서버도 스케줄러도 거치지 않아요. kubelet이 자기 노드의 특정 폴더(보통 /etc/kubernetes/manifests)를 지켜보다가, 거기 있는 YAML을 직접 Pod로 띄워요. 1부 07에서 본 staticPodPath가 그 폴더예요.

폴더의 YAML → kubelet이 직접 실행 → API에는 거울 Pod만
Static Pod 폴더 찾기
$ ps -aux | grep kubelet | grep -o -- '--config=[^ ]*'
--config=/var/lib/kubelet/config.yaml
$ grep staticPodPath /var/lib/kubelet/config.yaml
staticPodPath: /etc/kubernetes/manifests
$ ls /etc/kubernetes/manifests
etcd.yaml  kube-apiserver.yaml  kube-controller-manager.yaml  kube-scheduler.yaml
$ kubectl get pods -A | grep -- -controlplane     # 이름 끝에 노드 이름이 붙으면 Static Pod
kube-system   etcd-controlplane      1/1   Running

3. 컨트롤 플레인이 Static Pod로 뜨는 이유

API 서버를 Pod로 띄우고 싶은데, Pod를 만들려면 API 서버가 있어야 해요. 닭과 달걀 문제죠. Static Pod는 API 서버 없이 kubelet만으로 뜨니까 이 문제를 풀어 줘요. kubeadm이 컨트롤 플레인 노드의 폴더에 etcd · kube-apiserver · kube-controller-manager · kube-scheduler YAML을 넣어 두는 이유예요.

DaemonSetStatic Pod
만드는 주체DaemonSet 컨트롤러 (API 서버 경유)각 노드의 kubelet
스케줄러거침안 거침
고치는 법kubectl edit · apply노드의 YAML 파일 수정
주로노드 에이전트 (로그 · 모니터링 · kube-proxy)컨트롤 플레인 부품

08. Priority Class

클러스터 자리가 꽉 찼을 때 무엇이 먼저냐를 정하는 게 PriorityClass예요. 결제 서비스와 야간 배치 작업이 자리를 다툰다면, 결제 서비스가 이겨야 하죠.

1. Pod의 우선순위

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 1000000              # 클수록 우선 (사용자는 10억 이하)
globalDefault: false        # true면 클래스를 안 쓴 Pod의 기본값
preemptionPolicy: PreemptLowerPriority   # Never면 기다리기만 하고 밀어내지 않음
description: "결제 등 핵심 서비스"
---
# Pod에서 사용
spec:
  priorityClassName: high-priority
PriorityClassvalue누가
system-node-critical2000001000노드에 꼭 있어야 하는 부품 (kube-proxy 등)
system-cluster-critical2000000000클러스터 핵심 부품 (CoreDNS 등)
사용자 정의~ 1000000000우리 앱
(없음)0클래스를 안 쓴 Pod (globalDefault가 없을 때)

2. 자리가 없을 때 선점(Preemption)

높은 우선순위 Pod가 오면 — 낮은 Pod를 밀어내고 자리를 만든다
우선순위는 순서이지 보장이 아니다 스케줄링 대기열에서 높은 Pod가 먼저 처리되고, 자리가 없으면 낮은 Pod를 밀어낼 뿐이에요. 밀려나는 Pod는 정상 종료 시간(graceful termination)을 받고 끝나요. 모든 걸 높게 잡으면 의미가 없어지니 등급은 몇 개만 두는 게 좋아요.

09. 여러 스케줄러

기본 스케줄러의 규칙이 맞지 않으면 다른 규칙의 스케줄러를 쓸 수 있어요. 방법은 두 가지예요. 스케줄러를 하나 더 띄우거나, 스케줄러 하나에 프로필을 여러 개 두는 것.

1. 스케줄러를 하나 더 띄우기

스케줄링 프레임워크 — 대기열 · 필터 · 점수 · 바인딩, 그리고 schedulerName
my-scheduler-config.yaml — 스케줄러 설정
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: my-scheduler
leaderElection:
  leaderElect: false        # 한 대만 띄울 때
Pod에서 고르기 · 누가 배치했는지 확인
spec:
  schedulerName: my-scheduler     # 비우면 default-scheduler
$ kubectl get events -o wide | grep Scheduled
... Scheduled   pod/nginx   my-scheduler   Successfully assigned default/nginx to node01
$ kubectl logs -n kube-system my-scheduler

추가 스케줄러는 보통 기본 스케줄러와 같은 이미지(registry.k8s.io/kube-scheduler)를 Deployment나 Static Pod로 띄우고, 위 설정 파일을 --config로 넘겨요. 설정 파일은 ConfigMap으로 만들어 볼륨으로 넣는 게 일반적이에요(5부 03).

2. 스케줄러 프로필과 플러그인

스케줄러를 여러 대 띄우면 서로 같은 노드에 동시에 Pod를 놓으려다 부딪칠 수 있어요. 그래서 요즘은 스케줄러 하나에 프로필을 여러 개 두는 방식을 권해요. 1부 06의 필터링 · 점수 단계는 실제로 확장 지점(extension point)마다 꽂힌 플러그인들이에요.

확장 지점하는 일대표 플러그인
queueSort대기열 순서PrioritySort (08의 우선순위)
filter안 되는 노드 빼기NodeResourcesFit · NodeName · TaintToleration · NodeAffinity · NodeUnschedulable
score남은 노드에 점수NodeResourcesFit · ImageLocality · TaintToleration · NodeAffinity
bind노드에 묶기DefaultBinder
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
profiles:
- schedulerName: default-scheduler
- schedulerName: no-scoring-scheduler    # 같은 스케줄러, 다른 규칙
  plugins:
    preScore:
      disabled:
      - name: '*'
    score:
      disabled:
      - name: '*'

3. 3부 셀프 체크

  1. 스케줄러가 없는 클러스터에서 Pod를 특정 노드에 띄우는 방법은?
    Pod를 만들 때 spec.nodeName을 써요. 이미 있는 Pod는 nodeName을 바꿀 수 없어서 Binding 객체를 보내거나, 지우고 다시 만들어요.
  2. 라벨과 어노테이션의 가장 큰 차이는?
    라벨은 셀렉터로 고를 수 있고, 어노테이션은 고를 수 없는 부가 정보예요.
  3. node01에 app=blue:NoSchedule taint가 있고 Pod에 맞는 toleration이 있으면, 그 Pod는 반드시 node01에 가나요?
    아니요. toleration은 “갈 수 있다”는 허락일 뿐이에요. 반드시 보내려면 Node Affinity(또는 nodeSelector)를 같이 써요.
  4. NoSchedule과 NoExecute의 차이는?
    NoSchedule은 새 Pod만 막고, NoExecute는 이미 돌던 Pod도 toleration이 없으면 쫓아내요.
  5. requiredDuringSchedulingIgnoredDuringExecution에서 뒤 절반의 뜻은?
    이미 실행 중인 Pod는 나중에 노드 라벨이 바뀌어도 쫓아내지 않는다는 뜻이에요.
  6. 레플리카 3개를 노드마다 흩어 놓고 싶어요. 노드가 2대일 때 required Anti-Affinity와 Topology Spread(maxSkew 1)의 결과 차이는?
    required Anti-Affinity는 2개만 배치되고 1개가 Pending이에요. Topology Spread는 2:1로 3개 모두 배치돼요(차이 1 이하).
  7. CPU limit과 메모리 limit을 넘으면 각각 어떻게 되나요?
    CPU는 느려지고(throttling), 메모리는 OOMKilled로 컨테이너가 재시작돼요.
  8. 스케줄러는 노드 자리를 실제 사용량으로 계산하나요?
    아니요, requests의 합으로 계산해요. 그래서 실제로는 한가해도 requests가 꽉 차면 Pending이 생겨요.
  9. Static Pod를 kubectl delete로 지우면?
    API에 보이는 건 거울(mirror) Pod라서, kubelet이 폴더의 YAML을 보고 다시 띄워요. 지우려면 노드의 /etc/kubernetes/manifests에서 파일을 지워요.
  10. 높은 PriorityClass의 Pod가 왔는데 자리가 없으면?
    스케줄러가 낮은 우선순위 Pod를 선점(축출)해서 자리를 만들어요. preemptionPolicy: Never면 밀어내지 않고 기다려요.
  11. Pod를 다른 스케줄러로 배치시키려면?
    Pod의 spec.schedulerName에 그 스케줄러(또는 프로필)의 이름을 써요. 누가 배치했는지는 이벤트의 source로 확인해요.
4부로깅과 모니터링지표와 kubectl top컴포넌트 상태로그 · EventsPrometheus · Grafana

01. 무엇을 지켜봐야 하나

클러스터가 돌기 시작하면 “지금 괜찮은가?”를 계속 확인해야 해요. 확인할 거리는 크게 지표(metrics) — 숫자로 된 상태, 로그(logs) — 프로그램이 남긴 글, 이벤트(events) — 쿠버네티스가 남긴 사건 기록, 이렇게 세 가지예요. 입문 7부 03에서 맛본 내용을 실제 명령과 구조로 정리해요.

1. 노드 · Pod · 컴포넌트 지표

대상지켜볼 것기본으로 보는 방법
노드CPU · 메모리 사용률, 디스크 · PID 부족, Ready 여부kubectl top node · kubectl describe node의 Conditions
Pod · 컨테이너CPU · 메모리 사용량, 재시작 횟수, 상태kubectl top pod · kubectl get pods
컨트롤 플레인API 서버 · etcd · 스케줄러 · 컨트롤러가 살아 있는지kubectl get pods -n kube-system · /readyz
애플리케이션요청 수 · 오류율 · 응답 시간 (앱이 직접 내보내는 지표)쿠버네티스 기본 기능에는 없음 → 04의 Prometheus
쿠버네티스에는 완성된 모니터링이 들어 있지 않다 쿠버네티스는 “지금 이 순간”의 자원 사용량을 보는 최소한의 통로(metrics-server)만 제공해요. 과거 기록, 그래프, 알림은 별도 도구(Prometheus · Grafana 등)로 꾸려요. 학습 자료에서도 이 둘을 구분해서 다뤄요.

2. metrics-server와 kubectl top

각 노드의 kubelet 안에는 cAdvisor라는 부품이 있어서 컨테이너별 CPU · 메모리 사용량을 재고 있어요. metrics-server는 모든 노드의 kubelet에서 이 숫자를 주기적으로 모아 메모리에만 들고 있다가, API 서버를 통해 kubectl top과 HPA(5부 07)에 내줘요.

kubelet(cAdvisor) → metrics-server → Metrics API → kubectl top · HPA
설치하고 보기
$ kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
$ kubectl top node
NAME           CPU(cores)   CPU(%)   MEMORY(bytes)   MEMORY(%)
controlplane   212m         10%      1180Mi          31%
node01         95m          4%       812Mi           21%
$ kubectl top pod -A --sort-by=memory           # 메모리 많이 쓰는 순
$ kubectl top pod web-7d4b9c-x2kfp --containers  # 컨테이너별로
error: Metrics API not available metrics-server가 없거나 아직 준비가 안 된 거예요. 설치 직후에는 첫 수집까지 1분 정도 걸려요. 실습 클러스터에서 kubelet 인증서 문제로 수집에 실패하면 metrics-server에 --kubelet-insecure-tls 옵션을 추가하는 경우가 많은데, 실습용 우회라 운영 환경에서는 쓰지 않아요.

02. 클러스터 컴포넌트 모니터링

1부에서 본 부품들이 살아 있는지 확인하는 방법이에요. 핵심은 부품이 어떻게 떠 있느냐에 따라 보는 방법이 다르다는 점이에요. Static Pod로 뜬 컨트롤 플레인은 kubectl로, systemd 서비스인 kubelet은 노드에 들어가서 봐요.

1. 컨트롤 플레인 Pod 상태 보기

$ kubectl get pods -n kube-system                         # 컨트롤 플레인 Pod가 Running인가
$ kubectl logs -n kube-system kube-apiserver-controlplane  # Static Pod 로그도 kubectl logs로
$ kubectl get --raw='/readyz?verbose'                     # API 서버의 자체 점검
[+]ping ok
[+]log ok
[+]etcd ok
...
readyz check passed
$ kubectl describe node node01 | grep -A6 Conditions      # 노드 상태
  Type             Status  Reason
  MemoryPressure   False   KubeletHasSufficientMemory
  DiskPressure     False   KubeletHasNoDiskPressure
  PIDPressure      False   KubeletHasSufficientPID
  Ready            True    KubeletReady
학습 자료의 kubectl get componentstatuses 예전에는 이 명령으로 스케줄러 · 컨트롤러 · etcd 상태를 한 번에 봤어요. 1.19부터 지원 중단 예고 상태라 결과가 정확하지 않을 수 있어요. 지금은 위처럼 kube-system Pod와 /readyz · /livez를 보는 게 표준이에요.

2. kubelet은 systemd 서비스: systemctl · journalctl

노드가 NotReady면 대개 그 노드의 kubelet에 문제가 있어요. kubelet은 Pod가 아니라서 kubectl logs로는 못 보고, 노드에 접속해서 systemd 도구로 봐요. 노드 장애 대응(16부)의 기본 순서예요.

노드가 NotReady일 때 — kubectl에서 시작해 노드 안의 kubelet까지
부품떠 있는 방식로그 · 상태 보는 법
API 서버 · etcd · 스케줄러 · 컨트롤러Static Pod (kubeadm)kubectl logs -n kube-system … / API 서버가 죽었으면 노드에서 crictl ps -a · crictl logs
kube-proxy · CoreDNSDaemonSet · Deploymentkubectl logs -n kube-system …
kubelet · containerdsystemd 서비스노드에서 systemctl status kubelet · journalctl -u kubelet
직접 설치(“the hard way”)한 컨트롤 플레인systemd 서비스journalctl -u kube-apiserver 등

03. 애플리케이션 로그

컨테이너 앱은 로그를 파일이 아니라 표준 출력(stdout) · 표준 에러(stderr)로 내보내는 게 원칙이에요. 그러면 컨테이너 런타임이 받아서 노드에 저장하고, kubectl logs로 어디서든 볼 수 있어요.

1. kubectl logs

$ kubectl logs web-7d4b9c-x2kfp                  # 기본 (컨테이너가 하나일 때)
$ kubectl logs web-7d4b9c-x2kfp -c log-agent     # 컨테이너가 여러 개면 -c로 지정 (5부 05)
$ kubectl logs -f web-7d4b9c-x2kfp               # 계속 따라 보기 (tail -f처럼)
$ kubectl logs web-7d4b9c-x2kfp --previous       # 재시작 직전 컨테이너의 로그 (CrashLoopBackOff 원인 찾기)
$ kubectl logs web-7d4b9c-x2kfp --tail=50 --since=10m
$ kubectl logs deploy/web                        # Deployment의 Pod 중 하나
$ kubectl logs -l app=web --prefix --all-containers   # 라벨로 여러 Pod 한꺼번에

2. 로그는 노드 어디에 쌓이나

stdout → 노드의 파일 → kubelet → kubectl logs, 그리고 Pod가 사라지면
$ ls /var/log/pods/                               # 노드에서
default_web-7d4b9c-x2kfp_6f1c2a9e-.../
$ ls /var/log/pods/default_web-7d4b9c-x2kfp_*/nginx/
0.log  1.log                                      # 숫자 = 재시작 횟수
$ ls -l /var/log/containers/ | head -2            # 찾기 쉬운 이름의 바로가기(심볼릭 링크)
web-7d4b9c-x2kfp_default_nginx-9b1e0c7a....log -> /var/log/pods/...
kubectl logs는 저장소가 아니다 노드의 로그 파일은 kubelet이 크기 기준(기본 10Mi × 5개)으로 돌려 쓰고, Pod가 지워지면 함께 사라져요. 노드가 죽으면 그 노드의 로그도 같이 잃어요. 그래서 운영 환경에서는 노드마다 로그 수집기(Fluent Bit 같은 DaemonSet, 3부 07)를 띄워 로그를 중앙 저장소(Loki · Elasticsearch · 클라우드 로깅)로 보내요.

3. Events로 원인 찾기

로그가 앱이 남긴 기록이라면, Event는 쿠버네티스 부품이 남긴 기록이에요. 1부 01에서 본 Scheduled · Pulling · Started가 Event였죠. Pod가 아예 안 뜨면 앱 로그가 없으니 Event가 유일한 단서예요.

$ kubectl describe pod web-7d4b9c-x2kfp | tail -5      # 맨 아래 Events
$ kubectl events --for pod/web-7d4b9c-x2kfp            # 특정 객체의 이벤트
$ kubectl get events --field-selector type=Warning -A  # 경고만, 모든 Namespace
$ kubectl get events --sort-by=.lastTimestamp          # 시간순
자주 보는 Warning남긴 곳뜻
FailedScheduling스케줄러갈 노드가 없음 (3부)
FailedCreate컨트롤러쿼터 · 어드미션에 막혀 Pod를 못 만듦 (2부 04)
Failed · ErrImagePullkubelet이미지를 못 받음
BackOffkubelet컨테이너가 계속 죽어서 재시작 대기 (CrashLoopBackOff)
Unhealthykubeletprobe 실패 (5부 06)
Evictedkubelet노드 자원 부족으로 축출 (3부 06)
Event는 1시간 뒤 사라진다 Event는 etcd에 쌓이는 걸 막으려고 기본 1시간(API 서버의 --event-ttl)만 보관해요. 어제 일어난 일은 Event로 찾을 수 없으니, 오래 남겨야 하면 로그처럼 수집 도구로 따로 모아요.

04. 모니터링 도구 맛보기

metrics-server는 “지금”만 알아요. “어젯밤 3시에 메모리가 왜 튀었지?”, “오류율이 5%를 넘으면 알려 줘” 같은 일은 별도 도구로 해요. 사실상 표준 조합은 Prometheus + Grafana예요. 학습 범위 밖이지만, 실무에서 반드시 만나는 도구라 구조만 짚어요.

Prometheus가 가져가고(pull), Grafana가 그리고, Alertmanager가 알린다
구성 요소하는 일
Prometheus대상들의 /metrics를 주기적으로 가져와서(pull) 시계열로 저장. PromQL로 조회
node-exporter노드의 CPU · 메모리 · 디스크 · 네트워크 지표 (DaemonSet)
kube-state-metrics쿠버네티스 객체 상태를 지표로 (Deployment의 원하는/현재 레플리카, Pod 재시작 수 등)
GrafanaPrometheus를 데이터 소스로 대시보드 · 그래프
AlertmanagerPrometheus 경고 규칙이 울리면 슬랙 · 메일 등으로 알림 (중복 묶기 · 무음 처리)
PromQL 맛보기
# 지난 5분 동안 Pod별 CPU 사용량 (코어)
sum by (pod) (rate(container_cpu_usage_seconds_total{namespace="default"}[5m]))

# 최근 1시간 재시작이 있었던 컨테이너
increase(kube_pod_container_status_restarts_total[1h]) > 0
설치는 보통 Helm 차트 한 번으로 Prometheus · Grafana · Alertmanager · node-exporter · kube-state-metrics를 한 번에 설치하는 kube-prometheus-stack Helm 차트를 많이 써요(Helm은 13부). 로그 쪽은 Loki(+Grafana), 지표 · 로그 · 추적을 하나의 규격으로 모으는 OpenTelemetry도 함께 알아 두면 좋아요.

1. 4부 셀프 체크

  1. kubectl top이 보여 주는 숫자는 누가 재고, 누가 모으나요?
    각 노드 kubelet 안의 cAdvisor가 재고, metrics-server가 주기적으로 모아 Metrics API로 내줘요.
  2. metrics-server로 “어제 오후 3시의 메모리 사용량”을 볼 수 있나요?
    없어요. metrics-server는 최신 값만 메모리에 들고 있어요. 과거 기록은 Prometheus 같은 시계열 저장소가 필요해요.
  3. 노드가 NotReady일 때 kubelet 로그를 보는 방법은?
    노드에 접속해 systemctl status kubelet으로 상태를, journalctl -u kubelet으로 로그를 봐요. kubelet은 Pod가 아니라서 kubectl logs로는 못 봐요.
  4. 컨테이너가 계속 재시작될 때 죽기 직전 로그를 보는 옵션은?
    kubectl logs <pod> --previous (-p). 바로 이전 컨테이너 하나의 로그만 남아 있어요.
  5. 컨테이너가 두 개인 Pod의 로그를 볼 때 필요한 옵션은?
    -c <컨테이너 이름>. 모두 보려면 --all-containers.
  6. 운영 환경에서 로그 수집기를 따로 두는 이유는?
    노드의 로그 파일은 크기 제한으로 돌려 쓰이고 Pod가 지워지거나 노드가 죽으면 사라지기 때문이에요. DaemonSet 수집기로 중앙 저장소에 보내요.
  7. Pod가 Pending에서 안 넘어가요. 로그와 Event 중 무엇을 먼저 보나요?
    Event예요. 컨테이너가 뜨지 않았으니 앱 로그가 없어요. kubectl describe pod의 Events에서 FailedScheduling 같은 원인을 봐요.
  8. Prometheus가 지표를 모으는 방식은 push와 pull 중 무엇인가요?
    pull이에요. 대상들이 /metrics로 지표를 내놓으면 Prometheus가 주기적으로 가져가요.
5부애플리케이션 라이프사이클배포와 롤백명령 · 인자ConfigMap · Secret멀티 컨테이너헬스 체크오토스케일링Job

01. 롤링 업데이트와 롤백

앱을 만들고, 설정을 넣고, 새 버전으로 바꾸고, 문제가 생기면 되돌리고, 부하에 따라 늘리고 줄이는 것까지 — 앱이 태어나서 운영되는 동안의 일을 모은 부예요. 먼저 새 버전 배포부터 봐요. 입문 4부 03에서 롤링 업데이트가 “하나씩 바꾼다”는 걸 봤다면, 여기서는 몇 개씩, 어떤 순서로 바꿀지 정하는 손잡이를 봐요.

1. RollingUpdate와 Recreate

Recreate는 끊기고, RollingUpdate는 끊기지 않는다 — maxSurge · maxUnavailable
spec:
  replicas: 4
  strategy:
    type: RollingUpdate      # 기본값. Recreate면 전부 지우고 새로 띄움
    rollingUpdate:
      maxSurge: 25%          # 원하는 개수보다 “더” 띄워도 되는 수 (기본 25%)
      maxUnavailable: 25%    # 원하는 개수보다 “덜” 있어도 되는 수 (기본 25%)
  minReadySeconds: 10        # Ready 후 이만큼 버텨야 “사용 가능”으로 인정
  revisionHistoryLimit: 10   # 롤백용으로 남길 옛 ReplicaSet 수

2. maxSurge · maxUnavailable

설정 (replicas 4)한 번에특징
maxSurge: 1 · maxUnavailable: 1 (기본 25%)최대 5개까지 · 최소 3개 유지균형. 대부분 이걸로 충분
maxSurge: 1 · maxUnavailable: 0새 것 하나 Ready → 옛 것 하나 삭제가용 수가 절대 안 줄어 안전, 대신 느리고 자원이 더 필요
maxSurge: 0 · maxUnavailable: 1하나 지우고 → 하나 띄우기추가 자원 없이 교체 (노드가 빠듯할 때)
type: Recreate전부 지우고 전부 띄우기중단 시간 발생. 두 버전이 동시에 돌면 안 되는 앱에만

3. rollout status · history · undo

$ kubectl set image deployment/web nginx=nginx:1.28
$ kubectl annotate deployment/web kubernetes.io/change-cause="nginx 1.28로 업데이트"   # 기록 남기기
$ kubectl rollout status deployment/web           # 끝날 때까지 진행 상황
Waiting for deployment "web" rollout to finish: 2 of 4 updated replicas are available...
deployment "web" successfully rolled out
$ kubectl rollout history deployment/web
REVISION  CHANGE-CAUSE
1         <none>
2         nginx 1.28로 업데이트
$ kubectl rollout undo deployment/web             # 바로 이전 리비전으로
$ kubectl rollout undo deployment/web --to-revision=1
$ kubectl rollout restart deployment/web          # 같은 버전으로 Pod만 차례로 새로 (설정 다시 읽기 등)
학습 자료의 --record 예전에는 kubectl set image ... --record로 CHANGE-CAUSE를 남겼어요. 이 옵션은 지원 중단 예고 상태라, 지금은 위처럼 kubernetes.io/change-cause 어노테이션을 직접 붙여요.

02. 명령과 인자

컨테이너가 시작할 때 무엇을 실행할지는 이미지에 정해져 있어요. 쿠버네티스는 이걸 Pod YAML에서 바꿀 수 있게 해 주는데, Docker와 이름이 달라서 헷갈려요. 짝만 외우면 끝이에요.

1. Docker의 ENTRYPOINT · CMD

ENTRYPOINT ↔ command, CMD ↔ args
Dockerfile — ubuntu-sleeper 이미지
FROM ubuntu
ENTRYPOINT ["sleep"]     # 항상 실행할 프로그램
CMD ["5"]                # 기본 인자 (바꿀 수 있음)

2. 쿠버네티스의 command · args

spec:
  containers:
  - name: sleeper
    image: ubuntu-sleeper
    command: ["sleep2.0"]   # ENTRYPOINT를 덮어씀
    args: ["10"]            # CMD를 덮어씀
Docker (이미지)쿠버네티스 (Pod)쓰지 않으면
ENTRYPOINTcommand이미지의 ENTRYPOINT
CMDargs이미지의 CMD
$ kubectl run sleeper --image=ubuntu-sleeper -- 10                 # -- 뒤는 args
$ kubectl run sleeper --image=busybox --command -- sleep 1000      # --command면 command
이름이 엇갈려서 생기는 실수 쿠버네티스의 command는 Docker의 CMD가 아니에요. ENTRYPOINT예요. 그리고 배열은 원소마다 따로 써야 해요. command: ["sleep 10"]은 “sleep 10”이라는 이름의 프로그램을 찾다가 실패해요. ["sleep", "10"]으로 나눠요.

03. 환경 변수와 ConfigMap

같은 이미지를 dev · prod에서 다른 설정으로 돌리려면 설정을 이미지 밖에 둬야 해요(입문 6부 01). 가장 단순한 방법은 환경 변수이고, 여러 값을 묶어 관리하는 게 ConfigMap이에요.

1. env로 값 넣기

spec:
  containers:
  - name: web
    image: web:1.0
    env:
    - name: APP_COLOR          # 직접 값
      value: blue
    - name: APP_MODE           # ConfigMap의 키 하나
      valueFrom:
        configMapKeyRef:
          name: app-config
          key: APP_MODE
    - name: DB_PASSWORD        # Secret의 키 하나 (04)
      valueFrom:
        secretKeyRef:
          name: db-secret
          key: DB_PASSWORD

2. ConfigMap — 통째로 넣기와 파일로 넣기

ConfigMap을 환경 변수로, 파일로 — 그리고 값이 바뀌었을 때의 차이
$ kubectl create configmap app-config \
    --from-literal=APP_COLOR=blue --from-literal=APP_MODE=prod
$ kubectl create configmap nginx-conf --from-file=nginx.conf     # 파일 이름이 키, 내용이 값
spec:
  containers:
  - name: web
    image: web:1.0
    envFrom:                       # 모든 키를 환경 변수로
    - configMapRef:
        name: app-config
    volumeMounts:                  # 키마다 파일 하나로
    - name: conf
      mountPath: /etc/nginx/conf.d
  volumes:
  - name: conf
    configMap:
      name: nginx-conf
넣는 방법ConfigMap을 바꾸면주로
env · envFrom바뀌지 않음 — 컨테이너를 다시 시작해야 반영 (rollout restart)짧은 설정값
볼륨 (파일)잠시 뒤(보통 1분 안팎) 파일이 자동으로 바뀜. 앱이 다시 읽어야 효과 (subPath로 넣은 파일은 안 바뀜)설정 파일 통째
바뀌면 안 되는 설정은 immutable ConfigMap · Secret에 immutable: true를 쓰면 실수로 고치는 걸 막고, kubelet이 변경을 계속 지켜볼 필요가 없어 큰 클러스터에서 API 서버 부담도 줄어요. 바꾸려면 새 이름으로 만들어 Deployment가 가리키게 해요.

04. Secret과 암호화

비밀번호 · 토큰 · 인증서는 ConfigMap 대신 Secret에 둬요(입문 6부 02). 사용법은 ConfigMap과 거의 같지만, 이름과 달리 기본 상태로는 그렇게 비밀스럽지 않다는 점을 꼭 알아야 해요.

1. Secret 사용법

$ kubectl create secret generic db-secret \
    --from-literal=DB_HOST=mysql --from-literal=DB_PASSWORD='p@ssw0rd'
$ kubectl get secret db-secret -o yaml
data:
  DB_HOST: bXlzcWw=
  DB_PASSWORD: cEBzc3cwcmQ=
spec:
  containers:
  - name: web
    image: web:1.0
    envFrom:
    - secretRef:
        name: db-secret           # 모든 키를 환경 변수로
    volumeMounts:
    - name: db
      mountPath: /etc/db          # 키마다 파일 (메모리 기반 tmpfs라 디스크에 안 남음)
      readOnly: true
  volumes:
  - name: db
    secret:
      secretName: db-secret

2. base64는 암호화가 아니다

base64는 포장일 뿐 — etcd에 암호화해서 저장하려면 따로 설정한다
$ echo 'cEBzc3cwcmQ=' | base64 -d
p@ssw0rd

3. etcd에 암호화해서 저장하기

API 서버에 EncryptionConfiguration 파일을 주면, Secret을 etcd에 쓸 때 암호화하고 읽을 때 풀어요. 사용자 입장에서는 아무것도 바뀌지 않고, etcd를 통째로 훔쳐 가도 값을 읽을 수 없게 돼요.

/etc/kubernetes/enc/enc.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
  - secrets
  providers:
  - aescbc:                     # 맨 위 provider로 “쓰기”
      keys:
      - name: key1
        secret: <32바이트 키를 base64로>   # head -c 32 /dev/urandom | base64
  - identity: {}                # 암호화 안 된 옛 데이터 “읽기”용
# kube-apiserver.yaml에 플래그 추가 + 위 파일을 볼륨으로 마운트
    - --encryption-provider-config=/etc/kubernetes/enc/enc.yaml

$ kubectl get secrets -A -o json | kubectl replace -f -     # 이미 있던 Secret을 다시 써서 암호화
$ etcdctl get /registry/secrets/default/db-secret | hexdump -C | head -2
... 6b 38 73 3a 65 6e 63 3a 61 65 73 63 62 63 3a 76  |k8s:enc:aescbc:v|   ← 암호화됨
Secret을 지키는 건 암호화 하나가 아니다 etcd 암호화는 “etcd 파일을 훔쳐 간 경우”를 막아요. 그 밖에 RBAC으로 Secret을 읽을 수 있는 사람을 줄이고(7부 07), Secret을 Git에 그대로 올리지 않는 게 기본이에요. 운영 환경에서는 키를 외부 KMS에 두는 kms provider나, Vault · 클라우드 비밀 관리자와 연결하는 External Secrets · Secrets Store CSI Driver를 많이 써요.

05. 멀티 컨테이너 Pod

Pod 하나에 컨테이너를 여러 개 넣을 수 있어요(입문 3부 03). 같은 Pod의 컨테이너는 네트워크(localhost)와 볼륨을 공유하고, 함께 만들어지고 함께 사라져요.

1. 한 Pod에 여러 컨테이너를 두는 이유

원칙은 “Pod 하나 = 앱 하나”예요. 그래도 컨테이너를 묶는 건 본 앱을 돕는 작은 일이 본 앱과 반드시 같이 있어야 할 때예요. 웹 서버와 DB처럼 따로 늘리고 줄여야 하는 것은 묶지 않아요.

2. 디자인 패턴: 사이드카 · 어댑터 · 앰배서더

사이드카는 거들고, 어댑터는 바꾸고, 앰배서더는 대신 나간다

3. Init Containers

Init 컨테이너는 본 컨테이너보다 먼저, 하나씩 차례로 실행되고 끝나야 하는 컨테이너예요. DB가 뜰 때까지 기다리기, 설정 파일 내려받기, 권한 맞추기 같은 준비 작업에 써요. 하나라도 실패하면 Pod의 restartPolicy에 따라 다시 시도하고, 본 컨테이너는 시작하지 않아요.

Init 컨테이너는 순서대로 끝나야 하고, 네이티브 사이드카는 먼저 떠서 끝까지 남는다
spec:
  initContainers:
  - name: wait-db                     # 1번: DB Service가 DNS에 뜰 때까지
    image: busybox
    command: ["sh", "-c", "until nslookup db; do sleep 2; done"]
  - name: fetch-config                # 2번: 1번이 끝나야 시작
    image: busybox
    command: ["sh", "-c", "wget -O /work/app.conf http://config/app.conf"]
  containers:
  - name: web                         # init이 모두 끝나야 시작
    image: web:1.0
$ kubectl get pod web
NAME   READY   STATUS     RESTARTS
web    0/1     Init:1/2   0            # init 2개 중 1개 완료
$ kubectl logs web -c wait-db          # init 컨테이너 로그도 -c로

4. 네이티브 사이드카 컨테이너

예전 사이드카는 그냥 containers에 같이 넣었어요. 그러면 시작 순서를 보장할 수 없고, Job에서는 본 작업이 끝나도 사이드카가 안 끝나서 Job이 완료되지 않는 문제가 있었어요. 그래서 initContainers에 restartPolicy: Always를 단 네이티브 사이드카가 생겼고, 1.33에서 정식(GA)이 됐어요.

spec:
  initContainers:
  - name: log-shipper
    image: fluent/fluent-bit
    restartPolicy: Always       # ← 이 한 줄이 “사이드카”라는 표시
  containers:
  - name: web
    image: web:1.0
일반 init 컨테이너네이티브 사이드카예전 방식 사이드카 (containers)
시작본 컨테이너 전, 차례로본 컨테이너 전에 시작본 컨테이너와 동시 (순서 없음)
끝끝나야 다음으로끝나지 않음 · 본 컨테이너 후에 종료본 컨테이너와 따로
Job에서—Job 완료를 막지 않음사이드카가 안 끝나 Job이 완료 안 됨
probe없음readiness · liveness · startup 가능가능

06. 헬스 체크

학습 자료에는 없지만 운영에서 빠질 수 없는 설정이에요(입문 3부 05). 컨테이너 프로세스가 살아 있어도 앱이 멈춰 있거나 아직 준비 중일 수 있어요. kubelet은 probe로 앱에 직접 물어보고, 결과에 따라 트래픽을 끊거나 컨테이너를 재시작해요.

startup은 기다려 주고, readiness는 트래픽을 끊고, liveness는 재시작한다
containers:
- name: web
  image: web:1.0
  startupProbe:                 # 성공할 때까지 아래 둘을 멈춰 둠
    httpGet: { path: /healthz, port: 8080 }
    failureThreshold: 30        # 30번 × 10초 = 최대 5분 기다림
    periodSeconds: 10
  readinessProbe:               # 실패하면 Service 트래픽에서 제외
    httpGet: { path: /ready, port: 8080 }
    periodSeconds: 5
  livenessProbe:                # 연속 실패하면 컨테이너 재시작
    httpGet: { path: /healthz, port: 8080 }
    periodSeconds: 10
    failureThreshold: 3
probe실패하면주의
startupProbe계속 기다림 (한도를 넘으면 재시작)시작이 느린 앱에 — 이게 있으면 나머지는 성공 후에 시작
readinessProbeEndpointSlice에서 빠짐 (2부 03)Pod는 그대로. 잠깐 바쁠 때 트래픽만 쉬게
livenessProbe컨테이너 재시작DB 같은 외부 의존성까지 검사하면 DB 장애 때 전부 재시작되는 사고가 남

검사 방식은 httpGet(2xx · 3xx면 성공), tcpSocket(포트 연결), exec(명령 종료 코드 0), grpc 네 가지예요. 기본값은 periodSeconds: 10 · timeoutSeconds: 1 · failureThreshold: 3이에요.

07. 오토스케일링

부하에 맞춰 자원을 바꾸는 방법은 두 방향이에요. Pod 개수를 바꾸는 수평(HPA), Pod 크기(requests · limits)를 바꾸는 수직(VPA · In-place Resize). 노드 개수를 바꾸는 클러스터 오토스케일러는 입문 7부 02에서 봤어요.

1. HPA

HPA는 개수를, In-place Resize와 VPA는 크기를 바꾼다
$ kubectl autoscale deployment web --cpu-percent=50 --min=2 --max=10
$ kubectl get hpa
NAME   REFERENCE        TARGETS        MINPODS   MAXPODS   REPLICAS
web    Deployment/web   cpu: 90%/50%   2         10        3
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50     # requests 대비 평균 50%를 목표로
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300   # 줄일 때는 5분간 지켜보고 (기본값)
HPA의 계산식 필요한 레플리카 = 올림(현재 레플리카 × 현재 값 ÷ 목표 값). 3개가 평균 90%이고 목표가 50%면 올림(3 × 90 ÷ 50) = 6개예요. 사용률은 requests 대비라서 requests가 없는 Pod는 HPA가 계산하지 못해요(3부 06). metrics-server(4부 01)도 꼭 있어야 해요.

2. In-place Pod Resize (재시작 없이 자원 바꾸기)

예전에는 Pod의 requests · limits를 바꾸려면 Pod를 새로 만들어야 했어요. 1.35에서 정식(GA)이 된 In-place Pod Resize는 실행 중인 Pod의 CPU · 메모리를 그 자리에서 바꿔요. 학습 자료의 “Place Resize of Pods”가 이거예요.

$ kubectl patch pod web-1 --subresource resize --patch \
    '{"spec":{"containers":[{"name":"web","resources":{"requests":{"cpu":"1"},"limits":{"cpu":"1"}}}]}}'
$ kubectl get pod web-1 -o jsonpath='{.status.containerStatuses[0].resources}'   # 실제 적용된 값
containers:
- name: web
  image: web:1.0
  resizePolicy:                  # 자원별로 재시작이 필요한지
  - resourceName: cpu
    restartPolicy: NotRequired   # 재시작 없이 (기본값)
  - resourceName: memory
    restartPolicy: RestartContainer   # 메모리는 컨테이너 재시작으로 반영

노드에 자리가 없으면 바로 적용되지 않고 Pod 조건에 PodResizePending이 붙어요. Deployment의 템플릿은 바뀌지 않으니, 영구적으로 바꾸려면 Deployment도 같이 고쳐요.

3. VPA

VPA(Vertical Pod Autoscaler)는 사용량 기록을 보고 적정 requests를 추천하고, 설정에 따라 적용까지 해요. 쿠버네티스 기본 기능이 아니라 별도로 설치하는 프로젝트예요(kubernetes/autoscaler).

updateMode동작
Off추천만 계산 (kubectl describe vpa로 확인). 처음엔 이걸로 시작해 보는 게 안전
Initial새로 만들어지는 Pod에만 추천값 적용
Recreate추천과 많이 다르면 Pod를 축출하고 다시 만들어 적용
InPlaceOrRecreate먼저 In-place Resize로 시도하고, 안 되면 다시 만들기 (최근 버전)
HPA와 VPA를 같은 지표로 같이 쓰지 않는다 둘 다 CPU를 보면 HPA는 개수를, VPA는 크기를 바꾸려고 서로 싸워요. 함께 쓸 땐 HPA는 요청 수 같은 다른 지표로, VPA는 추천(Off)만 쓰는 식으로 나눠요.

08. Job과 CronJob

웹 서버처럼 계속 떠 있는 게 아니라, 할 일을 마치면 끝나는 작업용 리소스예요(입문 4부 05). 학습 목차엔 없지만 CKA 범위이고 실무에서도 자주 써서 넣었어요.

1. 끝나는 작업 — Job

Job은 성공 횟수를 채우고, CronJob은 정해진 시간마다 Job을 만든다
apiVersion: batch/v1
kind: Job
metadata:
  name: report
spec:
  completions: 5               # 성공이 5번 필요
  parallelism: 2               # 동시에 2개까지
  backoffLimit: 4              # 실패 재시도 한도 (기본 6)
  activeDeadlineSeconds: 600   # 10분 넘으면 강제 종료
  ttlSecondsAfterFinished: 3600   # 끝나고 1시간 뒤 자동 삭제
  template:
    spec:
      restartPolicy: Never     # Job은 Never 또는 OnFailure만
      containers:
      - name: report
        image: busybox
        command: ["sh", "-c", "echo making report; sleep 5"]

2. 정해진 시간에 실행 — CronJob

apiVersion: batch/v1
kind: CronJob
metadata:
  name: nightly-report
spec:
  schedule: "30 2 * * *"        # 분 시 일 월 요일 → 매일 02:30
  timeZone: "Asia/Seoul"        # 없으면 컨트롤러 기준 시간대(보통 UTC)
  concurrencyPolicy: Forbid     # 이전 실행이 안 끝났으면 건너뜀 (Allow · Replace)
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
          - name: report
            image: busybox
            command: ["sh", "-c", "date; echo nightly"]
$ kubectl create cronjob nightly-report --image=busybox --schedule="30 2 * * *" -- date
$ kubectl create job manual-run --from=cronjob/nightly-report   # 일정과 상관없이 지금 한 번
$ kubectl get cronjob,job,pods

3. 5부 셀프 체크

  1. replicas 4, maxSurge 1, maxUnavailable 0이면 업데이트 중 Pod 수는 어떻게 움직이나요?
    최대 5개까지 늘었다가, 새 Pod가 Ready가 될 때마다 옛 Pod를 하나씩 지워요. 사용 가능한 Pod는 4개 아래로 내려가지 않아요.
  2. 배포가 잘못됐을 때 바로 전 버전으로 되돌리는 명령은?
    kubectl rollout undo deployment/web. 특정 리비전은 --to-revision=N.
  3. Docker의 ENTRYPOINT와 CMD에 대응하는 Pod의 칸은?
    ENTRYPOINT ↔ command, CMD ↔ args.
  4. ConfigMap 값을 바꿨는데 앱이 옛 값을 써요. 왜 그럴까요?
    환경 변수(env · envFrom)로 넣었다면 컨테이너가 시작할 때 한 번만 읽어요. kubectl rollout restart로 다시 시작해야 해요. 볼륨으로 넣었다면 파일은 바뀌지만 앱이 다시 읽어야 해요.
  5. Secret의 값이 base64로 되어 있으니 안전한가요?
    아니요. base64는 누구나 풀 수 있는 인코딩이에요. etcd 저장 암호화(EncryptionConfiguration) · RBAC · 외부 비밀 관리자로 보호해요.
  6. EncryptionConfiguration을 적용한 뒤 기존 Secret도 암호화하려면?
    kubectl get secrets -A -o json | kubectl replace -f -로 다시 써요. 새로 쓰일 때 암호화돼요.
  7. init 컨테이너와 네이티브 사이드카의 차이는?
    init은 차례로 실행되어 끝나야 다음으로 가요. 네이티브 사이드카(restartPolicy: Always)는 본 컨테이너보다 먼저 떠서 끝까지 함께 돌고, 본 컨테이너가 끝난 뒤 멈춰요.
  8. readiness와 liveness가 실패했을 때의 차이는?
    readiness 실패는 트래픽에서만 제외, liveness 실패는 컨테이너 재시작이에요.
  9. HPA 목표 50%, 현재 4개가 평균 75%면 몇 개로 늘어나나요?
    올림(4 × 75 ÷ 50) = 6개.
  10. Pod를 재시작하지 않고 CPU를 늘리는 방법은?
    In-place Pod Resize: kubectl patch pod ... --subresource resize 또는 kubectl edit pod ... --subresource resize.
  11. CronJob이 이전 실행이 끝나지 않았을 때 새 실행을 건너뛰게 하려면?
    concurrencyPolicy: Forbid. Replace는 이전 것을 멈추고 새로 시작해요.
6부클러스터 유지보수노드 점검 · drainPodDisruptionBudget버전 규칙kubeadm 업그레이드etcd 백업 · 복구

01. 노드 점검과 OS 업그레이드

노드도 결국 리눅스 서버라 보안 패치 · 커널 업데이트 · 재부팅이 필요해요. 그냥 껐다 켜면 그 위의 Pod가 갑자기 사라지니, 미리 Pod를 다른 노드로 비워 두고 작업하는 게 정석이에요.

1. drain · cordon · uncordon

cordon으로 막고, drain으로 비우고, 작업한 뒤 uncordon
$ kubectl cordon node01                     # 새 Pod 배치만 막기 (기존 Pod는 그대로)
$ kubectl drain node01 --ignore-daemonsets  # cordon + Pod 내보내기
node/node01 cordoned
evicting pod default/web-7d4b9c-x2kfp
pod/web-7d4b9c-x2kfp evicted
node/node01 drained
$ kubectl get nodes
node01   Ready,SchedulingDisabled   <none>   3d   v1.35.2
# … OS 업데이트 · 재부팅 …
$ kubectl uncordon node01                   # 다시 Pod를 받을 수 있게
상황drain이 하는 일필요한 옵션
Deployment · ReplicaSet Pod내보내면 컨트롤러가 다른 노드에 새로 만듦—
DaemonSet Pod내보내도 바로 다시 생기니 그대로 둠--ignore-daemonsets (없으면 drain이 멈춤)
emptyDir를 쓰는 Pod임시 데이터가 사라짐을 확인받음--delete-emptydir-data
컨트롤러 없는 단독 Pod지우면 다시 안 생김 — 기본은 거부--force (정말 지워도 될 때만)
uncordon해도 Pod는 돌아오지 않는다 쿠버네티스는 이미 잘 돌고 있는 Pod를 옮기지 않아요. 비워진 노드에는 앞으로 새로 만들어지는 Pod가 배치돼요. 노드 장애 시 자동으로 옮겨지는 건 1부 05의 300초 축출이고, drain은 그걸 미리, 질서 있게 하는 거예요.

2. PodDisruptionBudget

노드 여러 대를 동시에 drain하면 같은 앱의 Pod가 한꺼번에 내보내져 서비스가 끊길 수 있어요. PodDisruptionBudget(PDB)은 “이 앱은 자발적인 중단(drain · 업그레이드) 중에도 최소 몇 개는 살아 있어야 한다”는 약속이에요. 학습 자료에는 없지만 실제 운영의 노드 점검에서 꼭 쓰여요.

PDB minAvailable 2 — drain이 기다려야 할 때
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: web-pdb
spec:
  minAvailable: 2          # 또는 maxUnavailable: 1 (둘 중 하나)
  selector:
    matchLabels:
      app: web
$ kubectl create pdb web-pdb --selector=app=web --min-available=2
$ kubectl get pdb
NAME      MIN AVAILABLE   MAX UNAVAILABLE   ALLOWED DISRUPTIONS
web-pdb   2               N/A               1
$ kubectl drain node02 --ignore-daemonsets
error when evicting pods/"web-7d4b9c-5mq8r" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
PDB가 막는 것과 못 막는 것 PDB는 drain처럼 Eviction API를 쓰는 자발적 중단만 제어해요. 노드가 갑자기 죽는 것, kubectl delete pod로 직접 지우는 것은 막지 못해요. 또 minAvailable을 레플리카 수와 같게 잡으면 drain이 영원히 끝나지 않으니 여유를 둬야 해요.

02. 클러스터 업그레이드

쿠버네티스는 약 4개월마다 새 마이너 버전(1.35 → 1.36 …)이 나오고, 최근 3개 버전만 패치를 받아요. 1년 남짓 지나면 지원이 끝나니 정기적인 업그레이드는 피할 수 없어요. 이 절은 직접 설치한(kubeadm) 클러스터 기준이에요. GKE 같은 관리형은 컨트롤 플레인을 클라우드가 올려 주고 노드 풀만 우리가 올려요.

1. 버전 규칙과 컴포넌트 간 버전 차이

버전 차이 규칙 — API 서버가 기준, 한 번에 한 마이너씩
컴포넌트API 서버가 1.36일 때 허용규칙
kube-apiserver (여러 대)1.36 · 1.35서로 1 마이너 이내 — 모든 것의 기준
controller-manager · scheduler1.36 · 1.35API 서버보다 새로우면 안 됨, 1 낮은 것까지
kubelet · kube-proxy1.36 ~ 1.33API 서버보다 새로우면 안 됨, 3 낮은 것까지
kubectl1.37 · 1.36 · 1.35위아래 1
마이너 버전은 건너뛰지 않는다 1.34 → 1.36으로 바로 갈 수 없어요. 1.34 → 1.35 → 1.36처럼 한 단계씩 올려요. 순서는 항상 컨트롤 플레인 먼저, 워커 노드 나중이에요. 위 표처럼 kubelet이 API 서버보다 낮은 건 괜찮지만 높으면 안 되기 때문이에요.

2. kubeadm으로 컨트롤 플레인 → 워커 순서로 올리기

kubeadm 업그레이드 — 컨트롤 플레인 노드 하나, 워커 노드 하나씩
① 컨트롤 플레인 노드 (예: 1.35 → 1.36)
$ sudo vi /etc/apt/sources.list.d/kubernetes.list      # 저장소 주소의 v1.35 → v1.36
$ sudo apt-get update
$ sudo apt-mark unhold kubeadm && sudo apt-get install -y kubeadm='1.36.*' && sudo apt-mark hold kubeadm
$ sudo kubeadm upgrade plan                           # 올릴 수 있는 버전 · 바뀌는 것 확인
$ sudo kubeadm upgrade apply v1.36.2                  # API 서버 · 컨트롤러 · 스케줄러 · etcd(Static Pod) 교체
$ kubectl drain controlplane --ignore-daemonsets
$ sudo apt-mark unhold kubelet kubectl && sudo apt-get install -y kubelet='1.36.*' kubectl='1.36.*' && sudo apt-mark hold kubelet kubectl
$ sudo systemctl daemon-reload && sudo systemctl restart kubelet
$ kubectl uncordon controlplane
② 워커 노드 — 한 대씩
$ kubectl drain node01 --ignore-daemonsets            # 내 PC(컨트롤 플레인)에서
$ ssh node01
$ (저장소 주소 변경 · apt-get update 후) kubeadm 패키지를 1.36으로
$ sudo kubeadm upgrade node                           # 워커는 apply가 아니라 node
$ (kubelet · kubectl 패키지를 1.36으로) && sudo systemctl daemon-reload && sudo systemctl restart kubelet
$ exit
$ kubectl uncordon node01
$ kubectl get nodes                                   # VERSION 열 확인
학습 자료와 달라진 패키지 저장소 예전 학습 자료의 apt.kubernetes.io 저장소는 2024년에 닫혔어요. 지금은 pkgs.k8s.io를 쓰고, 저장소 주소에 마이너 버전이 들어가서(…/core:/stable:/v1.36/deb/) 업그레이드할 때마다 이 주소부터 바꿔야 새 버전이 보여요. 다른 컨트롤 플레인 노드가 더 있으면 그 노드들에서는 kubeadm upgrade apply 대신 kubeadm upgrade node를 써요.

03. 백업과 복구

1부 03에서 etcd를 잃으면 클러스터가 모든 기억을 잃는다고 했어요. 업그레이드처럼 큰 작업 전에는 꼭, 평소에도 정기적으로 백업해요.

1. 무엇을 백업하나: 리소스 YAML과 etcd

방법무엇이 남나장단점
리소스 YAML (Git · kubectl get -o yaml)내가 만든 리소스 정의사람이 읽고 고칠 수 있음. 관리형(GKE)에서도 가능. 빠뜨린 리소스는 못 살림
etcd 스냅샷클러스터 상태 전부 (한 시점)완전하지만 etcd에 직접 접근해야 해서 직접 운영하는 클러스터에서만. 볼륨 데이터는 미포함
Velero 같은 백업 도구리소스 + PV 스냅샷 → 오브젝트 스토리지Namespace 단위 복구 · 다른 클러스터로 이전도 가능
$ kubectl get all -A -o yaml > all-resources.yaml   # 간단하지만 'all'에 ConfigMap · Secret 등은 빠져 있음
어느 쪽도 볼륨 속 데이터는 아니다 etcd 스냅샷과 리소스 YAML에는 “PVC가 있다”는 정의만 있고, 디스크 안의 실제 데이터(DB 파일 등)는 없어요. 데이터는 볼륨 스냅샷(9부)이나 앱 자체의 백업(DB 덤프)으로 따로 지켜요.

2. etcd 스냅샷 저장과 복구

snapshot save → 사고 → etcdutl restore → etcd가 새 폴더를 보게
저장 (etcdctl — etcd에 접속해서)
$ ETCDCTL_API=3 etcdctl snapshot save /opt/backup/etcd-snap.db \
    --endpoints=https://127.0.0.1:2379 \
    --cacert=/etc/kubernetes/pki/etcd/ca.crt \
    --cert=/etc/kubernetes/pki/etcd/server.crt \
    --key=/etc/kubernetes/pki/etcd/server.key
Snapshot saved at /opt/backup/etcd-snap.db
$ etcdutl snapshot status /opt/backup/etcd-snap.db -w table   # 파일 확인
복구 (etcdutl — 파일만으로)
$ etcdutl snapshot restore /opt/backup/etcd-snap.db \
    --data-dir /var/lib/etcd-from-backup             # 새 폴더에 풀기
$ sudo vi /etc/kubernetes/manifests/etcd.yaml       # 데이터 폴더를 새 경로로
  volumes:
  - hostPath:
      path: /var/lib/etcd-from-backup     # ← /var/lib/etcd 에서 변경
      type: DirectoryOrCreate
    name: etcd-data
학습 자료의 etcdctl snapshot restore etcd 3.5까지는 복구도 etcdctl snapshot restore로 했고 학습 자료도 그렇게 보여 줘요. etcd 3.6에서 복구 · 상태 확인 명령은 etcdctl에서 빠지고 etcdutl로 옮겨졌어요. 저장(save)은 실행 중인 etcd에 접속해야 해서 여전히 etcdctl, 복구(restore)는 파일만 다루니 etcdutl이라고 기억하면 쉬워요. 실습 환경의 etcd 버전에 맞춰 써요.
복구 후 확인이유
etcd Pod가 다시 떴는가 (crictl ps · kubectl get pods -n kube-system)Static Pod라 파일을 고치면 kubelet이 재시작 — 잠시 API 서버도 응답이 없을 수 있어요
지워졌던 리소스가 돌아왔는가스냅샷 시점 이후의 변경은 사라져요
(직접 설치형이면) systemctl로 etcd 서비스의 --data-dir 변경Static Pod가 아니라 systemd로 도는 etcd라면 서비스 파일을 고쳐요

3. 백업 도구 Velero 맛보기

Velero는 쿠버네티스 API로 리소스를 내려받고, 클라우드 디스크 스냅샷이나 파일 복사로 볼륨 데이터까지 S3 같은 오브젝트 스토리지에 저장하는 도구예요. etcd에 접근할 수 없는 관리형 클러스터에서도 쓸 수 있고, Namespace 하나만 골라 복구하거나 다른 클러스터로 옮길 때도 써요.

$ velero backup create nightly --include-namespaces prod
$ velero schedule create daily --schedule="0 3 * * *"
$ velero restore create --from-backup nightly

4. 6부 셀프 체크

  1. cordon과 drain의 차이는?
    cordon은 새 Pod 배치만 막아요. drain은 cordon에 더해 기존 Pod까지 내보내요.
  2. drain이 DaemonSet Pod 때문에 멈췄어요. 어떻게 하나요?
    --ignore-daemonsets를 붙여요. DaemonSet Pod는 내보내도 바로 다시 생기니 그대로 두고 진행해요.
  3. 컨트롤러 없이 만든 단독 Pod가 있는 노드를 --force로 drain하면?
    그 Pod는 지워지고 다시 만들어지지 않아요. 그래서 기본은 거부하고 --force를 요구해요.
  4. uncordon하면 다른 노드로 옮겨 간 Pod가 돌아오나요?
    아니요. 앞으로 새로 만들어지는 Pod만 배치될 수 있어요.
  5. PDB minAvailable: 2인 Pod 3개가 있을 때 두 노드를 동시에 drain하면?
    하나는 내보낼 수 있지만 두 번째 축출은 2개 미만이 되므로 거절되고 재시도해요. 대체 Pod가 Ready가 되면 진행돼요.
  6. API 서버가 1.36일 때 kubelet으로 허용되는 가장 낮은 버전은?
    1.33 (3 마이너 낮은 것까지). kubelet이 API 서버보다 높은 건 안 돼요.
  7. 1.34 클러스터를 1.36으로 올리는 순서는?
    마이너를 건너뛰지 않고 1.34 → 1.35 → 1.36. 각 단계에서 컨트롤 플레인 먼저(kubeadm upgrade apply), 워커 노드는 하나씩(drain → kubeadm upgrade node → kubelet → uncordon).
  8. 워커 노드에서 쓰는 kubeadm 명령은 apply와 node 중 무엇인가요?
    kubeadm upgrade node예요. apply는 첫 컨트롤 플레인 노드에서만.
  9. etcd 스냅샷을 저장 · 복구하는 명령은?
    저장은 etcdctl snapshot save(인증서 · 엔드포인트 필요), 복구는 etcdutl snapshot restore --data-dir 새폴더 후 etcd 설정의 데이터 경로를 새 폴더로 바꿔요.
  10. etcd 스냅샷으로 DB Pod의 데이터도 돌아오나요?
    아니요. 리소스 정의만 돌아와요. 볼륨 속 데이터는 볼륨 스냅샷이나 앱 백업으로 따로 지켜요.
스터디를 하며 부마다 이어서 정리합니다. 그림은 개념 이해를 위해 단순화했어요.