본문으로 건너뛰기
인프라 · infra/kubernetes/02 전반 ③ — 설치 · 도구 · 트러블슈팅

← 스터디 홈

Kubernetes Study · 02 전반 ③

쿠버네티스 전반 ③ — 설치 · 도구 · 트러블슈팅

전반 세 노트의 마지막이에요. 12~17부에서 클러스터를 직접 설계 · 설치하고, Helm · Kustomize로 배포를 관리하고, CRD · 오퍼레이터로 확장한 뒤, 장애를 좁혀 가는 법과 kubectl 실전 도구로 마무리해요. 앞 내용은 ① 클러스터 운영, ② 보안 · 스토리지 · 네트워크예요.

전반 ③ · 12~17부 설치 · 도구 · 트러블슈팅 · 2026.10.30 시작 · 2026.11.02 업데이트

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

01. 클러스터 설계

지금까지는 이미 만들어진 클러스터를 다뤘어요. 12부는 클러스터를 직접 짓는 이야기예요. 짓기 전에 먼저 정할 것은 “어디에 쓸 클러스터인가”예요. 목적에 따라 노드 수, 설치 도구, 누가 컨트롤 플레인을 관리할지가 전부 달라져요.

1. 목적에 따라 고르기

학습 · 개발 · 운영 — 목적이 바뀌면 모양도 바뀐다
도구모양어울리는 곳
minikube노트북에 노드 하나(VM 또는 컨테이너)혼자 학습 · 기능 맛보기
kindDocker 컨테이너를 노드처럼 써서 여러 노드 흉내학습 · CI 테스트
k3s부품을 한 바이너리로 묶은 가벼운 배포판작은 서버 · 엣지 · 라즈베리 파이
kubeadm표준 부품을 그대로 설치하는 공식 도구 (04)VM · 물리 서버에 직접 구축, CKA 시험
EKS · GKE · AKS클라우드가 컨트롤 플레인을 운영, 우리는 워커와 앱만운영 환경 대부분

2. 규모와 노드 크기

쿠버네티스 한 클러스터가 감당하도록 설계된 한계가 있어요. 이 숫자를 넘길 일은 드물지만, “노드당 Pod 110개”는 작은 클러스터에서도 자주 만나요.

항목권장 한계메모
노드 수5,000그 이상이면 클러스터를 나눠요
노드당 Pod110kubelet maxPods 기본값. 노드의 Pod 대역 /24(254개)와도 맞물려요(11부 02)
전체 Pod150,000
전체 컨테이너300,000
노드 최소 사양 (kubeadm)2 CPU · 2GB RAM컨트롤 플레인은 2 CPU 이상 필수. 실습용 워커는 1 CPU도 동작은 해요

운영에서는 컨트롤 플레인 노드에 앱 Pod를 올리지 않아요. kubeadm은 컨트롤 플레인 노드에 node-role.kubernetes.io/control-plane:NoSchedule taint를 자동으로 달아 줘요(3부 03). etcd는 디스크 속도에 민감해서, 큰 클러스터에서는 SSD를 쓰거나 etcd만 따로 떼어 내요(03).

3. 어디에, 어떻게 설치하나

방식뜻예
직접 설치VM · 서버를 준비하고 도구로 부품을 설치. 업그레이드 · 백업도 우리가kubeadm, Kubespray, 손으로 하나씩(The Hard Way)
배포판설치 · 업그레이드 · 보안 설정을 묶어 제공하는 제품OpenShift, Rancher(RKE2), k3s
관리형(hosted)컨트롤 플레인을 클라우드가 운영. 노드 그룹만 고름EKS, GKE, AKS
Kubernetes The Hard Way 학습 자료에서 소개하는 “The Hard Way”는 인증서 만들기부터 etcd · API 서버 설치까지 모든 부품을 손으로 설치하는 실습 가이드예요. 운영용이 아니라 이해용이에요. 7부의 인증서, 1부의 부품, 11부의 네트워크가 한 번에 이어지는 경험이라 시간 여유가 있을 때 해 보면 좋아요.
Windows 노드 Windows 서버는 워커 노드로만 붙일 수 있어요. 컨트롤 플레인은 항상 Linux예요.

02. 고가용성(HA) 구성

고가용성(High Availability)은 “한 대가 죽어도 클러스터는 계속 동작한다”는 뜻이에요. 워커는 원래 여러 대라 괜찮지만, 컨트롤 플레인이 한 대뿐이면 그 한 대가 단일 장애점이에요.

1. 컨트롤 플레인 여러 대와 로드밸런서

컨트롤 플레인 한 대가 죽으면 — 그리고 세 대와 로드밸런서가 있으면

API 서버는 상태를 etcd에 두고 자기는 아무것도 기억하지 않아서, 여러 대가 동시에 요청을 받아도 돼요(active-active). 그래서 앞에 로드밸런서를 두고, kubectl · kubelet · 다른 부품이 모두 로드밸런서 주소로 접속하게 해요. kubeadm에서는 이 주소를 --control-plane-endpoint로 정해요.

로드밸런서방식메모
클라우드 LB클라우드가 제공하는 L4 로드밸런서가장 간단
HAProxy + keepalived별도 서버(또는 컨트롤 플레인 노드)에 프록시 + 가상 IP온프레미스에서 전통적인 방법
kube-vip컨트롤 플레인 노드에 Static Pod로 떠서 가상 IP를 나눠 가짐추가 서버 없이 HA
엔드포인트는 처음부터 컨트롤 플레인 한 대로 시작하더라도, 나중에 HA로 늘릴 계획이면 kubeadm init 때부터 --control-plane-endpoint에 DNS 이름이나 가상 IP를 넣어 두세요. 이 주소가 API 서버 인증서와 모든 kubeconfig에 박히기 때문에, 나중에 바꾸기 어려워요.

2. 리더 선출 — 스케줄러와 컨트롤러 매니저

API 서버는 다 같이, 스케줄러 · 컨트롤러 매니저는 한 대만 — Lease로 정하는 리더

스케줄러나 컨트롤러 매니저가 세 대 모두 일하면 같은 Pod를 세 번 배치하거나 ReplicaSet이 Pod를 세 배로 만들 수 있어요. 그래서 이 둘은 active-standby예요. kube-system의 Lease 객체를 먼저 잡은 쪽이 리더가 되고, 리더는 주기적으로 Lease를 갱신해요. 갱신이 끊기면 대기하던 쪽이 Lease를 가져가요.

$ kubectl get lease -n kube-system
NAME                      HOLDER                                       AGE
kube-controller-manager   cp1_7f3c…                                    3d
kube-scheduler            cp1_a91e…                                    3d
$ kubectl get lease kube-scheduler -n kube-system -o yaml | grep -E "holder|renew"
  holderIdentity: cp1_a91e…
  renewTime: "2026-10-30T09:12:41.118Z"
옵션 (스케줄러 · 컨트롤러 매니저)기본값뜻
--leader-electtrue리더 선출 사용
--leader-elect-lease-duration15s리더가 갱신을 멈춘 뒤 다른 쪽이 가져가기까지 기다리는 시간
--leader-elect-renew-deadline10s리더가 이 시간 안에 갱신 못 하면 스스로 물러남
--leader-elect-retry-period2s갱신 · 획득 시도 간격
학습 자료의 Endpoints 기반 리더 선출 예전 버전은 Endpoints · ConfigMap에 리더 정보를 적었어요. 지금은 Lease(coordination.k8s.io)만 써요. 노드 하트비트(kube-node-lease Namespace)도 같은 Lease 객체예요.

03. etcd HA

etcd는 클러스터의 기억이에요(1부 03). API 서버는 여러 대여도 결국 etcd에 읽고 쓰니까, etcd도 여러 대여야 HA가 완성돼요. 그런데 etcd는 API 서버처럼 “아무 데나 쓰면 되는” 구조가 아니에요. 여러 대가 같은 내용을 갖도록 합의하는 규칙이 필요해요.

1. Raft와 쿼럼

리더가 받아 적고, 과반이 따라 적으면 확정 — Raft의 쓰기와 리더 선출

etcd는 Raft라는 합의 알고리즘을 써요. 핵심은 세 가지예요.

규칙뜻
쓰기는 리더만어느 멤버에 쓰기가 와도 리더에게 넘겨요. 리더가 로그에 적고 팔로워에게 복제해요
과반(쿼럼)이 적어야 확정멤버 N대 중 N/2 + 1대(소수점 버림)가 적었다고 답해야 커밋돼요
리더가 조용하면 선거리더의 하트비트(기본 100ms)가 선거 시간(기본 1000ms) 동안 안 오면, 팔로워가 후보가 되어 투표를 요청하고 과반 표를 얻으면 새 리더

2. 왜 홀수인가

멤버 수쿼럼 (과반)죽어도 되는 수
110
220
321
431
532
642
743

표에서 보듯 4대는 3대와, 6대는 5대와 견디는 수가 같아요. 한 대를 더 사도 안전해지지 않고, 복제할 곳만 늘어 쓰기가 느려져요. 짝수는 네트워크가 반으로 갈라졌을 때도 불리해요. 6대가 3 · 3으로 나뉘면 어느 쪽도 과반(4)이 안 돼서 클러스터 전체가 멈춰요. 그래서 3대나 5대가 표준이에요. 2대는 1대보다 오히려 위험해요(둘 중 하나만 죽어도 멈춤).

쿼럼을 잃으면 과반이 사라지면 etcd는 쓰기를 거부해요. API 서버는 떠 있어도 Pod 생성 · 삭제 같은 모든 변경이 실패하고, 이미 떠 있는 Pod는 kubelet이 계속 돌려요. 복구는 죽은 멤버를 살리거나, 최악이면 스냅샷으로 새 클러스터를 만들어요(6부 03).

3. stacked vs external

etcd를 컨트롤 플레인 노드에 같이 둘까, 따로 둘까
stacked (kubeadm 기본)external
배치컨트롤 플레인 노드마다 etcd Static Podetcd 전용 서버 따로
최소 서버3대6대 (컨트롤 플레인 3 + etcd 3)
장애노드 하나에 API 서버와 etcd 멤버를 같이 잃음서로 영향 없음
관리간단, kubeadm이 알아서etcd 설치 · 인증서 · 백업을 따로

어느 쪽이든 API 서버는 --etcd-servers에 etcd 멤버 전부를 적어서, 한 멤버가 죽으면 다른 멤버로 붙어요. etcd끼리는 2380 포트로 서로를 알아요.

etcd 멤버 설정 (일부) · API 서버 쪽 설정
# etcd1 (10.0.0.11)
--name=etcd1
--initial-advertise-peer-urls=https://10.0.0.11:2380
--listen-client-urls=https://10.0.0.11:2379,https://127.0.0.1:2379
--initial-cluster=etcd1=https://10.0.0.11:2380,etcd2=https://10.0.0.12:2380,etcd3=https://10.0.0.13:2380

# kube-apiserver
--etcd-servers=https://10.0.0.11:2379,https://10.0.0.12:2379,https://10.0.0.13:2379
멤버와 리더 확인
$ export ETCDCTL_CACERT=/etc/kubernetes/pki/etcd/ca.crt \
         ETCDCTL_CERT=/etc/kubernetes/pki/etcd/server.crt \
         ETCDCTL_KEY=/etc/kubernetes/pki/etcd/server.key
$ etcdctl --endpoints=https://10.0.0.11:2379 member list -w table
$ etcdctl --endpoints=https://10.0.0.11:2379,https://10.0.0.12:2379,https://10.0.0.13:2379 \
    endpoint status -w table          # IS LEADER 열에서 리더 확인
ETCDCTL_API=3은 이제 안 써도 된다 학습 자료에서 매번 붙이던 ETCDCTL_API=3은 etcd 3.4부터 기본값이라 생략해도 돼요. 스냅샷 복구는 3.6부터 etcdutl로 옮겨졌어요(6부 03).

04. kubeadm으로 설치하기

kubeadm은 쿠버네티스 프로젝트가 만든 공식 설치 도구예요. 인증서 · kubeconfig · Static Pod 매니페스트를 대신 만들어 주고, 노드를 붙이는 과정을 자동화해요. 컨트롤 플레인 1대 + 워커 2대 클러스터를 만들어 봐요. 6부의 업그레이드(6부 02)도 같은 도구였어요.

1. VM 준비 — 모든 노드에서

학습 자료는 Vagrant + VirtualBox로 VM 3대(controlplane · node01 · node02)를 만들어요. VM이 준비되면 모든 노드에서 같은 준비를 해요. 각 노드는 hostname · MAC 주소 · product_uuid가 서로 달라야 해요(VM을 복제했다면 확인).

