본문으로 건너뛰기
인프라 · infra/kubernetes/01 입문 — 인프라 0부터 읽는 쿠버네티스

← 스터디 홈

Kubernetes Study · 01 입문

인프라 0부터 읽는 쿠버네티스

입문 스터디를 따라가며 부(部) 단위로 기초 개념을 정리합니다. 인프라를 처음 접하는 사람 기준이라, 낯선 단어는 처음 나올 때 풀어서 쓰고 움직이는 그림으로 따라가 봅니다. 그림은 화면에 보이면 자동으로 재생되고, 아래 버튼으로 한 단계씩 넘겨 볼 수도 있어요.

진행 · 7부 운영 기초 (입문 완료) · 2026.10.28 시작 · 2026.10.29 업데이트

그림의 색 = 역할
컨테이너 · 앱 이미지 · 레이어 레지스트리 가상 머신 OS · 커널 하드웨어 · 서버 원하는 상태 · 컨트롤 플레인 새 버전 · 변경 정상 장애
1부컨테이너와 오케스트레이션왜 컨테이너어떻게 격리이미지오케스트레이션쿠버네티스

01. 왜 컨테이너인가

서버는 거창한 기계가 아니라 24시간 켜져서 요청을 받아 주는 컴퓨터예요. 우리가 만든 앱은 결국 누군가의 서버 위에서 하나의 프로세스(실행 중인 프로그램)로 돌아갑니다. 문제는 앱이 코드만으로는 돌지 않는다는 점이에요. 코드를 실행해 줄 런타임(Node, Java, Python…), 코드가 가져다 쓰는 라이브러리, 그리고 그 밑의 OS까지 맞아떨어져야 실행됩니다.

1. "제 컴퓨터에선 되는데요"

개발자 노트북과 운영 서버는 거의 항상 환경이 다릅니다. 노트북엔 최신 Node가, 서버엔 2년 전에 깔아 둔 Node가 있는 식이죠. 코드만 옮기면 이 차이 때문에 서버에서 터집니다. 컨테이너는 이 문제를 “코드와 실행 환경을 한 상자에 담아 통째로 옮기는” 방식으로 풉니다.

코드만 옮기기 vs 실행 환경째 옮기기
Mac에서 Docker를 쓰면? 컨테이너는 리눅스 커널의 기능이에요(03에서 자세히). 그래서 Mac · Windows용 Docker Desktop은 내부에 작은 리눅스 가상 머신을 몰래 띄우고 그 위에서 컨테이너를 돌립니다. 운영 서버는 대부분 리눅스라 이런 중간 단계 없이 바로 돌아요.

2. 컨테이너 = 표준 규격의 상자

이름은 화물 운송의 컨테이너에서 왔어요. 규격 컨테이너가 나오기 전엔 짐 모양마다 싣고 내리는 방법이 달랐지만, 상자 크기가 표준화되자 배 · 기차 · 트럭이 안에 뭐가 들었든 똑같은 방법으로 옮길 수 있게 됐죠. 소프트웨어 컨테이너도 같아요. 안에 Node 앱이 있든 Java 앱이 있든, 밖에서는 실행 · 중지 · 복사를 똑같은 명령으로 다룹니다.

먼저 알아둘 단어한 줄 뜻더 자세히
이미지앱 + 실행 환경을 굳혀 둔 읽기 전용 꾸러미. 상자의 설계도이자 내용물04
컨테이너이미지를 실제로 실행한 것. 격리된 프로세스03
컨테이너 런타임서버에서 이미지를 받아 컨테이너로 실행해 주는 프로그램 (Docker, containerd 등)02 · 04
레지스트리이미지를 올리고 내려받는 저장소. 코드의 GitHub 같은 곳 (Docker Hub, AWS ECR 등)04

02. 물리 서버 · 가상 머신 · 컨테이너

컴퓨터 한 대에서 여러 앱을 돌리는 방법은 크게 세 가지예요. 차이는 결국 “앱끼리 무엇을 같이 쓰고, 무엇을 따로 갖느냐”입니다.

1. 층을 쌓아 보면 보이는 차이

아래 그림은 바닥(하드웨어)부터 위로 층을 쌓아 갑니다. 가상 머신(VM)은 하드웨어를 흉내 내서 앱마다 OS를 통째로 하나씩 주고, 컨테이너는 OS의 핵심인 커널은 같이 쓰면서 앱과 라이브러리만 따로 담아요.

물리 서버 · 가상 머신 · 컨테이너의 층 구조
커널(kernel)이 뭐예요? OS의 가장 안쪽에서 CPU · 메모리 · 디스크 · 네트워크 같은 하드웨어를 직접 다루는 핵심 프로그램이에요. 앱이 파일을 열거나 네트워크로 데이터를 보낼 때는 직접 하지 않고 커널에 부탁(시스템 콜)합니다. 컨테이너들은 이 커널 하나에 다 같이 부탁하는 구조예요.

2. 표로 정리

물리 서버에 직접가상 머신컨테이너
격리 단위없음 (모두 한 OS에)OS째로프로세스 단위
OS1개 공유VM마다 하나씩 (Guest OS)호스트 커널 1개 공유
크기—수 GB수 MB ~ 수백 MB
시작 시간—수십 초 ~ 분 (OS 부팅)1초 안팎 (프로세스 시작)
격리 강도약함 — 라이브러리 충돌강함적당함 — 커널을 공유하므로 VM보다는 약함
대표 도구—VMware, KVM, VirtualBox, AWS EC2Docker, containerd, Podman
VM과 컨테이너는 경쟁 관계가 아니에요 클라우드에서는 보통 VM 위에서 컨테이너를 돌립니다. AWS에서 빌린 서버(EC2)가 이미 VM이고, 그 위에 컨테이너 런타임을 깔아 컨테이너를 여러 개 띄우는 식이에요. 쿠버네티스의 “노드”도 대부분 이런 VM입니다.

03. 컨테이너는 어떻게 격리되나

컨테이너를 흔히 “가벼운 VM”이라고 설명하지만, 정확히는 리눅스 커널 기능으로 울타리를 친 평범한 프로세스예요. 호스트에서 ps로 보면 그냥 프로세스 하나로 보입니다. 울타리는 두 가지 기능으로 만들어요. 무엇을 볼 수 있는지를 나누는 네임스페이스, 그리고 얼마나 쓸 수 있는지를 제한하는 cgroup입니다.

1. 같은 프로세스, 다른 시야

호스트에서 본 프로세스와 컨테이너 안에서 본 세상

2. 네임스페이스와 cgroup

기능하는 일종류 / 예시비유
네임스페이스
namespace
프로세스가 볼 수 있는 범위를 나눔pid 프로세스 번호
mnt 파일 시스템
net 네트워크(IP, 포트)
uts 호스트 이름
ipc · user 등
칸막이 — 옆 칸이 안 보임
cgroup
control group
프로세스가 쓸 수 있는 양을 제한CPU, 메모리, 디스크 I/O, 프로세스 수계량기와 차단기 — 정해진 양을 넘으면 끊김
이름이 같지만 다른 것 — 쿠버네티스의 Namespace 나중에 쿠버네티스에서 Namespace라는 단어를 또 만나는데, 그건 리소스를 묶는 논리적인 폴더(예: dev / prod)예요. 여기서 말한 리눅스 커널의 네임스페이스와는 다른 개념입니다.
미리 익혀 둘 상태 — OOMKilled · exit 137 메모리 한도를 넘으면 커널이 그 프로세스를 강제로 종료해요(OOM = Out Of Memory). 강제 종료 신호(SIGKILL)의 번호가 9라서 종료 코드는 128 + 9 = 137이 됩니다. 쿠버네티스에서 Pod가 갑자기 재시작될 때 가장 자주 보는 원인 중 하나예요.

04. 이미지와 컨테이너

JavaScript로 비유하면 이미지는 class, 컨테이너는 인스턴스예요. 클래스 하나로 객체를 여러 개 만들듯, 이미지 하나로 컨테이너를 여러 개 띄웁니다. 이미지는 읽기 전용이고, 컨테이너는 그 위에 자기만의 얇은 쓰기 레이어를 얹어 실행돼요.

