컨테이너와 쿠버네티스
컨테이너가 가상 머신과 어떻게 다른지 리눅스 커널의 네임스페이스와 cgroup에서 출발해 설명하고, OCI 이미지의 레이어와 레지스트리, 멀티 스테이지 빌드를 거쳐 쿠버네티스가 원하는 상태를 유지하는 방식 — 구성 요소, 기본 객체, 스케줄러, 조정 루프, 자원 등급, 자동 확장, 헬스 프로브 — 을 계산과 단계 그림으로 정리한다.
목차 (15)
프로세스·가상 메모리 같은 운영체제 바탕은 운영체제 노트에, 서버를 여러 대로 늘리는 설계는 시스템 설계 › 규모 확장에 있다. 이 노트는 그 둘 사이, 프로그램을 포장해 여러 서버에 배치하고 살려 두는 층을 다룬다. 쿠버네티스 동작은 2026년 10월의 공식 문서를 기준으로 했다.
가상 머신과 컨테이너
가상 머신은 하이퍼바이저가 하드웨어를 흉내 내고 그 위에 운영체제를 통째로 하나 더 띄운다. 컨테이너는 하드웨어를 흉내 내지 않는다. 호스트의 커널 하나를 함께 쓰면서, 프로세스마다 보이는 것과 쓸 수 있는 양을 커널이 갈라 줄 뿐이다. 그래서 컨테이너는 "가벼운 VM"이 아니라 "울타리를 친 프로세스"다.
- 시작 시간·밀도: 부팅할 OS가 없으니 프로세스 시작 시간만큼 걸리고, 한 서버에 훨씬 많이 올린다.
- 격리: 커널 취약점 하나가 모든 컨테이너에 걸린다. 서로 믿지 못하는 고객의 코드를 함께 돌리는 클라우드는 컨테이너를 작은 VM(마이크로 VM)이나 사용자 공간 커널로 한 겹 더 감싸기도 한다.
- 이식성: 앱과 라이브러리를 한 이미지로 묶어 "내 컴퓨터에서는 되는데" 문제를 줄인다. 다만 커널은 호스트 것이므로 리눅스 컨테이너는 리눅스 커널 위에서 돈다(맥·윈도에서는 작은 리눅스 VM을 띄워 그 안에서 돌린다).
네임스페이스와 cgroup
컨테이너는 커널 기능 두 가지의 조합이다. 네임스페이스는 프로세스가 보는 세계를 나눈다. pid 네임스페이스 안의 첫 프로세스는 자기가 1번이라고 보고, net 네임스페이스는 자기만의 네트워크 장치·라우팅 표를, mnt 네임스페이스는 자기만의 파일시스템 트리를 본다(namespaces(7)). cgroup(control group)은 프로세스 묶음이 쓰는 CPU·메모리·입출력의 양을 재고 제한한다(cgroup v2 문서).
쿠버네티스에서 CPU limit 500m(0.5코어)은 cgroup v2의 cpu.max에 "50000 100000", 곧 100ms마다 50ms로 적힌다. 스레드 여러 개가 동시에 돌면 할당량을 기간 앞부분에 다 써 버리고 나머지 시간 동안 멈추므로, 평균 사용률은 낮은데 응답 시간 꼬리가 길어지는 일이 생긴다. 이 때문에 지연에 민감한 서비스는 CPU limit을 넉넉히 두거나 아예 두지 않고 requests만 정하기도 한다. 메모리는 나눠 쓸 방법이 없어 memory.max를 넘으면 커널의 OOM 킬러가 프로세스를 죽인다.
OCI 이미지와 레이어
컨테이너 이미지의 형식은 OCI 이미지 명세가 정한다. 이미지는 세 부분이다. 레이어는 파일시스템 변경분을 담은 tar 묶음(보통 압축)이고, 설정(config)은 실행 명령·환경 변수·사용자 같은 메타데이터, 매니페스트는 설정과 레이어들의 다이제스트(sha256 해시) 목록이다. 여러 CPU 아키텍처용 매니페스트를 묶는 이미지 인덱스도 있다. 실행할 때는 레이어를 아래부터 겹쳐(overlay 파일시스템) 하나의 파일 트리로 보이게 하고, 그 위에 컨테이너마다 쓰기 층을 얹는다.
세 이미지를 단순히 더하면 374MB지만, 같은 다이제스트를 한 번만 세면 206MB다. 노드가 api를 받은 뒤 worker를 받을 때는 2MB만 더 받는다. 이 재사용은 공통 부분을 아래 레이어에 같은 내용으로 둘 때만 일어난다. 레이어 하나가 바뀌면 그 위 레이어는 모두 새로 만들어지므로, 자주 바뀌는 앱 코드는 위에, 잘 안 바뀌는 OS·런타임·의존성은 아래에 둔다.
레지스트리
레지스트리는 이미지를 저장하고 나눠 주는 서버다(Docker Hub, 각 클라우드의 레지스트리, 사내 Harbor 등). 주고받는 HTTP API는 OCI 배포 명세가 정한다. 저장 단위는 다이제스트로 찾는 블롭(레이어·설정)과 그 목록인 매니페스트이고, app:1.4 같은 태그는 매니페스트를 가리키는 옮겨 붙일 수 있는 이름표다.
태그(app:1.4)를 레지스트리에 물어 매니페스트 다이제스트를 얻는다. 태그는 옮겨 붙일 수 있는 이름표다.
- 태그 대신 다이제스트:
latest나 같은 태그를 다시 밀어 넣으면 어제와 오늘 받은 이미지가 달라진다. 배포에는app@sha256:…처럼 다이제스트로 고정하면 무엇이 돌고 있는지 정확히 안다. - 서명과 출처: 다이제스트는 내용이 바뀌지 않았음을 보장하지만 누가 만들었는지는 말해 주지 않는다. 이미지 서명과 빌드 출처 증명(provenance), 구성 목록(SBOM)을 레지스트리에 함께 두는 공급망 보안이 이 빈틈을 메운다.
- 가까운 레지스트리: 노드 수백 대가 같은 이미지를 동시에 받으면 레지스트리와 전송비가 병목이 된다. 같은 리전의 레지스트리나 캐시(pull-through) 레지스트리를 둔다.
Dockerfile과 멀티 스테이지 빌드
Dockerfile의 명령(FROM, COPY, RUN 등) 하나하나가 대체로 레이어 하나를 만든다. 빌드 도구는 명령과 입력 파일이 같으면 예전 레이어를 다시 쓰므로(빌드 캐시) 순서가 곧 빌드 속도다. 컴파일이 필요한 언어는 컴파일러와 소스가 최종 이미지에 남지 않도록 멀티 스테이지 빌드로 나눈다.
# 1단계: 빌드 — 컴파일러와 소스가 있는 큰 이미지
FROM golang:1.25 AS build
WORKDIR /src
# 의존성 목록만 먼저 복사해 받아 둔다: 코드만 바뀌면 이 레이어는 캐시에서 나온다
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/server ./cmd/server
# 2단계: 실행 — 결과물만 담은 작은 이미지
FROM gcr.io/distroless/static-debian12
COPY --from=build /out/server /server
USER nonroot
ENTRYPOINT ["/server"]- 작은 기본 이미지: 셸·패키지 관리자가 없는 이미지는 크기가 작고 취약점 검사에 걸릴 패키지도 적다. 대신 컨테이너 안에 들어가 디버깅하기 어려워, 필요할 때 디버그용 임시 컨테이너를 붙이는 방식을 쓴다.
- root로 돌리지 않기:
USER로 일반 사용자를 지정한다. 컨테이너 탈출 취약점이 있을 때 피해가 줄어든다. - 비밀은 이미지에 넣지 않기: 레이어는 지워도 이전 레이어에 남는다. 빌드 중 필요한 토큰은 빌드 비밀(secret mount)로 넘기고, 실행 중 비밀은 환경 변수나 비밀 저장소로 주입한다.
쿠버네티스 구조
쿠버네티스는 컨테이너를 여러 서버(노드)에 배치하고 계속 살려 두는 시스템이다. 구글의 내부 시스템 Borg의 경험에서 나와 2014년에 공개됐다. 구성은 클러스터 전체를 결정하는 제어부와 실제로 컨테이너를 돌리는 노드로 나뉜다(구성 요소 문서).
- kube-apiserver: 모든 요청의 입구. 인증·권한·검증을 거쳐 객체를 etcd에 저장하고, 변경을 지켜보는(watch) 구성 요소들에게 알린다.
- etcd: 클러스터 상태를 담는 분산 키-값 저장소. Raft 합의로 복제된다. 이것을 잃으면 클러스터를 잃으므로 백업이 필수다.
- kube-scheduler: 노드가 정해지지 않은 Pod에 노드를 정해 준다(아래 절).
- kube-controller-manager: Deployment·ReplicaSet·노드·Job 등 여러 컨트롤러를 돌린다. cloud-controller-manager는 로드 밸런서·디스크 같은 클라우드 자원을 다루는 컨트롤러다.
- kubelet: 노드마다 하나. 이 노드에 배정된 Pod 목록을 받아 컨테이너 런타임(containerd, CRI-O 등)에 실행을 맡기고 상태를 보고한다. 쿠버네티스는 1.24부터 Docker 엔진을 직접 부르던 dockershim을 빼고 CRI라는 표준 인터페이스로만 런타임과 대화한다. Docker로 만든 이미지도 OCI 형식이라 그대로 돈다.
- kube-proxy: Service의 가상 IP로 오는 트래픽을 실제 Pod로 보내는 규칙(iptables·IPVS 등)을 노드에 설치한다. 네트워크 플러그인(CNI)이 이 일을 대신하기도 한다.
Pod·Deployment·Service·Ingress
쿠버네티스에서는 "Pod 세 개를 띄워라"라고 명령하지 않고, "이런 상태여야 한다"는 객체를 YAML로 적어 API 서버에 넣는다. 자주 쓰는 객체는 네 가지다.
- Pod: 배치의 최소 단위. 컨테이너 하나 이상이 네트워크(같은 IP)와 볼륨을 나눠 쓴다. Pod는 언제든 죽고 새 IP로 다시 태어나는 소모품이다.
- Deployment: "web:1.4 이미지로 Pod 3개"를 선언한다. 이미지를 바꾸면 새 ReplicaSet을 만들어 Pod를 조금씩 바꿔 끼우는 롤링 업데이트를 하고, 문제가 생기면
kubectl rollout undo로 이전 ReplicaSet으로 되돌린다. 자동으로 되돌리지는 않는다(Deployment). - Service: 바뀌는 Pod들 앞에 고정된 가상 IP와 DNS 이름(
web.default.svc)을 준다. 레이블 선택자(app=web)에 맞고 준비된(ready) Pod로만 트래픽을 나눈다. - Ingress: 클러스터 밖의 HTTP 요청을 호스트·경로에 따라 Service로 보내는 규칙이다. 규칙을 실제로 처리하는 것은 따로 설치한 Ingress 컨트롤러다. Ingress API는 더 기능을 넣지 않는 동결 상태이고 후속은 역할을 나눠 설계한 Gateway API다. 널리 쓰이던 ingress-nginx 컨트롤러는 2026년 3월로 유지보수를 끝낸다고 발표됐으므로, 새로 만든다면 Gateway API 구현을 고른다.
네 객체 말고도 자주 만나는 것이 있다. 이름이 비슷해 헷갈리기 쉬우니 무엇을 맡는지로 구분한다.
| 객체 | 맡는 일 |
|---|---|
| StatefulSet | Pod마다 고정된 이름(db-0, db-1)과 저장소를 붙여 준다. 다시 떠도 같은 이름·같은 디스크로 돌아와야 하는 DB 같은 상태 있는 앱용이다. |
| DaemonSet | 모든(또는 고른) 노드에 Pod를 하나씩 둔다. 로그 수집기, 노드 모니터링, 네트워크 플러그인처럼 노드마다 있어야 하는 것이다. |
| Job·CronJob | 끝나는 일을 돌린다. Job은 정해진 수만큼 성공할 때까지 Pod를 다시 띄우고, CronJob은 일정에 따라 Job을 만든다. |
| ConfigMap | 설정 값을 이미지 밖에 두고 환경 변수나 파일로 넣는다. 비밀을 지켜 주지 않고, 한 개에 1MiB까지만 담는다. |
| Secret | 비밀번호·토큰용 객체다. 값이 base64로 적혀 있지만 그것은 인코딩일 뿐 암호화가 아니다. |
Secret에서 가장 흔한 오해가 이 base64다. 쿠버네티스 문서는 Secret이 기본으로는 etcd에 암호화되지 않은 채 저장되고, API 접근 권한이 있는 사람이나 그 네임스페이스에 Pod를 만들 수 있는 사람이 값을 읽을 수 있다고 경고한다. 저장 시 암호화를 켜고, RBAC으로 읽기 권한을 좁히고, Secret YAML을 Git에 그대로 올리지 않는다.
스케줄러: 거르기와 점수
kube-scheduler는 노드가 비어 있는 Pod를 하나씩 꺼내 두 단계로 노드를 고른다. 거르기(filter)는 아예 못 가는 노드 — requests를 받을 여유가 없거나, 견디지 못하는 taint가 있거나, nodeSelector·친화성(affinity) 조건에 안 맞는 노드 — 를 뺀다. 점수(score)는 남은 노드에 0~100점을 매긴 플러그인들의 가중 합으로 순위를 정한다. 동점이면 무작위로 고른다.
새 Pod(CPU 500m, 메모리 1024MiB 요청)가 노드 없이 대기열에 있다.
예에서 LeastAllocated 점수는 Pod를 놓은 뒤 남는 CPU 비율과 메모리 비율의 평균 × 100이다. node-2는 CPU가 많이 남지만 메모리가 덜 남아 50점, node-3은 둘 다 고르게 남아 63점이다. 스케줄러가 보는 것은 실제 사용량이 아니라 requests의 합이라는 점이 중요하다. requests를 적지 않은 Pod는 0으로 계산돼 이미 바쁜 노드에도 놓일 수 있다.
조정 루프
쿠버네티스의 거의 모든 동작은 컨트롤러의 조정 루프(reconciliation loop)다. 컨트롤러는 자기가 맡은 객체의 spec(원하는 상태)과 실제 상태를 관찰하고, 차이가 있으면 그만큼만 움직이고, 다시 관찰한다. 온도 조절기가 설정 온도와 실내 온도를 비교하는 것과 같다.
원하는 상태 replicas: 4, 실제로 4개가 돈다. 차이가 없어 할 일이 없다.
- 수준 기반: 컨트롤러는 "Pod가 하나 죽었다"는 사건(edge)이 아니라 "지금 3개여야 하는데 2개다"라는 상태(level)를 본다. 알림을 놓치거나 컨트롤러가 재시작돼도 다음 관찰에서 같은 결론에 이른다.
- 멱등: 같은 상태를 보고 몇 번을 돌아도 결과가 같아야 한다. 그래서 Deployment → ReplicaSet → Pod처럼 컨트롤러를 쌓아도 각자 자기 층의 차이만 메운다.
- 사람이
kubectl apply로 바꾸든, GitOps 도구가 저장소의 YAML을 밀어 넣든, 같은 루프가 차이를 메운다. 배포 파이프라인은 이 위에 서 있다.
requests·limits와 QoS 등급
컨테이너마다 requests(이만큼은 보장해 달라, 스케줄 기준)와 limits(이 이상은 쓰지 마라, cgroup 상한)를 적는다(자원 관리). 둘의 관계로 Pod의 QoS 등급이 정해지고, 노드 메모리가 모자랄 때 kubelet이 어느 Pod부터 쫓아낼지(eviction)가 이 등급을 따른다.
- Guaranteed: 모든 컨테이너가 CPU·메모리 둘 다 requests = limits. 가장 늦게 쫓겨난다. DB처럼 중요한 것에 쓴다.
- Burstable: requests보다 더 쓸 수 있다. 노드에 여유가 있으면 득을 보지만, 모두가 동시에 터지면 노드가 실제로 모자란다(과다 약정).
- BestEffort: 아무것도 적지 않았다. 스케줄러가 0으로 보므로 어디든 놓이고, 가장 먼저 쫓겨난다.
- requests는 실제 사용량에 가깝게 잡는다. 너무 크면 노드가 놀고(비용), 너무 작으면 노드에 Pod가 몰려 서로 굶는다. 지표로 실제 사용량을 보고 고쳐 나가는 것이 정석이고, 이를 도와주는 수직 자동 확장(VPA)도 있다.
HPA: 복제본 수 계산
HorizontalPodAutoscaler는 지표를 주기적으로(기본 15초) 보고 복제본 수를 desiredReplicas = ceil(currentReplicas × currentMetric ÷ targetMetric)으로 계산한다. CPU 사용률은 requests 대비 비율이므로 requests가 없으면 계산이 안 된다.
- 예: ceil(4 × 90 ÷ 60) = ceil(6) = 6개. 비율이 1.067처럼 1에서 10% 안이면 계산값(5)과 관계없이 4개를 그대로 둔다. 작은 흔들림에 복제본이 출렁이지 않게 하는 허용 오차다.
- 줄일 때는 지난 5분(기본 안정화 창) 동안의 권고값 중 가장 큰 것을 따라 급히 줄이지 않는다. 늘릴 때는 바로 늘린다.
- Pod를 늘려도 들어갈 노드가 없으면 Pending이 된다. 그때는 노드를 늘리는 클러스터 자동 확장(Cluster Autoscaler, Karpenter 등)이 받아 준다. 새 노드가 뜨는 데 몇 분이 걸리므로, 급한 부하에는 목표를 낮게 잡아 여유를 둔다.
- CPU 말고 초당 요청 수·큐 길이 같은 사용자 지표로도 확장한다. 부하 테스트로 Pod 하나가 견디는 양을 먼저 재 두면 목표값을 정하기 쉽다(부하 테스트 노트).
헬스 프로브
kubelet은 컨테이너가 건강한지 세 가지 프로브로 확인한다(HTTP 요청, TCP 연결, 명령 실행, gRPC 중 하나).
- startup: 시작이 느린 앱을 위한 것. 이것이 성공할 때까지 나머지 두 프로브를 미룬다.
- readiness: 트래픽을 받을 준비가 됐는지. 실패하면 Service 엔드포인트에서 빠질 뿐 재시작하지 않는다. 롤링 업데이트도 새 Pod가 ready가 돼야 다음으로 넘어간다.
- liveness: 살아 있는지. 정해진 횟수만큼 연속 실패하면 컨테이너를 재시작한다. 교착 상태처럼 스스로 못 빠져나오는 상황을 위한 것이다.
- 흔한 실수: liveness에서 DB 같은 외부 의존성을 검사하면, DB가 잠깐 느려질 때 모든 Pod가 함께 재시작돼 장애가 커진다. liveness는 프로세스 자신만 보고, 의존성 상태는 readiness나 지표로 본다.
종료: SIGTERM과 유예 시간
롤링 업데이트·축소·노드 비우기 때마다 Pod는 꺼진다. 이때 쿠버네티스는 컨테이너를 바로 죽이지 않는다. kubelet이 각 컨테이너의 PID 1에 SIGTERM을 보내고, 유예 시간(terminationGracePeriodSeconds, 기본 30초)이 지나도 살아 있으면 SIGKILL로 끝낸다(Pod 수명 주기). docker stop도 같은 순서다. 앱이 SIGTERM을 받으면 새 요청을 그만 받고, 처리 중인 요청과 DB 트랜잭션을 마무리하고, 연결을 닫고 나가야 배포 때마다 요청 몇 개가 실패하는 일이 사라진다.
- 신호가 앱에 닿지 않는 함정: Dockerfile에서
ENTRYPOINT java -jar app.jar처럼 셸 형식으로 쓰면/bin/sh -c가 PID 1이 되고, 셸은 신호를 앱에 넘기지 않는다. 앱은 SIGTERM을 못 받고 30초 뒤 SIGKILL을 맞는다.ENTRYPOINT ["java", "-jar", "app.jar"]처럼 exec 형식으로 쓰거나, 시작 스크립트라면 마지막 줄을exec로 실행한다(Dockerfile 참조). - 유예 시간은 정리 시간에 맞춘다: 긴 요청이나 일괄 작업을 마무리하는 데 30초보다 오래 걸리면 유예 시간을 늘린다. 종료 직전에 할 일은 preStop 훅으로 걸 수 있는데, 훅이 쓰는 시간도 같은 유예 시간 안에 든다.
관리형 쿠버네티스
제어부를 직접 운영하려면 etcd 백업, 인증서 갱신, 버전 업그레이드를 모두 챙겨야 한다. 그래서 대부분은 클라우드의 관리형 서비스 — Amazon EKS, Azure AKS, Google GKE — 를 쓴다. 클라우드 일반의 책임 나눔은 클라우드 노트에 있다.
- 관리형이라도 버전 업그레이드는 고객이 계획해야 한다. 쿠버네티스는 대략 넉 달마다 새 마이너 버전이 나오고 각 버전의 지원 기간이 정해져 있어, 미루면 사업자가 강제로 올리거나 연장 지원 요금이 붙는다.
- 워크로드의 설정·권한(RBAC)·네트워크 정책·이미지 보안은 어느 모드에서도 고객 몫이다.
- 서비스 몇 개를 돌리는 작은 팀이라면 쿠버네티스 없이 서버리스 컨테이너(Cloud Run, Azure Container Apps, ECS와 Fargate)가 운영 부담이 훨씬 적다. 쿠버네티스의 값은 많은 서비스를 한 방식으로 굴릴 때 나온다.
핵심 자료
| 자료 | 성격 | 한 줄 요지 |
|---|---|---|
| Large-scale cluster management at Google with Borg (Verma 등) | 논문(2015) | 쿠버네티스의 조상인 구글 내부 클러스터 관리자. 선언형 작업 명세, 우선순위와 할당량, 조정 루프의 뿌리가 여기 있다. |
| OCI Image Format Specification | 명세 | 매니페스트·설정·레이어로 이뤄진 컨테이너 이미지 형식. 어느 도구로 만들든 어느 런타임에서나 돌게 하는 약속이다. |
| OCI Distribution Specification | 명세 | 레지스트리에서 매니페스트와 블롭을 주고받는 HTTP API. |
| Linux namespaces(7) | 매뉴얼 | 네임스페이스 종류와 각각이 격리하는 자원의 목록. |
| Control Group v2 | 커널 문서 | cpu.max, memory.max 같은 cgroup v2 인터페이스의 공식 설명. |
| Kubernetes Components | 공식 문서 | 제어부와 노드 구성 요소가 각각 무엇을 하는지 정리한 개요. |
- Large-scale cluster management at Google with Borg (Verma 등)
- OCI Image Format Specification
- OCI Distribution Specification
- Linux namespaces(7)
- Control Group v2
- Kubernetes Components
- Kubernetes 문서: Pod Lifecycle — Pod의 종료
- Kubernetes 문서: Secrets
- Kubernetes 문서: ConfigMaps
- Kubernetes 문서: StatefulSets
- Kubernetes 문서: DaemonSet
- Kubernetes 문서: Jobs
- Kubernetes 문서: CronJob
- Docker 문서: Dockerfile reference
- Docker 문서: docker container stop
- Open Container Initiative
- Docker — 멀티 스테이지 빌드
- Docker — 빌드 캐시
- Kubernetes — 컨트롤러
- Kubernetes — Deployment
- Kubernetes — Service
- Kubernetes — Ingress
- Gateway API
- Kubernetes 블로그 — Ingress NGINX 은퇴(2025)
- Kubernetes — kube-scheduler
- Kubernetes — 컨테이너 자원 관리
- Kubernetes — Pod QoS 등급
- Kubernetes — Horizontal Pod Autoscaling
- Kubernetes — 프로브(liveness·readiness·startup)
- Amazon EKS
- Azure Kubernetes Service
- Google Kubernetes Engine