Apple Silicon Mac이라면 M 시리즈 Mac에서는 VirtualBox + Vagrant 조합이 잘 안 맞을 수 있어요. Multipass, Lima, UTM으로 Ubuntu VM을 만들고 아래 과정을 그대로 따라 하면 돼요. VM마다 고정 IP가 서로 통하는지만 확인하세요.
① swap 끄기 · 커널 모듈 · 네트워크 설정
$ sudo swapoff -a
$ sudo sed -i '/[[:space:]]swap[[:space:]]/ s/^[^#]/#&/' /etc/fstab     # 재부팅 후에도 꺼지게

$ cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
$ sudo modprobe overlay && sudo modprobe br_netfilter

$ cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables  = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward                 = 1
EOF
$ sudo sysctl --system
② 컨테이너 런타임 containerd
$ sudo apt-get update && sudo apt-get install -y containerd
$ sudo mkdir -p /etc/containerd
$ containerd config default | sudo tee /etc/containerd/config.toml > /dev/null
$ sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
$ sudo systemctl restart containerd
③ kubeadm · kubelet · kubectl (v1.36 저장소)
$ sudo apt-get install -y apt-transport-https ca-certificates curl gpg
$ sudo mkdir -p -m 755 /etc/apt/keyrings
$ curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.36/deb/Release.key \
    | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
$ echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.36/deb/ /' \
    | sudo tee /etc/apt/sources.list.d/kubernetes.list
$ sudo apt-get update
$ sudo apt-get install -y kubelet kubeadm kubectl
$ sudo apt-mark hold kubelet kubeadm kubectl        # apt upgrade로 멋대로 올라가지 않게

설치 직후 kubelet이 몇 초마다 재시작하는데 정상이에요. kubeadm이 설정 파일을 만들어 줄 때까지 기다리는 중이에요.

학습 자료와 달라진 것지금
패키지 저장소 apt.kubernetes.io2023년에 동결 · 폐쇄. pkgs.k8s.io를 쓰고, 마이너 버전마다 주소가 달라요(v1.36). 업그레이드할 때 이 줄을 먼저 바꿔요
런타임으로 Docker Engine1.24에서 dockershim이 빠져 containerd · CRI-O를 직접 써요(1부 02)
cgroup 드라이버를 양쪽에 맞추기1.34부터 kubelet이 런타임(containerd 2.0 이상)에게 물어서 자동으로 맞춰요. containerd 쪽 SystemdCgroup = true는 여전히 설정해요
cgroup v11.35부터 kubelet이 cgroup v1 노드에서 기본적으로 시작을 거부해요. 최신 Ubuntu는 v2라 문제없어요. stat -fc %T /sys/fs/cgroup/가 cgroup2fs면 v2
swap은 반드시 꺼야 한다1.34에서 swap 지원이 정식이 됐지만 kubelet 설정을 따로 해야 해요. 실습에서는 끄는 게 기본

2. kubeadm init · join

준비 → init → join → CNI — 노드 세 대가 Ready가 되기까지
컨트롤 플레인에서
$ sudo kubeadm init \
    --apiserver-advertise-address=192.168.56.11 \
    --pod-network-cidr=10.244.0.0/16
…
Your Kubernetes control-plane has initialized successfully!
…
kubeadm join 192.168.56.11:6443 --token abcdef.0123456789abcdef \
    --discovery-token-ca-cert-hash sha256:1a2b3c…

$ mkdir -p $HOME/.kube
$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config

kubeadm init은 정해진 순서(phase)대로 일해요. 각 phase가 앞에서 배운 내용과 그대로 이어져요.

phase하는 일이어지는 곳
preflightswap · 포트 · 런타임 · 권한 점검, 이미지 받기11부 01 포트
certsCA와 각 부품 인증서를 /etc/kubernetes/pki에7부 04
kubeconfigadmin · kubelet · 스케줄러 · 컨트롤러 매니저용 kubeconfig7부 05
control-plane · etcd/etc/kubernetes/manifests에 Static Pod 매니페스트3부 07
upload-config설정을 ConfigMap(kubeadm-config)으로 저장6부 02 업그레이드가 읽어요
mark-control-planecontrol-plane 레이블 · NoSchedule taint3부 03
bootstrap-token워커가 붙을 때 쓸 토큰 (기본 24시간)
addonCoreDNS · kube-proxy 설치11부 04 · 05
워커(node01 · node02)에서
$ sudo kubeadm join 192.168.56.11:6443 --token abcdef.0123456789abcdef \
    --discovery-token-ca-cert-hash sha256:1a2b3c…

# 토큰이 만료됐거나 출력을 잃어버렸다면 컨트롤 플레인에서 다시 만들기
$ kubeadm token create --print-join-command

join은 토큰으로 API 서버에 “처음 인사”를 하고, --discovery-token-ca-cert-hash로 상대가 진짜 우리 클러스터인지 확인해요. 그다음 kubelet이 자기 인증서를 신청해서 받는 TLS 부트스트랩을 해요(7부 04의 CSR이 자동으로 승인되는 과정).

옵션 대신 설정 파일로 (v1beta4)
apiVersion: kubeadm.k8s.io/v1beta4
kind: ClusterConfiguration
kubernetesVersion: v1.36.0
controlPlaneEndpoint: "k8s-api.example.local:6443"   # HA를 대비한 이름 (02)
networking:
  podSubnet: 10.244.0.0/16
  serviceSubnet: 10.96.0.0/12
---
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
cgroupDriver: systemd
$ sudo kubeadm init --config kubeadm.yaml --upload-certs
HA라면 컨트롤 플레인을 join으로 늘린다 --control-plane-endpoint와 --upload-certs로 init하면 출력에 컨트롤 플레인용 join 명령이 하나 더 나와요. 두 번째 · 세 번째 컨트롤 플레인에서 kubeadm join … --control-plane --certificate-key …를 실행하면 인증서를 받아 와서 API 서버와 stacked etcd 멤버가 하나씩 늘어요. 올려 둔 인증서는 2시간 뒤 지워지니, 늦었다면 kubeadm init phase upload-certs --upload-certs로 새 키를 받아요.

3. CNI 설치까지

init과 join이 끝나도 노드는 전부 NotReady예요. Pod 네트워크가 없어서 kubelet이 “아직 Pod를 받을 수 없다”고 보고하는 거예요(11부 03). CoreDNS Pod도 Pending이에요. CNI를 설치하면 풀려요.

$ kubectl get nodes
NAME           STATUS     ROLES           VERSION
controlplane   NotReady   control-plane   v1.36.0
node01         NotReady   <none>          v1.36.0
node02         NotReady   <none>          v1.36.0

# 예: Flannel (기본 Pod 대역이 10.244.0.0/16 이라 init 값과 맞음)
$ kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml

$ kubectl get nodes
NAME           STATUS   ROLES           VERSION
controlplane   Ready    control-plane   v1.36.0
node01         Ready    <none>          v1.36.0
node02         Ready    <none>          v1.36.0
$ kubectl get pods -A                      # coredns · kube-proxy · flannel 모두 Running
증상흔한 원인해결
init이 preflight에서 멈춤swap 켜짐, ip_forward 0, 포트 사용 중메시지에 나온 항목을 고치고 다시. 망가졌으면 kubeadm reset
노드가 계속 NotReadyCNI 미설치, CNI의 Pod 대역 ≠ --pod-network-cidrCNI 설정의 대역을 init 값과 맞춤
노드 INTERNAL-IP가 10.0.2.15Vagrant VM의 NAT 카드 주소를 잡음init에 --apiserver-advertise-address, 노드마다 kubelet --node-ip 지정
kubelet이 계속 재시작cgroup 드라이버 불일치, cgroup v1journalctl -u kubelet로 확인 (16부)
Vagrant처럼 네트워크 카드가 두 개인 VM — 노드마다
$ echo 'KUBELET_EXTRA_ARGS=--node-ip=192.168.56.12' | sudo tee /etc/default/kubelet
$ sudo systemctl restart kubelet

4. 12부 셀프 체크

  1. 혼자 학습할 때, 직접 서버에 구축할 때, 운영할 때 각각 어울리는 방식은?
    학습: minikube · kind. 직접 구축: kubeadm. 운영: 컨트롤 플레인 3대 이상의 HA 구성이나 EKS · GKE · AKS 같은 관리형.
  2. kubelet의 노드당 기본 최대 Pod 수는?
    110개(maxPods).
  3. 컨트롤 플레인이 한 대일 때 그 노드가 죽으면 이미 떠 있는 Pod는 어떻게 되나요?
    kubelet이 계속 돌려서 Pod는 살아 있어요. 하지만 API 서버가 없어서 배포 · 스케일 · 재스케줄 같은 변경은 전부 안 돼요.
  4. HA 구성에서 kubectl과 kubelet은 어느 주소로 접속하나요?
    컨트롤 플레인들 앞의 로드밸런서(또는 가상 IP) 주소. kubeadm의 --control-plane-endpoint로 정해요.
  5. API 서버는 여러 대가 동시에 일하는데, 스케줄러는 왜 한 대만 일하나요?
    여러 대가 동시에 결정하면 같은 Pod를 중복 처리할 수 있어서요. kube-system의 Lease를 잡은 리더 하나만 일하고 나머지는 대기해요.
  6. etcd 5대 클러스터의 쿼럼과, 견딜 수 있는 장애 수는?
    쿼럼 3, 2대까지 죽어도 동작해요.
  7. etcd를 4대로 두지 않는 이유는?
    4대의 쿼럼은 3이라 견디는 수가 3대와 같은 1대예요. 복제만 늘어 느려지고, 2 · 2로 갈라지면 양쪽 다 과반이 안 돼요.
  8. etcd의 쿼럼이 깨지면 클러스터는 어떻게 되나요?
    etcd가 쓰기를 거부해서 API를 통한 모든 변경이 실패해요. 이미 떠 있는 Pod는 계속 돌아요.
  9. stacked etcd와 external etcd의 차이는?
    stacked는 컨트롤 플레인 노드마다 etcd가 함께 있어 서버가 적게 들지만 노드 하나에 둘을 같이 잃어요. external은 etcd 전용 서버를 따로 둬서 서버가 더 들지만 장애가 서로 번지지 않아요.
  10. kubeadm 설치 전 모든 노드에서 하는 준비 세 가지는?
    swap 끄기, 커널 모듈(overlay · br_netfilter)과 sysctl(ip_forward 등) 설정, 컨테이너 런타임(containerd) 설치. 그다음 kubeadm · kubelet · kubectl 설치와 hold.
  11. 워커 join 명령을 잃어버렸어요. 어떻게 다시 얻나요?
    컨트롤 플레인에서 kubeadm token create --print-join-command.
  12. init과 join이 끝났는데 노드가 모두 NotReady예요. 왜인가요?
    CNI 플러그인이 아직 없어서 Pod 네트워크가 준비되지 않았기 때문이에요. CNI를 설치하면 Ready가 되고 CoreDNS도 Running이 돼요.
13부Helm패키지 관리자Helm 2 · 3 · 4chart · release템플릿 · valuesupgrade · rollback

01. Helm이란

지금까지는 리소스를 YAML 하나씩 만들고 지웠어요. 그런데 실제 앱 하나는 Deployment · Service · PVC · Secret · ConfigMap · Ingress처럼 YAML 여러 개의 묶음이에요. Helm은 이 묶음을 하나의 앱 단위로 설치 · 업그레이드 · 되돌리기 · 삭제하는 쿠버네티스의 패키지 관리자예요. 앱의 apt · Homebrew, Flutter의 pub 같은 존재예요.

1. YAML 묶음을 손으로 관리하면

앱 하나 = YAML 여러 개 — 손으로 관리할 때와 Helm으로 관리할 때
하고 싶은 일kubectl로Helm으로
설치파일마다 kubectl apply, 순서도 신경helm install my-site <chart> 한 번
설정 변경여러 파일에 흩어진 값을 찾아서 고침values.yaml 한 곳 또는 --set
버전 올리기 · 되돌리기예전 파일을 따로 보관해야 함helm upgrade · helm rollback
삭제무엇을 만들었는지 기억해서 하나씩helm uninstall my-site 한 번

2. 설치와 첫 명령

Helm은 클러스터에 아무것도 설치하지 않는 CLI 하나예요. kubectl과 같은 kubeconfig(7부 05)로 클러스터에 접속해요.

$ brew install helm                        # macOS (Linux는 공식 설치 스크립트나 패키지)
$ helm version
version.BuildInfo{Version:"v4.x.x", …}