1. 만들고 · 올리고 · 받고 · 실행하기

이미지가 태어나서 서버에서 실행되기까지는 항상 같은 네 단계를 거칩니다. build → push → pull → run. 마지막 단계에서는 코드를 조금 고쳤을 때 무슨 일이 일어나는지도 봅니다.

Dockerfile에서 실행 중인 컨테이너까지

2. Dockerfile 한 줄씩 읽기

Dockerfile은 “이미지를 만드는 레시피”예요. 위에서 아래로 한 줄씩 실행되고, 파일을 바꾸는 명령(COPY, RUN 등)마다 레이어가 하나씩 쌓입니다.

Dockerfile
# 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. 이미지 이름 읽는 법

이름 docker.io/ library/nginx: 1.27
뜻 레지스트리 주소/ 저장소(이미지 이름): 태그(버전) — 레지스트리를 생략하면 Docker Hub(docker.io)로 간주
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컨테이너 멈추기 → 지우기 (쓰기 레이어도 함께 사라짐)
컨테이너를 지우면 안에서 만든 파일도 사라져요 컨테이너 안에서 생긴 파일은 그 컨테이너의 쓰기 레이어에만 있어서, 컨테이너를 지우면 같이 없어집니다. 그래서 컨테이너는 언제든 버리고 새로 띄울 수 있는 일회용으로 다루고, DB 데이터처럼 남겨야 하는 건 볼륨(바깥 저장 공간)에 따로 둬요. 이 “일회용” 성질이 바로 05의 오케스트레이션을 가능하게 합니다.
Docker = 컨테이너? Docker는 컨테이너를 쉽게 쓰게 해 준 도구 모음(빌드 · 실행 · 레지스트리 연동)이에요. 이미지 형식은 OCI라는 표준이라, Docker로 만든 이미지를 containerd · CRI-O 같은 다른 런타임도 그대로 실행합니다. 쿠버네티스는 1.24부터 Docker Engine 대신 containerd 같은 런타임을 직접 쓰지만, Docker로 빌드한 이미지는 그대로 잘 돌아요.

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. 멈추지 않고 새 버전 배포하기

새 버전을 배포할 때 컨테이너를 전부 내렸다가 다시 올리면 그 사이 서비스가 멈춰요. 오케스트레이터는 새 버전을 하나 띄우고 → 준비되면 → 옛 버전을 하나 내리는 과정을 반복해서, 사용자가 눈치채지 못하게 교체합니다. 이걸 롤링 업데이트라고 해요.

롤링 업데이트 — v1을 v2로 하나씩 교체

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 한 번을 따라가 볼게요. 각 부품은 이후 스터디에서 자세히 다룹니다.

kubectl apply 한 번이 컨테이너가 되기까지
위치부품하는 일
컨트롤 플레인API 서버
kube-apiserver
클러스터의 유일한 창구. kubectl도, 다른 부품도 전부 API 서버를 통해서만 대화
etcd클러스터의 모든 상태(원하는 상태 + 현재 상태)를 저장하는 데이터베이스
스케줄러
kube-scheduler
새로 만들 컨테이너(Pod)를 어느 노드에 둘지 결정
컨트롤러 매니저
kube-controller-manager
여러 제어 루프의 묶음. “3개여야 하는데 2개네?”를 계속 확인하고 맞춤
워커 노드kubelet노드마다 있는 관리인. 자기 노드에 배정된 일을 받아 런타임에 실행을 시키고 상태를 보고
kube-proxyService로 들어온 요청이 알맞은 컨테이너로 가도록 노드의 네트워크 규칙을 관리
컨테이너 런타임실제로 이미지를 받아 컨테이너를 실행 (containerd, CRI-O)
Pod — 쿠버네티스가 다루는 가장 작은 단위 쿠버네티스는 컨테이너를 직접 다루지 않고 Pod라는 껍데기에 한 개(가끔 여러 개)를 담아 다뤄요. 그림의 초록 상자가 Pod입니다. 지금은 “쿠버네티스에서 컨테이너를 감싸는 단위” 정도로만 알아 두면 충분해요.

3. 1부 셀프 체크

  1. “제 컴퓨터에선 되는데요” 문제를 컨테이너는 어떻게 해결하나요?
    코드만 옮기지 않고 런타임 · 라이브러리까지 이미지에 담아 통째로 옮깁니다. 서버에는 컨테이너 런타임만 있으면 어디서든 같은 환경으로 실행돼요.
  2. VM과 컨테이너의 가장 큰 차이는?
    VM은 각자 OS(Guest OS)를 통째로 갖고, 컨테이너는 호스트의 커널 하나를 공유합니다. 그래서 컨테이너가 훨씬 가볍고 빨리 뜨지만, 격리 강도는 VM이 더 강해요.
  3. 컨테이너 격리를 만드는 리눅스 기능 두 가지는?
    네임스페이스 — 볼 수 있는 범위를 나눔(PID, 파일 시스템, 네트워크, 호스트 이름…).
    cgroup — 쓸 수 있는 양을 제한(CPU, 메모리…). 메모리 한도를 넘으면 OOMKilled(exit 137).
  4. 이미지와 컨테이너의 관계를 JavaScript로 비유하면?
    이미지 = class, 컨테이너 = 인스턴스. 이미지는 읽기 전용 레이어의 묶음이고, 컨테이너는 그 위에 자기만의 쓰기 레이어를 얹어 실행됩니다.
  5. 코드 한 줄만 고치고 다시 빌드하면 왜 빠를까요?
    바뀌지 않은 레이어는 캐시를 재사용하고, 바뀐 줄부터 아래만 다시 만들기 때문이에요. push할 때도 바뀐 레이어만 전송합니다.
  6. 오케스트레이터가 하는 일을 한 문장으로?
    원하는 상태와 현재 상태를 계속 비교해서 차이를 없애는 것(제어 루프). 스케줄링 · 자가 치유 · 스케일링 · 롤링 업데이트가 모두 여기서 나와요.
  7. 쿠버네티스에서 kubectl과 모든 부품이 대화하는 창구는?
    API 서버예요. 상태는 etcd에 저장되고, 스케줄러 · 컨트롤러 · kubelet은 모두 API 서버를 지켜보다가(watch) 자기 할 일을 합니다.
2부쿠버네티스 시작하기클러스터 구조노드실습 환경kubectlYAML

01. 클러스터 구조 — 마스터 노드와 워커 노드

쿠버네티스는 서버 여러 대를 묶어 한 대의 큰 컴퓨터처럼 쓰게 해 줘요. 이 묶음이 클러스터이고, 묶인 서버 한 대 한 대를 노드(node)라고 부릅니다. 노드는 맡은 일에 따라 두 종류로 나뉘어요.

  • 마스터 노드 — 클러스터 전체를 결정하고 관리합니다. “무엇을 어디에 몇 개 띄울지”를 정하는 두뇌예요.
  • 워커 노드 — 마스터가 정한 대로 실제로 Pod(컨테이너)를 실행합니다. 우리 앱이 돌아가는 곳이에요.

식당으로 비유하면 마스터 노드는 주문을 받아 어느 조리대에 맡길지 정하는 매니저, 워커 노드는 실제로 요리하는 조리대예요. 매니저는 직접 요리하지 않고, 조리대는 메뉴를 정하지 않아요.

1. 역할 분담과 장애가 났을 때

