Chapter 1. 컨테이너 이전의 세계
* 서버 한 대에 다 올리던 시절
2000년대 초반을 상상해보자. 회사에서 웹 서비스를 하나 만들었다. Apache 웹서버, Java 애플리케이션, MySQL DB — 이 세 가지를 물리 서버 한 대에 다 설치했다.
잘 돌아간다. 그런데 문제가 하나둘 생긴다.
- 문제 1: 서로 발목을 잡는다. Java 앱이 메모리를 많이 먹으면 MySQL이 느려진다. 하나가 죽으면 같은 서버의 다른 것도 영향을 받는다.
- 문제 2: 환경이 꼬인다. Java 앱 A는 Java 8이 필요하고, 앱 B는 Java 11이 필요하다. 같은 서버에 두 버전을 공존시키는 건 악몽이다. 라이브러리 버전 충돌도 마찬가지다.
- 문제 3: 확장이 어렵다. 트래픽이 늘어서 웹서버만 늘리고 싶은데, 서버 한 대에 다 묶여있으니 서버 전체를 복제해야 한다. DB까지 같이 복제할 순 없지 않은가.
이 문제들을 해결하려고 가상화(Virtualization)가 등장했다.
용어: 가상화(Virtualization) — 물리 서버 한 대 위에 소프트웨어로 가상의 컴퓨터 여러 대를 만드는 기술이다. VMware, VirtualBox, KVM 같은 것들이 이에 해당한다. 각 가상 컴퓨터를 VM(Virtual Machine)이라고 부른다.
VM을 쓰면 서버 한 대에서 앱 A용 VM, 앱 B용 VM, DB용 VM을 분리할 수 있다. 서로 독립된 OS가 돌아가니까 라이브러리 충돌도 없고, 하나가 죽어도 다른 VM은 멀쩡하다.
그런데 VM에도 단점이 있다.
* VM의 한계
VM 하나마다 OS 전체가 들어간다. Ubuntu든 CentOS든, 커널부터 시스템 라이브러리까지 통째로다.
그래서:
- VM 하나가 최소 수백 MB ~ 수 GB의 디스크를 먹는다.
- 부팅하는 데 수십 초 ~ 수 분이 걸린다.
- 같은 물리 서버에 올릴 수 있는 VM 수가 제한적이다.
운영을 해보면 VM 수십~수백 대를 관리하는 것이 어떤 느낌인지 알 것이다. 각 VM마다 OS 패치도 해야 하고, 보안 업데이트도 따로 해야 한다.
여기서 핵심적인 질문이 나온다:
“앱을 서로 격리하고 싶은 건데, 꼭 OS 전체를 복제해야 하나?”
이 질문의 답이 컨테이너다.
Chapter 2. 컨테이너란 무엇인가
* OS 커널은 공유하고, 앱만 격리한다
용어: 커널(Kernel) — OS의 핵심 엔진이다. 하드웨어(CPU, 메모리, 디스크, 네트워크)를 직접 제어하고, 프로세스 관리, 파일 시스템 접근 등을 담당한다. Linux에서 uname -r로 확인할 수 있는 그 커널이다.
컨테이너의 핵심 아이디어는 이것이다: 호스트 OS의 커널을 같이 쓰되, 각 앱이 자기만의 파일 시스템, 네트워크, 프로세스 공간을 갖게 하자.
이걸 가능하게 하는 Linux 커널 기능이 두 가지 있다.
1) 네임스페이스(Namespace) — 격리를 담당
용어: Linux 네임스페이스(Namespace) — 프로세스가 볼 수 있는 시스템 자원의 범위를 제한하는 Linux 커널 기능이다. 참고로 이것은 나중에 배울 K8s의 Namespace와는 별개 개념이다.
PID namespace: 컨테이너 안의 프로세스는 자기 컨테이너 안의 프로세스만 보인다.
- Network namespace: 컨테이너마다 독립적인 네트워크 인터페이스, IP, 포트 공간을 가진다.
- Mount namespace: 컨테이너마다 독립적인 파일 시스템 뷰를 가진다.
- UTS namespace: 컨테이너마다 자기 hostname을 가질 수 있다.
2) cgroups(Control Groups) — 자원 제한을 담당
용어: cgroups — 프로세스 그룹이 사용할 수 있는 CPU, 메모리, 디스크 I/O 등의 양을 제한하는 Linux 커널 기능이다.
네임스페이스가 “너는 이것만 볼 수 있어”라면, cgroups는 “너는 이만큼만 쓸 수 있어”이다.
이 두 가지를 조합하면: 같은 커널 위에서 돌아가지만, 각 앱은 마치 자기만의 서버에서 도는 것처럼 느끼게 된다.
* VM vs 컨테이너 비교
[ VM 방식 ] [ 컨테이너 방식 ]
┌──────────┐ ┌──────────┐ ┌──────┐ ┌──────┐ ┌──────┐
│ App A │ │ App B │ │App A │ │App B │ │App C │
├──────────┤ ├──────────┤ ├──────┤ ├──────┤ ├──────┤
│Guest OS │ │Guest OS │ │ Libs │ │ Libs │ │ Libs │
│(Ubuntu) │ │(CentOS) │ └──┬───┘ └──┬───┘ └──┬───┘
└────┬─────┘ └────┬─────┘ │ │ │
│ │ ┌──┴────────┴────────┴──┐
┌────┴────────────┴─────┐ │ Container Runtime │
│ Hypervisor │ │ (Docker 등) │
├───────────────────────┤ ├────────────────────────┤
│ Host OS │ │ Host OS (커널 공유) │
├───────────────────────┤ ├────────────────────────┤
│ Hardware │ │ Hardware │
└───────────────────────┘ └────────────────────────┘
VM은 각각 Guest OS 전체를 들고 있고, 컨테이너는 앱과 필요한 라이브러리만 들고 있다. 그래서:
- 컨테이너 이미지 크기: 수십 MB (VM: 수 GB)
- 시작 시간: 1~2초 (VM: 수십 초~분)
- 같은 하드웨어에 훨씬 더 많은 앱을 올릴 수 있다
* 컨테이너 이미지란?
용어: 컨테이너 이미지(Container Image) — 앱을 실행하는 데 필요한 모든 것(코드, 라이브러리, 설정 파일, 환경 변수)을 하나의 패키지로 묶은 것이다. “앱 설치 USB”라고 생각하면 된다.
이미지는 읽기 전용이다. 이 이미지를 실행하면 그게 컨테이너가 된다. 붕어빵 틀(이미지)과 붕어빵(컨테이너)의 관계다. 하나의 이미지로 컨테이너를 100개든 1000개든 만들 수 있다.
이미지는 Dockerfile이라는 레시피로 만든다:
FROM python:3.11-slim # 베이스 이미지 (Python이 설치된 가벼운 Linux)
WORKDIR /app # 작업 디렉토리 설정
COPY requirements.txt . # 의존성 파일 복사
RUN pip install -r requirements.txt # 패키지 설치
COPY . . # 앱 코드 전체 복사
CMD ["python", "app.py"] # 컨테이너 시작 시 실행할 명령
이 Dockerfile을 docker build 하면 이미지가 만들어지고, docker run 하면 컨테이너가 실행된다.
용어: 레이어(Layer) — Dockerfile의 각 명령(FROM, RUN, COPY 등)이 하나의 레이어를 만든다. 레이어는 읽기 전용이고 캐싱된다. 코드만 바꾸면 COPY 레이어부터만 다시 빌드하면 되니까 빠르고 효율적이다.
Chapter 3. 컨테이너만으로는 부족하다
* 컨테이너 1~2개일 때는 행복하다
docker run으로 컨테이너 띄우고, 잘 돌아가고, 문제가 생기면 docker restart 하면 된다. 그런데 실무에서는 이런 상황이 벌어진다.
- 상황 1: 수십~수백 개의 컨테이너.
- 마이크로서비스 아키텍처로 가면 하나의 서비스가 수십 개의 컨테이너로 구성된다. 프론트엔드, 백엔드 API 여러 개, DB, 캐시, 메시지 큐, 로그 수집기…
용어: 마이크로서비스(Microservices) — 하나의 큰 앱(모놀리스)을 기능별로 작은 서비스들로 쪼개는 아키텍처다. 예: “주문 서비스”, “결제 서비스”, “사용자 서비스”가 각각 독립적으로 배포·확장된다.
- 상황 2: 컨테이너가 죽으면?
- Docker 자체는 “컨테이너가 죽으면 자동으로 새로 띄워줘”라는 기능이 기본적으로 약하다. --restart=always 옵션이 있지만, 그 서버 자체가 죽으면? 다른 서버에서 자동으로 띄워주지는 못한다.
- 상황 3: 트래픽이 늘면?
- “지금 웹서버 컨테이너 3개인데, 5개로 늘려줘” — 수동으로 하려면 어느 서버에 빈자리가 있는지 확인하고, 거기서 docker run 하고, 로드밸런서에 등록하고… 이걸 매번 해야 한다.
- 상황 4: 업데이트.
- v1.0에서 v1.1로 업데이트할 때, 100개 컨테이너를 한꺼번에 내리면 서비스 중단이다. 몇 개씩 순차적으로 교체하고 싶은데, 수동으로 해야 한다.
- 상황 5: 서버 여러 대에 분산.
- 서버가 10대 있을 때, 어떤 컨테이너를 어떤 서버에 배치할지 — CPU, 메모리 여유를 보고 결정해야 한다.
이 모든 걸 사람이 하면 운영자가 밤낮으로 일해야 한다. 그래서 필요한 게 컨테이너 오케스트레이션(Container Orchestration)이다.
용어: 오케스트레이션(Orchestration) — 오케스트라 지휘자가 수십 명의 연주자를 조율하듯, 수많은 컨테이너의 배포, 확장, 네트워킹, 복구를 자동으로 관리하는 것이다.
Chapter 4. Kubernetes의 탄생
* Google의 비밀 병기, Borg
Google은 2000년대 초반부터 내부적으로 Borg라는 시스템을 써왔다. Gmail, YouTube, Google Search — Google의 모든 서비스가 Borg 위에서 돌아갔다. 수십만 대의 서버에서 수십억 개의 컨테이너를 관리하는 시스템이었다.
2014년, Google은 Borg의 경험과 교훈을 바탕으로 오픈소스 프로젝트를 시작한다. 그게 Kubernetes(쿠버네티스)다.
용어: Kubernetes — 그리스어로 “조타수, 항해사”라는 뜻이다. K와 s 사이에 8글자가 있어서 K8s라고 줄여 쓴다. CNCF(Cloud Native Computing Foundation)에서 관리하는 오픈소스 프로젝트이며, 현재 컨테이너 오케스트레이션의 업계 표준이다.
* K8s가 해주는 것들
한 문장으로: “너는 원하는 상태(desired state)를 선언해. 나머지는 K8s가 알아서 할게.”
이게 K8s의 가장 중요한 철학이다. 선언적 관리(Declarative Management)라고 한다.
예를 들어 이렇게 말하는 것이다: “nginx 컨테이너를 3개 띄워줘. 각각 CPU 0.5코어, 메모리 256MB를 써. 항상 3개가 유지돼야 해.”
이걸 YAML 파일로 작성해서 K8s에 전달하면:
- K8s가 클러스터 안에서 자원 여유가 있는 노드를 찾아서 컨테이너를 배치한다. (스케줄링)
- 컨테이너 하나가 죽으면 자동으로 새 걸 띄운다. (자가 치유)
- 트래픽이 늘면 3개에서 5개로 늘린다. (오토스케일링)
- v1에서 v2로 업데이트할 때 하나씩 교체한다. (롤링 업데이트)
- 문제가 생기면 이전 버전으로 돌린다. (롤백)
- 컨테이너들 앞에 로드밸런서를 자동으로 붙여준다. (서비스 디스커버리)
※ 명령적 vs 선언적
명령적(Imperative) — “이렇게 해라”
- 서버 A에 SSH 접속해
- docker pull nginx 해
- docker run -d -p 80:80 nginx 해
- 방화벽에 80포트 열어
- 로드밸런서에 서버 A 등록해
선언적(Declarative) — “이런 상태가 되길 원해”
spec:
replicas: 3
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
명령적은 “How(어떻게)”를 지시하고, 선언적은 “What(무엇을)”을 선언한다.
선언적 방식의 장점은 K8s가 현재 상태와 원하는 상태의 차이를 스스로 감지하고 맞춰준다는 것이다. 컨테이너가 2개밖에 안 돌고 있으면? K8s가 “3개여야 하는데?” 하고 1개를 더 띄운다. 이 과정을 Reconciliation Loop(조정 루프)라고 부른다. K8s 전체를 관통하는 핵심 메커니즘이다.
'k8s 이론 공부' 카테고리의 다른 글
| 부록: 핵심 용어 사전 (가나다순) (0) | 2026.04.02 |
|---|---|
| Part 5 — 운영과 확장 (0) | 2026.04.02 |
| Part 4 — 네트워킹과 스토리지 (0) | 2026.04.02 |
| Part 3 — 핵심 리소스 (0) | 2026.04.02 |
| Part 2 — K8s 아키텍처 (0) | 2026.04.02 |