$ helm repo add podinfo https://stefanprodan.github.io/podinfo   # 저장소 등록
$ helm repo update                                           # 목록 새로 받기
$ helm search repo podinfo                                   # 등록한 저장소에서 찾기
$ helm search hub wordpress                                  # Artifact Hub 전체에서 찾기

$ helm install web podinfo/podinfo                           # release 이름 web으로 설치
$ helm list                                                  # 설치된 release 목록 (-A: 모든 Namespace)
$ helm uninstall web                                         # 묶음 전체 삭제

02. Helm 2 · 3 · 4

학습 자료는 Helm 2와 3의 차이를 설명해요. Helm 2는 이미 오래전에 끝났고, 지금은 Helm 4가 나와 있어요. 차이를 알면 오래된 블로그 글을 읽을 때 헷갈리지 않아요.

1. Tiller 제거와 3방향 병합

Helm 2의 Tiller가 사라진 이유, 그리고 실제 상태까지 비교하는 3방향 병합
Helm 2Helm 3 이후
클러스터 쪽 부품Tiller (kube-system의 Pod)없음. CLI가 API 서버에 직접
권한Tiller의 큰 권한을 모두가 빌려 씀각 사용자의 RBAC 그대로 (7부 07)
변경 비교2방향: 이전 차트 ↔ 새 차트3방향: 이전 차트 ↔ 실제 상태 ↔ 새 차트
release 기록kube-system의 ConfigMaprelease가 있는 Namespace의 Secret
release 이름클러스터 전체에서 하나Namespace마다 따로
Chart.yamlapiVersion: v1, 의존성은 requirements.yamlapiVersion: v2, 의존성도 Chart.yaml에

3방향 병합 덕분에 누군가 kubectl edit으로 손댄 부분도 Helm이 알아채요. 롤백하면 손으로 바꾼 값까지 차트 값으로 돌아가고, 업그레이드할 때는 차트가 다루지 않는 필드(예: 손으로 붙인 레이블)는 남겨 둬요.

2. Helm 4에서 달라진 것

Helm 4.0은 2025년 11월에 나왔어요. Helm 3 차트(apiVersion: v2)와 기존 release는 그대로 동작해서, CLI만 바꾸면 돼요. 저장 방식이 같아 변환 작업도 없어요.

바뀐 것내용
적용 방식새로 설치하는 release는 server-side apply가 기본 (Helm 3에서 만든 release는 예전 방식 유지)
--waitkstatus로 리소스가 정말 준비됐는지 자세히 확인. 리소스를 watch할 권한이 필요
옵션 이름--atomic → --rollback-on-failure, --force → --force-replace (옛 이름은 경고와 함께 동작)
플러그인WebAssembly 플러그인 지원. --post-renderer에는 실행 파일 경로 대신 플러그인 이름을 넘김
values값을 여러 YAML 문서로 나눠 쓸 수 있음
레지스트리helm registry login에는 도메인만 (경로 없이)
Helm 3 지원 종료 Helm 3는 2026년 9월에 마지막 기능 릴리스(3.22)가 나왔고, 보안 수정은 2027년 2월 10일까지예요. 지금 새로 배운다면 Helm 4를 설치하세요. 명령은 거의 같아서 학습 자료를 그대로 따라 할 수 있어요.

03. 구성 요소

1. chart · release · repository · values

저장소의 chart + values → 클러스터의 release — 같은 chart로 release를 여러 개
용어뜻비유
chart앱 하나를 설치하는 데 필요한 템플릿 · 기본값 · 설명의 묶음 (폴더 또는 .tgz)앱 설치 파일
values템플릿의 빈칸에 들어갈 값. 기본은 chart의 values.yaml, 설치할 때 덮어씀설치 옵션
releasechart를 클러스터에 설치한 한 번의 인스턴스. 이름이 붙고 버전(revision)이 쌓임설치된 앱
revisionrelease를 설치 · 업그레이드 · 롤백할 때마다 1씩 늘어나는 번호앱 업데이트 기록
repositorychart를 모아 둔 곳 (HTTP 저장소 또는 OCI 레지스트리)앱 스토어

release 이름은 리소스 이름의 앞부분이 되기 때문에, 같은 chart로 my-site와 my-second-site를 설치해도 리소스가 서로 겹치지 않아요. release 기록은 그 Namespace의 Secret에 버전마다 하나씩 저장돼요.

$ kubectl get secret -l owner=helm
NAME                            TYPE                 DATA
sh.helm.release.v1.my-site.v1   helm.sh/release.v1   1
sh.helm.release.v1.my-site.v2   helm.sh/release.v1   1

2. 저장소 — Artifact Hub와 OCI

공개 chart는 Artifact Hub(artifacthub.io)에서 찾아요. helm search hub가 여기를 검색해요. chart를 담는 방식은 두 가지예요.

방식사용법
HTTP 저장소 (전통적)helm repo add 이름 URL → helm install 릴리스 이름/chart
OCI 레지스트리 (요즘 추세)repo add 없이 helm install 릴리스 oci://레지스트리/경로/chart --version 1.2.3. 컨테이너 이미지와 같은 레지스트리에 chart를 저장
학습 자료 예제의 bitnami 차트 학습 자료는 bitnami/wordpress 같은 Bitnami 차트를 써요. Bitnami는 2025년 8월 28일부터 무료 이미지를 일부(최신 태그, 개발용)만 남기고 나머지는 bitnamilegacy로 옮겼어요. 그래서 학습 자료 명령을 그대로 치면 이미지를 못 받아 Pod가 ImagePullBackOff가 될 수 있어요. 실습은 이 노트처럼 podinfo 같은 다른 공개 차트로 해도 개념은 똑같아요.

04. Chart

1. chart 폴더 구조

$ helm create web            # 뼈대 chart 만들기
web/
├── Chart.yaml               # chart 이름 · 버전 · 의존성
├── values.yaml              # 기본값
├── values.schema.json       # (선택) values 검사 규칙
├── charts/                  # 의존 chart가 들어오는 곳
├── templates/               # 빈칸이 있는 YAML들
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── _helpers.tpl         # 이름 만들기 같은 공통 조각 (_로 시작하면 리소스가 아님)
│   └── NOTES.txt            # 설치 후 화면에 보여 줄 안내
└── README.md
apiVersion: v2               # Helm 3 · 4 차트
name: web
description: 작은 웹 앱
type: application            # application 또는 library(공통 템플릿만 제공)
version: 0.1.0               # chart 자체의 버전 — 템플릿을 고치면 올림
appVersion: "1.27.0"         # 안에 든 앱의 버전 — 참고용
dependencies:                # 다른 chart를 함께 설치
  - name: postgresql
    version: "15.x.x"
    repository: https://charts.example.com
    condition: postgresql.enabled
version과 appVersion version은 chart 포장의 버전, appVersion은 그 안의 앱 버전이에요. 앱은 그대로인데 템플릿만 고쳐도 version을 올려요. pubspec의 version과 의존 패키지 버전이 다른 것과 비슷해요.

2. 템플릿과 values

빈칸이 있는 템플릿 + values + --set → 완성된 YAML

템플릿은 Go 템플릿 문법이에요. {{ }} 안에 넣을 값을 적고, Helm이 설치 직전에 채워서 평범한 YAML로 만들어요.

templates/deployment.yaml (일부)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}-web
  labels:
    app.kubernetes.io/instance: {{ .Release.Name }}
spec:
  replicas: {{ .Values.replicaCount }}
  template:
    spec:
      containers:
        - name: web
          image: "{{ .Values.image.repo }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          {{- if .Values.resources }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          {{- end }}
쓸 수 있는 값예
.Valuesvalues.yaml과 설치 때 덮어쓴 값
.Release.Release.Name, .Release.Namespace, .Release.Revision
.ChartChart.yaml의 내용 (.Chart.Name, .Chart.AppVersion)
.Capabilities클러스터 버전 · 지원하는 API
함수 · 파이프default, quote, toYaml, nindent, include, if · range
$ helm lint ./web                                  # 문법 · 모범 사례 검사
$ helm template my-site ./web --set replicaCount=3 # 채운 결과만 화면에 (클러스터와 무관)
$ helm install my-site ./web --dry-run=server      # API 서버 검증까지, 실제 생성은 안 함
$ helm get manifest my-site                        # 설치된 release의 최종 YAML 보기

3. values 덮어쓰기 순서

값은 여러 곳에서 올 수 있고, 뒤에 올수록 이겨요.

순서어디서예
1 (가장 약함)chart의 values.yamlreplicaCount: 1
2-f · --values 파일 (여러 개면 뒤 파일이 이김)-f prod.yaml
3 (가장 강함)--set · --set-string--set replicaCount=3
$ helm show values podinfo/podinfo > my-values.yaml   # 기본값을 받아서
$ vi my-values.yaml                                    # 필요한 것만 고치고
$ helm install web podinfo/podinfo -f my-values.yaml   # 파일로 적용
$ helm get values web                                  # 내가 덮어쓴 값만 보기 (--all: 전체)

$ helm pull podinfo/podinfo --untar                    # chart를 폴더로 받아서 직접 고친 뒤
$ helm install web2 ./podinfo                          # 로컬 폴더로 설치
--set은 기록으로 남기기 어렵다 --set은 빠르지만 무엇을 바꿨는지 파일로 남지 않아요. 운영에서는 환경마다 values-dev.yaml · values-prod.yaml을 Git에 두고 -f로 적용해요. 14부의 Kustomize도 같은 고민에서 나온 도구예요.

05. 라이프사이클

1. install · upgrade · rollback · history

revision이 쌓인다 — 롤백도 새 revision이다
$ helm install web podinfo/podinfo                       # revision 1
$ helm upgrade web podinfo/podinfo --set replicaCount=3  # revision 2
$ helm history web
REVISION  STATUS      CHART           DESCRIPTION
1         superseded  podinfo-6.x.x   Install complete
2         deployed    podinfo-6.x.x   Upgrade complete

$ helm rollback web 1                                    # 1번 설정으로 → revision 3
$ helm history web
REVISION  STATUS      CHART           DESCRIPTION
1         superseded  podinfo-6.x.x   Install complete
2         superseded  podinfo-6.x.x   Upgrade complete
3         deployed    podinfo-6.x.x   Rollback to 1
자주 쓰는 옵션뜻
helm upgrade --install없으면 설치, 있으면 업그레이드 (CI에서 많이 씀)
-n · --create-namespacerelease를 둘 Namespace · 없으면 만들기
--versionchart 버전 고정 (안 쓰면 최신)
--wait · --timeout리소스가 준비될 때까지 기다림
--rollback-on-failure실패하면 자동으로 이전 revision으로 (Helm 3의 --atomic)
--reuse-values업그레이드할 때 지난번에 준 값을 이어서 씀

2. 롤백이 되돌리지 못하는 것

롤백은 쿠버네티스 리소스의 설정을 예전 revision으로 돌려요. 그 사이 데이터베이스에 쌓인 데이터, PV 안의 파일, 외부 서비스에 보낸 요청은 되돌리지 않아요. 데이터 구조를 바꾸는 업그레이드라면 먼저 백업해야 해요.

chart hook chart는 "helm.sh/hook": pre-upgrade 같은 어노테이션을 단 Job으로 “업그레이드 직전에 DB 백업”, “설치 직후 초기 데이터 넣기” 같은 일을 끼워 넣을 수 있어요. hook은 pre-install · post-install · pre-upgrade · post-upgrade · pre-rollback · pre-delete 등이 있어요.