마스터 노드는 결정하고, 워커 노드는 실행한다
마스터 노드 (= 컨트롤 플레인)워커 노드
하는 일원하는 상태 저장 · 배치 결정 · 제어 루프Pod 실행 · 상태 보고 · 네트워크 규칙 관리
구성 요소API 서버 · etcd · 스케줄러 · 컨트롤러 매니저kubelet · kube-proxy · 컨테이너 런타임
우리 앱이 도는 곳?보통 아니에요 (관리 전용)네, 여기서 돌아요
보통 몇 대?운영 환경은 3대 (하나가 죽어도 버티게)필요한 만큼 — 늘리고 줄일 수 있음
멈추면돌던 앱은 계속 돌지만, 새 배포 · 자가 치유 · 스케일링이 멈춤그 위의 Pod가 다른 워커 노드로 옮겨짐
마스터 노드 = 컨트롤 플레인 학습 자료에서는 마스터 노드라고 부르지만, 쿠버네티스 공식 문서는 요즘 컨트롤 플레인(control plane)이라는 이름을 써요. kubectl get nodes를 해 보면 역할(ROLES) 칸에 control-plane이라고 나와요. 1부 06 그림의 “컨트롤 플레인”이 바로 이 마스터 노드예요. 이 문서에서는 둘을 같은 뜻으로 씁니다.

02. 노드 안에는 무엇이 있나

노드는 특별한 기계가 아니라 쿠버네티스에 등록된 평범한 서버 한 대예요. 물리 서버일 수도 있지만, 클라우드에서는 대부분 VM입니다(1부 02 참고). 그 서버에 쿠버네티스용 프로그램 세 개가 깔려 있어서 워커 노드 역할을 해요.

1. 워커 노드 하나를 확대해 보면

워커 노드 안의 kubelet · 컨테이너 런타임 · kube-proxy
구성 요소하는 일비유
kubeletAPI 서버를 지켜보다가 자기 노드에 배정된 Pod를 실행시키고, Pod · 노드 상태를 보고현장 관리인
컨테이너 런타임kubelet의 지시로 이미지를 받아 컨테이너를 실제로 실행 (containerd 등)실제로 일하는 손
kube-proxyService 주소로 온 요청이 알맞은 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
GKE에서는 마스터 노드가 목록에 없어요 위 결과에 control-plane 역할의 노드가 보이지 않죠. GKE는 마스터 노드(컨트롤 플레인)를 구글이 대신 운영하기 때문에, 우리 프로젝트에는 워커 노드만 보여요. 다음 절에서 자세히 볼게요. (버전 · 숫자는 2 vCPU · 8GB 노드 기준 예시예요.)

03. 실습 환경 — GCP와 GKE

쿠버네티스를 연습하려면 클러스터가 필요해요. 방법은 크게 두 가지입니다.

내 PC에 작은 클러스터클라우드의 관리형 쿠버네티스
예minikube · kind · Docker DesktopGKE(Google) · EKS(AWS) · AKS(Azure)
노드보통 1대 (내 PC 안의 VM이나 컨테이너)진짜 VM 여러 대
비용무료쓰는 만큼 과금
좋은 점빠르게 켜고 끄기실제 운영과 같은 구조 (여러 노드, 외부 로드밸런서)

학습 자료는 GCP(Google Cloud Platform, 구글의 클라우드)의 GKE(Google Kubernetes Engine)를 써요. GKE는 관리형 쿠버네티스예요. 클러스터를 만들어 달라고 하면 구글이 마스터 노드를 대신 운영하고, 우리는 워커 노드와 그 위의 앱만 신경 쓰면 됩니다.

1. 누가 무엇을 관리하나

직접 설치 vs GKE — 관리 책임의 경계

2. 클러스터 만들기까지의 명령

GCP 콘솔(웹 화면)에서 버튼으로 만들 수도 있고, gcloud 명령줄 도구로 만들 수도 있어요. 브라우저에서 바로 쓰는 Cloud Shell에는 gcloud와 kubectl이 이미 깔려 있어서 실습에 편해요.

터미널 (Cloud Shell 또는 gcloud CLI를 설치한 내 PC)
$ 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대 확인
내 PC에서 kubectl로 GKE에 붙을 때 내 PC에서 GKE에 접속하려면 인증 플러그인이 하나 더 필요해요. 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 공식 가격 페이지 기준)
Standard와 Autopilot GKE 클러스터는 두 가지 모드가 있어요. Standard는 워커 노드(VM)를 우리가 고르고 관리하며 노드 단위로 돈을 내요. Autopilot은 노드까지 구글이 맡고 Pod가 요청한 CPU · 메모리만큼 돈을 내요. 노드 구조를 눈으로 보며 배우기에는 Standard가 좋아요.

04. kubectl 기본 명령

kubectl(큐브컨트롤, 큐브시티엘)은 쿠버네티스를 다루는 명령줄 도구예요. 하는 일은 단순해요. 우리가 친 명령을 API 서버에 보내는 요청으로 바꿔 전달하고, 돌아온 답을 표로 보여 줍니다. 1부 06에서 본 것처럼, 클러스터의 모든 대화는 API 서버를 거쳐요.

1. 명령 한 줄이 가는 길

kubectl 명령이 kubeconfig를 거쳐 API 서버로, 그리고 그 너머로

2. 명령의 모양과 자주 쓰는 명령

모양 kubectl 동사 리소스 종류 이름 옵션
예 kubectl get pod hello -o wide
명령하는 일
kubectl get pods목록 보기. nodes, deployments 등 종류만 바꾸면 됨
kubectl get pods -o wideIP · 어느 노드에 있는지까지 넓게 보기
kubectl get pod hello -o yaml객체 전체를 YAML로 보기 (spec과 status 모두)
kubectl describe pod hello자세한 설명 + 맨 아래 Events(무슨 일이 있었나). 문제 생기면 제일 먼저 보는 곳
kubectl apply -f hello.yamlYAML에 적은 원하는 상태를 제출 (없으면 만들고, 있으면 바꿈)
kubectl delete -f hello.yamlYAML에 적힌 객체 지우기 (kubectl delete pod hello도 가능)
kubectl logs hello컨테이너가 출력한 로그 보기 (-f로 계속 따라가기)
kubectl exec -it hello -- sh실행 중인 컨테이너 안으로 들어가기
kubectl get pods -w바뀌는 상태를 실시간으로 지켜보기 (watch)
kubectl explain pod.specYAML에 어떤 필드를 쓸 수 있는지 설명 보기
줄임말과 네임스페이스 자주 쓰는 리소스는 줄여 쓸 수 있어요. 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 하나를 띄우는 가장 짧은 매니페스트로 하나씩 읽어 볼게요.

YAML 매니페스트 → 쿠버네티스 객체
hello-pod.yaml
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

kindapiVersionkindapiVersion
Pod · Service · ConfigMap · Secret · Namespacev1Job · CronJobbatch/v1
ReplicaSet · Deployment · DaemonSet · StatefulSetapps/v1Ingressnetworking.k8s.io/v1
YAML을 처음부터 다 외울 필요는 없어요 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부 셀프 체크

  1. 마스터 노드와 워커 노드는 각각 무슨 일을 하나요?
    마스터 노드(컨트롤 플레인)는 원하는 상태를 저장하고, 어디에 둘지 결정하고, 제어 루프를 돌려요. 워커 노드는 그 결정대로 Pod를 실제로 실행하고 상태를 보고합니다.
  2. 마스터 노드가 멈추면 이미 돌던 앱은 어떻게 되나요?
    워커 노드의 Pod는 계속 돌아요. 대신 새 배포, 죽은 Pod 다시 띄우기, 스케일링 같은 “결정”이 필요한 일은 멈춰요. 그래서 운영에서는 마스터를 3대로 둡니다.
  3. 워커 노드의 세 구성 요소와 역할은?
    kubelet은 배정된 Pod를 실행시키고 보고하는 관리인이에요. 컨테이너 런타임은 실제로 컨테이너를 실행하는 손이고, kube-proxy는 Service로 온 요청을 Pod로 보내는 교통 정리 담당이에요.
  4. GKE에서 kubectl get nodes에 마스터 노드가 안 보이는 이유는?
    GKE는 관리형 쿠버네티스라 마스터 노드(컨트롤 플레인)를 구글이 대신 운영하기 때문이에요. 우리 프로젝트에는 워커 노드(VM)만 보여요.
  5. kubectl logs와 kubectl get은 요청이 어디까지 가나요?
    get은 API 서버가 저장된 상태(etcd)를 읽어 바로 답해요. logs와 exec은 API 서버를 지나 그 Pod가 있는 노드의 kubelet까지 가서 컨테이너에서 가져와요.
  6. 매니페스트의 네 칸은? 그리고 파일에 쓰지 않는 칸은?
    apiVersion · kind · metadata · spec. status는 쿠버네티스가 현재 상태를 채우는 칸이라 직접 쓰지 않아요. spec(원하는 상태)과 status(현재 상태)의 차이를 없애는 게 제어 루프예요.
  7. YAML에서 들여쓰기에 탭을 쓰면?
    오류가 나요. YAML 들여쓰기는 스페이스만 허용돼요. 들여쓰기 깊이가 어긋나면 필드가 엉뚱한 곳에 들어가서 “unknown field” 같은 오류가 납니다.
