소프트웨어 개발의 역사는 '환경'과의 전쟁이었습니다. "제 컴퓨터에서는 잘 되는데요?"라는 말은 개발자와 운영자 사이에 놓인 거대한 장벽을 상징하는 문장이었습니다. 개발자는 자신의 로컬 환경에서 완벽하게 동작하는 애플리케이션을 만들지만, 이를 테스트 서버나 운영 서버로 옮기는 순간 수많은 문제에 직면합니다. 운영체제의 미묘한 차이, 설치된 라이브러리 버전의 불일치, 네트워크 설정의 차이 등 예측 불가능한 변수들이 애플리케이션의 발목을 잡았습니다. 이 고질적인 문제를 해결하기 위한 여정이 오늘날 우리가 이야기할 도커(Docker)와 쿠버네티스(Kubernetes)라는 두 거대한 기술을 탄생시켰습니다.
초기 해결책은 가상 머신(VM)이었습니다. VM은 하드웨어를 가상화하여 호스트 운영체제 위에 완전히 독립된 게스트 운영체제를 설치하고, 그 안에 애플리케이션과 모든 종속성을 가두는 방식입니다. 이는 완벽한 격리를 제공했지만, 치명적인 단점을 가졌습니다. 각 VM마다 완전한 운영체제를 포함해야 하므로 용량이 수 기가바이트에 달하고, 부팅 시간도 수 분이 걸리는 등 매우 무겁고 비효율적이었습니다. 하나의 물리 서버에 올릴 수 있는 VM의 수는 제한적이었고, 이는 곧 비용 증가로 이어졌습니다.
이러한 배경 속에서 '컨테이너'라는 개념이 대두되었습니다. 컨테이너는 운영체제 수준의 가상화 기술을 사용하여 애플리케이션과 그 종속성을 격리합니다. VM처럼 하드웨어를 가상화하고 게스트 OS를 통째로 올리는 대신, 호스트 시스템의 커널을 공유하면서 프로세스만 격리하는 방식입니다. 그 결과, 컨테이너는 VM보다 훨씬 가볍고(수십 메가바이트), 빠르며(수초 내 시작), 효율적입니다. 바로 이 컨테이너 기술을 누구나 쉽게 사용할 수 있도록 대중화시킨 일등공신이 바로 도커(Docker)입니다.
그리고 애플리케이션이 점점 더 복잡해지고 수십, 수백 개의 컨테이너를 동시에 운영해야 하는 상황이 오자, 이들을 체계적으로 관리하고 조율(Orchestration)할 필요성이 생겼습니다. 여기서 등장한 것이 바로 쿠버네티스(Kubernetes)입니다. 쿠버네티스는 분산된 서버 클러스터 전반에 걸쳐 컨테이너화된 애플리케이션을 자동으로 배포, 확장 및 관리하기 위한 오픈소스 플랫폼입니다. 이제 도커와 쿠버네티스는 현대적인 클라우드 네이티브 애플리케이션 개발과 DevOps 문화의 핵심을 이루는 표준 기술로 자리 잡았습니다. 이 글에서는 두 기술의 핵심 개념부터 아키텍처, 그리고 이들이 어떻게 협력하여 개발과 운영의 패러다임을 바꾸고 있는지 심도 있게 탐구해 보겠습니다.
컨테이너의 시작, 도커(Docker) 바로 알기
도커는 단순히 컨테이너를 실행하는 도구가 아닙니다. 도커는 애플리케이션을 신속하게 구축, 테스트 및 배포할 수 있는 개방형 플랫폼입니다. 그 핵심 철학은 'Build, Ship, and Run Any App, Anywhere'라는 슬로건에 담겨 있습니다. 즉, 어떤 애플리케이션이든 그 종속성과 함께 '이미지'라는 표준화된 유닛으로 패키징하고(Build), 이 이미지를 어디로든 쉽게 옮기며(Ship), 어떤 환경에서든 일관되게 실행(Run)하는 것을 목표로 합니다. 이는 마치 국제 무역에서 표준 규격의 컨테이너가 물류 혁명을 일으킨 것과 같습니다. 내용물이 무엇이든 표준 컨테이너에 담기만 하면 전 세계 어느 항구에서나 동일한 장비로 하역하고 운송할 수 있는 것처럼, 도커 이미지는 개발자의 노트북, 테스트 서버, 클라우드 환경 어디에서나 동일하게 동작함을 보장합니다.
도커 아키텍처의 세 가지 기둥
도커가 어떻게 동작하는지 이해하려면 그 아키텍처를 구성하는 세 가지 핵심 요소를 알아야 합니다. 바로 도커 클라이언트(Client), 도커 데몬(Daemon), 그리고 도커 레지스트리(Registry)입니다.
+----------------------+ +-------------------+ | Your Machine | | Registry Server | | | | | | +-----------------+ | docker pull/push +------------> | +---------------+ | | | Docker Client | | <------------------+ | | Docker Hub, | | | | (docker CLI) | | | | Private Registry| | | +-----------------+ | | +---------------+ | | ^ | +-------------------+ | | REST API | | v | | +-----------------+ | | | Docker Daemon | | | | (dockerd) | | | |-----------------| | | | Images | | | | Containers | | | +-----------------+ | +----------------------+
- 도커 클라이언트 (Docker Client): 개발자가 도커와 상호작용하는 주된 창구입니다. 우리가 터미널에 입력하는
docker run,docker build,docker pull과 같은 명령어들이 바로 도커 클라이언트입니다. 이 클라이언트는 사용자의 명령을 받아 도커 데몬에게 전달하는 역할을 합니다. 클라이언트와 데몬은 같은 시스템에 있을 수도 있고, 원격 시스템에 있을 수도 있습니다. - 도커 데몬 (Docker Daemon,
dockerd): 도커의 '엔진'이자 '심장'입니다. 도커 데몬은 백그라운드에서 실행되며, 도커 API 요청을 수신하고 이미지, 컨테이너, 네트워크, 볼륨과 같은 도커 객체(Object)를 관리하는 실질적인 작업을 수행합니다. 클라이언트로부터 "nginx이미지를 실행해줘"라는 요청을 받으면, 데몬은 로컬에nginx이미지가 있는지 확인합니다. 만약 없다면 레지스트리에서 이미지를 가져오고(pull), 그 이미지를 기반으로 컨테이너를 생성하여 실행합니다. - 도커 레지스트리 (Docker Registry): 도커 이미지를 저장하고 배포하는 중앙 저장소입니다. 가장 유명한 공개 레지스트리는 Docker Hub이며, 수많은 공식 이미지와 사용자들이 만든 이미지가 저장되어 있습니다. 기업 환경에서는 보안이나 성능상의 이유로 사내에 비공개 레지스트리(Private Registry)를 구축하여 사용하기도 합니다.
docker pull명령은 레지스트리에서 이미지를 가져오는 것이고,docker push는 내가 만든 이미지를 레지스트리에 업로드하는 것입니다.
이미지(Image)와 레이어(Layer)의 마법
도커의 효율성을 이해하는 데 가장 중요한 개념은 바로 '이미지'와 그 기반이 되는 '레이어' 시스템입니다. 도커 이미지는 단순히 파일 묶음이 아니라, 여러 개의 읽기 전용(Read-only) 레이어들이 겹겹이 쌓여있는 구조를 가집니다. 각 레이어는 Dockerfile의 특정 명령어(FROM, RUN, COPY 등)에 해당하며, 파일 시스템의 변경 사항을 담고 있습니다.
예를 들어, 다음과 같은 간단한 Node.js 애플리케이션용 Dockerfile을 생각해 봅시다.
# 1. 베이스 이미지 지정 (첫 번째 레이어)
FROM node:18-alpine
# 2. 작업 디렉토리 설정
WORKDIR /app
# 3. package.json 복사 (새로운 레이어 생성)
COPY package*.json ./
# 4. 의존성 설치 (새로운 레이어 생성)
RUN npm install
# 5. 소스 코드 복사 (새로운 레이어 생성)
COPY . .
# 6. 애플리케이션 실행
CMD ["node", "server.js"]
이 Dockerfile로 이미지를 빌드하면 다음과 같은 레이어 구조가 만들어집니다.
- 레이어 1: `node:18-alpine` 베이스 이미지 자체의 여러 레이어들.
- 레이어 2: `COPY package*.json ./` 명령으로 인해 `package.json` 파일이 추가된 변경 사항.
- 레이어 3: `RUN npm install` 명령으로 인해 `node_modules` 디렉토리에 수많은 파일이 추가된 변경 사항.
- 레이어 4: `COPY . .` 명령으로 인해 애플리케이션 소스 코드가 추가된 변경 사항.
이러한 레이어 구조는 놀라운 이점을 제공합니다. 만약 우리가 소스 코드(server.js)만 수정하고 이미지를 다시 빌드한다면, 도커는 레이어 1, 2, 3이 변경되지 않았음을 인지하고 캐시된 레이어를 그대로 재사용합니다. 그리고 변경이 발생한 레이어 4만 새로 빌드합니다. 이 덕분에 이미지 빌드 속도가 매우 빨라집니다. 또한, 여러 이미지가 동일한 베이스 이미지(예: `node:18-alpine`)를 사용한다면, 해당 베이스 이미지의 레이어들은 디스크에 단 한 번만 저장되므로 스토리지 공간을 매우 효율적으로 사용할 수 있습니다.
컨테이너: 살아있는 이미지의 인스턴스
이미지가 '설계도'라면, 컨테이너는 그 설계도로 지은 '집'입니다. 이미지는 읽기 전용 상태로 존재하지만, docker run 명령을 통해 컨테이너를 실행하면 이미지의 레이어들 위에 '쓰기 가능한 컨테이너 레이어(Writable Container Layer)'가 추가됩니다. 애플리케이션이 실행되면서 생성하는 로그 파일, 임시 데이터 등 모든 변경 사항은 이 컨테이너 레이어에 저장됩니다. 원래 이미지는 전혀 변경되지 않습니다.
따라서 하나의 이미지로부터 수십, 수백 개의 동일한 컨테이너를 순식간에 만들어낼 수 있습니다. 각 컨테이너는 자신만의 격리된 파일 시스템과 네트워크 공간을 가지지만, 기반이 되는 이미지는 공유하므로 매우 효율적입니다. 컨테이너를 삭제하면 이 쓰기 가능한 레이어만 사라질 뿐, 원본 이미지는 그대로 남아있어 언제든 다시 새로운 컨테이너를 생성할 수 있습니다. 이것이 바로 컨테이너의 '불변성(Immutability)'이라는 중요한 특징으로 이어집니다.
하나의 컨테이너를 넘어, 오케스트레이션의 필요성
도커는 단일 호스트에서 컨테이너를 다루는 데 있어 혁신적인 편리함을 제공했습니다. 개발자들은 자신의 노트북에서 복잡한 애플리케이션 스택(웹 서버, 데이터베이스, 캐시 등)을 단 몇 줄의 명령어로 손쉽게 구성하고 실행할 수 있게 되었습니다. 하지만 프로덕션 환경, 즉 실제 서비스 환경은 훨씬 더 복잡하고 가혹합니다.
하나의 웹 애플리케이션 컨테이너를 운영한다고 상상해 봅시다. 만약 사용자가 몰려 트래픽이 급증하면 어떻게 될까요? 컨테이너 하나로는 감당할 수 없을 것입니다. 그렇다면 수동으로 컨테이너를 5개, 10개로 늘려야 할까요? 그 컨테이너들로 들어오는 트래픽은 어떻게 분산시켜야 할까요? 만약 컨테이너가 실행 중이던 서버에 하드웨어 장애가 발생하면 어떻게 될까요? 누군가 밤새 모니터링하다가 다른 서버에 다시 컨테이너를 띄워야 할까요? 애플리케이션을 업데이트하려면, 기존 컨테이너를 모두 중지하고 새 버전의 컨테이너를 실행해야 할까요? 그렇게 하면 서비스가 중단되는 시간(downtime)이 발생할 텐데요.
이러한 문제들은 컨테이너의 수가 늘어나고, 여러 서버(호스트)에 걸쳐 배포되기 시작하면서 기하급수적으로 복잡해집니다. 이것이 바로 **컨테이너 오케스트레이션(Container Orchestration)**이 필요한 이유입니다. 오케스트레이션은 마치 오케스트라의 지휘자처럼, 수많은 컨테이너 연주자들이 조화롭게 협연하도록 지휘하고 관리하는 자동화된 프로세스를 의미합니다.
컨테이너 오케스트레이션 도구가 해결해야 할 핵심 과제들은 다음과 같습니다.
- 스케줄링 (Scheduling): 수많은 서버(노드)들 중에서 어떤 노드에 새로운 컨테이너를 배치할지 결정하는 것. 노드의 CPU, 메모리 자원 상황, 이미 정의된 제약 조건 등을 고려하여 최적의 위치를 찾아냅니다.
- 스케일링 (Scaling): 트래픽 부하에 따라 컨테이너의 수를 자동으로 늘리거나(Scale-out) 줄이는(Scale-in) 기능. 이를 통해 비용을 최적화하고 안정적인 서비스 성능을 유지합니다.
- 서비스 디스커버리 및 로드 밸런싱 (Service Discovery & Load Balancing): 컨테이너는 언제든지 죽고 새로 생성될 수 있기 때문에 IP 주소가 계속 바뀝니다. 내부 서비스들이 서로를 어떻게 찾아서 통신할 수 있는지, 그리고 외부 사용자의 요청을 여러 컨테이너에 어떻게 효율적으로 분산할지 관리해야 합니다.
- 자체 회복 (Self-healing): 실행 중이던 컨테이너나 컨테이너가 동작하던 노드에 문제가 생겼을 때, 이를 자동으로 감지하고 건강하지 않은 컨테이너를 종료시킨 후 새로운 컨테이너를 다시 시작하여 서비스의 가용성을 보장합니다.
- 무중단 업데이트 및 롤백 (Zero-downtime Update & Rollback): 애플리케이션을 새 버전으로 업데이트할 때, 서비스 중단 없이 점진적으로 교체(Rolling Update)하는 기능을 제공합니다. 만약 새 버전에 문제가 발견되면, 이전 버전으로 신속하게 되돌리는(Rollback) 기능도 필수적입니다.
이러한 복잡한 문제들을 해결하기 위해 Docker Swarm, Apache Mesos 등 여러 오케스트레이션 도구들이 경쟁했지만, 결국 구글이 내부적으로 사용하던 Borg 시스템의 경험을 바탕으로 만든 오픈소스 프로젝트, 쿠버네티스가 사실상의 표준(De facto standard)으로 자리 잡게 되었습니다.
대규모 컨테이너 관리의 해답, 쿠버네티스(Kubernetes)
쿠버네티스(Kubernetes, 종종 K8s로 축약)는 컨테이너화된 워크로드와 서비스를 관리하기 위한 이식 가능하고 확장 가능한 오픈소스 플랫폼입니다. 쿠버네티스는 단순히 컨테이너를 실행하고 중지하는 도구를 넘어, 분산 시스템을 안정적으로 운영하기 위한 강력한 프레임워크와 패턴을 제공합니다. 쿠버네티스를 이해하는 가장 중요한 키워드는 바로 '선언적 API(Declarative API)'와 '상태 관리(State Management)'입니다.
선언적 접근 방식: '무엇을' 할 것인가
기존의 인프라 관리는 대부분 '명령형(Imperative)' 방식이었습니다. "A 서버에 가서 Nginx 패키지를 설치하고, 설정 파일을 복사한 뒤, 서비스를 시작해라"와 같이 작업의 절차를 하나하나 명령하는 방식입니다. docker run ... 명령어 역시 명령형에 가깝습니다.
반면, 쿠버네티스는 '선언적(Declarative)' 방식을 채택합니다. 우리는 쿠버네티스에게 "어떻게 해"라고 명령하는 대신, "내가 원하는 최종 상태(Desired State)는 이것이야"라고 YAML 형식의 파일로 선언합니다. 예를 들어, "Nginx 컨테이너 3개가 항상 실행되고 있어야 하며, 이들은 외부로부터 80번 포트로 접근 가능해야 한다"라고 정의된 YAML 파일을 쿠버네티스에 제출하는 것입니다. 그러면 쿠버네티스는 현재 상태(Current State)를 지속적으로 관찰하며, 만약 현재 상태가 우리가 선언한 원하는 상태와 다르다면, 그 차이를 없애기 위해 필요한 작업을 스스로 수행합니다. 이 과정을 **조정(Reconciliation)** 또는 **제어 루프(Control Loop)**라고 부릅니다. Nginx 컨테이너 중 하나가 죽어서 2개만 실행 중이라면, 쿠버네티스는 이를 감지하고 즉시 새로운 컨테이너 하나를 추가하여 '원하는 상태'인 3개를 맞춥니다. 우리는 그 과정에 개입할 필요가 없습니다. 이 선언적 모델이 바로 쿠버네티스의 강력한 자동화와 자체 회복 능력의 근간입니다.
쿠버네티스 아키텍처: 컨트롤 플레인과 워커 노드
쿠버네티스 클러스터는 크게 전체 클러스터를 관리하고 지휘하는 '컨트롤 플레인(Control Plane, 과거에는 마스터 노드라 불림)'과 실제 컨테이너들이 실행되는 '워커 노드(Worker Node)'들로 구성됩니다.
+-------------------------------------------------+
| Control Plane |
| +----------------+ +------------------------+ |
| | etcd |<- | API Server | |
| | (Cluster State)|-- | (Gateway to Cluster) | |
| +----------------+ +------------------------+ |
| ^ ^ ^ |
| | | watch | kubectl |
| +-----|-----|---+ +-----|-----------+ |
| | Scheduler | | Controller Mgr. | |
| +-------------+ +-----------------+ |
+-------------------------------------------------+
|
(Manages Nodes & Pods)
|
+----------------------+--------------------------+
| Node 1 | Node 2 | ...
| +------------------+ | +------------------+ |
| | Kubelet | | | Kubelet | |
| |----------------| | |----------------| |
| | Container | | | Container | |
| | Runtime (e.g., | | | Runtime | |
| | containerd) | | | | |
| |----------------| | |----------------| |
| | - Pod | | | - Pod | |
| | - Pod | | | - Pod | |
| +------------------+ | +------------------+ |
| kube-proxy | kube-proxy |
+----------------------+--------------------------+
컨트롤 플레인 구성 요소
- API 서버 (kube-apiserver): 쿠버네티스 클러스터의 유일한 관문(Gateway)이자 두뇌입니다. 모든 외부(
kubectl, UI 등) 및 내부 컴포넌트 간의 통신은 API 서버를 통해 이루어집니다. 사용자가 제출한 YAML 명세를 검증하고, 이를 클러스터의 데이터베이스인 etcd에 저장합니다. - etcd: 클러스터의 모든 상태 데이터를 저장하는 고가용성의 키-값 저장소입니다. 어떤 노드에 어떤 파드(Pod)가 실행 중인지, 어떤 서비스가 생성되었는지 등 클러스터의 모든 구성 정보와 상태가 여기에 기록됩니다. 클러스터의 '진실의 원천(Source of Truth)'입니다.
- 스케줄러 (kube-scheduler): 새로 생성된 파드를 어떤 워커 노드에 배치할지 결정하는 역할을 합니다. 각 노드의 자원 요구량, 레이블, 어피니티/안티-어피니티 규칙 등 다양한 조건을 고려하여 가장 적합한 노드를 찾아냅니다.
- 컨트롤러 매니저 (kube-controller-manager): 쿠버네티스의 핵심적인 제어 루프들을 실행하는 컴포넌트입니다. 예를 들어, '디플로이먼트 컨트롤러'는 디플로이먼트 객체에 정의된 레플리카(복제본) 수가 실제 실행 중인 파드 수와 일치하는지 계속 감시하고, 다르다면 파드를 추가하거나 삭제하여 상태를 맞춥니다. 이 외에도 노드 컨트롤러, 서비스 컨트롤러 등 다양한 컨트롤러들이 각자의 역할을 수행합니다.
워커 노드 구성 요소
- Kubelet: 각 워커 노드에서 실행되는 에이전트입니다. API 서버와 통신하며, 해당 노드에 할당된 파드들이 의도한 대로 건강하게 실행되도록 관리하고 감독합니다. 파드 내 컨테이너의 생성, 시작, 중지, 상태 점검 등을 직접 수행합니다.
- 컨테이너 런타임 (Container Runtime): 실제로 컨테이너를 실행하는 소프트웨어입니다. Kubelet의 지시에 따라 컨테이너 이미지를 가져오고 컨테이너를 실행합니다. 도커(정확히는 containerd), CRI-O 등 CRI(Container Runtime Interface) 표준을 만족하는 다양한 런타임을 사용할 수 있습니다.
- Kube-proxy: 클러스터의 네트워킹을 담당하는 프록시입니다. 각 노드에서 실행되며, 서비스(Service)라는 쿠버네티스 객체를 실제 네트워크 규칙(예: iptables)으로 변환하여, 파드들 간의 통신이나 외부와의 통신이 원활하게 이루어지도록 합니다.
쿠버네티스의 핵심 오브젝트(Object)
쿠버네티스에서 우리는 컨테이너를 직접 다루기보다는, 컨테이너를 감싸고 관리하는 더 높은 수준의 추상화된 '오브젝트'들을 다룹니다. 이 오브젝트들이 바로 우리가 YAML 파일로 선언하는 '원하는 상태'의 실체입니다.
- 파드 (Pod): 쿠버네티스에서 생성하고 관리할 수 있는 가장 작은 배포 단위입니다. 파드는 하나 이상의 컨테이너 그룹이며, 이 컨테이너들은 스토리지와 네트워크를 공유하고, 항상 같은 노드에서 함께 실행됩니다. 왜 컨테이너가 아닌 파드가 기본 단위일까요? 웹 서버 컨테이너와 그 로그를 수집하는 사이드카(Sidecar) 컨테이너처럼, 아주 긴밀하게 연결되어 생명주기를 같이 해야 하는 컨테이너들을 하나의 단위로 묶어 관리하기 위함입니다.
- 서비스 (Service): 파드는 언제든 죽고 새로 생성될 수 있어 IP 주소가 유동적입니다. 서비스는 이렇게 변하는 파드 그룹에 대한 고정적인 네트워크 엔드포인트(고정 IP 주소와 DNS 이름)를 제공합니다. 예를 들어, 여러 개의 '프론트엔드' 파드가 있다면, '프론트엔드-서비스'를 생성하여 클러스터 내부의 다른 파드들이나 외부 사용자가 항상 동일한 주소로 프론트엔드 기능에 접근할 수 있도록 합니다. 서비스는 로드 밸런싱 기능도 내장하고 있습니다.
- 디플로이먼트 (Deployment): 파드의 복제본 수(Replica)를 관리하고, 애플리케이션의 무중단 업데이트를 처리하는 가장 일반적인 방법입니다. 디플로이먼트 YAML 파일에 "나는 이 파드 템플릿을 3개 유지하고 싶다"라고 선언하면, 디플로이먼트 컨트롤러가 항상 3개의 파드가 실행되도록 보장합니다. 이미지 버전을 변경하면, 롤링 업데이트(Rolling Update) 전략에 따라 기존 파드를 하나씩 점진적으로 새 버전의 파드로 교체하여 서비스 중단을 방지합니다.
- 컨피그맵 (ConfigMap) & 시크릿 (Secret): 애플리케이션의 설정값이나 데이터베이스 접속 정보 같은 민감한 데이터를 컨테이너 이미지와 분리하여 관리하기 위한 오브젝트입니다. 컨피그맵은 일반 설정 정보를 키-값 쌍으로 저장하고, 시크릿은 암호, API 키 등 민감한 정보를 Base64로 인코딩하여 저장합니다. 이를 통해 동일한 이미지를 다른 환경(개발, 스테이징, 운영)에서 다른 설정으로 배포할 수 있습니다.
- 퍼시스턴트 볼륨 (PersistentVolume, PV) & 퍼시스턴트 볼륨 클레임 (PersistentVolumeClaim, PVC): 컨테이너는 기본적으로 상태가 없으며(stateless), 컨테이너가 사라지면 데이터도 함께 사라집니다. 데이터베이스처럼 데이터를 영구적으로 저장해야 하는 애플리케이션을 위해 쿠버네티스는 스토리지 추상화를 제공합니다. PV는 관리자가 프로비저닝한 클러스터의 스토리지 조각이고, PVC는 사용자가 요청하는 스토리지의 사양(예: "10GB의 빠른 SSD가 필요해")입니다. 쿠버네티스는 PVC 요청에 맞는 PV를 찾아 연결해주어, 파드가 사라져도 데이터는 영속적으로 보존될 수 있도록 합니다.
도커와 쿠버네티스, 경쟁자인가 협력자인가?
기술에 입문하는 많은 사람들이 "도커를 배워야 하나요, 쿠버네티스를 배워야 하나요?" 또는 "도커와 쿠버네티스 중 무엇이 더 좋은가요?"와 같은 질문을 합니다. 이는 "자동차와 고속도로 중 무엇이 더 중요한가?"를 묻는 것과 같습니다. 두 가지는 경쟁 관계가 아니라, 각자의 역할이 뚜렷한 강력한 협력 관계입니다.
간단히 비유하자면 다음과 같습니다.
- 도커는 애플리케이션을 담는 표준화된 '컨테이너 박스'를 만들고, 그 박스를 한두 개 옮기는 '지게차' 역할을 합니다. 즉, 컨테이너 이미지를 빌드하고, 단일 호스트에서 실행하는 데 탁월합니다.
- 쿠버네티스는 수백, 수천 개의 컨테이너 박스를 실은 '거대한 화물선, 항만 시스템, 그리고 글로벌 물류 네트워크'와 같습니다. 어떤 컨테이너를 어떤 배에 실을지, 악천후 시 어떻게 항로를 변경할지, 특정 항구에 컨테이너가 너무 많으면 어떻게 분산시킬지를 총괄하는 거대한 오케스트레이션 시스템입니다.
쿠버네티스는 컨테이너를 실행하기 위해 컨테이너 런타임이 필요하며, 도커는 그 런타임 중 가장 유명하고 널리 사용되던 선택지였습니다. 쿠버네티스는 도커가 만든 표준 컨테이너 이미지를 완벽하게 지원하며, 실제로 쿠버네티스 클러스터 위에서 실행되는 대부분의 애플리케이션은 Dockerfile을 통해 도커로 빌드된 이미지입니다.
진실을 향한 한 걸음: CRI와 Dockershim의 시대
하지만 기술적인 관점에서 두 기술의 관계는 시간이 지나면서 조금 더 복잡하고 흥미롭게 발전했습니다. 초창기 쿠버네티스는 도커에 매우 의존적이었습니다. Kubelet은 도커 데몬과 직접 통신하여 컨테이너를 관리했습니다. 하지만 쿠버네티스 생태계가 성장하면서 rkt, CRI-O 등 다양한 컨테이너 런타임이 등장했고, 쿠버네티스는 특정 런타임에 종속되지 않고 다양한 선택지를 지원하고자 했습니다.
이를 위해 **CRI(Container Runtime Interface)**라는 표준 인터페이스가 탄생했습니다. CRI는 Kubelet과 컨테이너 런타임 사이에 필요한 통신 규약(gRPC 프로토콜 기반)을 정의한 것입니다. 이제 Kubelet은 도커에 직접 이야기하는 대신, CRI라는 표준 언어로 "파드를 시작해줘", "컨테이너 상태를 알려줘"라고 말합니다. CRI 표준을 준수하는 런타임이라면 어떤 것이든 쿠버네티스와 함께 작동할 수 있게 된 것입니다.
문제는 도커 데몬이 원래 CRI 표준을 위해 만들어지지 않았다는 점입니다. 이 간극을 메우기 위해 'Dockershim'이라는 작은 어댑터 컴포넌트가 만들어졌습니다. Dockershim은 Kubelet으로부터 CRI 호출을 받아서, 이를 도커 데몬이 이해할 수 있는 API 호출로 번역해주는 역할을 했습니다.
Kubelet <--> (CRI) <--> Dockershim <--> (Docker API) <--> Docker Daemon
하지만 이 구조는 불필하게 복잡했습니다. 사실 도커의 핵심 컨테이너 실행 기능은 **containerd**라는 더 낮은 수준의 컴포넌트가 담당하고 있었습니다. 도커 데몬은 이미지 빌드, 볼륨 관리 등 다양한 부가 기능을 포함한 거대한 툴이었고, 쿠버네티스는 오직 '컨테이너 실행' 기능만 필요했습니다. 그래서 쿠버네티스 커뮤니티는 "왜 containerd를 직접 사용하지 않고, 굳이 Dockershim과 무거운 도커 데몬을 거쳐야 하는가?"라는 결론에 도달했습니다.
그 결과, 쿠버네티스 1.24 버전부터 **Dockershim은 공식적으로 제거(deprecated)되었습니다.** 이는 많은 오해를 낳았습니다. "이제 쿠버네티스에서 도커를 못 쓰는 건가?"라는 질문이 쏟아졌지만, 이는 사실과 다릅니다. 진실은 다음과 같습니다.
- 여전히
docker build로 만든 이미지를 사용할 수 있습니다. 도커가 만든 이미지는 OCI(Open Container Initiative)라는 산업 표준을 따르며, containerd를 포함한 모든 현대적인 컨테이너 런타임은 이 표준 이미지를 완벽하게 지원합니다. 개발자가 이미지를 만드는 방식은 전혀 변하지 않습니다. - 쿠버네티스가 더 이상 도커 '데몬'과 직접 통신하지 않는다는 의미입니다. 대신, 쿠버네티스는 CRI 표준을 구현한 containerd와 직접 통신합니다. 아이러니하게도 containerd는 원래 도커 프로젝트에서 분리되어 CNCF(Cloud Native Computing Foundation)에 기증된 오픈소스 프로젝트입니다. 즉, 도커의 심장부를 직접 사용하는, 더 효율적인 방식으로 바뀐 것입니다.
결론적으로, 도커와 쿠버네티스의 관계는 더욱 성숙해졌습니다. 개발자는 여전히 도커의 편리한 개발 도구(Dockerfile, docker build)를 사용하여 애플리케이션을 패키징하고, 쿠버네티스는 이 패키지를 받아 containerd와 같은 표준 런타임을 통해 대규모로 안정적으로 운영하는, 각자의 역할이 더욱 명확해진 이상적인 협력 모델이 완성된 것입니다.
DevOps 파이프라인의 혁신, CI/CD와의 통합
도커와 쿠버네티스의 진정한 힘은 이들이 DevOps 문화와 결합하여 CI/CD(Continuous Integration/Continuous Deployment, 지속적인 통합/지속적인 배포) 파이프라인을 자동화하고 혁신할 때 발휘됩니다. CI/CD는 개발자가 코드 변경사항을 정기적으로 중앙 리포지토리에 병합하고, 이후 빌드, 테스트, 배포 단계를 자동화하여 고객에게 더 빠르고 안정적으로 새로운 가치를 전달하는 것을 목표로 합니다.
도커와 쿠버네티스가 없는 전통적인 CI/CD 파이프라인은 환경 불일치 문제로 인해 여전히 불안정했습니다. CI 서버에서 빌드된 아티팩트(예: JAR, WAR 파일)가 테스트 서버나 운영 서버에서는 다른 라이브러리 버전이나 설정 때문에 실패하는 경우가 잦았습니다. 하지만 이제 도커 이미지가 바로 그 '아티팩트'가 되면서 모든 문제가 해결됩니다.
현대적인 CI/CD 파이프라인은 다음과 같은 흐름으로 동작합니다.
- Code (코드 작성 및 푸시): 개발자가 로컬 환경에서 코드를 작성하고 변경 사항을 Git과 같은 버전 관리 시스템에 푸시합니다.
- Build (빌드 및 이미지 생성): Git 푸시 이벤트는 Jenkins, GitLab CI, GitHub Actions와 같은 CI 서버를 자동으로 트리거합니다. CI 서버는 소스 코드를 가져와 단위 테스트와 통합 테스트를 실행합니다. 테스트가 성공하면, 프로젝트의
Dockerfile을 사용하여 애플리케이션과 모든 종속성이 포함된 도커 이미지를 빌드합니다. 이 이미지는 `앱이름:git-commit-hash`와 같이 고유한 태그가 붙습니다. - Store (이미지 저장): 빌드된 이미지는 Docker Hub, Google Container Registry(GCR), Amazon Elastic Container Registry(ECR)와 같은 프라이빗 이미지 레지스트리에 푸시됩니다. 이제 이 이미지는 어디서든 가져다 쓸 수 있는 불변의 배포 패키지가 됩니다.
- Deploy (배포): CI/CD 파이프라인의 마지막 단계는 이 새로운 이미지를 쿠버네티스 클러스터에 배포하는 것입니다. 이는 보통 다음과 같이 이루어집니다.
- CD 도구(예: Argo CD, Spinnaker, 또는 간단한 셸 스크립트)가 쿠버네티스 디플로이먼트 YAML 파일의 이미지 태그를 방금 빌드한 새 이미지 태그로 업데이트합니다.
kubectl apply -f deployment.yaml명령을 실행하여 변경된 설정을 쿠버네티스 클러스터에 적용합니다.- 쿠버네티스의 디플로이먼트 컨트롤러는 이 변경을 감지하고, 선언된 '롤링 업데이트' 전략에 따라 서비스 중단 없이 점진적으로 기존 파드를 새 버전의 이미지를 사용하는 파드로 교체합니다.
이 파이프라인은 개발자에게 놀라운 경험을 제공합니다. 개발자는 단지 코드를 Git에 푸시하는 것만으로, 복잡한 배포 과정을 신경 쓸 필요 없이 자신의 코드가 자동으로 테스트, 패키징되어 실제 운영 환경에까지 안전하게 반영되는 것을 볼 수 있습니다. 운영팀 역시 수동 배포 작업의 부담에서 벗어나, 인프라를 코드로 관리하고(Infrastructure as Code), 전체 시스템의 안정성과 가시성을 확보하는 데 더 집중할 수 있습니다. 도커와 쿠버네티스는 이렇게 개발과 운영의 경계를 허물고, 두 팀이 공통의 언어(컨테이너 이미지, YAML)와 플랫폼 위에서 협업하도록 만드는 DevOps 문화의 핵심 촉매제 역할을 합니다.
미래를 향한 여정, 컨테이너 생태계의 전망
도커와 쿠버네티스는 단순히 두 개의 기술을 넘어, 클라우드 네이티브(Cloud Native)라는 거대한 패러다임을 이끄는 중심축이 되었습니다. 클라우드 네이티브는 애플리케이션을 처음부터 클라우드 환경의 유연성, 확장성, 복원력을 최대한 활용하도록 설계하고 구축하는 접근 방식이며, 컨테이너, 마이크로서비스 아키텍처(MSA), 서비스 메시, 선언적 API는 이를 구성하는 핵심 요소입니다.
이제 우리의 여정은 쿠버네티스에서 끝나지 않습니다. 쿠버네티스는 강력한 기반을 제공하지만, 복잡한 마이크로서비스 환경을 운영하기 위해서는 더 많은 도구들이 필요합니다. 쿠버네티스를 중심으로 거대한 생태계가 형성되고 있습니다.
- 서비스 메시 (Service Mesh): Istio, Linkerd와 같은 서비스 메시 도구들은 쿠버네티스 클러스터 내의 서비스 간 통신을 제어, 보호, 관찰하는 전용 인프라 계층을 제공합니다. 이를 통해 고급 트래픽 관리(Canary 배포, A/B 테스팅), 상호 TLS를 통한 강력한 보안, 서비스 간 의존성 및 성능에 대한 깊은 가시성을 확보할 수 있습니다.
- 모니터링 및 관측 가능성 (Monitoring & Observability): Prometheus는 사실상의 표준으로 자리 잡은 오픈소스 모니터링 및 경고 시스템입니다. 쿠버네티스 클러스터의 상태, 노드 자원 사용량, 애플리케이션 성능 메트릭 등을 수집하여 Grafana와 같은 시각화 도구를 통해 보여줍니다. Jaeger, Zipkin 같은 분산 추적 시스템은 마이크로서비스 아키텍처에서 요청이 여러 서비스를 거치는 전체 경로를 추적하여 병목 구간과 에러의 원인을 쉽게 파악하도록 돕습니다.
- 패키지 관리 (Package Management): Helm은 '쿠버네티스를 위한 패키지 매니저'입니다. 복잡한 애플리케이션을 배포하는 데 필요한 여러 쿠버네티스 오브젝트(디플로이먼트, 서비스, 컨피그맵 등)의 YAML 파일들을 '차트(Chart)'라는 하나의 패키지로 묶어 관리하고, 재사용하며, 버전 관리를 할 수 있게 해줍니다.
도커로 시작된 컨테이너 혁명은 쿠버네티스를 통해 만개했으며, 이제는 CNCF(Cloud Native Computing Foundation)를 중심으로 한 수많은 오픈소스 프로젝트들이 이 생태계를 더욱 풍성하게 만들고 있습니다. 컨테이너 기술은 더 이상 선택이 아닌 현대 소프트웨어 개발의 필수 역량이 되었습니다. 애플리케이션이 어디서 실행되든 일관된 환경을 보장하고, 인프라의 복잡성을 추상화하며, 개발팀과 운영팀이 더 빠르고 안정적으로 혁신을 만들어낼 수 있는 강력한 기반을 제공하기 때문입니다. 도커와 쿠버네티스를 이해하는 것은 단순히 새로운 도구를 배우는 것을 넘어, 미래의 소프트웨어가 어떻게 만들어지고 운영될 것인지에 대한 통찰을 얻는 과정일 것입니다.
0 개의 댓글:
Post a Comment