3. 13부 셀프 체크

  1. Helm을 한 문장으로 설명하면?
    여러 YAML로 이루어진 앱을 하나의 단위(release)로 설치 · 업그레이드 · 롤백 · 삭제하는 쿠버네티스 패키지 관리자.
  2. Helm 3에서 Tiller를 없앤 이유는?
    Tiller가 큰 권한으로 모든 사용자의 요청을 대신 처리해서 보안 구멍이 됐기 때문이에요. Helm 3부터는 CLI가 사용자의 kubeconfig와 RBAC로 API 서버에 직접 요청해요.
  3. 3방향 병합 패치에서 비교하는 세 가지는?
    이전 revision의 차트, 클러스터의 실제(live) 상태, 새로 적용할 차트.
  4. chart · release · revision의 차이는?
    chart는 설치 묶음, release는 그 chart를 클러스터에 설치한 인스턴스(이름 있음), revision은 release가 바뀔 때마다 1씩 늘어나는 버전 번호예요.
  5. 같은 chart로 release를 두 개 설치해도 리소스가 겹치지 않는 이유는?
    템플릿이 리소스 이름에 .Release.Name을 넣기 때문에 release마다 다른 이름이 만들어져요.
  6. Helm 3 · 4는 release 기록을 어디에 저장하나요?
    release가 있는 Namespace의 Secret(sh.helm.release.v1.<이름>.v<revision>, 타입 helm.sh/release.v1).
  7. Chart.yaml의 version과 appVersion의 차이는?
    version은 chart 자체의 버전, appVersion은 chart가 설치하는 앱의 버전이에요.
  8. values.yaml에 replicaCount: 1, -f prod.yaml에 2, --set replicaCount=3을 주면 결과는?
    3. --set이 가장 나중에 적용돼서 이겨요.
  9. 클러스터에 설치하지 않고 템플릿 결과만 보려면?
    helm template 이름 ./chart (API 서버 검증까지 하려면 helm install --dry-run=server).
  10. revision 2에서 helm rollback web 1을 하면 history는 어떻게 되나요?
    revision 1의 설정으로 새 revision 3이 생기고 STATUS가 deployed가 돼요. 기록은 지워지지 않아요.
  11. Helm 4에서 --atomic은 무엇으로 바뀌었나요?
    --rollback-on-failure. --force는 --force-replace로 바뀌었어요.
  12. 롤백했는데 DB 데이터가 그대로예요. 왜인가요?
    롤백은 쿠버네티스 리소스 설정만 되돌리고, PV 안의 데이터는 건드리지 않기 때문이에요. 데이터는 hook이나 별도 백업으로 관리해요.
14부Kustomizebase · overlaykustomization.yamltransformerpatchcomponent

01. Kustomize가 푸는 문제

13부의 Helm은 빈칸이 있는 템플릿에 값을 채웠어요. Kustomize는 다른 길을 가요. 평범한 YAML을 그대로 두고, 환경마다 “여기만 이렇게 바꿔”라는 덮어쓰기만 따로 적어요. 템플릿 문법이 없어서 원본 YAML이 그대로 읽히는 게 장점이에요.

1. 환경마다 조금씩 다른 YAML

dev · stg · prod — 폴더를 복사하면 어긋나고, base + overlay로 나누면 한 곳만 고친다

Flutter로 치면 flavor(dev · prod)마다 앱 전체를 복사하지 않고, 공통 코드는 하나로 두고 환경 설정만 바꾸는 것과 같은 생각이에요.

2. Helm과 비교 · 설치

Helm (13부)Kustomize
방식템플릿 {{ }} + values평범한 YAML + 덮어쓰기(patch)
원본 YAML템플릿이라 그대로는 적용 불가그대로 kubectl apply 가능
조건 · 반복if · range 같은 로직 가능없음 (일부러 단순)
패키지 · 버전 · 롤백chart 배포, release · revision · rollback없음. 결과 YAML만 만들어 줌 (Git이 기록)
설치helm CLI 따로kubectl에 내장 (-k), 단독 CLI도 있음
잘 맞는 곳남이 만든 앱을 설치 · 배포 (공개 chart)우리 팀 YAML을 환경별로 관리 (GitOps)

둘은 경쟁이라기보다 함께 쓰기도 해요. 공개 chart를 helm template으로 YAML로 뽑은 뒤 Kustomize로 우리 환경에 맞게 덮어쓰는 식이에요.

$ kubectl version --client
Client Version: v1.36.x
Kustomize Version: v5.x.x             # kubectl 안에 들어 있는 Kustomize

$ brew install kustomize               # 단독 CLI (kubectl 내장보다 새 버전이 먼저 나옴)
$ kustomize version

02. kustomization.yaml

1. 무엇을 · 어떻게 바꿀지 적는 파일

Kustomize는 폴더 안의 kustomization.yaml 하나만 봐요. 여기에 두 가지를 적어요. ① 어떤 YAML을 가져올지(resources), ② 그 YAML을 어떻게 바꿀지(transformer · patch).

k8s/
├── kustomization.yaml
├── nginx-deployment.yaml
└── nginx-service.yaml
apiVersion: kustomize.config.k8s.io/v1beta1   # 생략해도 되지만 적는 걸 권장
kind: Kustomization

resources:                  # ① 가져올 YAML (파일 · 폴더 · URL)
  - nginx-deployment.yaml
  - nginx-service.yaml

labels:                     # ② 바꿀 내용 — 모든 리소스에 레이블 추가 (04)
  - pairs:
      company: example
kustomization.yaml은 클러스터 리소스가 아니다 kind: Kustomization이라고 적지만 API 서버에 보내는 객체가 아니에요. Kustomize 도구가 읽는 설정 파일이에요. 그래서 kubectl get kustomization 같은 건 없어요.

2. 결과 보기와 적용

resources를 읽고 → transformer를 적용하고 → 하나의 YAML로 출력
$ kubectl kustomize k8s/                     # 결과 YAML을 화면에만 (= kustomize build k8s/)
$ kubectl kustomize k8s/ | kubectl apply -f -  # 결과를 파이프로 넘겨 적용
$ kubectl apply -k k8s/                      # 위와 같은 일을 한 번에
$ kubectl diff -k k8s/                       # 적용하면 무엇이 바뀌는지 미리
$ kubectl delete -k k8s/                     # 그 결과물을 삭제
build는 적용하지 않는다 kustomize build · kubectl kustomize는 결과를 출력만 해요. 13부의 helm template과 같은 역할이에요. 클러스터에 보내려면 apply -k를 쓰거나 파이프로 kubectl apply -f -에 넘겨요.

03. 디렉터리 관리

1. 폴더마다 kustomization.yaml

파일이 늘면 폴더로 나누고, 폴더마다 kustomization.yaml — 루트에서 한 번에

파일이 많아져 폴더로 나누면, 예전에는 폴더마다 kubectl apply -f api/ · -f db/를 따로 했어요. 루트에 kustomization.yaml을 두면 한 번에 돼요. 폴더 안에도 kustomization.yaml을 두면 루트는 폴더 이름만 알면 돼요.

k8s/
├── kustomization.yaml        # resources: [api/, cache/, db/]
├── api/
│   ├── kustomization.yaml    # resources: [api-depl.yaml, api-service.yaml]
│   ├── api-depl.yaml
│   └── api-service.yaml
├── cache/ …
└── db/ …
# k8s/kustomization.yaml
resources:
  - api/
  - cache/
  - db/

04. Transformer

transformer는 “모든 리소스에 이걸 해 줘”처럼 여러 리소스를 한꺼번에 바꾸는 규칙이에요. kustomization.yaml에 필드 하나만 적으면 돼요.

1. 공통 transformer

레이블 · 이름 앞뒤 · Namespace · 어노테이션 — 모든 리소스에 한꺼번에
labels:                      # 학습 자료의 commonLabels 대신
  - pairs:
      org: example
    includeSelectors: true     # selector · Pod 템플릿에도 넣기 (commonLabels와 같은 동작)
namePrefix: shop-              # api → shop-api
nameSuffix: -dev               # shop-api → shop-api-dev
namespace: dev                 # 모든 리소스의 metadata.namespace
commonAnnotations:
  owner: team-a

transformer는 그 kustomization.yaml이 가져온 모든 것에 적용돼요. 루트에 적으면 하위 폴더(api · db)까지 전부, db/kustomization.yaml에 적으면 db 리소스에만 적용돼요. 이름을 바꿔도 Service의 selector나 Deployment가 참조하는 ConfigMap 이름처럼 서로 가리키는 부분은 Kustomize가 함께 고쳐 줘요.

selector 레이블은 나중에 바꾸기 어렵다 includeSelectors: true는 Deployment의 selector까지 바꿔요. 이미 있는 Deployment의 selector는 수정할 수 없어서(2부 02) 적용이 실패해요. 처음부터 정하거나, 나중에 붙이는 레이블은 includeSelectors: false(기본값)로 두세요.

2. Image transformer

images:
  - name: nginx                # 원본 YAML의 image 이름이 nginx인 곳을 찾아
    newName: haproxy           # 이미지를 바꾸고   → haproxy
    newTag: "2.9"              # 태그도 바꿈        → haproxy:2.9
  - name: web
    digest: sha256:4f1c…       # 태그 대신 digest로 고정 (8부 01)

name은 컨테이너 이름이 아니라 image 값을 찾는 조건이에요. 원본 YAML을 건드리지 않고 환경마다 다른 버전을 쓰게 할 때, CI가 빌드한 새 태그를 넣을 때(kustomize edit set image web=registry/web:1.4.2) 많이 써요. 같은 방식의 replicas 필드도 있어요.

replicas:
  - name: api-deployment
    count: 5

3. ConfigMap · Secret generator

학습 자료에는 없지만 실무에서 아주 많이 써요. 파일이나 값에서 ConfigMap · Secret을 만들어 주고, 이름 뒤에 내용의 해시를 붙여요.

configMapGenerator:
  - name: app-config
    files:
      - application.properties
    literals:
      - LOG_LEVEL=info
$ kubectl kustomize .
kind: ConfigMap
metadata:
  name: app-config-7g2h9k5f8m          # 내용이 바뀌면 해시도 바뀜
…
        envFrom:
        - configMapRef:
            name: app-config-7g2h9k5f8m  # Deployment의 참조도 자동으로 고쳐 줌

ConfigMap 내용만 바꾸면 Pod는 그대로라 새 설정을 안 읽어요(5부 03). generator를 쓰면 이름이 바뀌어 Deployment의 Pod 템플릿이 달라지니까 롤링 업데이트가 자동으로 일어나요.

05. Patch

transformer가 “모두에게 똑같이”라면, patch는 “이 리소스의 이 필드만”이에요. 예를 들어 api Deployment의 replicas만 5로 바꾸는 식이에요.

1. 두 가지 patch 방식

같은 목표, 두 가지 적는 법 — JSON 6902와 strategic merge

patch에는 세 가지 정보가 있어요. 무엇을(target: kind · name · label), 어떻게(add · remove · replace), 무슨 값으로.

JSON 6902 patchstrategic merge patch
모양op · path · value 목록바꿀 부분만 남긴 평범한 YAML
대상 지정target에 kind · nameYAML 안의 kind · metadata.name
장점정확함. 리스트 위치까지 지정읽기 쉬움. 원본과 같은 모양
# ① JSON 6902 — inline
patches:
  - target:
      kind: Deployment
      name: api-deployment
    patch: |-
      - op: replace
        path: /spec/replicas
        value: 5
# ② strategic merge — inline
patches:
  - patch: |-
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: api-deployment
      spec:
        replicas: 5

2. inline과 별도 파일

patch가 길어지면 별도 파일로 빼고 path로 가리켜요. 두 방식 모두 가능해요.

patches:
  - path: replicas-patch.yaml          # strategic merge 파일
  - path: replicas-json.yaml           # JSON 6902 파일은 target을 함께
    target:
      kind: Deployment
      name: api-deployment

3. 딕셔너리(키-값) patch

레이블처럼 키-값으로 된 곳을 바꾸는 예예요. JSON 6902에서 경로의 /는 ~1로 적어요(예: app.kubernetes.io~1name).

하고 싶은 일JSON 6902strategic merge
값 바꾸기op: replace, path: /spec/template/metadata/labels/component, value: weblabels: {component: web}
키 추가op: add, path: …/labels/org, value: examplelabels: {org: example}
키 삭제op: remove, path: …/labels/orglabels: {org: null}

4. 리스트 patch

컨테이너 목록처럼 리스트를 바꿀 때가 가장 헷갈려요. JSON 6902는 위치 번호로, strategic merge는 name으로 짝을 맞춰 찾아요.

하고 싶은 일JSON 6902strategic merge
첫 컨테이너 교체op: replace, path: /spec/template/spec/containers/0, value: {name: haproxy, image: haproxy}같은 name의 항목을 적으면 그 항목만 합쳐짐
맨 뒤에 추가op: add, path: …/containers/- (- = 맨 끝)새 name의 항목을 적으면 추가
삭제op: remove, path: …/containers/1그 항목에 $patch: delete
# strategic merge — name이 sidecar인 컨테이너 삭제
spec:
  template:
    spec:
      containers:
        - name: sidecar
          $patch: delete
위치 번호의 함정 JSON 6902의 containers/1은 “두 번째”라는 뜻이라, 원본에서 컨테이너 순서가 바뀌면 엉뚱한 컨테이너를 고쳐요. 리스트는 name으로 찾는 strategic merge가 더 안전한 경우가 많아요.

06. Overlay와 Component

1. base와 overlays

base 한 벌 + 환경별 overlay — 그리고 일부 환경에만 넣는 component
k8s/
├── base/
│   ├── kustomization.yaml       # resources: [nginx-depl.yaml, service.yaml, redis-depl.yaml]
│   ├── nginx-depl.yaml          # replicas: 1
│   ├── service.yaml
│   └── redis-depl.yaml
└── overlays/
    ├── dev/
    │   └── kustomization.yaml
    ├── stg/
    │   └── kustomization.yaml
    └── prod/
        ├── kustomization.yaml
        └── grafana-depl.yaml    # prod에만 있는 리소스