3부PodPod란라벨사이드카상태헬스 체크리소스

01. Pod란

쿠버네티스는 컨테이너를 직접 다루지 않고, Pod라는 상자에 담아서 다뤄요. Pod는 쿠버네티스가 만들고 · 배치하고 · 지우는 가장 작은 단위예요. 대부분의 Pod에는 컨테이너가 하나만 들어 있어서, 처음에는 “Pod ≈ 컨테이너 하나를 감싼 껍데기”로 생각해도 충분해요.

그럼 왜 껍데기가 필요할까요? 가끔 꼭 붙어 다녀야 하는 컨테이너들이 있기 때문이에요. 같은 Pod 안의 컨테이너는 IP 하나를 같이 쓰고, 항상 같은 노드에서 함께 뜨고 함께 사라져요. 한 집에 사는 룸메이트처럼 주소(IP)와 거실(볼륨)을 같이 쓰는 거예요.

1. Pod와 컨테이너의 관계

Pod — 컨테이너를 감싸는 가장 작은 단위
같은 Pod 안의 컨테이너는뜻
IP · 포트를 공유Pod마다 IP가 하나. 컨테이너끼리는 localhost로 통신하고, 같은 포트를 둘이 쓸 수는 없음
볼륨을 공유할 수 있음같은 볼륨을 붙이면 파일을 주고받을 수 있음 (03에서)
같은 노드에 함께Pod는 쪼개지지 않음. 컨테이너가 다른 노드로 흩어지지 않음
함께 생기고 함께 사라짐Pod를 지우면 안의 컨테이너가 전부 같이 지워짐
Pod는 일회용이에요 직접 만든 Pod는 지워지거나 노드가 죽으면 그걸로 끝이에요. 누구도 다시 만들어 주지 않아요. 새로 만들면 이름과 IP도 바뀌어요. 그래서 실제 운영에서는 Pod를 직접 만들지 않고, Pod를 대신 만들고 지켜 주는 ReplicaSet · Deployment(4부)를 써요. 3부에서는 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. 꼬리표를 붙이고 골라내기

라벨로 Pod를 묶고, 셀렉터로 골라낸다
셀렉터뜻명령 예
app=webapp이 web인 것kubectl get pods -l app=web
app=web,env=prod두 조건을 모두 만족 (쉼표 = 그리고)kubectl get pods -l app=web,env=prod
env!=prodenv가 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-              # 라벨 떼기 (키 뒤에 -)
라벨이 쿠버네티스의 접착제인 이유 쿠버네티스 객체끼리는 이름이 아니라 라벨로 연결돼요. ReplicaSet은 “app=web인 Pod가 3개여야 해”(4부), Service는 “app=web인 Pod에게 요청을 나눠 줘”(5부)라고 셀렉터로 대상을 정해요. 그래서 새 Pod가 같은 라벨을 달고 태어나면 자동으로 그 묶음에 들어가요. 라벨 오타 하나가 “Service가 Pod를 못 찾는” 흔한 장애 원인이에요.

03. 한 Pod에 컨테이너 여러 개 — 사이드카

대부분은 Pod 하나에 컨테이너 하나지만, 주 컨테이너를 돕는 보조 컨테이너를 같은 Pod에 넣을 때가 있어요. 오토바이 옆에 붙는 사이드카처럼 본체 옆에 붙어 다닌다고 해서 사이드카 패턴이라고 불러요. 로그 수집, 프록시, 설정 파일 동기화 같은 일이 대표적이에요.

1. 같이 사는 컨테이너들

주 컨테이너 + 사이드카 + init 컨테이너
web-with-log.yaml
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개)
READY 2/2와 -c 옵션 컨테이너가 둘이면 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의 한살이와 재시작

Pending → Running → 재시작 → CrashLoopBackOff
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다시 시작 안 함한 번만 돌리고 결과를 볼 작업
재시작 ≠ Pod 재생성 kubelet의 재시작은 같은 Pod 안에서 컨테이너만 다시 띄우는 거예요. 이름 · IP · 노드가 그대로고 RESTARTS 숫자만 올라가요. 반면 Pod가 지워지거나 노드가 죽으면 Pod 자체가 사라지고, 다시 만드는 건 4부의 컨트롤러 몫이에요.

05. 헬스 체크 — liveness · readiness

04의 재시작은 컨테이너 프로세스가 죽었을 때만 일어나요. 그런데 프로세스는 살아 있는데 앱이 멈춰 버리면(무한 대기, 데드락) 어떨까요? 겉보기엔 Running인데 요청은 하나도 처리 못 해요. 그래서 kubelet이 주기적으로 앱에 직접 “괜찮아?”를 물어보는 기능이 프로브(probe)예요.

1. 두 가지 질문, 두 가지 대응

readiness는 트래픽을 끊고, liveness는 재시작한다
프로브묻는 것실패하면
readinessProbe“지금 요청 받을 준비 됐어?”Service에서 빠짐 → 트래픽이 안 옴. 재시작은 안 함
livenessProbe“살아 있어? 멈춘 거 아니지?”컨테이너 재시작
startupProbe“시작 다 끝났어?” (느리게 뜨는 앱용)성공할 때까지 다른 두 프로브를 미룸
probe 예 — containers 항목 안에
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번 연속 실패하면 재시작
확인하는 방법은 세 가지 (+ gRPC) httpGet(주소에 요청해서 200~399면 성공), tcpSocket(포트에 연결되면 성공), exec(컨테이너 안에서 명령을 실행해 종료 코드 0이면 성공). 기본값은 10초마다 확인하고, 1초 안에 답이 없으면 실패, 3번 연속 실패하면 “실패”로 판단해요.
liveness를 너무 엄격하게 잡으면 앱이 잠깐 바쁜 것뿐인데 liveness가 실패로 판단하면, 멀쩡한 컨테이너를 계속 재시작해서 오히려 장애를 키워요. liveness는 “정말 멈췄을 때만” 실패하도록 넉넉하게, 일시적인 과부하는 readiness로 처리하는 게 기본이에요. 외부 DB 상태까지 liveness에서 확인하는 것도 피해요. DB가 잠깐 끊기면 모든 Pod가 같이 재시작되거든요.

06. 리소스 요청과 제한

2부 02에서 노드마다 Allocatable이라는 자리가 있다고 했죠. Pod는 그 자리를 얼마나 쓸지 두 숫자로 알려 줘요. requests는 “최소 이만큼은 예약해 줘”, limits는 “아무리 많아도 이 이상은 못 써”예요. 식당으로 치면 requests는 예약한 좌석 수, limits는 최대 수용 인원이에요.

1. 예약과 상한

requests는 자리 예약, limits는 넘을 수 없는 선
resources 예 — containers 항목 안에
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 · 메모리 둘 다)가장 마지막에 쫓겨남
Burstablerequests는 있지만 limits와 다르거나 일부만 있음중간
BestEffortrequests · limits를 하나도 안 씀가장 먼저 쫓겨남
처음엔 이렇게 requests는 평소 사용량 정도로, 메모리 limits는 최대 사용량보다 넉넉하게 잡아요. requests를 아예 안 쓰면 스케줄러가 자리를 계산할 수 없어서 한 노드에 Pod가 몰리고, 바쁠 때 가장 먼저 쫓겨나요. kubectl top pods로 실제 사용량을 보며 조정하면 돼요.

