Kubernetes Study · 01 입문
인프라 0부터 읽는 쿠버네티스
입문 스터디를 따라가며 부(部) 단위로 기초 개념을 정리합니다. 인프라를 처음 접하는 사람 기준이라, 낯선 단어는 처음 나올 때 풀어서 쓰고 움직이는 그림으로 따라가 봅니다. 그림은 화면에 보이면 자동으로 재생되고, 아래 버튼으로 한 단계씩 넘겨 볼 수도 있어요.
01. 왜 컨테이너인가
서버는 거창한 기계가 아니라 24시간 켜져서 요청을 받아 주는 컴퓨터예요. 우리가 만든 앱은 결국 누군가의 서버 위에서 하나의 프로세스(실행 중인 프로그램)로 돌아갑니다. 문제는 앱이 코드만으로는 돌지 않는다는 점이에요. 코드를 실행해 줄 런타임(Node, Java, Python…), 코드가 가져다 쓰는 라이브러리, 그리고 그 밑의 OS까지 맞아떨어져야 실행됩니다.
1. "제 컴퓨터에선 되는데요"
개발자 노트북과 운영 서버는 거의 항상 환경이 다릅니다. 노트북엔 최신 Node가, 서버엔 2년 전에 깔아 둔 Node가 있는 식이죠. 코드만 옮기면 이 차이 때문에 서버에서 터집니다. 컨테이너는 이 문제를 “코드와 실행 환경을 한 상자에 담아 통째로 옮기는” 방식으로 풉니다.
2. 컨테이너 = 표준 규격의 상자
이름은 화물 운송의 컨테이너에서 왔어요. 규격 컨테이너가 나오기 전엔 짐 모양마다 싣고 내리는 방법이 달랐지만, 상자 크기가 표준화되자 배 · 기차 · 트럭이 안에 뭐가 들었든 똑같은 방법으로 옮길 수 있게 됐죠. 소프트웨어 컨테이너도 같아요. 안에 Node 앱이 있든 Java 앱이 있든, 밖에서는 실행 · 중지 · 복사를 똑같은 명령으로 다룹니다.
| 먼저 알아둘 단어 | 한 줄 뜻 | 더 자세히 |
|---|---|---|
| 이미지 | 앱 + 실행 환경을 굳혀 둔 읽기 전용 꾸러미. 상자의 설계도이자 내용물 | 04 |
| 컨테이너 | 이미지를 실제로 실행한 것. 격리된 프로세스 | 03 |
| 컨테이너 런타임 | 서버에서 이미지를 받아 컨테이너로 실행해 주는 프로그램 (Docker, containerd 등) | 02 · 04 |
| 레지스트리 | 이미지를 올리고 내려받는 저장소. 코드의 GitHub 같은 곳 (Docker Hub, AWS ECR 등) | 04 |
02. 물리 서버 · 가상 머신 · 컨테이너
컴퓨터 한 대에서 여러 앱을 돌리는 방법은 크게 세 가지예요. 차이는 결국 “앱끼리 무엇을 같이 쓰고, 무엇을 따로 갖느냐”입니다.
1. 층을 쌓아 보면 보이는 차이
아래 그림은 바닥(하드웨어)부터 위로 층을 쌓아 갑니다. 가상 머신(VM)은 하드웨어를 흉내 내서 앱마다 OS를 통째로 하나씩 주고, 컨테이너는 OS의 핵심인 커널은 같이 쓰면서 앱과 라이브러리만 따로 담아요.
2. 표로 정리
| 물리 서버에 직접 | 가상 머신 | 컨테이너 | |
|---|---|---|---|
| 격리 단위 | 없음 (모두 한 OS에) | OS째로 | 프로세스 단위 |
| OS | 1개 공유 | VM마다 하나씩 (Guest OS) | 호스트 커널 1개 공유 |
| 크기 | — | 수 GB | 수 MB ~ 수백 MB |
| 시작 시간 | — | 수십 초 ~ 분 (OS 부팅) | 1초 안팎 (프로세스 시작) |
| 격리 강도 | 약함 — 라이브러리 충돌 | 강함 | 적당함 — 커널을 공유하므로 VM보다는 약함 |
| 대표 도구 | — | VMware, KVM, VirtualBox, AWS EC2 | Docker, containerd, Podman |
03. 컨테이너는 어떻게 격리되나
컨테이너를 흔히 “가벼운 VM”이라고 설명하지만, 정확히는 리눅스 커널 기능으로 울타리를 친 평범한 프로세스예요. 호스트에서 ps로 보면 그냥 프로세스 하나로 보입니다. 울타리는 두 가지 기능으로 만들어요. 무엇을 볼 수 있는지를 나누는 네임스페이스, 그리고 얼마나 쓸 수 있는지를 제한하는 cgroup입니다.
1. 같은 프로세스, 다른 시야
2. 네임스페이스와 cgroup
| 기능 | 하는 일 | 종류 / 예시 | 비유 |
|---|---|---|---|
| 네임스페이스 namespace | 프로세스가 볼 수 있는 범위를 나눔 | pid 프로세스 번호mnt 파일 시스템net 네트워크(IP, 포트)uts 호스트 이름ipc · user 등 | 칸막이 — 옆 칸이 안 보임 |
| cgroup control group | 프로세스가 쓸 수 있는 양을 제한 | CPU, 메모리, 디스크 I/O, 프로세스 수 | 계량기와 차단기 — 정해진 양을 넘으면 끊김 |
Namespace라는 단어를 또 만나는데, 그건 리소스를 묶는 논리적인 폴더(예: dev / prod)예요. 여기서 말한 리눅스 커널의 네임스페이스와는 다른 개념입니다.
128 + 9 = 137이 됩니다. 쿠버네티스에서 Pod가 갑자기 재시작될 때 가장 자주 보는 원인 중 하나예요.
04. 이미지와 컨테이너
JavaScript로 비유하면 이미지는 class, 컨테이너는 인스턴스예요. 클래스 하나로 객체를 여러 개 만들듯, 이미지 하나로 컨테이너를 여러 개 띄웁니다. 이미지는 읽기 전용이고, 컨테이너는 그 위에 자기만의 얇은 쓰기 레이어를 얹어 실행돼요.
1. 만들고 · 올리고 · 받고 · 실행하기
이미지가 태어나서 서버에서 실행되기까지는 항상 같은 네 단계를 거칩니다. build → push → pull → run. 마지막 단계에서는 코드를 조금 고쳤을 때 무슨 일이 일어나는지도 봅니다.
2. Dockerfile 한 줄씩 읽기
Dockerfile은 “이미지를 만드는 레시피”예요. 위에서 아래로 한 줄씩 실행되고, 파일을 바꾸는 명령(COPY, RUN 등)마다 레이어가 하나씩 쌓입니다.
# 1) 베이스 이미지 — Node 20이 깔린 가벼운 리눅스에서 출발
FROM node:20-slim
# 2) 컨테이너 안의 작업 폴더
WORKDIR /app
# 3) 의존성 목록만 먼저 복사하고 설치
# → 코드만 바뀌었을 땐 이 두 줄의 레이어를 캐시로 재사용
COPY package*.json ./
RUN npm ci
# 4) 나머지 코드 복사 (가장 자주 바뀌는 건 맨 아래로)
COPY . .
# 5) 컨테이너가 시작될 때 실행할 명령 (레이어가 아니라 설정)
CMD ["node", "app.js"]
npm ci를 다시 돌리지 않습니다.
3. 이미지 이름 읽는 법
latest는 “최신”이 아니에요
태그를 생략하면 latest가 붙는데, 이건 그냥 기본 태그 이름일 뿐 최신 버전이라는 보장이 없어요. 언제 받느냐에 따라 내용이 달라질 수 있어서, 운영에서는 myapp:1.0.3처럼 버전을 꼭 명시합니다.
4. 자주 쓰는 docker 명령
| 명령 | 하는 일 |
|---|---|
docker build -t myapp:1.0 . | 현재 폴더(.)의 Dockerfile로 이미지를 만들고 이름(-t)을 붙임 |
docker images | 내 PC에 있는 이미지 목록 |
docker push myrepo/myapp:1.0 | 레지스트리에 이미지 올리기 (이름 앞에 저장소 주소가 붙어 있어야 함) |
docker pull nginx:1.27 | 레지스트리에서 이미지 내려받기 |
docker run -d -p 8080:3000 --name web myapp:1.0 | 컨테이너 실행. -d 백그라운드, -p 8080:3000 내 PC 8080번 포트 → 컨테이너 3000번 포트 |
docker ps / docker ps -a | 실행 중인 컨테이너 / 멈춘 것까지 전부 |
docker logs web | 컨테이너가 출력한 로그 보기 |
docker exec -it web sh | 실행 중인 컨테이너 안으로 들어가 셸 열기 |
docker stop web → docker rm web | 컨테이너 멈추기 → 지우기 (쓰기 레이어도 함께 사라짐) |
05. 오케스트레이션 — 컨테이너가 많아지면
컨테이너 하나 띄우는 건 docker run 한 줄이면 끝나요. 그런데 실제 서비스는 서버 수십 대에 컨테이너 수백 개가 돌아갑니다. 그러면 질문이 바뀌어요. “어떻게 띄우지?”가 아니라 “어느 서버에 몇 개를, 죽으면 누가, 배포는 어떻게?”가 됩니다.
1. 사람이 직접 운영하면
오케스트레이션(orchestration)은 오케스트라의 지휘에서 온 말이에요. 연주자(컨테이너)는 각자 자기 악기만 연주하고, 지휘자(오케스트레이터)가 누가 · 언제 · 몇 명 연주할지를 맞춥니다. 위 그림에서 사람이 하던 판단을 프로그램이 대신 계속 해 주는 거예요.
2. 원하는 상태를 선언하면 맞춰 준다
오케스트레이터의 핵심 아이디어는 에어컨 온도 설정과 같아요. 우리는 “24도로 해 줘”라고 원하는 상태만 정하고, 에어컨이 현재 온도를 계속 재서 차이가 나면 알아서 냉방을 켜고 끄죠. 오케스트레이터도 원하는 상태(예: 이 앱 3개)와 현재 상태(실제로 떠 있는 개수)를 끝없이 비교해서 차이를 없앱니다. 이 반복을 제어 루프(control loop)라고 불러요.
3. 명령형 vs 선언형
사람이 직접 운영할 때는 무엇을 할지 명령을 하나씩 내렸어요(명령형). 오케스트레이터에게는 어떤 상태여야 하는지만 적어서 건넵니다(선언형). Flutter에서 build()에 “지금 상태면 화면은 이렇게 생겼다”라고 선언하면 프레임워크가 알아서 화면을 맞추는 것과 같은 발상이에요.
$ ssh server-1 "docker run -d myapp:1.0"
$ ssh server-2 "docker run -d myapp:1.0"
$ ssh server-3 "docker run -d myapp:1.0"
# 하나가 죽으면? 서버가 죽으면? → 다시 사람이 판단하고 명령
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3 # 원하는 개수
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myapp:1.0 # 원하는 이미지
$ kubectl apply -f myapp.yaml # 상태를 제출하면 나머지는 쿠버네티스가
| 명령형 | 선언형 | |
|---|---|---|
| 적는 것 | 과정 (실행해, 지워, 옮겨) | 결과 (3개, 이 이미지, 이 포트) |
| 장애 대응 | 사람이 다시 명령 | 제어 루프가 원하는 상태로 되돌림 |
| 두 번 실행하면 | 컨테이너가 6개가 될 수도 | 여전히 3개 (이미 같은 상태라 할 일 없음) |
| 기록 | 누가 뭘 했는지 흩어짐 | YAML 파일 자체가 기록 → Git으로 관리 가능 |
4. 멈추지 않고 새 버전 배포하기
새 버전을 배포할 때 컨테이너를 전부 내렸다가 다시 올리면 그 사이 서비스가 멈춰요. 오케스트레이터는 새 버전을 하나 띄우고 → 준비되면 → 옛 버전을 하나 내리는 과정을 반복해서, 사용자가 눈치채지 못하게 교체합니다. 이걸 롤링 업데이트라고 해요.
5. 오케스트레이터가 맡는 일
| 일 | 내용 | 쿠버네티스에서 부르는 이름 (뒤에서 자세히) |
|---|---|---|
| 스케줄링 | 남은 CPU · 메모리를 보고 어느 서버(노드)에 띄울지 정함 | Scheduler |
| 자가 치유 | 컨테이너가 죽으면 다시 띄우고, 서버가 죽으면 다른 서버로 옮김 | Deployment · ReplicaSet |
| 스케일링 | 개수를 늘리고 줄임. 부하에 따라 자동으로도 | replicas, HPA |
| 롤링 업데이트 · 롤백 | 멈춤 없이 버전 교체, 문제 생기면 되돌리기 | Deployment |
| 서비스 디스커버리 · 로드밸런싱 | 컨테이너는 계속 새로 생겨서 IP가 바뀜 → 고정된 이름 하나로 묶어 요청을 나눠 줌 | Service |
| 설정 · 비밀값 관리 | 환경 변수, 설정 파일, 비밀번호를 이미지 밖에서 주입 | ConfigMap · Secret |
| 저장소 연결 | 컨테이너가 옮겨 다녀도 데이터는 유지되게 디스크를 붙여 줌 | Volume · PV · PVC |
이런 일을 하는 도구로 Docker Swarm, HashiCorp Nomad, AWS ECS 등이 있지만, 사실상 업계 표준은 쿠버네티스예요.
06. 쿠버네티스 미리보기
1. 이름과 출신
- Kubernetes는 그리스어 κυβερνήτης(조타수, 배의 키를 잡는 사람)에서 왔어요. 로고가 배의 조타륜인 이유입니다. 컨테이너(화물)를 실은 배를 모는 사람이라는 뜻이죠.
- K8s라고 줄여 쓰는데, K와 s 사이 글자가 8개(ubernete)라서예요.
- 구글이 사내에서 컨테이너를 대규모로 돌리던 시스템(Borg)의 경험을 바탕으로 2014년에 오픈소스로 공개했고, 2015년 1.0과 함께 CNCF(클라우드 네이티브 컴퓨팅 재단)에 기증했어요.
2. 클러스터 한 장 그림
쿠버네티스는 여러 서버를 묶은 클러스터로 동작해요. 결정을 내리는 컨트롤 플레인(두뇌)과 실제로 컨테이너를 돌리는 워커 노드(손발)로 나뉩니다. 05에서 본 제어 루프가 실제로 어떻게 돌아가는지, kubectl apply 한 번을 따라가 볼게요. 각 부품은 이후 스터디에서 자세히 다룹니다.
| 위치 | 부품 | 하는 일 |
|---|---|---|
| 컨트롤 플레인 | API 서버kube-apiserver | 클러스터의 유일한 창구. kubectl도, 다른 부품도 전부 API 서버를 통해서만 대화 |
| etcd | 클러스터의 모든 상태(원하는 상태 + 현재 상태)를 저장하는 데이터베이스 | |
스케줄러kube-scheduler | 새로 만들 컨테이너(Pod)를 어느 노드에 둘지 결정 | |
컨트롤러 매니저kube-controller-manager | 여러 제어 루프의 묶음. “3개여야 하는데 2개네?”를 계속 확인하고 맞춤 | |
| 워커 노드 | kubelet | 노드마다 있는 관리인. 자기 노드에 배정된 일을 받아 런타임에 실행을 시키고 상태를 보고 |
| kube-proxy | Service로 들어온 요청이 알맞은 컨테이너로 가도록 노드의 네트워크 규칙을 관리 | |
| 컨테이너 런타임 | 실제로 이미지를 받아 컨테이너를 실행 (containerd, CRI-O) |
3. 1부 셀프 체크
“제 컴퓨터에선 되는데요” 문제를 컨테이너는 어떻게 해결하나요?
코드만 옮기지 않고 런타임 · 라이브러리까지 이미지에 담아 통째로 옮깁니다. 서버에는 컨테이너 런타임만 있으면 어디서든 같은 환경으로 실행돼요.VM과 컨테이너의 가장 큰 차이는?
VM은 각자 OS(Guest OS)를 통째로 갖고, 컨테이너는 호스트의 커널 하나를 공유합니다. 그래서 컨테이너가 훨씬 가볍고 빨리 뜨지만, 격리 강도는 VM이 더 강해요.컨테이너 격리를 만드는 리눅스 기능 두 가지는?
네임스페이스 — 볼 수 있는 범위를 나눔(PID, 파일 시스템, 네트워크, 호스트 이름…).
cgroup — 쓸 수 있는 양을 제한(CPU, 메모리…). 메모리 한도를 넘으면 OOMKilled(exit 137).이미지와 컨테이너의 관계를 JavaScript로 비유하면?
이미지 = class, 컨테이너 = 인스턴스. 이미지는 읽기 전용 레이어의 묶음이고, 컨테이너는 그 위에 자기만의 쓰기 레이어를 얹어 실행됩니다.코드 한 줄만 고치고 다시 빌드하면 왜 빠를까요?
바뀌지 않은 레이어는 캐시를 재사용하고, 바뀐 줄부터 아래만 다시 만들기 때문이에요. push할 때도 바뀐 레이어만 전송합니다.오케스트레이터가 하는 일을 한 문장으로?
원하는 상태와 현재 상태를 계속 비교해서 차이를 없애는 것(제어 루프). 스케줄링 · 자가 치유 · 스케일링 · 롤링 업데이트가 모두 여기서 나와요.쿠버네티스에서 kubectl과 모든 부품이 대화하는 창구는?
API 서버예요. 상태는 etcd에 저장되고, 스케줄러 · 컨트롤러 · kubelet은 모두 API 서버를 지켜보다가(watch) 자기 할 일을 합니다.
01. 클러스터 구조 — 마스터 노드와 워커 노드
쿠버네티스는 서버 여러 대를 묶어 한 대의 큰 컴퓨터처럼 쓰게 해 줘요. 이 묶음이 클러스터이고, 묶인 서버 한 대 한 대를 노드(node)라고 부릅니다. 노드는 맡은 일에 따라 두 종류로 나뉘어요.
- 마스터 노드 — 클러스터 전체를 결정하고 관리합니다. “무엇을 어디에 몇 개 띄울지”를 정하는 두뇌예요.
- 워커 노드 — 마스터가 정한 대로 실제로 Pod(컨테이너)를 실행합니다. 우리 앱이 돌아가는 곳이에요.
식당으로 비유하면 마스터 노드는 주문을 받아 어느 조리대에 맡길지 정하는 매니저, 워커 노드는 실제로 요리하는 조리대예요. 매니저는 직접 요리하지 않고, 조리대는 메뉴를 정하지 않아요.
1. 역할 분담과 장애가 났을 때
| 마스터 노드 (= 컨트롤 플레인) | 워커 노드 | |
|---|---|---|
| 하는 일 | 원하는 상태 저장 · 배치 결정 · 제어 루프 | Pod 실행 · 상태 보고 · 네트워크 규칙 관리 |
| 구성 요소 | API 서버 · etcd · 스케줄러 · 컨트롤러 매니저 | kubelet · kube-proxy · 컨테이너 런타임 |
| 우리 앱이 도는 곳? | 보통 아니에요 (관리 전용) | 네, 여기서 돌아요 |
| 보통 몇 대? | 운영 환경은 3대 (하나가 죽어도 버티게) | 필요한 만큼 — 늘리고 줄일 수 있음 |
| 멈추면 | 돌던 앱은 계속 돌지만, 새 배포 · 자가 치유 · 스케일링이 멈춤 | 그 위의 Pod가 다른 워커 노드로 옮겨짐 |
kubectl get nodes를 해 보면 역할(ROLES) 칸에 control-plane이라고 나와요. 1부 06 그림의 “컨트롤 플레인”이 바로 이 마스터 노드예요. 이 문서에서는 둘을 같은 뜻으로 씁니다.
02. 노드 안에는 무엇이 있나
노드는 특별한 기계가 아니라 쿠버네티스에 등록된 평범한 서버 한 대예요. 물리 서버일 수도 있지만, 클라우드에서는 대부분 VM입니다(1부 02 참고). 그 서버에 쿠버네티스용 프로그램 세 개가 깔려 있어서 워커 노드 역할을 해요.
1. 워커 노드 하나를 확대해 보면
| 구성 요소 | 하는 일 | 비유 |
|---|---|---|
| kubelet | API 서버를 지켜보다가 자기 노드에 배정된 Pod를 실행시키고, Pod · 노드 상태를 보고 | 현장 관리인 |
| 컨테이너 런타임 | kubelet의 지시로 이미지를 받아 컨테이너를 실제로 실행 (containerd 등) | 실제로 일하는 손 |
| kube-proxy | Service 주소로 온 요청이 알맞은 Pod로 가도록 노드의 네트워크 규칙을 관리 | 교통 정리 담당 |
2. 노드의 자리 — Capacity와 Allocatable
노드마다 CPU · 메모리에 한계가 있어요. Capacity는 서버의 전체 크기, Allocatable은 그중 OS와 kubelet 몫을 떼고 Pod에 내줄 수 있는 양이에요. 스케줄러는 이 남은 자리를 보고 Pod를 어느 노드에 둘지 정합니다.
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
gke-my-cluster-default-pool-1a2b-xk3d Ready <none> 12m v1.xx.x
gke-my-cluster-default-pool-1a2b-p9qz Ready <none> 12m v1.xx.x
gke-my-cluster-default-pool-1a2b-m4tw Ready <none> 12m v1.xx.x
$ kubectl describe node gke-my-cluster-default-pool-1a2b-xk3d
Capacity:
cpu: 2
memory: 8148820Ki
pods: 110
Allocatable:
cpu: 1930m # 0.07코어는 시스템 몫
memory: 6077268Ki # 약 2GiB는 시스템 몫
pods: 110
control-plane 역할의 노드가 보이지 않죠. GKE는 마스터 노드(컨트롤 플레인)를 구글이 대신 운영하기 때문에, 우리 프로젝트에는 워커 노드만 보여요. 다음 절에서 자세히 볼게요. (버전 · 숫자는 2 vCPU · 8GB 노드 기준 예시예요.)
03. 실습 환경 — GCP와 GKE
쿠버네티스를 연습하려면 클러스터가 필요해요. 방법은 크게 두 가지입니다.
| 내 PC에 작은 클러스터 | 클라우드의 관리형 쿠버네티스 | |
|---|---|---|
| 예 | minikube · kind · Docker Desktop | GKE(Google) · EKS(AWS) · AKS(Azure) |
| 노드 | 보통 1대 (내 PC 안의 VM이나 컨테이너) | 진짜 VM 여러 대 |
| 비용 | 무료 | 쓰는 만큼 과금 |
| 좋은 점 | 빠르게 켜고 끄기 | 실제 운영과 같은 구조 (여러 노드, 외부 로드밸런서) |
학습 자료는 GCP(Google Cloud Platform, 구글의 클라우드)의 GKE(Google Kubernetes Engine)를 써요. GKE는 관리형 쿠버네티스예요. 클러스터를 만들어 달라고 하면 구글이 마스터 노드를 대신 운영하고, 우리는 워커 노드와 그 위의 앱만 신경 쓰면 됩니다.
1. 누가 무엇을 관리하나
2. 클러스터 만들기까지의 명령
GCP 콘솔(웹 화면)에서 버튼으로 만들 수도 있고, gcloud 명령줄 도구로 만들 수도 있어요. 브라우저에서 바로 쓰는 Cloud Shell에는 gcloud와 kubectl이 이미 깔려 있어서 실습에 편해요.
$ gcloud auth login # 구글 계정으로 로그인
$ gcloud config set project my-k8s-study # 사용할 GCP 프로젝트 선택
$ gcloud container clusters create my-cluster \
--zone asia-northeast3-a --num-nodes 3 # 서울 리전 한 구역에 워커 노드 3대
$ gcloud container clusters get-credentials my-cluster \
--zone asia-northeast3-a # kubectl이 이 클러스터를 보도록 설정
$ kubectl get nodes # 워커 노드 3대 확인
gcloud components install gke-gcloud-auth-plugin으로 설치합니다. Cloud Shell에는 이미 들어 있어요.
- 신규 가입자는 $300 크레딧을 90일 동안 쓸 수 있고, 체험 기간에는 자동으로 유료 전환되지 않아요.
- GKE는 클러스터마다 관리비 시간당 $0.10이 붙어요. 결제 계정마다 매달 $74.40 크레딧이 있어서, 존(zonal) 클러스터나 Autopilot 클러스터 1개의 관리비는 사실상 무료예요.
- 하지만 그 크레딧은 관리비에만 쓰여요. 워커 노드(VM) 요금은 켜 둔 시간만큼 따로 나갑니다.
gcloud container clusters delete my-cluster --zone asia-northeast3-a로 지워 두는 습관을 들이세요. (2026년 10월 GCP 공식 가격 페이지 기준)
04. kubectl 기본 명령
kubectl(큐브컨트롤, 큐브시티엘)은 쿠버네티스를 다루는 명령줄 도구예요. 하는 일은 단순해요. 우리가 친 명령을 API 서버에 보내는 요청으로 바꿔 전달하고, 돌아온 답을 표로 보여 줍니다. 1부 06에서 본 것처럼, 클러스터의 모든 대화는 API 서버를 거쳐요.
1. 명령 한 줄이 가는 길
2. 명령의 모양과 자주 쓰는 명령
| 명령 | 하는 일 |
|---|---|
kubectl get pods | 목록 보기. nodes, deployments 등 종류만 바꾸면 됨 |
kubectl get pods -o wide | IP · 어느 노드에 있는지까지 넓게 보기 |
kubectl get pod hello -o yaml | 객체 전체를 YAML로 보기 (spec과 status 모두) |
kubectl describe pod hello | 자세한 설명 + 맨 아래 Events(무슨 일이 있었나). 문제 생기면 제일 먼저 보는 곳 |
kubectl apply -f hello.yaml | YAML에 적은 원하는 상태를 제출 (없으면 만들고, 있으면 바꿈) |
kubectl delete -f hello.yaml | YAML에 적힌 객체 지우기 (kubectl delete pod hello도 가능) |
kubectl logs hello | 컨테이너가 출력한 로그 보기 (-f로 계속 따라가기) |
kubectl exec -it hello -- sh | 실행 중인 컨테이너 안으로 들어가기 |
kubectl get pods -w | 바뀌는 상태를 실시간으로 지켜보기 (watch) |
kubectl explain pod.spec | YAML에 어떤 필드를 쓸 수 있는지 설명 보기 |
po(pods) · no(nodes) · rs(replicasets) · deploy(deployments) · svc(services). 예: kubectl get po. 그리고 대부분의 명령은 default 네임스페이스를 기본으로 봐요. 다른 곳을 보려면 -n 이름, 전부 보려면 -A를 붙입니다.
05. YAML 파일 작성
1부 05에서 쿠버네티스는 선언형이라고 했죠. 그 “원하는 상태”를 적는 파일이 YAML 매니페스트예요. YAML은 사람이 읽기 쉽게 만든 데이터 형식이고, JSON과 같은 내용을 괄호 대신 들여쓰기로 표현해요.
1. YAML 문법 다섯 가지만
| 규칙 | 예 | 뜻 |
|---|---|---|
| 키: 값 | name: hello | 콜론 뒤에 한 칸 띄우기 필수 |
| 들여쓰기 = 포함 | metadata: name: hello | name은 metadata 안에 있음. 스페이스로만(보통 2칸), 탭은 오류 |
| - = 목록의 한 항목 | containers: - name: web | containers는 목록이고 그 첫 항목. 컨테이너가 둘이면 -가 둘 |
| # = 주석 | replicas: 3 # 개수 | 쿠버네티스는 무시 |
| 숫자처럼 보이는 문자열 | version: "1.0" | 따옴표가 없으면 숫자로 읽힐 수 있음 |
2. 매니페스트의 네 칸
쿠버네티스 YAML은 무엇을 만들든 맨 위 네 칸이 같아요. Pod 하나를 띄우는 가장 짧은 매니페스트로 하나씩 읽어 볼게요.
apiVersion: v1 # 이 종류가 속한 API 버전
kind: Pod # 만들 객체의 종류
metadata: # 이름표
name: hello
labels:
app: hello
spec: # 원하는 상태 — 종류마다 내용이 다름
containers:
- name: web
image: nginx:1.27
ports:
- containerPort: 80
| 칸 | 뜻 | 누가 채우나 |
|---|---|---|
apiVersion | 이 종류를 다루는 API의 그룹과 버전 | 나 |
kind | 무엇을 만들지 (Pod, Deployment, Service …) | 나 |
metadata | 이름(name), 라벨(labels), 네임스페이스 등 이름표 | 나 (+ 쿠버네티스가 uid · 생성 시각 추가) |
spec | 원하는 상태 | 나 |
status | 현재 상태 — 파일에는 안 씀 | 쿠버네티스 |
종류별 apiVersion
| kind | apiVersion | kind | apiVersion |
|---|---|---|---|
| Pod · Service · ConfigMap · Secret · Namespace | v1 | Job · CronJob | batch/v1 |
| ReplicaSet · Deployment · DaemonSet · StatefulSet | apps/v1 | Ingress | networking.k8s.io/v1 |
kubectl run hello --image=nginx:1.27 --dry-run=client -o yaml > hello-pod.yaml 처럼 --dry-run=client -o yaml을 붙이면, 실제로 만들지 않고 YAML 뼈대만 뽑아 줘요. 여기에 필요한 걸 고쳐 쓰고, 필드가 궁금하면 kubectl explain pod.spec.containers로 찾아보면 됩니다.
3. 2부 셀프 체크
마스터 노드와 워커 노드는 각각 무슨 일을 하나요?
마스터 노드(컨트롤 플레인)는 원하는 상태를 저장하고, 어디에 둘지 결정하고, 제어 루프를 돌려요. 워커 노드는 그 결정대로 Pod를 실제로 실행하고 상태를 보고합니다.마스터 노드가 멈추면 이미 돌던 앱은 어떻게 되나요?
워커 노드의 Pod는 계속 돌아요. 대신 새 배포, 죽은 Pod 다시 띄우기, 스케일링 같은 “결정”이 필요한 일은 멈춰요. 그래서 운영에서는 마스터를 3대로 둡니다.워커 노드의 세 구성 요소와 역할은?
kubelet은 배정된 Pod를 실행시키고 보고하는 관리인이에요. 컨테이너 런타임은 실제로 컨테이너를 실행하는 손이고, kube-proxy는 Service로 온 요청을 Pod로 보내는 교통 정리 담당이에요.GKE에서
kubectl get nodes에 마스터 노드가 안 보이는 이유는?GKE는 관리형 쿠버네티스라 마스터 노드(컨트롤 플레인)를 구글이 대신 운영하기 때문이에요. 우리 프로젝트에는 워커 노드(VM)만 보여요.kubectl logs와kubectl get은 요청이 어디까지 가나요?get은 API 서버가 저장된 상태(etcd)를 읽어 바로 답해요.logs와exec은 API 서버를 지나 그 Pod가 있는 노드의 kubelet까지 가서 컨테이너에서 가져와요.매니페스트의 네 칸은? 그리고 파일에 쓰지 않는 칸은?
apiVersion·kind·metadata·spec.status는 쿠버네티스가 현재 상태를 채우는 칸이라 직접 쓰지 않아요. spec(원하는 상태)과 status(현재 상태)의 차이를 없애는 게 제어 루프예요.YAML에서 들여쓰기에 탭을 쓰면?
오류가 나요. YAML 들여쓰기는 스페이스만 허용돼요. 들여쓰기 깊이가 어긋나면 필드가 엉뚱한 곳에 들어가서 “unknown field” 같은 오류가 납니다.
01. Pod란
쿠버네티스는 컨테이너를 직접 다루지 않고, Pod라는 상자에 담아서 다뤄요. Pod는 쿠버네티스가 만들고 · 배치하고 · 지우는 가장 작은 단위예요. 대부분의 Pod에는 컨테이너가 하나만 들어 있어서, 처음에는 “Pod ≈ 컨테이너 하나를 감싼 껍데기”로 생각해도 충분해요.
그럼 왜 껍데기가 필요할까요? 가끔 꼭 붙어 다녀야 하는 컨테이너들이 있기 때문이에요. 같은 Pod 안의 컨테이너는 IP 하나를 같이 쓰고, 항상 같은 노드에서 함께 뜨고 함께 사라져요. 한 집에 사는 룸메이트처럼 주소(IP)와 거실(볼륨)을 같이 쓰는 거예요.
1. Pod와 컨테이너의 관계
| 같은 Pod 안의 컨테이너는 | 뜻 |
|---|---|
| IP · 포트를 공유 | Pod마다 IP가 하나. 컨테이너끼리는 localhost로 통신하고, 같은 포트를 둘이 쓸 수는 없음 |
| 볼륨을 공유할 수 있음 | 같은 볼륨을 붙이면 파일을 주고받을 수 있음 (03에서) |
| 같은 노드에 함께 | Pod는 쪼개지지 않음. 컨테이너가 다른 노드로 흩어지지 않음 |
| 함께 생기고 함께 사라짐 | Pod를 지우면 안의 컨테이너가 전부 같이 지워짐 |
$ kubectl apply -f hello-pod.yaml # 2부 05의 YAML
$ kubectl get pods -o wide
NAME READY STATUS RESTARTS AGE IP NODE
hello 1/1 Running 0 12s 10.8.0.12 gke-...-xk3d
# READY 1/1 = 컨테이너 1개 중 1개 준비됨
02. 라벨과 셀렉터
Pod는 수십, 수백 개로 늘어나요. 이름으로 하나씩 부르면 감당이 안 되죠. 그래서 Pod에 라벨(label, 꼬리표)을 붙이고, 셀렉터(selector, 조건)로 원하는 묶음을 골라내요. 2부 05에서 metadata.labels에 적었던 app: hello가 바로 라벨이에요.
1. 꼬리표를 붙이고 골라내기
| 셀렉터 | 뜻 | 명령 예 |
|---|---|---|
app=web | app이 web인 것 | kubectl get pods -l app=web |
app=web,env=prod | 두 조건을 모두 만족 (쉼표 = 그리고) | kubectl get pods -l app=web,env=prod |
env!=prod | env가 prod가 아닌 것 | kubectl get pods -l 'env!=prod' |
env in (dev,stage) | env가 목록 중 하나 | kubectl get pods -l 'env in (dev,stage)' |
$ kubectl get pods --show-labels # 라벨까지 보기
$ kubectl label pod hello env=dev # 라벨 붙이기
$ kubectl label pod hello env=prod --overwrite # 값 바꾸기
$ kubectl label pod hello env- # 라벨 떼기 (키 뒤에 -)
app=web인 Pod가 3개여야 해”(4부), Service는 “app=web인 Pod에게 요청을 나눠 줘”(5부)라고 셀렉터로 대상을 정해요. 그래서 새 Pod가 같은 라벨을 달고 태어나면 자동으로 그 묶음에 들어가요. 라벨 오타 하나가 “Service가 Pod를 못 찾는” 흔한 장애 원인이에요.
03. 한 Pod에 컨테이너 여러 개 — 사이드카
대부분은 Pod 하나에 컨테이너 하나지만, 주 컨테이너를 돕는 보조 컨테이너를 같은 Pod에 넣을 때가 있어요. 오토바이 옆에 붙는 사이드카처럼 본체 옆에 붙어 다닌다고 해서 사이드카 패턴이라고 불러요. 로그 수집, 프록시, 설정 파일 동기화 같은 일이 대표적이에요.
1. 같이 사는 컨테이너들
apiVersion: v1
kind: Pod
metadata:
name: web
labels:
app: web
spec:
volumes:
- name: logs # 두 컨테이너가 같이 쓸 빈 폴더
emptyDir: {}
initContainers:
- name: wait-db # 먼저 실행되고 끝나야 본 컨테이너가 시작됨
image: busybox:1.36
command: ["sh", "-c", "until nslookup db; do sleep 2; done"] # DB가 생길 때까지 대기
containers:
- name: web # 주 컨테이너
image: nginx:1.27
volumeMounts:
- name: logs
mountPath: /var/log/nginx
- name: log-agent # 사이드카 — 로그를 읽어 밖으로 보냄
image: fluent/fluent-bit:3.0
volumeMounts:
- name: logs
mountPath: /logs
| 같은 Pod에 넣어도 되는 경우 | 따로 Pod로 나눠야 하는 경우 |
|---|---|
| 주 컨테이너 없이는 의미가 없는 보조 역할 (로그 수집, 프록시) | 각자 독립된 서비스 (웹 서버와 DB) |
| 파일 · localhost로 아주 가깝게 붙어 일해야 함 | 네트워크로 통신해도 충분함 |
| 항상 같은 개수로 늘고 줄어야 함 | 개수를 따로 조절해야 함 (웹은 10개, DB는 1개) |
kubectl get pods의 READY가 2/2로 나와요. 로그를 볼 때는 어느 컨테이너인지 정해야 해요. kubectl logs web -c log-agent처럼 -c로 컨테이너 이름을 붙입니다. 참고로 쿠버네티스 1.33부터는 사이드카를 initContainers에 restartPolicy: Always로 적는 공식 방식도 안정화됐어요. 입문 단계에서는 위처럼 containers에 같이 적는 형태로 이해하면 충분해요.
04. 상태와 재시작
kubectl get pods의 STATUS와 RESTARTS 칸은 Pod의 건강 상태를 알려 주는 가장 중요한 단서예요. Pod는 태어나서 사라질 때까지 정해진 단계(phase)를 지나고, 컨테이너가 죽으면 kubelet이 같은 자리에서 다시 띄워요.
1. Pod의 한살이와 재시작
| STATUS에 보이는 값 | 뜻 | 먼저 볼 곳 |
|---|---|---|
Pending | 아직 노드에 배정 안 됨 (자리 부족 등) | describe의 Events |
ContainerCreating | 노드에 배정됐고 이미지 받는 중 | 잠시 기다리기 |
Running | 컨테이너가 실행 중 | — |
Completed | 할 일을 마치고 정상 종료 (Job 등) | — |
CrashLoopBackOff | 컨테이너가 시작하자마자 계속 죽어서 재시작 대기 중 | kubectl logs --previous |
ImagePullBackOff | 이미지를 못 받음 (이름 · 태그 오타, 권한) | describe의 Events |
OOMKilled | 메모리 한도 초과로 강제 종료 (1부 03) | 06의 limits |
restartPolicy | 컨테이너가 끝나면 | 주로 쓰는 곳 |
|---|---|---|
Always (기본값) | 정상 종료든 오류든 항상 다시 시작 | 웹 서버처럼 계속 떠 있어야 하는 앱 |
OnFailure | 오류로 끝났을 때만 다시 시작 | 배치 작업 (Job) |
Never | 다시 시작 안 함 | 한 번만 돌리고 결과를 볼 작업 |
05. 헬스 체크 — liveness · readiness
04의 재시작은 컨테이너 프로세스가 죽었을 때만 일어나요. 그런데 프로세스는 살아 있는데 앱이 멈춰 버리면(무한 대기, 데드락) 어떨까요? 겉보기엔 Running인데 요청은 하나도 처리 못 해요. 그래서 kubelet이 주기적으로 앱에 직접 “괜찮아?”를 물어보는 기능이 프로브(probe)예요.
1. 두 가지 질문, 두 가지 대응
| 프로브 | 묻는 것 | 실패하면 |
|---|---|---|
| readinessProbe | “지금 요청 받을 준비 됐어?” | Service에서 빠짐 → 트래픽이 안 옴. 재시작은 안 함 |
| livenessProbe | “살아 있어? 멈춘 거 아니지?” | 컨테이너 재시작 |
| startupProbe | “시작 다 끝났어?” (느리게 뜨는 앱용) | 성공할 때까지 다른 두 프로브를 미룸 |
containers:
- name: web
image: myapp:1.0
readinessProbe:
httpGet:
path: /ready # 이 주소가 200이면 준비됨
port: 8080
periodSeconds: 5 # 5초마다 확인
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 10 # 시작하고 10초 뒤부터
periodSeconds: 10
failureThreshold: 3 # 3번 연속 실패하면 재시작
httpGet(주소에 요청해서 200~399면 성공), tcpSocket(포트에 연결되면 성공), exec(컨테이너 안에서 명령을 실행해 종료 코드 0이면 성공). 기본값은 10초마다 확인하고, 1초 안에 답이 없으면 실패, 3번 연속 실패하면 “실패”로 판단해요.
06. 리소스 요청과 제한
2부 02에서 노드마다 Allocatable이라는 자리가 있다고 했죠. Pod는 그 자리를 얼마나 쓸지 두 숫자로 알려 줘요. requests는 “최소 이만큼은 예약해 줘”, limits는 “아무리 많아도 이 이상은 못 써”예요. 식당으로 치면 requests는 예약한 좌석 수, limits는 최대 수용 인원이에요.
1. 예약과 상한
containers:
- name: web
image: myapp:1.0
resources:
requests:
cpu: 250m # 0.25코어 예약 (1000m = 1코어)
memory: 256Mi # 256 메비바이트 예약
limits:
cpu: 500m # 넘으면 느려짐 (throttling)
memory: 512Mi # 넘으면 OOMKilled
| CPU | 메모리 | |
|---|---|---|
| 단위 | 1 = 1코어, 500m = 0.5코어 | Mi · Gi (1Mi = 1024×1024 바이트) |
| requests의 역할 | 스케줄러가 노드를 고를 때 쓰는 기준 — 실제 사용량이 아니라 requests 합계로 자리를 계산 | |
| limits를 넘으면 | 죽지 않고 느려짐 (나눠 쓰는 자원이라 눌러 둠) | OOMKilled → 재시작 (빌려 쓸 수 없는 자원) |
| QoS 등급 | 조건 | 노드 메모리가 부족할 때 |
|---|---|---|
| Guaranteed | 모든 컨테이너에 requests = limits (CPU · 메모리 둘 다) | 가장 마지막에 쫓겨남 |
| Burstable | requests는 있지만 limits와 다르거나 일부만 있음 | 중간 |
| BestEffort | requests · limits를 하나도 안 씀 | 가장 먼저 쫓겨남 |
kubectl top pods로 실제 사용량을 보며 조정하면 돼요.
2. 3부 셀프 체크
같은 Pod 안의 컨테이너들이 공유하는 것은?
IP와 포트 공간(서로localhost로 통신), 그리고 붙여 둔 볼륨이에요. 또 항상 같은 노드에서 함께 생기고 함께 사라져요.직접 만든 Pod를 지우면 다시 생기나요?
아니요. Pod는 일회용이라 직접 만든 Pod는 그걸로 끝이에요. 다시 만들어 주는 건 4부의 ReplicaSet · Deployment가 하는 일이에요.-l app=web,env=prod는 어떤 Pod를 고르나요?app이 web 이고 env가 prod인 Pod요. 쉼표는 “그리고(AND)”예요.웹 서버와 DB를 한 Pod에 넣으면 안 되는 이유는?
항상 같은 개수로 함께 늘고 줄어야 하기 때문이에요. 웹은 10개, DB는 1개처럼 따로 조절할 수 없어요. 서로 독립된 서비스는 다른 Pod로 나누고 네트워크로 통신해요.RESTARTS가 계속 오르고 STATUS가 CrashLoopBackOff예요. 무엇부터 보나요?
죽기 직전 로그를 봐요.kubectl logs 이름 --previous로 이전 컨테이너의 로그를,kubectl describe pod 이름으로 종료 이유(Exit Code, OOMKilled 등)와 Events를 확인해요.readiness가 실패할 때와 liveness가 실패할 때의 차이는?
readiness 실패는 트래픽만 빠지고 컨테이너는 그대로예요. liveness 실패는 컨테이너를 재시작해요.CPU limit과 메모리 limit을 넘으면 각각 어떻게 되나요?
CPU는 죽지 않고 느려져요(throttling). 메모리는 OOMKilled로 강제 종료된 뒤 재시작돼요.
01. 레플리카 — 왜 같은 Pod를 여러 개 띄우나
3부에서 Pod는 일회용이라고 했죠. 하나만 띄워 두면 그 Pod가 죽는 순간 서비스가 멈춰요. 그래서 실제 서비스는 똑같은 Pod를 여러 개 띄워 둬요. 이 복제본 하나하나를 레플리카(replica), 그 개수를 replicas라고 불러요.
4부에서 볼 워크로드 컨트롤러들은 모두 1부 05의 제어 루프를 Pod에 적용한 거예요. 우리가 “이런 Pod가 몇 개”를 선언하면, 컨트롤러가 Pod를 대신 만들고 지켜 줘요. 그래서 실무에서는 Pod를 직접 만들지 않아요.
1. 하나일 때와 여럿일 때
| 레플리카를 여러 개 두면 | 뜻 |
|---|---|
| 가용성 | 하나가 죽거나 노드 하나가 멈춰도 나머지가 요청을 받아요 |
| 부하 분산 | 요청이 여러 Pod로 나뉘어서 하나에 몰리지 않아요 |
| 무중단 배포 | 하나씩 바꿔 끼울 수 있어요 (03 롤링 업데이트) |
02. ReplicaSet — 개수를 지키는 컨트롤러
ReplicaSet은 “이 라벨을 단 Pod가 항상 N개 떠 있게”를 지키는 컨트롤러예요. 3개여야 하는데 2개면 하나를 만들고, 4개면 하나를 지워요. 필요한 건 세 가지예요.
replicas— 몇 개를 유지할지selector— 어떤 Pod를 내 것으로 셀지 (3부 02의 라벨 셀렉터)template— 모자랄 때 찍어 낼 Pod의 틀 (Pod YAML의 metadata · spec과 같음)
1. 세고, 모자라면 찍어 낸다
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: web
spec:
replicas: 3 # 몇 개
selector:
matchLabels:
app: web # 어떤 Pod를 셀지
template: # 모자라면 이 틀로 찍어 냄
metadata:
labels:
app: web # selector와 반드시 일치
spec:
containers:
- name: web
image: nginx:1.27
$ kubectl apply -f web-rs.yaml
$ kubectl get rs
NAME DESIRED CURRENT READY AGE
web 3 3 3 20s
$ kubectl scale rs web --replicas=5 # 개수 바꾸기
ReplicaSet 이름 + 무작위 5글자로 이름이 붙어요. 이름이 아니라 라벨로 세기 때문에 이름은 아무래도 상관없어요. 그래서 template의 라벨과 selector가 다르면 API 서버가 아예 거부해요. 그렇게 만들면 자기가 만든 Pod를 자기가 못 세는 상황이 되니까요.
03. Deployment — 롤링 업데이트와 롤백
Deployment는 ReplicaSet을 관리하는 한 단계 위의 컨트롤러예요. 구조는 Deployment → ReplicaSet → Pod의 세 층이에요. 이미지 같은 Pod 틀이 바뀌면 Deployment가 새 ReplicaSet을 만들고, 새 것은 늘리고 옛 것은 줄이면서 버전을 바꿔요. 1부 05에서 본 롤링 업데이트가 실제로는 이렇게 일어나요.
1. ReplicaSet 두 개로 버전 바꾸기
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
selector:
matchLabels:
app: web
strategy:
type: RollingUpdate # 기본값. 다른 하나는 Recreate
rollingUpdate:
maxSurge: 1 # 교체 중 원래 개수보다 최대 몇 개 더
maxUnavailable: 0 # 교체 중 최대 몇 개까지 빠져도 되나
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: myapp:1.0
| 명령 | 하는 일 |
|---|---|
kubectl apply -f web-deploy.yaml | YAML의 image를 바꾸고 다시 apply → 롤링 업데이트 시작 (선언형) |
kubectl set image deployment/web web=myapp:2.0 | 명령 한 줄로 이미지 바꾸기 (명령형) |
kubectl rollout status deployment/web | 교체가 끝날 때까지 진행 상황 보기 |
kubectl rollout history deployment/web | 지금까지의 버전(revision) 목록 |
kubectl rollout undo deployment/web | 바로 전 버전으로 되돌리기 (--to-revision=1로 특정 버전) |
kubectl rollout restart deployment/web | 이미지는 그대로, Pod만 하나씩 새로 띄우기 |
strategy.type | 방식 | 중단 |
|---|---|---|
RollingUpdate (기본) | 하나씩 새 것 올리고 옛 것 내리기. 기본값은 maxSurge · maxUnavailable 모두 25% | 없음 |
Recreate | 옛 것을 전부 내리고 새 것을 올리기 | 잠깐 있음 — 두 버전이 동시에 돌면 안 될 때만 |
6d4f9b 같은 값은 Pod 틀의 해시(pod-template-hash)예요. 틀이 같으면 같은 값이 나와요.
04. DaemonSet — 노드마다 하나씩
Deployment는 개수를 맞추지만, DaemonSet은 노드마다 딱 하나씩 띄워요. 그래서 replicas 칸이 없어요. 노드가 늘면 자동으로 하나 더 뜨고, 노드가 빠지면 같이 사라져요. 모든 노드에서 돌아야 하는 보조 프로그램에 써요.
| 이런 데 써요 | 예 |
|---|---|
| 노드의 로그 수집 | fluent-bit, Fluentd |
| 노드의 자원 · 상태 모니터링 | node-exporter, Datadog agent |
| 노드의 네트워크 · 스토리지 기능 | 네트워크 플러그인(CNI), 스토리지 드라이버 |
05. Job · CronJob — 끝나는 일
웹 서버는 계속 떠 있어야 하지만, 데이터 정리나 리포트 생성처럼 한 번 돌고 끝나야 하는 일도 있어요. Deployment로 돌리면 끝나도 다시 띄워서 끝없이 반복돼요. Job은 Pod가 성공적으로 끝나면 그걸로 완료로 쳐요. CronJob은 정해진 시간표마다 Job을 만들어요.
apiVersion: batch/v1
kind: CronJob
metadata:
name: daily-report
spec:
schedule: "0 3 * * *" # 매일 03:00
timeZone: Asia/Seoul # 없으면 클러스터 기준 시간(보통 UTC)
jobTemplate: # 매번 만들 Job의 틀
spec:
backoffLimit: 3 # 실패하면 최대 3번 재시도
template:
spec:
restartPolicy: OnFailure # Job은 Always를 못 씀
containers:
- name: report
image: myapp-report:1.0
| schedule 칸 | 분 | 시 | 일 | 월 | 요일(0=일) | 뜻 |
|---|---|---|---|---|---|---|
"0 3 * * *" | 0 | 3 | * | * | * | 매일 03:00 |
"*/10 * * * *" | */10 | * | * | * | * | 10분마다 |
"0 9 * * 1-5" | 0 | 9 | * | * | 1-5 | 평일 09:00 |
| Job 설정 | 기본값 | 뜻 |
|---|---|---|
completions | 1 | 성공해야 하는 횟수 |
parallelism | 1 | 동시에 몇 개까지 돌릴지 |
backoffLimit | 6 | 실패를 몇 번까지 다시 시도할지 |
ttlSecondsAfterFinished | 없음 | 끝난 Job을 몇 초 뒤 자동으로 지울지 |
06. StatefulSet 맛보기
01에서 레플리카는 서로 똑같아야 한다고 했죠. 그런데 DB 클러스터처럼 Pod마다 자기 이름 · 자기 데이터 · 자기 역할이 있어야 하는 경우가 있어요. 이럴 때 StatefulSet을 써요. 저장소(볼륨)를 다루는 6부와 이후 전반 스터디에서 자세히 보고, 여기서는 Deployment와 무엇이 다른지만 봐요.
| Deployment | StatefulSet | |
|---|---|---|
| Pod 이름 | 무작위 (web-7x2kq), 다시 만들면 바뀜 | 순번 고정 (web-0, web-1) |
| 만드는 순서 | 한꺼번에 | 0 → 1 → 2 순서대로, 지울 땐 반대로 |
| 저장소 | 보통 없음 · 있어도 공유 | Pod마다 자기 디스크 (volumeClaimTemplates) |
| 주소 | Service 하나로 묶어서 아무 Pod에게나 | Pod마다 고정 주소 (web-0.web) — headless Service 필요 |
| 예 | 웹 서버, API 서버 | MySQL · PostgreSQL 클러스터, Kafka, Elasticsearch |
1. 무엇을 쓸까
| 이런 일이라면 | 이걸 써요 |
|---|---|
| 계속 떠 있는 stateless 앱 (웹 · API) | Deployment |
| 모든 노드에 하나씩 필요한 보조 프로그램 | DaemonSet |
| 한 번 돌고 끝나는 작업 | Job |
| 정해진 시간마다 도는 작업 | CronJob |
| Pod마다 고유한 이름 · 데이터가 필요한 앱 (DB 등) | StatefulSet |
| ReplicaSet · Pod를 직접? | 거의 쓰지 않음 — 위의 것들이 대신 만들어 줌 |
2. 4부 셀프 체크
같은 Pod를 여러 개(레플리카) 두는 이유 세 가지는?
가용성(하나가 죽어도 나머지가 받음), 부하 분산(요청을 나눠 받음), 무중단 배포(하나씩 교체)예요.ReplicaSet은 자기 Pod를 무엇으로 세나요?
라벨 셀렉터로 세요. 이름이 아니라서, Pod의 라벨을 바꾸면 그 Pod는 관리 밖으로 빠지고 ReplicaSet은 모자란 개수만큼 새로 만들어요.Deployment · ReplicaSet · Pod의 관계는?
Deployment가 ReplicaSet을 만들고, ReplicaSet이 Pod를 만들어요. 이미지가 바뀌면 Deployment는 새 ReplicaSet을 만들어 늘리고, 옛 것은 줄여서 0개로 남겨 둬요.롤백(
rollout undo)이 빠른 이유는?옛 ReplicaSet이 0개로 남아 있어서, 새로 만들 필요 없이 숫자만 다시 올리면 되기 때문이에요.DaemonSet에 replicas 칸이 없는 이유는?
개수가 아니라 노드마다 하나가 규칙이라서요. 노드 수가 곧 Pod 수예요.배치 작업을 Deployment로 돌리면 어떻게 되나요?
작업이 끝나 컨테이너가 종료되면restartPolicy: Always때문에 다시 띄워져서 끝없이 반복돼요. 끝나는 일은 Job으로 돌려요.StatefulSet의 Pod가 죽었다 살아나면 Deployment와 무엇이 다르나요?
같은 이름(web-1)으로 다시 생기고, 원래 쓰던 자기 디스크에 다시 붙어요. Deployment는 새 이름으로 생기고 고정된 디스크가 없어요.
01. Service가 필요한 이유
3부에서 Pod마다 IP가 있다고 했죠. 그런데 이 IP로 직접 통신하면 두 가지 문제가 생겨요. 첫째, Pod는 일회용이라 다시 만들어지면 IP가 바뀌어요. 둘째, 4부처럼 레플리카가 여러 개면 어느 IP로 보내야 할지 모르죠.
Service는 Pod 묶음 앞에 세우는 고정된 대표 주소예요. 회사 대표번호처럼, 담당자(Pod)가 바뀌어도 번호는 그대로이고 전화는 지금 자리에 있는 사람에게 연결돼요. 어떤 Pod를 묶을지는 3부 02의 라벨 셀렉터로 정해요.
1. 바뀌는 IP 대신 고정된 이름과 주소
apiVersion: v1
kind: Service
metadata:
name: api # 이 이름으로 부를 수 있음 (03 DNS)
spec:
selector:
app: api # 이 라벨의 Pod들에게 보냄
ports:
- port: 80 # Service가 받는 포트
targetPort: 8080 # Pod(컨테이너)가 실제로 여는 포트
$ kubectl get svc api
NAME TYPE CLUSTER-IP PORT(S)
api ClusterIP 10.96.0.15 80/TCP
$ kubectl get endpointslices -l kubernetes.io/service-name=api
NAME ADDRESSTYPE PORTS ENDPOINTS
api-x7k2p IPv4 8080 10.8.0.12,10.8.1.9,10.8.2.4
| 용어 | 뜻 |
|---|---|
| ClusterIP | Service에 붙는 고정 가상 IP. Service를 지우기 전까지 바뀌지 않음 |
port / targetPort | Service가 받는 포트 / Pod로 넘겨줄 포트. 달라도 됨 (80 → 8080) |
| EndpointSlice | 지금 셀렉터에 맞고 준비(Ready)된 Pod IP 목록. 쿠버네티스가 자동으로 고쳐 씀 |
| kube-proxy | 2부 02의 그것. 노드마다 “ClusterIP로 온 요청 → 목록의 Pod 중 하나로” 규칙을 깔아 둠 |
02. ClusterIP · NodePort · LoadBalancer
Service는 누가 접근할 수 있느냐에 따라 종류(type)가 나뉘어요. 세 가지는 따로 노는 게 아니라 겹겹이 쌓이는 구조예요. LoadBalancer는 NodePort를 포함하고, NodePort는 ClusterIP를 포함해요.
1. 안에서만 · 노드 포트로 · 외부 로드밸런서로
type | 누가 접근하나 | 주소 | 주로 쓰는 곳 |
|---|---|---|---|
| ClusterIP (기본값) | 클러스터 안의 Pod만 | 10.96.0.15:80 | 내부 API, DB 등 대부분의 Service |
| NodePort | 노드 IP를 알 수 있는 곳 어디서나 | 노드IP:30080 (30000~32767) | 실습, 직접 만든 로드밸런서 뒤 |
| LoadBalancer | 인터넷 | 외부 IP 34.64.x.x:80 | 클라우드에서 외부에 공개할 서비스 |
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: LoadBalancer # ClusterIP(기본) · NodePort · LoadBalancer
selector:
app: web
ports:
- port: 80
targetPort: 8080
$ kubectl get svc web -w
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
web LoadBalancer 10.96.12.40 <pending> 80:31942/TCP
web LoadBalancer 10.96.12.40 34.64.120.7 80:31942/TCP # 1~2분 뒤
# PORT(S)의 31942가 자동으로 같이 생긴 NodePort
type: LoadBalancer를 만들면 구글 클라우드 로드밸런서가 실제로 하나 생기고 요금이 붙어요. 외부에 공개할 서비스가 여러 개라고 LoadBalancer를 여러 개 만들면 로드밸런서도 그만큼 생겨요. 그래서 웹 서비스들은 보통 04의 Ingress 하나로 입구를 합쳐요. 실습이 끝나면 클러스터와 함께 지워 주세요.
03. DNS와 서비스 디스커버리
ClusterIP는 고정이지만, 숫자를 코드에 적어 두는 건 여전히 불편해요. 그래서 쿠버네티스 안에는 DNS 서버(CoreDNS)가 있어서 Service 이름을 ClusterIP로 바꿔 줘요. 앱 코드에는 http://api처럼 이름만 쓰면 돼요. 이렇게 이름으로 상대를 찾는 걸 서비스 디스커버리라고 해요.
1. 이름 → ClusterIP → Pod
| 이렇게 부르면 | 뜻 | 언제 |
|---|---|---|
api | 같은 네임스페이스의 Service api | 가장 흔함 |
api.shop | shop 네임스페이스의 Service api | 다른 네임스페이스 호출 |
api.shop.svc.cluster.local | 전체 이름 (FQDN) | 헷갈림 없이 정확히 |
web-0.web.shop.svc.cluster.local | StatefulSet Pod 하나의 고정 주소 (4부 06) | headless Service 뒤의 특정 Pod |
$ kubectl run tmp --rm -it --image=busybox:1.36 -- sh # 임시 Pod로 들어가 보기
/ # nslookup api
Name: api.default.svc.cluster.local
Address: 10.96.0.15
/ # cat /etc/resolv.conf
search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
/etc/resolv.conf에 search 목록이 있어서, api라고만 써도 api.default.svc.cluster.local 순서로 붙여 가며 찾아 줘요. nameserver는 CoreDNS의 ClusterIP예요.
04. Ingress — 하나의 입구로 여러 Service에
웹 서비스가 여러 개면 Service마다 LoadBalancer를 만드는 대신, Ingress 하나를 입구로 두고 주소(호스트)와 경로를 보고 알맞은 Service로 나눠 보내요. 건물 1층 안내 데스크처럼 “쇼핑은 2층, API는 3층”으로 안내하는 역할이에요. HTTPS 인증서도 이 입구에서 한 번에 처리할 수 있어요.
1. 호스트와 경로로 나눠 보내기
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop
spec:
# ingressClassName: ... # 처리할 Ingress 컨트롤러 지정 (GKE 기본 컨트롤러는 생략 가능)
tls:
- hosts: [shop.example.com]
secretName: shop-tls # HTTPS 인증서 (6부 Secret)
rules:
- host: shop.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api
port:
number: 80
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
- host: admin.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: admin
port:
number: 80
2. 5부 셀프 체크
Pod IP로 직접 통신하면 생기는 문제 두 가지는?
Pod가 다시 만들어지면 IP가 바뀌고, 레플리카가 여러 개면 어느 IP로 보낼지 정할 수 없어요. Service가 고정된 주소와 분배를 맡아요.Service는 요청을 보낼 Pod를 어떻게 정하나요?
라벨 셀렉터로요. 셀렉터에 맞고 준비(Ready)된 Pod의 IP 목록(EndpointSlice)을 쿠버네티스가 계속 갱신해요.port와targetPort의 차이는?port는 Service가 받는 포트,targetPort는 Pod 안 컨테이너가 실제로 여는 포트예요. 80으로 받아 8080으로 넘길 수 있어요.ClusterIP · NodePort · LoadBalancer는 각각 어디서 접근할 수 있나요?
ClusterIP는 클러스터 안에서만, NodePort는 모든 노드의 IP와 30000~32767 포트로, LoadBalancer는 클라우드 로드밸런서의 외부 IP로 인터넷에서 접근해요. 뒤의 것이 앞의 것을 포함해요.앱 코드에서 다른 서비스를
http://api로 부를 수 있는 이유는?클러스터 안의 DNS(CoreDNS)가 Service 이름을 ClusterIP로 바꿔 주기 때문이에요. 다른 네임스페이스면api.shop처럼 네임스페이스를 붙여요.외부에 공개할 웹 서비스가 3개일 때 LoadBalancer 3개 대신 무엇을 쓰나요?
Ingress 하나를 두고 호스트 · 경로 규칙으로 3개의 Service에 나눠 보내요. 로드밸런서 비용도 줄고 HTTPS도 한 곳에서 처리해요.Ingress YAML을 만들었는데 아무 일도 안 일어나요. 무엇부터 확인하나요?
클러스터에 Ingress 컨트롤러가 있는지, 그리고ingressClassName을 적었다면 그 컨트롤러를 제대로 가리키는지 확인해요. Ingress는 규칙일 뿐이라 처리할 컨트롤러가 필요해요.
01. ConfigMap — 설정을 이미지 밖으로
앱에는 DB 주소, 로그 레벨, 기능 스위치 같은 설정이 필요해요. 이걸 이미지 안에 넣어 버리면 개발 · 운영 환경마다 이미지를 따로 만들어야 하죠. ConfigMap은 설정을 이미지 밖의 쿠버네티스 객체로 빼 두는 거예요. 같은 이미지에 환경마다 다른 ConfigMap만 붙이면 돼요.
1. 같은 이미지, 다른 설정
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
LOG_LEVEL: "info" # 키: 값 (값은 모두 문자열)
DB_HOST: "mysql.shop"
app.properties: | # 파일 하나를 통째로 넣을 수도 있음
feature.newCart=true
cache.ttl=300
containers:
- name: web
image: myapp:1.0
envFrom:
- configMapRef:
name: app-config # 모든 키를 환경 변수로
volumeMounts:
- name: config
mountPath: /etc/app # 키 하나 = 파일 하나
volumes:
- name: config
configMap:
name: app-config
| 넣는 방법 | 앱에서 보이는 모습 | ConfigMap을 바꾸면 |
|---|---|---|
환경 변수 (env · envFrom) | process.env.LOG_LEVEL | 그대로 — Pod를 다시 띄워야 반영 (kubectl rollout restart) |
| 파일 (volume) | /etc/app/app.properties | 잠시(보통 1분 안팎) 뒤 파일이 바뀜. 앱이 파일을 다시 읽어야 적용 |
$ kubectl create configmap app-config --from-literal=LOG_LEVEL=info # 명령으로 만들기
$ kubectl get configmap app-config -o yaml
$ kubectl exec web-7x2kq -- env | grep LOG_LEVEL # 들어갔는지 확인
02. Secret — 비밀값은 따로
DB 비밀번호, API 키, HTTPS 인증서처럼 노출되면 안 되는 값은 Secret에 둬요. 쓰는 방법은 ConfigMap과 거의 같지만, 쿠버네티스가 비밀값으로 따로 취급해요. 접근 권한을 따로 나눌 수 있고, 파일로 넣으면 디스크가 아닌 메모리에만 올라가요.
1. base64는 암호화가 아니에요
$ kubectl create secret generic db-secret --from-literal=password='s3cr3t!'
$ kubectl get secret db-secret -o jsonpath='{.data.password}'
czNjcjN0IQ==
$ echo 'czNjcjN0IQ==' | base64 -d # 누구나 바로 되돌릴 수 있음
s3cr3t!
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
Secret 종류 (type) | 용도 |
|---|---|
Opaque (기본) | 비밀번호, API 키 등 일반 비밀값 |
kubernetes.io/tls | HTTPS 인증서와 키 (5부 04의 Ingress tls) |
kubernetes.io/dockerconfigjson | 비공개 레지스트리에서 이미지를 받을 때 쓰는 로그인 정보 |
03. Volume — 데이터는 어디에 남나
1부 04에서 컨테이너의 파일은 쓰기 레이어에 있어서 컨테이너가 사라지면 같이 사라진다고 했죠. 쿠버네티스에서는 컨테이너가 재시작만 해도 새 쓰기 레이어로 시작해서 파일이 사라져요. 남겨야 하는 데이터는 컨테이너 바깥의 볼륨에 둬요. 볼륨은 종류마다 얼마나 오래 남는지가 달라요.
1. 재시작 · Pod 삭제에서 살아남는 것
| 볼륨 종류 | 어디에 | 얼마나 남나 | 쓰는 곳 |
|---|---|---|---|
| (볼륨 없음) | 컨테이너 쓰기 레이어 | 컨테이너 재시작까지 | 임시 파일만 |
emptyDir | 노드의 임시 공간 (또는 메모리) | Pod가 지워질 때까지 | 사이드카와 파일 공유(3부 03), 캐시 |
configMap · secret | 쿠버네티스 객체 | 객체가 있는 동안 | 설정 파일 · 인증서 (01 · 02) |
hostPath | 노드의 특정 폴더 | 그 노드가 있는 동안 | 노드 관리용 DaemonSet 정도. 일반 앱엔 쓰지 않음 |
persistentVolumeClaim | 클러스터 밖의 디스크 | PVC를 지울 때까지 (Pod와 무관) | DB 등 남아야 하는 데이터 (04) |
04. PV · PVC · StorageClass
오래 남는 디스크는 세 가지 객체로 다뤄요. 집 구하기로 비유하면 이래요.
- PVC(PersistentVolumeClaim) — “방 2개, 남향으로 주세요” 같은 요청서. 앱 개발자가 크기와 사용 방식만 적어요.
- PV(PersistentVolume) — 실제로 계약된 집. 클라우드 디스크 한 개에 해당해요.
- StorageClass — 요청서가 들어오면 집을 새로 지어 주는 건설사. 어떤 종류의 디스크(일반 · SSD)를 만들지 정해요.
요즘은 PVC만 만들면 StorageClass가 디스크와 PV를 자동으로 만들어 줘요(동적 프로비저닝). 그래서 실제로 우리가 쓰는 건 대부분 PVC 하나예요.
1. 요청서 → 디스크 → Pod
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-data
spec:
storageClassName: standard-rwo # GKE: 균형 PD. premium-rwo는 SSD
accessModes: [ReadWriteOnce] # 한 노드에서만 읽고 쓰기
resources:
requests:
storage: 10Gi
---
# Pod(Deployment template)에서
containers:
- name: mysql
image: mysql:8.4
volumeMounts:
- name: data
mountPath: /var/lib/mysql
volumes:
- name: data
persistentVolumeClaim:
claimName: mysql-data
$ kubectl get storageclass # (default) 표시가 붙은 게 기본값
$ kubectl get pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS
mysql-data Bound pvc-3f9c1e2a-... 10Gi RWO standard-rwo
$ kubectl get pv
accessModes | 뜻 | 예 |
|---|---|---|
ReadWriteOnce (RWO) | 한 노드에서만 읽고 쓰기 | 일반 클라우드 디스크(GCE PD) — 가장 흔함 |
ReadOnlyMany (ROX) | 여러 노드에서 읽기만 | 공유 읽기 전용 데이터 |
ReadWriteMany (RWX) | 여러 노드에서 함께 읽고 쓰기 | NFS 계열 (GCP Filestore 등) |
reclaimPolicy: Delete라서 PVC를 지우면 클라우드 디스크까지 같이 지워져요. 중요한 데이터라면 Retain으로 바꾸거나 스냅샷으로 백업해 두세요. 반대로 실습이 끝났는데 PVC를 안 지우면 디스크 요금이 계속 나가요.
standard-rwo는 WaitForFirstConsumer 방식이라, 이 PVC를 쓰는 Pod가 노드에 배정될 때까지 디스크를 만들지 않고 Pending으로 기다려요. Pod가 어느 구역(zone)에 뜰지 보고 같은 구역에 디스크를 만들기 위해서예요. Pod를 띄우면 Bound로 바뀌어요.
2. 6부 셀프 체크
설정을 이미지에 넣지 않고 ConfigMap으로 빼는 이유는?
같은 이미지를 개발 · 운영 등 여러 환경에서 쓰고, 환경마다 설정만 바꿔 붙이기 위해서예요. 설정을 바꿀 때 이미지를 다시 빌드하지 않아도 돼요.ConfigMap을 환경 변수로 넣었을 때 값을 바꾸면 바로 반영되나요?
아니요. 환경 변수는 컨테이너가 시작될 때 정해지므로 Pod를 다시 띄워야 해요(kubectl rollout restart). 파일로 마운트하면 잠시 뒤 파일 내용이 바뀌어요.Secret의 base64는 암호화인가요?
아니요. 누구나base64 -d로 바로 되돌릴 수 있는 인코딩일 뿐이에요. 보호는 접근 권한(RBAC), etcd 암호화, 외부 비밀 관리 서비스로 해요.볼륨 없이 컨테이너 안에 쓴 파일은 언제 사라지나요?
컨테이너가 재시작만 해도 사라져요. emptyDir은 재시작은 버티지만 Pod가 지워지면 사라지고, PVC 디스크는 Pod와 상관없이 남아요.PVC · PV · StorageClass를 한 줄씩 설명하면?
PVC는 “이만큼 필요해요”라는 요청서, PV는 실제 디스크, StorageClass는 요청이 오면 디스크를 자동으로 만들어 주는 규칙이에요.ReadWriteOnce디스크를 Deployment replicas 3개가 함께 쓰려고 하면?RWO는 한 노드에서만 붙일 수 있어서, Pod가 다른 노드에 뜨면 디스크를 붙이지 못해요. 여러 Pod가 각자 데이터를 가져야 하면 StatefulSet의volumeClaimTemplates로 Pod마다 디스크를 만들고, 정말 함께 써야 하면 RWX(Filestore 등)를 써요.실습 후 PVC를 지우지 않으면 어떻게 되나요? 반대로 지우면?
안 지우면 클라우드 디스크 요금이 계속 나가요. 지우면reclaimPolicy: Delete인 경우 디스크와 데이터도 같이 지워져요. 중요한 데이터는 백업하거나 Retain으로 둬요.
01. Namespace — 클러스터를 칸으로 나누기
클러스터 하나를 여러 팀 · 여러 환경이 같이 쓰면 이름이 겹치고, 누가 무엇을 만들었는지 헷갈려요. Namespace는 클러스터 안을 논리적인 칸(폴더)으로 나눠요. 3부 02에서 말했던 “리눅스 네임스페이스와는 다른 쿠버네티스 Namespace”가 바로 이거예요.
1. 칸마다 이름 · 권한 · 한도를 따로
$ kubectl get namespaces
NAME STATUS AGE
default Active 3d # 아무것도 지정 안 하면 여기
kube-system Active 3d # 쿠버네티스 자체 부품 (CoreDNS 등)
kube-public Active 3d
kube-node-lease Active 3d
$ kubectl create namespace shop
$ kubectl get pods -n shop # 특정 칸 보기
$ kubectl get pods -A # 모든 칸 보기
$ kubectl config set-context --current --namespace=shop # 기본 칸 바꾸기
| 칸 안에 있는 것 (namespaced) | 칸 밖에 있는 것 (cluster-scoped) |
|---|---|
| Pod · Deployment · Service · ConfigMap · Secret · PVC · Ingress · HPA … | Node · PV · StorageClass · Namespace 자신 |
api.shop처럼). 칸 사이 통신을 막으려면 NetworkPolicy를 따로 써야 해요. 그리고 kubectl delete namespace는 그 안의 모든 것을 지우니 조심하세요.
02. 오토스케일링 — HPA
4부에서는 replicas를 사람이 정했어요. 트래픽은 시간마다 달라지니, 바쁠 땐 늘리고 한가할 땐 줄이는 걸 자동으로 하고 싶죠. HPA(HorizontalPodAutoscaler)는 Pod들의 CPU 사용률 같은 지표를 보고 Deployment의 replicas를 자동으로 조절해요. Pod를 옆으로(가로로) 늘린다고 해서 Horizontal이에요.
1. 지표를 보고 개수를 바꾼다
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web
spec:
scaleTargetRef: # 누구의 replicas를 조절할지
apiVersion: apps/v1
kind: Deployment
name: web
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 50 # requests 대비 평균 50%를 목표로
$ kubectl autoscale deployment web --cpu-percent=50 --min=2 --max=10 # 같은 걸 명령으로
$ kubectl get hpa web -w
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS
web Deployment/web cpu: 92%/50% 2 10 2
web Deployment/web cpu: 46%/50% 2 10 4
<unknown>으로 나와요. 지표는 metrics-server가 모아 주는데, GKE에는 기본으로 들어 있어요.
| 오토스케일러 | 늘리는 것 | 언제 |
|---|---|---|
| HPA | Pod 개수 | 평균 지표가 목표를 넘을 때 — 가장 많이 씀 |
| 클러스터 오토스케일러 | 노드 개수 | 자리가 없어 Pending인 Pod가 생길 때 (GKE 노드 풀 자동 확장) |
| VPA | Pod 하나의 requests 크기 | Pod마다 requests를 자동으로 맞추고 싶을 때 |
03. 로그와 모니터링 기초
문제가 생겼을 때 어디부터 볼지 순서를 알고 있으면 당황하지 않아요. 쿠버네티스에서 확인할 곳은 크게 셋이에요. 상태(get), 사건 기록(describe의 Events), 앱 로그(logs)예요. 여기에 지표(top, 모니터링 도구)를 더해요.
1. STATUS를 보고 갈 길을 고른다
| 명령 | 무엇을 보나 |
|---|---|
kubectl get pods -o wide | STATUS · RESTARTS · 어느 노드인지 |
kubectl describe pod web-7x2kq | 맨 아래 Events — 배정 실패, 이미지 오류, 프로브 실패, OOMKilled |
kubectl get events --sort-by=.lastTimestamp | 네임스페이스 전체의 최근 사건을 시간순으로 |
kubectl logs web-7x2kq (-f, --previous, -c) | 앱 로그 · 계속 보기 · 직전에 죽은 컨테이너 · 컨테이너 지정 |
kubectl logs -l app=web --tail=50 | 라벨로 여러 Pod 로그 한꺼번에 |
kubectl exec -it web-7x2kq -- sh | 컨테이너 안에서 직접 확인 (설정 파일, 다른 Service로 접속 시험) |
kubectl top nodes / kubectl top pods | 지금 CPU · 메모리 사용량 (metrics-server) |
kubectl logs는 그 노드에 남아 있는 것만 보여 줘서, Pod가 지워지면 로그도 사라져요. 그래서 운영에서는 로그를 한곳에 모아 둬요. GKE는 Cloud Logging · Cloud Monitoring으로 자동으로 모아 주고, 직접 운영할 땐 Prometheus · Grafana 같은 도구를 많이 써요.
04. 앱 하나를 처음부터 끝까지 배포하기
지금까지 배운 걸 한 줄로 이어 볼게요. 코드에서 출발해서 이미지를 만들고, GKE에 올리고, 사용자가 접속하고, 새 버전으로 바꾸고, 정리하는 데까지예요. 각 단계 옆의 괄호는 처음 배운 곳이에요.
1. 전체 흐름
2. 명령과 YAML 한 벌
$ gcloud artifacts repositories create myrepo \
--repository-format=docker --location=asia-northeast3
$ gcloud auth configure-docker asia-northeast3-docker.pkg.dev
$ docker build --platform linux/amd64 \
-t asia-northeast3-docker.pkg.dev/my-k8s-study/myrepo/myapp:1.0 .
$ docker push asia-northeast3-docker.pkg.dev/my-k8s-study/myrepo/myapp:1.0
exec format error로 죽어요(CrashLoopBackOff). 그래서 위처럼 --platform linux/amd64를 붙여요.
---로 구분)apiVersion: v1
kind: Namespace # 7부 01
metadata:
name: shop
---
apiVersion: v1
kind: ConfigMap # 6부 01
metadata:
name: web-config
namespace: shop
data:
LOG_LEVEL: "info"
---
apiVersion: apps/v1
kind: Deployment # 4부 03
metadata:
name: web
namespace: shop
spec:
replicas: 2
selector:
matchLabels:
app: web # 3부 02
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: asia-northeast3-docker.pkg.dev/my-k8s-study/myrepo/myapp:1.0
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: web-config
resources: # 3부 06
requests: { cpu: 250m, memory: 256Mi }
limits: { memory: 512Mi }
readinessProbe: # 3부 05
httpGet: { path: /ready, port: 8080 }
livenessProbe:
httpGet: { path: /healthz, port: 8080 }
initialDelaySeconds: 10
---
apiVersion: v1
kind: Service # 5부 01
metadata:
name: web
namespace: shop
spec:
selector:
app: web
ports:
- port: 80
targetPort: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress # 5부 04
metadata:
name: web
namespace: shop
spec:
defaultBackend:
service:
name: web
port:
number: 80
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler # 7부 02
metadata:
name: web
namespace: shop
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: web }
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target: { type: Utilization, averageUtilization: 50 }
$ kubectl apply -f shop.yaml
$ kubectl get all -n shop # Deployment · ReplicaSet · Pod · Service
$ kubectl get ingress web -n shop -w # ADDRESS에 외부 IP가 생길 때까지 (몇 분)
$ curl http://<ADDRESS>/ # 접속 확인
$ kubectl set image deployment/web web=asia-northeast3-docker.pkg.dev/my-k8s-study/myrepo/myapp:2.0 -n shop
$ kubectl rollout status deployment/web -n shop # 4부 03 롤링 업데이트
$ kubectl delete namespace shop # 정리 — 안의 모든 것이 지워짐
$ gcloud container clusters delete my-cluster --zone asia-northeast3-a # 실습 끝 (2부 03)
{ } 표기
requests: { cpu: 250m, memory: 256Mi }는 여러 줄로 쓰는 것과 같은 뜻의 한 줄 표기예요. 짧은 항목을 간단히 적을 때 써요.
3. 입문을 마치며
여기까지 오면 쿠버네티스의 기본 흐름을 처음부터 끝까지 한 번 돌아본 거예요. 핵심은 처음부터 끝까지 하나였어요.
Deployment는 Pod 개수와 버전을, Service는 주소를, HPA는 개수를, StorageClass는 디스크를 맞춰요. 모두 1부 05의 제어 루프예요. 이후 02 전반 스터디에서는 같은 주제를 더 깊게 다룰 예정이에요. 스케줄링 규칙, 권한(RBAC), 네트워크 정책, Helm, 모니터링 구축, Gateway API 같은 것들이에요.
4. 7부 셀프 체크
Namespace로 나눌 수 있는 것과 나눠지지 않는 것은?
이름 · 권한 · 자원 한도(ResourceQuota)를 칸별로 나눌 수 있어요. 하지만 Node · PV · StorageClass 같은 객체는 칸 밖에 있고, 칸 사이 네트워크도 기본으로는 열려 있어요(NetworkPolicy로 막음).HPA가 replicas를 계산하는 기준은?
Pod들의 평균 지표(예: CPU 사용률)를 목표와 비교해요. 대략현재 개수 × 현재 사용률 ÷ 목표 사용률을 올림한 값으로 맞추고, min · max 사이에서 움직여요. CPU 사용률은 requests 대비 %예요.트래픽이 줄었는데 Pod가 바로 줄지 않는 이유는?
잠깐 줄었다 다시 늘 수 있어서, HPA는 줄일 때 기본 5분 정도 지켜본 뒤 줄여요. 늘었다 줄었다를 반복하는 걸 막기 위해서예요.STATUS가 Pending이에요. 무엇부터 보나요?
kubectl describe pod의 Events요. 대부분 “자원 부족(Insufficient cpu/memory)”이나 PVC가 묶이지 않은 경우예요.Pod를 지운 뒤 그 Pod의 로그를 보려면?
kubectl logs로는 볼 수 없어요. 미리 로그를 한곳에 모아 두는 도구(GKE의 Cloud Logging 등)에서 봐야 해요.Mac(Apple Silicon)에서 빌드한 이미지가 GKE에서 바로 죽어요. 원인은?
ARM용 이미지가 x86 노드에서 실행되지 못해서예요(exec format error).docker build --platform linux/amd64로 다시 빌드해요.입문 전체를 관통하는 쿠버네티스의 핵심 아이디어는?
YAML로 원하는 상태를 선언하면, 컨트롤러들이 현재 상태를 계속 관찰하고 그 차이를 없애는 제어 루프예요.