# overlays/prod/kustomization.yaml
resources:
  - ../../base                   # 학습 자료의 bases: 대신 resources에 적음
  - grafana-depl.yaml            # base에 없는 리소스 추가
namespace: prod
patches:
  - target:
      kind: Deployment
      name: nginx-deployment
    patch: |-
      - op: replace
        path: /spec/replicas
        value: 5
$ kubectl apply -k overlays/dev     # dev 환경
$ kubectl apply -k overlays/prod    # prod 환경 — base는 한 벌

overlay 폴더 구조는 자유예요. 환경(dev · prod) 말고 지역(kr · us)이나 고객별로 나눠도 돼요. 공통 수정은 base에서 한 번만 하면 모든 환경에 반영돼요.

2. Component — 골라 넣는 기능 묶음

overlay는 “환경”별 차이예요. 그런데 “캐시 기능” · “외부 DB 사용”처럼 일부 환경에만 넣는 기능이 있으면 어떨까요. dev와 prod에는 넣고 stg에는 빼야 한다면, overlay에 복사하면 또 중복이 생겨요. 이 기능을 component로 한 번 만들어 두고, 필요한 overlay만 가져다 써요.

k8s/
├── base/
├── components/
│   └── caching/
│       ├── kustomization.yaml   # kind: Component
│       ├── redis-depl.yaml
│       └── deployment-patch.yaml
└── overlays/
    ├── dev/      # components: [../../components/caching]
    ├── stg/      # (넣지 않음)
    └── prod/     # components: [../../components/caching]
# components/caching/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component                  # Kustomization이 아니라 Component
resources:
  - redis-depl.yaml
secretGenerator:
  - name: redis-creds
    literals:
      - password=change-me
patches:
  - path: deployment-patch.yaml  # 앱에 REDIS_HOST 환경 변수 추가
# overlays/dev/kustomization.yaml
resources:
  - ../../base
components:
  - ../../components/caching

3. 학습 자료와 달라진 필드

학습 자료의 일부 필드는 Kustomize v5에서 사용 중단(deprecated) 경고가 나와요. 아직 동작은 하지만 새로 쓸 때는 오른쪽 필드를 쓰세요. 단독 CLI의 kustomize edit fix가 옛 필드를 새 필드로 바꿔 줘요.

학습 자료의 필드지금 쓰는 필드
commonLabelslabels (+ includeSelectors: true면 같은 동작)
patchesStrategicMergepatches (path 또는 patch)
patchesJson6902patches (+ target)
basesresources
varsreplacements

4. 14부 셀프 체크

  1. Kustomize가 Helm과 가장 다른 점은?
    템플릿 없이 평범한 YAML을 두고, 바꿀 부분만 patch · transformer로 덧씌워요. 패키지 · release · 롤백 개념은 없어요.
  2. kustomization.yaml에 적는 두 가지는?
    가져올 YAML 목록(resources)과 그것을 어떻게 바꿀지(transformer · patch).
  3. 결과 YAML만 보려면? 바로 적용하려면?
    보기: kubectl kustomize 폴더(= kustomize build). 적용: kubectl apply -k 폴더.
  4. 하위 폴더에도 kustomization.yaml을 두면 루트 resources에는 무엇을 적나요?
    파일 경로 대신 폴더 이름(api/, db/)만 적어요.
  5. 루트 kustomization.yaml의 namePrefix는 하위 폴더 리소스에도 적용되나요?
    네. 그 kustomization이 가져온 모든 리소스에 적용돼요. 하위 폴더에 적으면 그 폴더 리소스에만 적용돼요.
  6. images transformer의 name은 무엇과 비교하나요?
    컨테이너 이름이 아니라 원본 YAML의 image 값(이미지 이름)이에요.
  7. configMapGenerator가 이름 뒤에 해시를 붙이면 좋은 점은?
    내용이 바뀌면 이름이 바뀌고 Deployment의 참조도 같이 바뀌어서, 설정 변경 때 롤링 업데이트가 자동으로 일어나요.
  8. patch에 필요한 세 가지 정보는?
    대상(target), 동작(add · remove · replace), 값(value).
  9. JSON 6902로 컨테이너 리스트 맨 뒤에 추가하는 path는?
    /spec/template/spec/containers/- (op: add).
  10. strategic merge patch로 리스트 항목을 지우는 방법은?
    그 항목의 name과 함께 $patch: delete를 적어요.
  11. overlay와 component의 차이는?
    overlay는 환경 하나(dev · prod)의 최종 설정이고, component는 여러 overlay가 골라서 가져다 쓰는 기능 묶음(kind: Component)이에요.
  12. 학습 자료의 commonLabels · patchesStrategicMerge · bases는 지금 무엇으로 쓰나요?
    labels, patches, resources.
15부쿠버네티스 확장하기CRDCustom Resource조정 루프Custom ControllerOperator

01. CRD

지금까지 쓴 Pod · Deployment · Service는 쿠버네티스에 처음부터 들어 있는 리소스였어요. 쿠버네티스는 여기에 우리만의 리소스 종류를 새로 추가할 수 있어요. 11부의 Gateway API, 9부의 VolumeSnapshot도 사실 이렇게 추가된 리소스예요.

1. 리소스 = API에 저장된 기록 + 그걸 실행하는 컨트롤러

Deployment는 기록 + 컨트롤러, FlightTicket은 CRD로 종류부터 등록해야 한다

Deployment를 만들면 두 가지가 일어나요. API 서버가 그 내용을 etcd에 기록하고, 컨트롤러 매니저 안의 Deployment 컨트롤러가 그 기록을 보고 ReplicaSet을 만들어요(1부 05). 새 리소스를 추가할 때도 이 두 가지가 필요해요.

용어뜻예
CRD (CustomResourceDefinition)“이런 종류의 리소스가 있다”고 API 서버에 등록하는 정의FlightTicket이라는 종류
Custom Resource (CR)그 종류로 만든 객체 하나서울 → 제주 항공권 my-flight-ticket
Custom ControllerCR을 지켜보다가 실제 일을 하는 프로그램 (02)항공사 API로 예약

2. CRD 만들기

학습 자료의 예제는 항공권 예약이에요. 먼저 만들고 싶은 CR을 정해요.

apiVersion: flights.com/v1      # CRD에서 정할 group/version
kind: FlightTicket
metadata:
  name: my-flight-ticket
spec:
  from: Seoul
  to: Jeju
  number: 2                     # 좌석 수

이 YAML을 그대로 적용하면 no matches for kind "FlightTicket" 오류가 나요. 그런 종류를 API 서버가 모르기 때문이에요. 그래서 CRD로 종류부터 등록해요.

apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: flighttickets.flights.com       # 반드시 <plural>.<group>
spec:
  group: flights.com                    # apiVersion의 앞부분
  scope: Namespaced                     # 또는 Cluster
  names:
    kind: FlightTicket
    singular: flightticket
    plural: flighttickets               # URL · kubectl get 에 쓰임
    shortNames: [ft]
  versions:
    - name: v1
      served: true                      # 이 버전으로 API 요청을 받음
      storage: true                     # etcd에 저장하는 버전 (딱 하나)
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                from:   { type: string }
                to:     { type: string }
                number: { type: integer, minimum: 1, maximum: 10 }
$ kubectl apply -f flightticket-crd.yaml
customresourcedefinition.apiextensions.k8s.io/flighttickets.flights.com created
$ kubectl api-resources | grep flight
flighttickets   ft   flights.com/v1   true   FlightTicket
$ kubectl apply -f my-flight-ticket.yaml
flightticket.flights.com/my-flight-ticket created
$ kubectl get ft
NAME               AGE
my-flight-ticket   5s
필드뜻
groupAPI 그룹 (7부 06). 보통 도메인 형식. CR의 apiVersion: flights.com/v1
scopeNamespaced(Namespace 안에) 또는 Cluster(클러스터 전체에 하나)
nameskind · 단수 · 복수 · 짧은 이름. RBAC에서는 복수 이름을 써요
versions여러 버전(v1alpha1 → v1)을 동시에 제공 가능. storage: true는 하나만
schema필드 이름 · 타입 · 범위. 맞지 않는 CR은 API 서버가 거부

3. 더 똑똑한 CRD — 검증 규칙 · 출력 열 · status

그 뒤로 CRD가 할 수 있는 일이 많아졌어요. 아래 필드는 같은 versions[] 항목 안에 적어요.

      schema:
        openAPIV3Schema:
          properties:
            spec:
              type: object
              x-kubernetes-validations:          # CEL 검증 규칙 (1.29 GA)
                - rule: "self.from != self.to"
                  message: "출발지와 도착지가 같을 수 없어요"
      additionalPrinterColumns:                  # kubectl get 에 보일 열
        - name: From
          type: string
          jsonPath: .spec.from
        - name: To
          type: string
          jsonPath: .spec.to
      subresources:
        status: {}                               # spec과 status를 따로 갱신
$ kubectl get ft
NAME               FROM    TO
my-flight-ticket   Seoul   Jeju
기능하는 일
x-kubernetes-validations필드끼리 비교 같은 복잡한 규칙을 CEL 식으로. 예전에는 웹훅(7부 09)을 따로 만들어야 했던 일
additionalPrinterColumnskubectl get 출력에 원하는 열 추가
subresources.status사용자는 spec(원하는 상태)을, 컨트롤러는 status(실제 상태)를 따로 갱신
CRD는 이미 많이 쓰고 있다 kubectl get crd를 쳐 보면 CNI(Calico · Cilium), Gateway API, VolumeSnapshot 등 이미 설치된 CRD가 보여요. 쿠버네티스 생태계의 많은 도구가 “CRD + 컨트롤러” 모양으로 만들어져 있어요.

02. Custom Controller

CRD만 있으면 FlightTicket은 etcd에 저장된 메모일 뿐이에요. 실제로 항공권을 예약하려면 이 메모를 읽고 일하는 컨트롤러가 필요해요.

1. 조정 루프 (reconcile loop)

지켜보고 → 비교하고 → 맞추고 → 기록한다 — 끝없이 반복

모든 컨트롤러는 같은 일을 반복해요. 원하는 상태(spec)와 실제 상태를 비교하고, 다르면 맞추고, 결과를 status에 적어요. 1부에서 본 ReplicaSet 컨트롤러가 “Pod 3개를 원하는데 2개다 → 하나 더”를 반복하는 것과 똑같은 구조예요.

2. 컨트롤러는 어떻게 만드나

컨트롤러는 보통 Go로 만들어요. 쿠버네티스 자체가 Go로 쓰여 있고, 공식 라이브러리가 가장 잘 갖춰져 있기 때문이에요(다른 언어도 가능해요).

부품하는 일
client-goAPI 서버와 대화하는 공식 Go 라이브러리
informer · cacheAPI 서버를 watch해서 변경을 받아 오고 메모리에 복사본을 둠. 매번 API 서버에 묻지 않아 부담이 적어요
workqueue바뀐 객체의 이름을 줄 세워 하나씩 처리. 실패하면 간격을 늘려 다시 넣음
Reconcile 함수우리가 실제로 짜는 부분 — 비교하고 맞추기

학습 자료는 이 부품들을 직접 엮은 sample-controller 저장소를 보여 줘요. 요즘은 kubebuilder(controller-runtime 기반)로 뼈대를 만들고 Reconcile 함수만 채우는 경우가 많아요.

Reconcile 함수의 모양 (Go, 핵심만)
func (r *FlightTicketReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    var ticket flightsv1.FlightTicket
    if err := r.Get(ctx, req.NamespacedName, &ticket); err != nil {
        return ctrl.Result{}, client.IgnoreNotFound(err) // 이미 지워졌으면 끝
    }
    booked := r.Airline.CountSeats(ticket.Spec.From, ticket.Spec.To) // 실제 상태
    if booked < ticket.Spec.Number {                                 // 원하는 상태와 비교
        r.Airline.Book(ticket.Spec.Number - booked)                  // 차이만큼 맞추기
    }
    ticket.Status.Booked = ticket.Spec.Number
    return ctrl.Result{}, r.Status().Update(ctx, &ticket)            // status 기록
}
$ kubebuilder init --domain flights.com
$ kubebuilder create api --group flights --version v1 --kind FlightTicket
$ make manifests                 # Go 구조체에서 CRD YAML 생성
$ make run                       # 노트북에서 실행 (kubeconfig로 접속)
$ make docker-build docker-push deploy   # 이미지로 만들어 클러스터에 Deployment로