2. 3부 셀프 체크

  1. 같은 Pod 안의 컨테이너들이 공유하는 것은?
    IP와 포트 공간(서로 localhost로 통신), 그리고 붙여 둔 볼륨이에요. 또 항상 같은 노드에서 함께 생기고 함께 사라져요.
  2. 직접 만든 Pod를 지우면 다시 생기나요?
    아니요. Pod는 일회용이라 직접 만든 Pod는 그걸로 끝이에요. 다시 만들어 주는 건 4부의 ReplicaSet · Deployment가 하는 일이에요.
  3. -l app=web,env=prod는 어떤 Pod를 고르나요?
    app이 web 이고 env가 prod인 Pod요. 쉼표는 “그리고(AND)”예요.
  4. 웹 서버와 DB를 한 Pod에 넣으면 안 되는 이유는?
    항상 같은 개수로 함께 늘고 줄어야 하기 때문이에요. 웹은 10개, DB는 1개처럼 따로 조절할 수 없어요. 서로 독립된 서비스는 다른 Pod로 나누고 네트워크로 통신해요.
  5. RESTARTS가 계속 오르고 STATUS가 CrashLoopBackOff예요. 무엇부터 보나요?
    죽기 직전 로그를 봐요. kubectl logs 이름 --previous로 이전 컨테이너의 로그를, kubectl describe pod 이름으로 종료 이유(Exit Code, OOMKilled 등)와 Events를 확인해요.
  6. readiness가 실패할 때와 liveness가 실패할 때의 차이는?
    readiness 실패는 트래픽만 빠지고 컨테이너는 그대로예요. liveness 실패는 컨테이너를 재시작해요.
  7. CPU limit과 메모리 limit을 넘으면 각각 어떻게 되나요?
    CPU는 죽지 않고 느려져요(throttling). 메모리는 OOMKilled로 강제 종료된 뒤 재시작돼요.
4부워크로드 컨트롤러레플리카ReplicaSetDeploymentDaemonSetJob · CronJobStatefulSet

01. 레플리카 — 왜 같은 Pod를 여러 개 띄우나

3부에서 Pod는 일회용이라고 했죠. 하나만 띄워 두면 그 Pod가 죽는 순간 서비스가 멈춰요. 그래서 실제 서비스는 똑같은 Pod를 여러 개 띄워 둬요. 이 복제본 하나하나를 레플리카(replica), 그 개수를 replicas라고 불러요.

4부에서 볼 워크로드 컨트롤러들은 모두 1부 05의 제어 루프를 Pod에 적용한 거예요. 우리가 “이런 Pod가 몇 개”를 선언하면, 컨트롤러가 Pod를 대신 만들고 지켜 줘요. 그래서 실무에서는 Pod를 직접 만들지 않아요.

1. 하나일 때와 여럿일 때

레플리카 — 똑같은 Pod를 여러 개 두는 이유
레플리카를 여러 개 두면뜻
가용성하나가 죽거나 노드 하나가 멈춰도 나머지가 요청을 받아요
부하 분산요청이 여러 Pod로 나뉘어서 하나에 몰리지 않아요
무중단 배포하나씩 바꿔 끼울 수 있어요 (03 롤링 업데이트)
레플리카는 서로 똑같아야 해요 — stateless 요청이 매번 아무 레플리카에게나 가기 때문에, 어느 Pod가 받아도 결과가 같아야 해요. 그래서 로그인 세션이나 업로드 파일처럼 남아야 하는 데이터는 Pod 안이 아니라 바깥(DB, Redis, 저장소)에 둬요. 이렇게 Pod 안에 상태를 두지 않는 앱을 stateless라고 해요. 반대로 DB처럼 Pod마다 자기 데이터가 있어야 하는 경우는 06의 StatefulSet을 써요.

02. ReplicaSet — 개수를 지키는 컨트롤러

ReplicaSet은 “이 라벨을 단 Pod가 항상 N개 떠 있게”를 지키는 컨트롤러예요. 3개여야 하는데 2개면 하나를 만들고, 4개면 하나를 지워요. 필요한 건 세 가지예요.

  • replicas — 몇 개를 유지할지
  • selector — 어떤 Pod를 내 것으로 셀지 (3부 02의 라벨 셀렉터)
  • template — 모자랄 때 찍어 낼 Pod의 틀 (Pod YAML의 metadata · spec과 같음)

1. 세고, 모자라면 찍어 낸다

ReplicaSet — replicas · selector · template
web-rs.yaml
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      # 개수 바꾸기
Pod 이름이 web-7x2kq처럼 붙는 이유 ReplicaSet이 만든 Pod는 ReplicaSet 이름 + 무작위 5글자로 이름이 붙어요. 이름이 아니라 라벨로 세기 때문에 이름은 아무래도 상관없어요. 그래서 template의 라벨과 selector가 다르면 API 서버가 아예 거부해요. 그렇게 만들면 자기가 만든 Pod를 자기가 못 세는 상황이 되니까요.
ReplicaSet은 직접 잘 안 써요 ReplicaSet은 개수만 지킬 뿐, 이미지를 바꿔도 이미 떠 있는 Pod는 그대로예요. template만 바뀌고 새로 찍는 Pod부터 새 버전이 돼요. 버전 교체까지 관리해 주는 게 다음의 Deployment예요. 실무에서는 Deployment를 쓰고, ReplicaSet은 Deployment가 알아서 만들어요.

03. Deployment — 롤링 업데이트와 롤백

Deployment는 ReplicaSet을 관리하는 한 단계 위의 컨트롤러예요. 구조는 Deployment → ReplicaSet → Pod의 세 층이에요. 이미지 같은 Pod 틀이 바뀌면 Deployment가 새 ReplicaSet을 만들고, 새 것은 늘리고 옛 것은 줄이면서 버전을 바꿔요. 1부 05에서 본 롤링 업데이트가 실제로는 이렇게 일어나요.

1. ReplicaSet 두 개로 버전 바꾸기

Deployment → ReplicaSet → Pod, 그리고 버전 교체
web-deploy.yaml — ReplicaSet과 거의 같고 kind만 다름
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.yamlYAML의 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옛 것을 전부 내리고 새 것을 올리기잠깐 있음 — 두 버전이 동시에 돌면 안 될 때만
롤백이 빠른 이유 교체가 끝나도 옛 ReplicaSet은 지우지 않고 0개로 남겨 둬요(기본 10개까지). 그래서 되돌릴 때 새로 만들 필요 없이 옛 ReplicaSet의 숫자만 다시 올리면 돼요. ReplicaSet 이름 뒤의 6d4f9b 같은 값은 Pod 틀의 해시(pod-template-hash)예요. 틀이 같으면 같은 값이 나와요.

04. DaemonSet — 노드마다 하나씩

Deployment는 개수를 맞추지만, DaemonSet은 노드마다 딱 하나씩 띄워요. 그래서 replicas 칸이 없어요. 노드가 늘면 자동으로 하나 더 뜨고, 노드가 빠지면 같이 사라져요. 모든 노드에서 돌아야 하는 보조 프로그램에 써요.

DaemonSet — 노드 수를 따라가는 Pod
이런 데 써요예
노드의 로그 수집fluent-bit, Fluentd
노드의 자원 · 상태 모니터링node-exporter, Datadog agent
노드의 네트워크 · 스토리지 기능네트워크 플러그인(CNI), 스토리지 드라이버

05. Job · CronJob — 끝나는 일

웹 서버는 계속 떠 있어야 하지만, 데이터 정리나 리포트 생성처럼 한 번 돌고 끝나야 하는 일도 있어요. Deployment로 돌리면 끝나도 다시 띄워서 끝없이 반복돼요. Job은 Pod가 성공적으로 끝나면 그걸로 완료로 쳐요. CronJob은 정해진 시간표마다 Job을 만들어요.

Job은 끝나면 완료, CronJob은 시간표대로 Job을 만든다
report-cronjob.yaml
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 * * *"03***매일 03:00
"*/10 * * * *"*/10****10분마다
"0 9 * * 1-5"09**1-5평일 09:00
Job 설정기본값뜻
completions1성공해야 하는 횟수
parallelism1동시에 몇 개까지 돌릴지
backoffLimit6실패를 몇 번까지 다시 시도할지
ttlSecondsAfterFinished없음끝난 Job을 몇 초 뒤 자동으로 지울지

06. StatefulSet 맛보기

01에서 레플리카는 서로 똑같아야 한다고 했죠. 그런데 DB 클러스터처럼 Pod마다 자기 이름 · 자기 데이터 · 자기 역할이 있어야 하는 경우가 있어요. 이럴 때 StatefulSet을 써요. 저장소(볼륨)를 다루는 6부와 이후 전반 스터디에서 자세히 보고, 여기서는 Deployment와 무엇이 다른지만 봐요.

StatefulSet — 고정된 이름, 순서, 자기 디스크
DeploymentStatefulSet
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부 셀프 체크

  1. 같은 Pod를 여러 개(레플리카) 두는 이유 세 가지는?
    가용성(하나가 죽어도 나머지가 받음), 부하 분산(요청을 나눠 받음), 무중단 배포(하나씩 교체)예요.
  2. ReplicaSet은 자기 Pod를 무엇으로 세나요?
    라벨 셀렉터로 세요. 이름이 아니라서, Pod의 라벨을 바꾸면 그 Pod는 관리 밖으로 빠지고 ReplicaSet은 모자란 개수만큼 새로 만들어요.
  3. Deployment · ReplicaSet · Pod의 관계는?
    Deployment가 ReplicaSet을 만들고, ReplicaSet이 Pod를 만들어요. 이미지가 바뀌면 Deployment는 새 ReplicaSet을 만들어 늘리고, 옛 것은 줄여서 0개로 남겨 둬요.
  4. 롤백(rollout undo)이 빠른 이유는?
    옛 ReplicaSet이 0개로 남아 있어서, 새로 만들 필요 없이 숫자만 다시 올리면 되기 때문이에요.
  5. DaemonSet에 replicas 칸이 없는 이유는?
    개수가 아니라 노드마다 하나가 규칙이라서요. 노드 수가 곧 Pod 수예요.
  6. 배치 작업을 Deployment로 돌리면 어떻게 되나요?
    작업이 끝나 컨테이너가 종료되면 restartPolicy: Always 때문에 다시 띄워져서 끝없이 반복돼요. 끝나는 일은 Job으로 돌려요.
  7. StatefulSet의 Pod가 죽었다 살아나면 Deployment와 무엇이 다르나요?
    같은 이름(web-1)으로 다시 생기고, 원래 쓰던 자기 디스크에 다시 붙어요. Deployment는 새 이름으로 생기고 고정된 디스크가 없어요.
5부네트워킹Service가 필요한 이유Service 종류DNSIngress

01. Service가 필요한 이유

3부에서 Pod마다 IP가 있다고 했죠. 그런데 이 IP로 직접 통신하면 두 가지 문제가 생겨요. 첫째, Pod는 일회용이라 다시 만들어지면 IP가 바뀌어요. 둘째, 4부처럼 레플리카가 여러 개면 어느 IP로 보내야 할지 모르죠.

Service는 Pod 묶음 앞에 세우는 고정된 대표 주소예요. 회사 대표번호처럼, 담당자(Pod)가 바뀌어도 번호는 그대로이고 전화는 지금 자리에 있는 사람에게 연결돼요. 어떤 Pod를 묶을지는 3부 02의 라벨 셀렉터로 정해요.

1. 바뀌는 IP 대신 고정된 이름과 주소

Pod IP로 직접 부르기 vs Service로 부르기
api-svc.yaml
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
용어뜻
ClusterIPService에 붙는 고정 가상 IP. Service를 지우기 전까지 바뀌지 않음
port / targetPortService가 받는 포트 / Pod로 넘겨줄 포트. 달라도 됨 (80 → 8080)
EndpointSlice지금 셀렉터에 맞고 준비(Ready)된 Pod IP 목록. 쿠버네티스가 자동으로 고쳐 씀
kube-proxy2부 02의 그것. 노드마다 “ClusterIP로 온 요청 → 목록의 Pod 중 하나로” 규칙을 깔아 둠
3부 05의 readiness와 연결돼요 readinessProbe가 실패한 Pod는 EndpointSlice 목록에서 빠져요. 그래서 “트래픽이 빠진다”는 말은 정확히는 “Service의 목록에서 빠진다”는 뜻이에요.

02. ClusterIP · NodePort · LoadBalancer

Service는 누가 접근할 수 있느냐에 따라 종류(type)가 나뉘어요. 세 가지는 따로 노는 게 아니라 겹겹이 쌓이는 구조예요. LoadBalancer는 NodePort를 포함하고, NodePort는 ClusterIP를 포함해요.

1. 안에서만 · 노드 포트로 · 외부 로드밸런서로

Service 종류별로 어디까지 열리나
type누가 접근하나주소주로 쓰는 곳
ClusterIP (기본값)클러스터 안의 Pod만10.96.0.15:80내부 API, DB 등 대부분의 Service
NodePort노드 IP를 알 수 있는 곳 어디서나노드IP:30080 (30000~32767)실습, 직접 만든 로드밸런서 뒤
LoadBalancer인터넷외부 IP 34.64.x.x:80클라우드에서 외부에 공개할 서비스
web-lb.yaml — type만 바꾸면 돼요
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
LoadBalancer는 하나당 클라우드 로드밸런서 하나 — 비용 GKE에서 type: LoadBalancer를 만들면 구글 클라우드 로드밸런서가 실제로 하나 생기고 요금이 붙어요. 외부에 공개할 서비스가 여러 개라고 LoadBalancer를 여러 개 만들면 로드밸런서도 그만큼 생겨요. 그래서 웹 서비스들은 보통 04의 Ingress 하나로 입구를 합쳐요. 실습이 끝나면 클러스터와 함께 지워 주세요.

03. DNS와 서비스 디스커버리

ClusterIP는 고정이지만, 숫자를 코드에 적어 두는 건 여전히 불편해요. 그래서 쿠버네티스 안에는 DNS 서버(CoreDNS)가 있어서 Service 이름을 ClusterIP로 바꿔 줘요. 앱 코드에는 http://api처럼 이름만 쓰면 돼요. 이렇게 이름으로 상대를 찾는 걸 서비스 디스커버리라고 해요.

1. 이름 → ClusterIP → Pod

http://api 한 줄이 Pod에 닿기까지
이렇게 부르면뜻언제
api같은 네임스페이스의 Service api가장 흔함
api.shopshop 네임스페이스의 Service api다른 네임스페이스 호출
api.shop.svc.cluster.local전체 이름 (FQDN)헷갈림 없이 정확히
web-0.web.shop.svc.cluster.localStatefulSet 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
짧은 이름이 통하는 이유 — search Pod의 /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. 호스트와 경로로 나눠 보내기

LoadBalancer 여러 개 vs Ingress 하나
shop-ingress.yaml
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
Ingress는 규칙일 뿐 — Ingress 컨트롤러가 있어야 동작해요 Ingress YAML은 “이렇게 나눠 줘”라는 규칙이고, 실제로 요청을 받아 나눠 주는 건 Ingress 컨트롤러예요. GKE에는 구글 클라우드 로드밸런서를 만들어 주는 컨트롤러가 기본으로 들어 있어요. 직접 설치한 클러스터라면 컨트롤러를 따로 설치해야 해요. 컨트롤러가 없으면 Ingress를 만들어도 아무 일도 일어나지 않아요.
요즘 흐름 — Gateway API 쿠버네티스는 Ingress API에 더 이상 새 기능을 넣지 않고, 후속 표준인 Gateway API를 권장해요. 많이 쓰이던 Ingress NGINX 컨트롤러도 2026년 3월로 유지보수가 끝났어요. 다만 개념(입구 하나 → 규칙에 따라 Service로)은 같아서, 입문 단계에서는 Ingress로 이해해 두면 Gateway API도 쉽게 따라갈 수 있어요. 자세한 내용은 이후 전반 스터디에서 다룰게요.