클러스터 안에서 돌릴 때는 Deployment로 띄우고, ServiceAccount에 FlightTicket을 읽고 status를 쓸 수 있는 RBAC 권한을 줘요(7부 08).

3. 좋은 컨트롤러의 조건 — 멱등성 · finalizer · ownerReference

지울 때 뒷정리 — finalizer가 삭제를 잠깐 붙잡는다
원칙뜻
멱등성같은 Reconcile을 여러 번 실행해도 결과가 같아야 해요. “예약하라”가 아니라 “원하는 만큼 예약돼 있게 하라”로 짜요. 컨트롤러는 재시작 · 중복 이벤트로 같은 객체를 여러 번 처리해요
finalizermetadata.finalizers에 이름이 있으면 삭제 요청이 와도 객체가 바로 사라지지 않고 deletionTimestamp만 붙어요. 컨트롤러가 외부 정리(예약 취소)를 끝내고 finalizer를 지우면 그때 삭제돼요
ownerReference컨트롤러가 만든 하위 리소스에 “주인”을 적어 두면, 주인이 지워질 때 쿠버네티스 가비지 컬렉터가 하위 리소스를 함께 지워요. Deployment → ReplicaSet → Pod가 같이 지워지는 원리
Terminating에서 멈춘 객체 Namespace나 CR이 계속 Terminating이면 대부분 finalizer를 지워 줄 컨트롤러가 없거나 죽은 경우예요. kubectl get <종류> <이름> -o jsonpath='{.metadata.finalizers}'로 확인해요. finalizer를 손으로 지우면 외부 정리가 안 된 채 사라지니, 원인 컨트롤러부터 살펴봐요.

03. Operator Framework

1. Operator = CRD + 컨트롤러 + 운영 지식

사람 운영자가 하던 일을 코드로 — etcd Operator

Operator는 특정 앱(데이터베이스 등)을 운영하는 사람의 노하우 — 설치 순서, 백업, 복구, 업그레이드, 장애 대응 — 를 CRD와 컨트롤러로 담은 묶음이에요. 학습 자료의 예는 etcd Operator예요. EtcdCluster · EtcdBackup · EtcdRestore 같은 CRD를 만들면, 12부에서 손으로 하던 멤버 관리와 6부의 스냅샷 백업을 Operator가 대신해요.

apiVersion: etcd.example.com/v1
kind: EtcdCluster
metadata:
  name: my-etcd
spec:
  size: 3                 # 멤버 3개 — 홀수 (12부 03)
  version: "3.6.0"
학습 자료의 etcd-operator 학습 자료의 CoreOS etcd-operator는 오래전에 개발이 멈춘 프로젝트예요. 2025년에 etcd 커뮤니티가 새 etcd-operator(v0.1.0)를 내놓았어요. 개념은 같으니 “CRD로 원하는 클러스터를 적으면 Operator가 맞춰 준다”는 흐름만 이해하면 돼요.

2. 도구와 OperatorHub

이름역할
kubebuilderCRD · 컨트롤러 뼈대를 만드는 쿠버네티스 공식 도구 (02)
Operator SDKkubebuilder 위에 Helm · Ansible로 Operator를 만드는 기능, 배포 묶음 생성 등을 더한 Operator Framework의 도구
OLM (Operator Lifecycle Manager)클러스터에 Operator를 설치 · 업그레이드 · 의존성 관리. Operator를 위한 패키지 관리자
OperatorHub.io공개 Operator를 찾는 사이트. 설치 방법과 성숙도를 보여 줘요

Operator 성숙도(capability level)는 5단계로 나눠요. ① 기본 설치 → ② 매끄러운 업그레이드 → ③ 백업 · 복구 등 전체 라이프사이클 → ④ 지표 · 알림 같은 깊은 관찰 → ⑤ 자동 조정(스케일 · 튜닝). Operator를 고를 때 몇 단계까지 해 주는지 보면 좋아요.

# Helm으로 Operator를 설치하는 경우가 많아요 (13부)
$ helm install cert-manager oci://quay.io/jetstack/charts/cert-manager \
    -n cert-manager --create-namespace --set crds.enabled=true
$ kubectl get crd | grep cert-manager
certificates.cert-manager.io    …
issuers.cert-manager.io         …

3. 자주 만나는 CRD · Operator

도구대표 CR하는 일
cert-managerCertificate · IssuerTLS 인증서 자동 발급 · 갱신 (11부 Ingress TLS)
Prometheus OperatorPrometheus · ServiceMonitor모니터링 서버 설치, 수집 대상 등록 (4부)
Argo CDApplicationGit의 YAML(Kustomize · Helm)을 클러스터와 계속 맞춤 (GitOps)
CloudNativePGClusterPostgreSQL 복제 · 백업 · 장애 조치
Gateway APIGateway · HTTPRoute11부 07 — CRD로 추가되는 표준 API
CRD 말고 다른 확장 방법 API 서버 앞에 우리 API 서버를 하나 더 붙이는 API 집계 계층(aggregation layer)도 있어요. metrics-server(4부)가 이 방식이에요. 저장 방식까지 직접 정해야 해서 어렵고, 대부분의 경우 CRD로 충분해요.

4. 15부 셀프 체크

  1. CRD와 Custom Resource의 차이는?
    CRD는 “이런 종류가 있다”는 정의(종류 등록), Custom Resource는 그 종류로 만든 객체 하나예요.
  2. CRD 없이 FlightTicket YAML을 적용하면 어떤 오류가 나나요?
    no matches for kind "FlightTicket" in version "flights.com/v1" — API 서버가 그 종류를 몰라요.
  3. CRD의 metadata.name은 어떤 형식이어야 하나요?
    <plural>.<group> — 예: flighttickets.flights.com.
  4. versions의 served와 storage의 차이는?
    served는 그 버전으로 API 요청을 받는지, storage는 etcd에 저장할 때 쓰는 버전(하나만 true)이에요.
  5. CRD만 만들고 컨트롤러가 없으면 어떻게 되나요?
    CR은 etcd에 저장되고 조회도 되지만, 그걸 보고 실제 일을 하는 쪽이 없어서 아무 일도 일어나지 않아요.
  6. 조정 루프의 네 단계는?
    지켜보기(watch) → 원하는 상태와 실제 상태 비교 → 차이만큼 맞추기 → status에 기록. 이를 계속 반복해요.
  7. informer가 하는 일은?
    API 서버를 watch해서 변경을 받아 오고 메모리 캐시에 복사본을 유지해요. 컨트롤러가 매번 API 서버에 묻지 않아도 돼요.
  8. 컨트롤러를 멱등하게 짜야 하는 이유는?
    재시작이나 중복 이벤트로 같은 객체를 여러 번 처리하기 때문이에요. 여러 번 실행해도 결과가 같아야 중복 예약 같은 사고가 안 나요.
  9. finalizer는 언제 쓰나요?
    객체가 지워지기 전에 외부 리소스 정리(예약 취소, 클라우드 디스크 삭제 등)를 해야 할 때요. 정리가 끝나고 finalizer를 지워야 삭제가 완료돼요.
  10. Deployment를 지우면 ReplicaSet과 Pod도 같이 지워지는 원리는?
    하위 리소스의 ownerReference에 주인이 적혀 있어서, 가비지 컬렉터가 주인이 사라진 하위 리소스를 지워요.
  11. Operator를 한 문장으로 설명하면?
    특정 앱을 운영하는 사람의 지식(설치 · 백업 · 복구 · 업그레이드)을 CRD와 컨트롤러로 자동화한 것.
  12. CRD에서 필드끼리 비교하는 검증은 어떻게 하나요?
    x-kubernetes-validations에 CEL 식(예: self.from != self.to)을 적어요. 1.29부터 정식 기능이에요.
16부트러블슈팅좁혀 가기앱컨트롤 플레인워커 노드네트워크kubectl debug

01. 좁혀 가는 순서

장애는 대부분 “안 돼요” 한마디로 시작해요. 쿠버네티스는 부품이 많아서 아무 데나 열어 보면 길을 잃어요. 그래서 요청이 지나가는 길을 따라 한 칸씩 확인하며 범위를 좁혀요. 지금까지 배운 1~15부가 전부 여기서 쓰여요.

1. 바깥에서 안으로

사용자 → Service → Pod → 컨테이너 → 노드 → 컨트롤 플레인 — 한 칸씩 확인한다
칸질문확인 명령
입구Ingress · LB가 요청을 받나?kubectl get ingress, curl -v
ServiceService가 Pod를 찾았나?kubectl get endpointslice -l kubernetes.io/service-name=web
PodRunning · Ready인가? 재시작했나?kubectl get pod -o wide, describe pod
컨테이너앱이 무슨 말을 남겼나?kubectl logs (--previous)
노드노드가 Ready인가? 자원은?kubectl describe node, kubectl top node
컨트롤 플레인API · 스케줄러 · 컨트롤러가 살아 있나?kubectl get pods -n kube-system

2. STATUS가 알려 주는 첫 단서

STATUS뜻먼저 볼 곳
Pending노드를 못 받음describe의 Events: 자원 부족 · taint · affinity · PVC (3부, 9부)
ContainerCreating노드는 정해졌는데 준비 중볼륨 마운트 · CNI(IP 할당) · Secret 없음
ImagePullBackOff · ErrImagePull이미지를 못 받음이름 · 태그 오타, 비공개 저장소 인증(8부 01)
CrashLoopBackOff시작했다 죽기를 반복logs --previous, 종료 코드, probe(5부 06)
OOMKilled메모리 limit 초과로 강제 종료 (종료 코드 137)limits · 앱 메모리 사용량(3부 06)
CreateContainerConfigError설정 재료가 없음참조한 ConfigMap · Secret · 키 이름(5부 03 · 04)
Running인데 0/1떠 있지만 Ready 아님readinessProbe 실패 → Service에서 빠짐
Terminating에서 멈춤삭제가 안 끝남finalizer(15부 02), 노드가 죽어 kubelet이 응답 없음
$ kubectl describe pod web-7d9f-x2kq        # 맨 아래 Events가 핵심
$ kubectl logs web-7d9f-x2kq --previous     # 직전에 죽은 컨테이너의 로그
$ kubectl events --for pod/web-7d9f-x2kq    # 그 객체의 이벤트만 시간 순으로
$ kubectl get events -A --sort-by=.lastTimestamp | tail   # 클러스터 전체 최근 이벤트
이벤트는 1시간만 이벤트는 기본 1시간이 지나면 사라져요. 오래전 장애는 이벤트로 못 보니, 로그 수집 · 모니터링(4부)이 따로 필요해요.

02. 애플리케이션 장애

1. 사용자에서 DB까지 따라가기

2계층 앱 — web Service → web Pod → db Service → db Pod 순서로 확인

학습 자료의 2계층 앱은 web(프론트)과 db(MySQL)로 이루어져요. 사용자에 가까운 쪽부터 한 칸씩 확인해요.

$ curl http://web-service-ip:node-port             # ① 입구가 응답하나
$ kubectl describe service web-service             # ② Selector · TargetPort · Endpoints
Selector:     name=webapp-mysql
TargetPort:   8080/TCP
Endpoints:    <none>                               #    ← 비어 있으면 selector와 Pod 레이블 비교
$ kubectl get pod --show-labels                    # ③ Pod 레이블
$ kubectl get pod web-… -o jsonpath='{.spec.containers[0].ports}'   # ④ 컨테이너 포트 = targetPort?
$ kubectl logs web-… -f                            # ⑤ 앱 로그 (DB 접속 오류 등)
$ kubectl get pod web-… -o yaml | grep -A1 DB_     # ⑥ 접속 정보 (DB_Host · DB_User · DB_Password)
$ kubectl describe service mysql-service           # ⑦ 다음 칸 — db Service 이름 · 포트 3306
흔한 원인증상확인
Service selector ≠ Pod 레이블Endpoints가 비어 있음, 연결 시간 초과describe svc의 Selector vs --show-labels
targetPort ≠ 컨테이너 포트Endpoints는 있는데 connection refusedService의 TargetPort vs 컨테이너 port
Service 이름 · Namespace 착오앱 로그에 “unknown host”앱의 환경 변수, DNS 이름(11부 05)
비밀번호 · 사용자 오타앱 로그에 “Access denied”Secret 값(5부 04)과 DB 설정
NetworkPolicy로 막힘같은 Namespace의 다른 Pod도 접속 불가kubectl get networkpolicy(8부 03)

2. CrashLoopBackOff 읽기

CrashLoopBackOff는 원인이 아니라 “계속 죽어서 재시작 간격을 늘리는 중”이라는 결과예요. 원인은 직전 컨테이너가 남긴 로그와 종료 코드에 있어요.