2. 5부 셀프 체크

  1. Pod IP로 직접 통신하면 생기는 문제 두 가지는?
    Pod가 다시 만들어지면 IP가 바뀌고, 레플리카가 여러 개면 어느 IP로 보낼지 정할 수 없어요. Service가 고정된 주소와 분배를 맡아요.
  2. Service는 요청을 보낼 Pod를 어떻게 정하나요?
    라벨 셀렉터로요. 셀렉터에 맞고 준비(Ready)된 Pod의 IP 목록(EndpointSlice)을 쿠버네티스가 계속 갱신해요.
  3. port와 targetPort의 차이는?
    port는 Service가 받는 포트, targetPort는 Pod 안 컨테이너가 실제로 여는 포트예요. 80으로 받아 8080으로 넘길 수 있어요.
  4. ClusterIP · NodePort · LoadBalancer는 각각 어디서 접근할 수 있나요?
    ClusterIP는 클러스터 안에서만, NodePort는 모든 노드의 IP와 30000~32767 포트로, LoadBalancer는 클라우드 로드밸런서의 외부 IP로 인터넷에서 접근해요. 뒤의 것이 앞의 것을 포함해요.
  5. 앱 코드에서 다른 서비스를 http://api로 부를 수 있는 이유는?
    클러스터 안의 DNS(CoreDNS)가 Service 이름을 ClusterIP로 바꿔 주기 때문이에요. 다른 네임스페이스면 api.shop처럼 네임스페이스를 붙여요.
  6. 외부에 공개할 웹 서비스가 3개일 때 LoadBalancer 3개 대신 무엇을 쓰나요?
    Ingress 하나를 두고 호스트 · 경로 규칙으로 3개의 Service에 나눠 보내요. 로드밸런서 비용도 줄고 HTTPS도 한 곳에서 처리해요.
  7. Ingress YAML을 만들었는데 아무 일도 안 일어나요. 무엇부터 확인하나요?
    클러스터에 Ingress 컨트롤러가 있는지, 그리고 ingressClassName을 적었다면 그 컨트롤러를 제대로 가리키는지 확인해요. Ingress는 규칙일 뿐이라 처리할 컨트롤러가 필요해요.
6부설정과 저장소ConfigMapSecretVolumePV · PVC · StorageClass

01. ConfigMap — 설정을 이미지 밖으로

앱에는 DB 주소, 로그 레벨, 기능 스위치 같은 설정이 필요해요. 이걸 이미지 안에 넣어 버리면 개발 · 운영 환경마다 이미지를 따로 만들어야 하죠. ConfigMap은 설정을 이미지 밖의 쿠버네티스 객체로 빼 두는 거예요. 같은 이미지에 환경마다 다른 ConfigMap만 붙이면 돼요.

1. 같은 이미지, 다른 설정

ConfigMap — 환경 변수로, 파일로
app-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  LOG_LEVEL: "info"                 # 키: 값 (값은 모두 문자열)
  DB_HOST: "mysql.shop"
  app.properties: |                 # 파일 하나를 통째로 넣을 수도 있음
    feature.newCart=true
    cache.ttl=300
Pod(Deployment template)에서 쓰기
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는 암호화가 아니에요

Secret — 저장 · 주입 · 보호
$ 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!
Pod에서 쓰기 — 키 하나만 환경 변수로
env:
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: db-secret
        key: password
Secret 종류 (type)용도
Opaque (기본)비밀번호, API 키 등 일반 비밀값
kubernetes.io/tlsHTTPS 인증서와 키 (5부 04의 Ingress tls)
kubernetes.io/dockerconfigjson비공개 레지스트리에서 이미지를 받을 때 쓰는 로그인 정보
Secret YAML을 Git에 올리지 마세요 Secret YAML의 값은 base64일 뿐이라, 저장소에 올리면 비밀번호를 공개한 것과 같아요. 실무에서는 비밀값을 클라우드의 비밀 관리 서비스(GCP Secret Manager 등)에 두고 클러스터로 가져오거나, 암호화된 형태로만 저장소에 넣는 도구를 써요. 이건 이후 전반 스터디에서 다룰게요.

03. Volume — 데이터는 어디에 남나

1부 04에서 컨테이너의 파일은 쓰기 레이어에 있어서 컨테이너가 사라지면 같이 사라진다고 했죠. 쿠버네티스에서는 컨테이너가 재시작만 해도 새 쓰기 레이어로 시작해서 파일이 사라져요. 남겨야 하는 데이터는 컨테이너 바깥의 볼륨에 둬요. 볼륨은 종류마다 얼마나 오래 남는지가 달라요.

1. 재시작 · Pod 삭제에서 살아남는 것

컨테이너 파일 · emptyDir · PVC의 수명 비교
볼륨 종류어디에얼마나 남나쓰는 곳
(볼륨 없음)컨테이너 쓰기 레이어컨테이너 재시작까지임시 파일만
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

PVC로 요청하면 StorageClass가 PV를 만들어 연결한다
mysql-pvc.yaml + 쓰는 쪽
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 등)
PVC를 지우면 디스크도 지워질 수 있어요 — reclaimPolicy 자동으로 만든 PV는 보통 reclaimPolicy: Delete라서 PVC를 지우면 클라우드 디스크까지 같이 지워져요. 중요한 데이터라면 Retain으로 바꾸거나 스냅샷으로 백업해 두세요. 반대로 실습이 끝났는데 PVC를 안 지우면 디스크 요금이 계속 나가요.
GKE에서 PVC가 Pending으로 머물러 있다면 GKE의 standard-rwo는 WaitForFirstConsumer 방식이라, 이 PVC를 쓰는 Pod가 노드에 배정될 때까지 디스크를 만들지 않고 Pending으로 기다려요. Pod가 어느 구역(zone)에 뜰지 보고 같은 구역에 디스크를 만들기 위해서예요. Pod를 띄우면 Bound로 바뀌어요.

2. 6부 셀프 체크

  1. 설정을 이미지에 넣지 않고 ConfigMap으로 빼는 이유는?
    같은 이미지를 개발 · 운영 등 여러 환경에서 쓰고, 환경마다 설정만 바꿔 붙이기 위해서예요. 설정을 바꿀 때 이미지를 다시 빌드하지 않아도 돼요.
  2. ConfigMap을 환경 변수로 넣었을 때 값을 바꾸면 바로 반영되나요?
    아니요. 환경 변수는 컨테이너가 시작될 때 정해지므로 Pod를 다시 띄워야 해요(kubectl rollout restart). 파일로 마운트하면 잠시 뒤 파일 내용이 바뀌어요.
  3. Secret의 base64는 암호화인가요?
    아니요. 누구나 base64 -d로 바로 되돌릴 수 있는 인코딩일 뿐이에요. 보호는 접근 권한(RBAC), etcd 암호화, 외부 비밀 관리 서비스로 해요.
  4. 볼륨 없이 컨테이너 안에 쓴 파일은 언제 사라지나요?
    컨테이너가 재시작만 해도 사라져요. emptyDir은 재시작은 버티지만 Pod가 지워지면 사라지고, PVC 디스크는 Pod와 상관없이 남아요.
  5. PVC · PV · StorageClass를 한 줄씩 설명하면?
    PVC는 “이만큼 필요해요”라는 요청서, PV는 실제 디스크, StorageClass는 요청이 오면 디스크를 자동으로 만들어 주는 규칙이에요.
  6. ReadWriteOnce 디스크를 Deployment replicas 3개가 함께 쓰려고 하면?
    RWO는 한 노드에서만 붙일 수 있어서, Pod가 다른 노드에 뜨면 디스크를 붙이지 못해요. 여러 Pod가 각자 데이터를 가져야 하면 StatefulSet의 volumeClaimTemplates로 Pod마다 디스크를 만들고, 정말 함께 써야 하면 RWX(Filestore 등)를 써요.
  7. 실습 후 PVC를 지우지 않으면 어떻게 되나요? 반대로 지우면?
    안 지우면 클라우드 디스크 요금이 계속 나가요. 지우면 reclaimPolicy: Delete인 경우 디스크와 데이터도 같이 지워져요. 중요한 데이터는 백업하거나 Retain으로 둬요.
7부운영 기초Namespace오토스케일링로그와 모니터링처음부터 끝까지 배포

01. Namespace — 클러스터를 칸으로 나누기

클러스터 하나를 여러 팀 · 여러 환경이 같이 쓰면 이름이 겹치고, 누가 무엇을 만들었는지 헷갈려요. Namespace는 클러스터 안을 논리적인 칸(폴더)으로 나눠요. 3부 02에서 말했던 “리눅스 네임스페이스와는 다른 쿠버네티스 Namespace”가 바로 이거예요.

1. 칸마다 이름 · 권한 · 한도를 따로

Namespace — 한 클러스터 안의 칸
$ 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 자신
칸이 나뉘어도 네트워크는 열려 있어요 Namespace는 이름 · 권한 · 자원 한도를 나눌 뿐, 기본적으로 다른 칸의 Pod와도 통신할 수 있어요(5부 03의 api.shop처럼). 칸 사이 통신을 막으려면 NetworkPolicy를 따로 써야 해요. 그리고 kubectl delete namespace는 그 안의 모든 것을 지우니 조심하세요.

02. 오토스케일링 — HPA

4부에서는 replicas를 사람이 정했어요. 트래픽은 시간마다 달라지니, 바쁠 땐 늘리고 한가할 땐 줄이는 걸 자동으로 하고 싶죠. HPA(HorizontalPodAutoscaler)는 Pod들의 CPU 사용률 같은 지표를 보고 Deployment의 replicas를 자동으로 조절해요. Pod를 옆으로(가로로) 늘린다고 해서 Horizontal이에요.

1. 지표를 보고 개수를 바꾼다

HPA — 평균 CPU를 목표에 맞추도록 replicas 조절
web-hpa.yaml
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
requests가 없으면 HPA가 계산을 못 해요 CPU 사용률은 requests 대비 몇 %로 계산해요(3부 06). requests를 안 적은 Deployment에 CPU 기준 HPA를 걸면 TARGETS가 <unknown>으로 나와요. 지표는 metrics-server가 모아 주는데, GKE에는 기본으로 들어 있어요.
오토스케일러늘리는 것언제
HPAPod 개수평균 지표가 목표를 넘을 때 — 가장 많이 씀
클러스터 오토스케일러노드 개수자리가 없어 Pending인 Pod가 생길 때 (GKE 노드 풀 자동 확장)
VPAPod 하나의 requests 크기Pod마다 requests를 자동으로 맞추고 싶을 때

03. 로그와 모니터링 기초

문제가 생겼을 때 어디부터 볼지 순서를 알고 있으면 당황하지 않아요. 쿠버네티스에서 확인할 곳은 크게 셋이에요. 상태(get), 사건 기록(describe의 Events), 앱 로그(logs)예요. 여기에 지표(top, 모니터링 도구)를 더해요.

1. STATUS를 보고 갈 길을 고른다

문제 해결 순서 — get → describe → logs → exec
명령무엇을 보나
kubectl get pods -o wideSTATUS · 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)
로그는 표준 출력으로, 모아서 보관은 따로 쿠버네티스는 컨테이너가 표준 출력(stdout · stderr)으로 내보낸 것을 로그로 모아요. 그래서 앱은 파일이 아니라 콘솔로 로그를 찍는 게 기본이에요. kubectl logs는 그 노드에 남아 있는 것만 보여 줘서, Pod가 지워지면 로그도 사라져요. 그래서 운영에서는 로그를 한곳에 모아 둬요. GKE는 Cloud Logging · Cloud Monitoring으로 자동으로 모아 주고, 직접 운영할 땐 Prometheus · Grafana 같은 도구를 많이 써요.

04. 앱 하나를 처음부터 끝까지 배포하기

지금까지 배운 걸 한 줄로 이어 볼게요. 코드에서 출발해서 이미지를 만들고, GKE에 올리고, 사용자가 접속하고, 새 버전으로 바꾸고, 정리하는 데까지예요. 각 단계 옆의 괄호는 처음 배운 곳이에요.

1. 전체 흐름

코드 → 이미지 → 레지스트리 → 클러스터 → 사용자

2. 명령과 YAML 한 벌

① 이미지 빌드 · 올리기 (1부 04) — Artifact Registry
$ 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
Apple Silicon Mac이라면 --platform linux/amd64 M1 이상 Mac에서 그냥 빌드하면 ARM용 이미지가 만들어져요. 일반 GKE 노드(x86)에서는 이 이미지가 exec format error로 죽어요(CrashLoopBackOff). 그래서 위처럼 --platform linux/amd64를 붙여요.
② shop.yaml — Namespace부터 HPA까지 한 파일에 (---로 구분)
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)
YAML의 { } 표기 requests: { cpu: 250m, memory: 256Mi }는 여러 줄로 쓰는 것과 같은 뜻의 한 줄 표기예요. 짧은 항목을 간단히 적을 때 써요.

3. 입문을 마치며

여기까지 오면 쿠버네티스의 기본 흐름을 처음부터 끝까지 한 번 돌아본 거예요. 핵심은 처음부터 끝까지 하나였어요.

핵심 원하는 상태를 YAML로 선언→ API 서버 · etcd에 저장→ 컨트롤러가 현재 상태를 계속 맞춤

Deployment는 Pod 개수와 버전을, Service는 주소를, HPA는 개수를, StorageClass는 디스크를 맞춰요. 모두 1부 05의 제어 루프예요. 이후 02 전반 스터디에서는 같은 주제를 더 깊게 다룰 예정이에요. 스케줄링 규칙, 권한(RBAC), 네트워크 정책, Helm, 모니터링 구축, Gateway API 같은 것들이에요.

4. 7부 셀프 체크

  1. Namespace로 나눌 수 있는 것과 나눠지지 않는 것은?
    이름 · 권한 · 자원 한도(ResourceQuota)를 칸별로 나눌 수 있어요. 하지만 Node · PV · StorageClass 같은 객체는 칸 밖에 있고, 칸 사이 네트워크도 기본으로는 열려 있어요(NetworkPolicy로 막음).
  2. HPA가 replicas를 계산하는 기준은?
    Pod들의 평균 지표(예: CPU 사용률)를 목표와 비교해요. 대략 현재 개수 × 현재 사용률 ÷ 목표 사용률을 올림한 값으로 맞추고, min · max 사이에서 움직여요. CPU 사용률은 requests 대비 %예요.
  3. 트래픽이 줄었는데 Pod가 바로 줄지 않는 이유는?
    잠깐 줄었다 다시 늘 수 있어서, HPA는 줄일 때 기본 5분 정도 지켜본 뒤 줄여요. 늘었다 줄었다를 반복하는 걸 막기 위해서예요.
  4. STATUS가 Pending이에요. 무엇부터 보나요?
    kubectl describe pod의 Events요. 대부분 “자원 부족(Insufficient cpu/memory)”이나 PVC가 묶이지 않은 경우예요.
  5. Pod를 지운 뒤 그 Pod의 로그를 보려면?
    kubectl logs로는 볼 수 없어요. 미리 로그를 한곳에 모아 두는 도구(GKE의 Cloud Logging 등)에서 봐야 해요.
  6. Mac(Apple Silicon)에서 빌드한 이미지가 GKE에서 바로 죽어요. 원인은?
    ARM용 이미지가 x86 노드에서 실행되지 못해서예요(exec format error). docker build --platform linux/amd64로 다시 빌드해요.
  7. 입문 전체를 관통하는 쿠버네티스의 핵심 아이디어는?
    YAML로 원하는 상태를 선언하면, 컨트롤러들이 현재 상태를 계속 관찰하고 그 차이를 없애는 제어 루프예요.
스터디를 하며 부마다 이어서 정리합니다. 그림은 개념 이해를 위해 단순화했어요.