$ kubectl get pod web-…
NAME     READY   STATUS             RESTARTS      AGE
web-…    0/1     CrashLoopBackOff   5 (40s ago)   4m
$ kubectl describe pod web-… | grep -A5 "Last State"
    Last State:     Terminated
      Reason:       Error
      Exit Code:    1                    # 1: 앱 오류 · 137: SIGKILL(OOM 등) · 143: SIGTERM
$ kubectl logs web-… --previous          # 죽기 직전의 마지막 말
종료 코드흔한 뜻
0정상 종료 — 계속 실행돼야 하는 앱인데 할 일이 끝나 버림 (명령 · 인자 확인, 5부 02)
1 · 2앱이 오류로 종료 — 로그에 이유가 있음
126 · 127명령을 실행할 수 없음 · 명령을 찾을 수 없음 (command 오타, 이미지에 없는 바이너리)
137SIGKILL — OOMKilled이거나 livenessProbe 실패로 강제 종료
143SIGTERM — 정상적인 종료 요청을 받음

03. 컨트롤 플레인 장애

1. 증상으로 부품 찾기

어느 부품이 죽었는지는 증상이 알려 준다 — 그리고 API 서버가 죽으면 노드로 내려간다
증상의심할 부품
새 Pod가 Pending인데 Events가 비어 있음 (NODE <none>)kube-scheduler (3부)
Deployment를 스케일 · 수정해도 Pod가 안 바뀜, 죽은 Pod가 다시 안 생김kube-controller-manager (1부 05)
kubectl이 connection refused … :6443kube-apiserver (또는 그 앞의 LB)
API가 느리거나 etcdserver: request timed outetcd (디스크 · 쿼럼, 12부 03)
인증서 만료 오류 x509: certificate has expired컨트롤 플레인 인증서 (7부 04, kubeadm certs check-expiration)

2. kubeadm 클러스터 — Static Pod 확인

$ kubectl get pods -n kube-system                  # API가 살아 있으면 여기서 상태 확인
kube-scheduler-controlplane   0/1   CrashLoopBackOff
$ kubectl logs kube-scheduler-controlplane -n kube-system
$ kubectl describe pod kube-scheduler-controlplane -n kube-system

# API 서버가 죽어 kubectl이 안 되면 컨트롤 플레인 노드에 접속해서
$ sudo crictl ps -a | grep kube-apiserver          # 컨테이너 상태 (Exited?)
$ sudo crictl logs <container-id>                # 마지막 로그
$ sudo vi /etc/kubernetes/manifests/kube-apiserver.yaml   # 옵션 · 경로 · 포트 오타 (3부 07)
$ sudo journalctl -u kubelet -f                    # kubelet이 매니페스트를 왜 못 띄우는지
매니페스트를 고치면 기다린다 Static Pod 매니페스트를 저장하면 kubelet이 알아서 다시 띄워요. 20~30초쯤 걸리니 바로 kubectl이 안 된다고 또 고치지 말고 crictl ps로 컨테이너가 새로 떴는지 봐요. 매니페스트 백업은 /etc/kubernetes/manifests 바깥에 두세요(안에 두면 그것도 Pod로 뜨려 해요).

3. 서비스로 설치된 클러스터

kubeadm이 아니라 “The Hard Way”(12부 01)처럼 부품을 systemd 서비스로 설치했다면 Pod가 아니라 서비스를 봐요.

$ sudo systemctl status kube-apiserver kube-controller-manager kube-scheduler
$ sudo journalctl -u kube-apiserver --since "10 min ago"
$ sudo systemctl status kubelet kube-proxy             # 워커 노드

04. 워커 노드 장애

1. 노드 조건과 NotReady

kubelet이 보고를 멈추면 — Unknown → taint → Pod 다른 노드로
$ kubectl get nodes
NAME     STATUS     ROLES    AGE
node01   NotReady   <none>   12d
$ kubectl describe node node01
Conditions:
  Type             Status    Reason
  MemoryPressure   Unknown   NodeStatusUnknown    # Unknown = kubelet이 보고를 멈춤
  DiskPressure     Unknown   NodeStatusUnknown
  PIDPressure      Unknown   NodeStatusUnknown
  Ready            Unknown   NodeStatusUnknown
조건True일 때 뜻
Ready정상. False면 kubelet은 살아 있지만 문제가 있음(런타임 · CNI), Unknown이면 보고가 끊김
MemoryPressure메모리 부족 — kubelet이 Pod를 내보내기(evict) 시작
DiskPressure디스크 부족 — 이미지 정리 · Pod 내보내기
PIDPressure프로세스가 너무 많음
NetworkUnavailable노드 네트워크(CNI 경로)가 설정되지 않음
학습 자료의 OutOfDisk 예전 노드 조건 OutOfDisk는 없어졌어요. 디스크 부족은 DiskPressure로 봐요.

2. 노드에 들어가서

$ ssh node01
$ top ; df -h ; free -m                               # 자원 — 메모리 · 디스크
$ sudo systemctl status kubelet                       # active (running)?
$ sudo journalctl -u kubelet -n 50 --no-pager         # 왜 멈췄나
$ sudo systemctl status containerd                    # 런타임도 같이
$ sudo systemctl restart kubelet

# 자주 나오는 kubelet 오류
$ sudo cat /var/lib/kubelet/config.yaml               # kubelet 설정 (clientCAFile · cgroupDriver 등)
$ sudo cat /etc/kubernetes/kubelet.conf               # API 서버 주소 · 포트 (6443?)
$ sudo openssl x509 -noout -subject -issuer -enddate \
    -in /var/lib/kubelet/pki/kubelet-client-current.pem   # 인증서 만료 · 발급자
journalctl에 보이는 말원인
failed to load Kubelet config fileconfig.yaml 경로 · 문법 오류
unable to load client CA fileCA 인증서 경로 오타
dial tcp …:6553: connect: connection refusedkubelet.conf의 API 서버 포트 오타
certificate has expiredkubelet 클라이언트 인증서 만료
failed to run Kubelet: running with swap on재부팅 후 swap이 다시 켜짐 (12부 04)

05. 네트워크 장애

1. CNI → kube-proxy → CoreDNS 순서로

아래 층이 무너지면 위 층도 무너진다 — CNI · kube-proxy · CoreDNS
층증상확인 · 해결
CNI (11부 03)노드 NotReady, Pod ContainerCreating, CoreDNS Pendingls /etc/cni/net.d, CNI DaemonSet Pod 상태, CNI의 Pod 대역 = --pod-network-cidr
kube-proxy (11부 04)Pod IP로는 되는데 Service IP로 안 됨kubectl get pods -n kube-system -l k8s-app=kube-proxy, 로그, ConfigMap kube-proxy의 설정 경로
CoreDNS (11부 05)IP로는 되는데 이름으로 안 됨CoreDNS Pod 상태, kube-dns Service의 Endpoints, Corefile
NetworkPolicy (8부 03)특정 Pod끼리만 안 됨그 Namespace의 정책, 기본 거부 정책 유무
CoreDNS CrashLoopBackOff — 루프 노드의 /etc/resolv.conf가 127.0.0.53 같은 자기 자신(로컬 DNS 캐시)을 가리키면, CoreDNS가 받은 질문을 자기에게 다시 보내는 루프가 생겨 loop 플러그인이 CoreDNS를 종료시켜요. kubelet의 resolvConf를 실제 업스트림 목록(/run/systemd/resolve/resolv.conf)으로 지정하거나, Corefile의 forward를 실제 DNS 서버(예: 8.8.8.8)로 바꿔요.

2. 진단용 Pod로 직접 확인

$ kubectl run tmp --rm -it --image=busybox:1.36 --restart=Never -- sh
/ # nslookup kubernetes.default                   # DNS — 실패하면 CoreDNS 쪽
/ # nslookup web-service.default.svc.cluster.local
/ # wget -qO- -T 3 http://web-service:80          # Service IP — 실패하면 kube-proxy · Endpoints
/ # wget -qO- -T 3 http://10.244.1.5:8080          # Pod IP 직접 — 실패하면 CNI · NetworkPolicy · 앱

$ kubectl run net --rm -it --image=nicolaka/netshoot -- bash   # dig · curl · tcpdump가 다 든 이미지

순서가 중요해요. Pod IP → Service IP → 이름 순으로 확인하면 어느 층에서 끊겼는지 바로 보여요.

06. kubectl debug와 임시 컨테이너

1. 셸이 없는 컨테이너 들여다보기

exec가 안 되는 Pod 옆에 임시 컨테이너를 붙인다

보안 때문에 요즘 이미지는 셸조차 없는 경우가 많아요(distroless, 8부 01). 그러면 kubectl exec -it … -- sh가 executable file not found로 실패해요. 임시 컨테이너(ephemeral container, 1.25 GA)는 이미 실행 중인 Pod에 디버깅용 컨테이너를 잠깐 붙이는 기능이에요. Pod를 다시 만들지 않아요.

$ kubectl exec -it web -- sh
error: … exec: "sh": executable file not found in $PATH

$ kubectl debug -it web --image=busybox:1.36 --target=app
/ # ps aux                       # --target 덕분에 app 컨테이너의 프로세스가 보임
/ # wget -qO- localhost:8080     # 같은 Pod라 네트워크(localhost)도 공유
/ # cat /proc/1/root/etc/config.yaml   # app 컨테이너의 파일 시스템 엿보기

2. 세 가지 사용법

방법명령쓸 때
임시 컨테이너kubectl debug -it web --image=busybox --target=app실행 중인 Pod를 그대로 두고 조사
복사본 Podkubectl debug web -it --copy-to=web-dbg --container=app -- shCrashLoopBackOff처럼 바로 죽는 Pod — 명령을 바꾼 복사본으로 들어가 보기
노드 디버깅kubectl debug node/node01 -it --image=busyboxssh 없이 노드 조사. 노드의 파일 시스템이 /host에 붙음 (chroot /host)

--profile로 디버그 컨테이너의 권한을 골라요: general(일반), baseline · restricted(제한), netadmin(tcpdump 등 네트워크 권한), sysadmin(노드 전체 권한). 필요한 만큼만 주고, 노드 디버깅이 끝나면 만든 Pod를 지워요(kubectl get pods에 node-debugger-…로 남아 있어요).

3. 16부 셀프 체크

  1. “웹이 안 열려요”를 받았을 때 확인 순서는?
    입구(Ingress · LB) → Service · Endpoints → Pod 상태 → 컨테이너 로그 → 노드 → 컨트롤 플레인. 바깥에서 안으로 한 칸씩.
  2. Service의 Endpoints가 비어 있으면 무엇을 비교하나요?
    Service의 selector와 Pod의 레이블(--show-labels). Pod가 Ready가 아니어도 빠져요.
  3. Endpoints는 있는데 connection refused가 나요. 의심할 곳은?
    Service의 targetPort와 컨테이너가 실제로 듣는 포트가 다른지, 앱이 0.0.0.0이 아니라 127.0.0.1에만 열려 있지 않은지.
  4. CrashLoopBackOff인 Pod의 원인을 보려면?
    kubectl logs --previous로 직전 컨테이너 로그, describe의 Last State 종료 코드를 봐요.
  5. 종료 코드 137은 보통 무엇을 뜻하나요?
    SIGKILL — 메모리 limit 초과(OOMKilled)나 livenessProbe 실패로 강제 종료된 경우가 많아요.
  6. 새 Pod가 Pending인데 Events에 아무것도 없어요. 무엇을 의심하나요?
    스케줄러. kubectl get pods -n kube-system으로 kube-scheduler 상태를 확인해요.
  7. kubectl이 6443 connection refused로 실패하면 어떻게 조사하나요?
    컨트롤 플레인 노드에 접속해 crictl ps -a · crictl logs로 API 서버 컨테이너를 보고, /etc/kubernetes/manifests/kube-apiserver.yaml과 journalctl -u kubelet을 확인해요.
  8. 노드 조건이 모두 Unknown이에요. 무슨 뜻인가요?
    kubelet이 상태 보고를 멈췄다는 뜻이에요. 노드에 들어가 systemctl status kubelet, journalctl -u kubelet을 봐요.
  9. Pod IP로는 되는데 Service 이름으로는 안 돼요. 다음 확인은?
    Service IP로 시도해 봐요. IP로 되면 CoreDNS 문제, IP로도 안 되면 kube-proxy · Endpoints 문제예요.
  10. CoreDNS가 CrashLoopBackOff이고 로그에 loop가 보여요. 원인은?
    노드의 resolv.conf가 로컬 DNS(127.0.0.53 등)를 가리켜 CoreDNS가 자기에게 질문을 되돌려 보내는 루프예요. kubelet의 resolvConf나 Corefile의 forward 대상을 바꿔요.
  11. 셸이 없는 컨테이너를 조사하려면?
    kubectl debug -it <pod> --image=busybox --target=<컨테이너>로 임시 컨테이너를 붙여요.
  12. ssh 없이 노드의 파일을 보려면?
    kubectl debug node/<노드> -it --image=busybox — 노드 파일 시스템이 /host에 붙어요. 끝나면 디버거 Pod를 지워요.
17부kubectl 실전 도구JSON Path-o jsonpathcustom-columnssort-by빠르게 쓰는 습관

01. JSON Path

지금까지 kubectl get은 사람이 보기 좋은 표를 보여 줬어요. 그런데 “모든 노드의 CPU 수만”, “이 Pod의 이미지 이름만” 같은 딱 한 값이 필요할 때가 많아요. 스크립트에 넣거나 시험에서 답을 파일로 낼 때는 더 그래요. 그때 쓰는 게 JSON Path예요.

1. JSON Path 문법

kubectl이 주고받는 데이터는 사실 JSON이에요. YAML은 같은 구조를 다르게 적은 것뿐이라, -o yaml로 보던 그 트리를 JSON Path로 따라 내려갈 수 있어요. Dart로 치면 json['car']['wheels'][1]['model']를 짧은 문자열로 쓰는 셈이에요.

루트에서 키를 따라 내려가고, 리스트는 번호 · 별표 · 조건으로 고른다
문법뜻예
$맨 위(루트)$.car
.키딕셔너리에서 키로 한 칸 내려가기$.car.color
[n]리스트의 n번째 (0부터)$.car.wheels[1]
[*]리스트의 모든 항목$.car.wheels[*].model
[a:b]a번부터 b번 앞까지 잘라 내기$[0:3] → 0 · 1 · 2
[-1:]뒤에서부터 (마지막 하나)$[-1:]
[?(조건)]조건에 맞는 항목만. @는 “지금 보고 있는 항목”$.car.wheels[?(@.location == "rear-right")]
결과는 항상 리스트 JSON Path의 결과는 하나뿐이어도 리스트예요. $.car.color의 결과는 "blue"가 아니라 ["blue"]예요. 학습 자료의 순수 JSON Path 연습 문제($.prizes[?(@.year == 2014)].laureates[*].firstname 같은)도 이 규칙대로 풀면 돼요.

2. kubectl -o jsonpath

-o json으로 구조를 보고 → 경로를 적고 → range로 줄 맞추기

순서는 항상 같아요. ① -o json(또는 -o yaml)으로 구조를 보고, ② 원하는 값까지의 경로를 적고, ③ -o jsonpath='{…}'로 뽑아요. kubectl에서는 맨 앞의 $를 생략하고 .으로 시작해요. 여러 개를 조회하면 결과는 items 리스트 안에 들어 있어요.

$ kubectl get nodes -o json | less                       # ① 구조 보기
$ kubectl get pod web -o jsonpath='{.spec.containers[0].image}'
nginx:1.27
$ kubectl get nodes -o jsonpath='{.items[*].metadata.name}'
controlplane node01
$ kubectl get nodes -o jsonpath='{.items[*].metadata.name}{"\n"}{.items[*].status.capacity.cpu}{"\n"}'
controlplane node01
4 2
$ kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.capacity.cpu}{"\n"}{end}'
controlplane	4
node01	2
자주 쓰는 경로뽑히는 값
{.items[*].status.addresses[?(@.type=="InternalIP")].address}노드마다 내부 IP
{.items[*].spec.containers[*].image}모든 Pod의 모든 이미지
{.metadata.labels.app\.kubernetes\.io/name}점(.)이 들어간 레이블 키는 \.로
{.data.password} + | base64 -dSecret 값 꺼내 디코딩 (5부 04)
{.contexts[*].name} (kubectl config view)kubeconfig의 컨텍스트 이름들 (7부 05)
따옴표와 경로 찾기 jsonpath는 작은따옴표로 감싸야 셸이 { } $ "를 건드리지 않아요. 필드 이름이 기억나지 않으면 kubectl explain pod.spec.containers로 찾거나, -o json을 먼저 봐요. kubectl의 JSON Path는 정규식을 지원하지 않아서, 복잡한 가공은 -o json | jq가 편해요.

02. 출력 다듬기

1. custom-columns · sort-by

내가 고른 열로 표를 만들고, 원하는 값으로 정렬하고, 조건으로 거르기
$ kubectl get nodes -o custom-columns=NODE:.metadata.name,CPU:.status.capacity.cpu
NODE           CPU
controlplane   4
node01         2

$ kubectl get nodes --sort-by=.status.capacity.cpu       # 정렬 (경로는 items[*] 아래 기준)
$ kubectl get pods --sort-by=.metadata.creationTimestamp # 오래된 순
$ kubectl get pv --sort-by=.spec.capacity.storage \
    -o custom-columns=NAME:.metadata.name,CAPACITY:.spec.capacity.storage

custom-columns의 경로에는 { }도 .items[*]도 안 써요. 항목 하나 기준의 경로만 적으면 kubectl이 항목마다 적용해요. 열 이름은 자유예요.

2. 출력 형식 한눈에

옵션결과
-o wide표 + 노드 · IP 같은 열 추가
-o yaml · -o json객체 전체 (구조 확인, 백업)
-o namepod/web 형식 이름만 — 다른 명령에 넘길 때
-o jsonpath=… · -o jsonpath-file=…원하는 값만 (01)
-o custom-columns=…원하는 열로 표
-o kyaml들여쓰기에 덜 민감한 YAML 방언 (1.35 beta, 기본 사용 가능)
--show-labels · -L app,tier레이블 전체 / 지정한 레이블을 열로
--no-headers제목 줄 빼기 — wc -l로 세기 좋음

3. 고르기 — 레이블과 필드

$ kubectl get pods -l app=web                          # 같음
$ kubectl get pods -l 'env in (prod,stg),tier!=db'     # 집합 · 같지 않음 (3부 02)
$ kubectl get pods -l '!canary'                        # 그 레이블이 없는 것
$ kubectl get pods --field-selector status.phase=Running
$ kubectl get pods -A --field-selector spec.nodeName=node01   # node01의 모든 Pod
$ kubectl get pods -A --field-selector status.phase!=Running --no-headers | wc -l

레이블(-l)은 우리가 붙인 표시로, 필드(--field-selector)는 status.phase · spec.nodeName · metadata.name처럼 리소스마다 정해진 몇몇 필드로 걸러요.

03. 빠르게 쓰는 습관

1. YAML은 손으로 처음부터 쓰지 않는다

명령으로 뼈대 → explain으로 필드 찾기 → 고쳐서 apply
$ export do="--dry-run=client -o yaml"                  # 자주 쓰는 꼬리를 변수로
$ kubectl create deployment web --image=nginx --replicas=3 $do > web.yaml
$ kubectl run tmp --image=busybox $do -- sleep 3600 > pod.yaml
$ kubectl create service clusterip web --tcp=80:8080 $do
$ kubectl expose deployment web --port=80 --target-port=8080 $do
$ kubectl create configmap app --from-literal=LOG=info $do
$ kubectl create job once --image=busybox $do -- date
$ kubectl create cronjob tick --image=busybox --schedule="*/5 * * * *" $do -- date

$ kubectl explain deployment.spec.strategy              # 필드 설명
$ kubectl explain pod.spec.containers --recursive | less  # 하위 필드 전체 트리

2부 05의 명령형 · 선언형을 합친 방법이에요. 뼈대는 명령으로 정확하게 만들고, 명령으로 안 되는 부분(probe · volume · resources)만 YAML에서 고쳐요.

2. alias · 자동 완성 · kuberc

$ alias k=kubectl
$ source <(kubectl completion bash)        # zsh면 completion zsh
$ complete -o default -F __start_kubectl k  # k에도 자동 완성
$ k get po<Tab>                             # pods · 이름까지 자동 완성

kubectl 1.34부터 kuberc(beta)로 kubectl 자체에 alias와 기본 옵션을 설정할 수 있어요. 파일은 ~/.kube/kuberc예요. kubeconfig(접속 정보)와 달리 개인 취향만 담아요.

apiVersion: kubectl.config.k8s.io/v1beta1
kind: Preference
defaults:
  - command: delete
    options:
      - name: interactive      # 지우기 전에 항상 확인
        default: "true"
  - command: apply
    options:
      - name: server-side
        default: "true"
aliases:
  - name: getw                 # kubectl getw pods → kubectl get pods -o wide
    command: get
    options:
      - name: output
        default: wide

3. 자주 쓰는 손버릇

하고 싶은 일명령
기본 Namespace 바꾸기kubectl config set-context --current --namespace=dev
컨텍스트 바꾸기 (7부 05)kubectl config use-context prod (도구: kubectx · kubens)
바뀌는 걸 지켜보기kubectl get pods -w
준비될 때까지 기다리기kubectl wait --for=condition=Ready pod/web --timeout=60s, kubectl rollout status deploy/web
고칠 수 없는 필드를 바꾸기kubectl replace --force -f pod.yaml (지우고 다시 만듦)
내 권한 확인 (7부 07)kubectl auth can-i create deploy -n dev, --as=system:serviceaccount:dev:app
노트북에서 Pod · Service 접속kubectl port-forward svc/web 8080:80
파일 주고받기kubectl cp web:/tmp/dump.sql ./dump.sql
무엇을 쓸 수 있나kubectl api-resources (짧은 이름 · Namespace 여부 · 그룹)
YAML 편집기 설정~/.vimrc에 set expandtab tabstop=2 shiftwidth=2 — 탭 대신 공백 2칸
터미널 밖의 도구 k9s(터미널 UI로 리소스 탐색 · 로그 · exec), krew(kubectl 플러그인 관리자 — kubectl krew install neat로 -o yaml의 잡다한 필드를 지우는 kubectl neat 등), stern(여러 Pod 로그를 한꺼번에)도 많이 써요. 시험 환경에는 없으니 기본 kubectl 손버릇이 먼저예요.

4. 17부 셀프 체크

  1. $.car.wheels[*].model은 무엇을 뽑나요?
    car의 wheels 리스트에 있는 모든 항목의 model 값(리스트).
  2. JSON Path 조건식에서 @는 무엇인가요?
    지금 검사하고 있는 리스트 항목 하나. [?(@.location == "rear-right")]처럼 써요.
  3. $[0:3]은 몇 번째 항목들을 고르나요?
    0 · 1 · 2번째. 끝 번호(3)는 포함하지 않아요.
  4. kubectl jsonpath에서 여러 객체를 조회할 때 경로는 무엇으로 시작하나요?
    .items[*] — 목록 결과는 items 리스트에 담겨 있어요.
  5. 노드마다 “이름 탭 CPU”를 한 줄씩 출력하려면?
    -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.capacity.cpu}{"\n"}{end}'
  6. custom-columns의 경로가 jsonpath와 다른 점은?
    { }와 .items[*] 없이 항목 하나 기준 경로만 적어요(NODE:.metadata.name).
  7. PV를 용량 순으로 보려면?
    kubectl get pv --sort-by=.spec.capacity.storage
  8. node01에서 도는 모든 Pod를 보려면?
    kubectl get pods -A --field-selector spec.nodeName=node01 (또는 -o wide 후 확인)
  9. Deployment YAML을 빠르게 만드는 방법은?
    kubectl create deployment web --image=nginx --dry-run=client -o yaml > web.yaml로 뼈대를 만들고 필요한 부분만 고쳐요.
  10. Pod의 probe 필드 이름이 기억나지 않으면?
    kubectl explain pod.spec.containers.livenessProbe (또는 --recursive).
  11. 현재 컨텍스트의 기본 Namespace를 dev로 바꾸려면?
    kubectl config set-context --current --namespace=dev
  12. kuberc와 kubeconfig의 차이는?
    kubeconfig는 클러스터 주소 · 인증 정보, kuberc는 alias · 기본 옵션 같은 개인 취향 설정(~/.kube/kuberc, 1.34 beta)이에요.
전반 완주 1부의 클러스터 구조부터 17부의 kubectl 도구까지, 학습 범위의 모든 주제를 다뤘어요. 각 부의 셀프 체크를 다시 풀어 보고, 막히는 곳은 그 부로 돌아가(1~6부는 전반 ①, 7~11부는 전반 ②, 12부부터는 이 노트의 사이드바) 그림을 한 번 더 돌려 보세요.
스터디를 하며 부마다 이어서 정리합니다. 그림은 개념 이해를 위해 단순화했어요.