Chapter 2

클러스터 아키텍처와 런타임

  • 2.1 클러스터 아키텍처
  • 2.2 Docker vs ContainerD
  • 2.3 Docker Deprecation

Kubernetes 클러스터가 어떤 구조로 돌아가는지, 그리고 컨테이너 런타임이 Docker에서 ContainerD로 넘어간 이유를 다뤄. 클러스터의 큰 그림을 먼저 잡고, 그 안에서 컨테이너를 실행하는 런타임 계층이 어떻게 바뀌었는지 이어서 보면 흐름이 자연스러워.

쿠버네티스 클러스터는 **마스터 노드(Control Plane)**랑 워커 노드, 이 두 종류의 노드로 구성돼. 마스터는 전체를 관리하는 쪽이고, 워커는 실제로 컨테이너를 돌리는 쪽이야. 이게 가장 큰 그림이야.

강의에서 배에 비유를 하는데 꽤 직관적이야. 화물선이 워커 노드고, 관제선이 마스터 노드야. 화물선은 컨테이너를 실어 나르는 실제 작업을 하고, 관제선은 어떤 배에 뭘 실을지 계획하고, 모니터링하고, 전체를 관리하는 거지.

쿠버네티스 클러스터는 물리 서버든 가상 머신이든, 온프레미스든 클라우드든 상관없이 노드들의 집합으로 이루어져. 워커 노드는 컨테이너를 적재할 수 있는 배인 셈인데, 누군가는 이 배들을 관리해야 하잖아. 어떤 배에 실을지 계획하고, 적합한 배를 찾고, 배에 대한 정보를 저장하고, 컨테이너 위치를 추적하고. 이 관리 작업을 하는 게 마스터 노드야. 마스터 노드는 이걸 컨트롤 플레인 컴포넌트라는 일련의 구성 요소를 통해서 수행해.

그럼 컨트롤 플레인 컴포넌트들을 하나씩 볼게.

먼저 etcd. 매일 수많은 컨테이너가 적재되고 하역되니까, 어떤 컨테이너가 어떤 배에 실렸는지, 몇 시에 선적됐는지 같은 정보를 관리해야 해. 이 모든 정보가 etcd라는 고가용성 키-값 저장소에 저장돼. 말 그대로 정보를 키-값 형식으로 저장하는 데이터베이스야. etcd가 뭔지, 어떤 데이터가 들어가는지는 다음 강의에서 더 자세히 다뤄.

다음은 kube-scheduler. 배가 도착하면 크레인으로 컨테이너를 적재하잖아. 이 크레인이 스케줄러야. 배의 크기, 용량, 이미 실려 있는 컨테이너 수, 목적지, 허용되는 컨테이너 유형 같은 조건을 다 따져서 적합한 배를 골라줘. 쿠버네티스에서는 컨테이너의 리소스 요구 사항, 워커 노드 용량, 테인트/톨러레이션, 노드 어피니티 같은 정책과 제약 조건에 따라 컨테이너를 배치할 올바른 노드를 결정하는 거야. 스케줄링은 나중에 별도 섹션에서 훨씬 더 깊이 다뤄.

그 다음은 컨트롤러들이야. 도크에는 각 업무를 담당하는 사무실들이 있어. 운영팀은 선박 취급이나 교통 통제를 처리하고, 화물팀은 컨테이너가 손상됐을 때 새 컨테이너를 준비하고, 서비스 사무소는 선박 간 통신을 관리하고. 쿠버네티스에서도 마찬가지야. Node Controller는 새 노드를 클러스터에 온보딩하고, 노드가 죽거나 응답 안 하는 상황을 처리해. Replication Controller는 원하는 수의 컨테이너가 항상 실행 중인 상태를 보장해.

그러면 이 사무실들, 배들, 데이터 저장소, 크레인 — 이것들이 어떻게 서로 소통할까? 누가 이 전체를 관리하냐면, kube-apiserver야. 쿠버네티스의 핵심 관리 컴포넌트인데, 클러스터 내 모든 작업을 오케스트레이션하는 역할을 해. 외부 사용자가 관리 작업을 수행하기 위한 Kubernetes API를 노출하고, 내부적으로는 클러스터 상태를 모니터링하는 컨트롤러들이나 워커 노드가 이 서버를 통해 통신해.

여기서 중요한 게 하나 있어. 우리가 다루는 게 전부 컨테이너잖아. 애플리케이션도 컨테이너 형태로 배포되고, 마스터 노드의 관리 컴포넌트들도 컨테이너로 호스팅할 수 있고, DNS 서비스나 네트워킹 솔루션도 전부 컨테이너로 배포할 수 있어. 그래서 컨테이너를 실행할 수 있는 소프트웨어가 필요한데, 이게 Container Runtime Engine이야. 가장 유명한 건 Docker인데, 꼭 Docker일 필요는 없어. containerd나 Rocket 같은 다른 런타임 엔진도 지원해. 중요한 건 마스터 노드 포함 클러스터의 모든 노드에 이 런타임이 설치되어 있어야 한다는 거야.

이제 워커 노드 쪽으로 가보자. 모든 배에는 선장이 있잖아. 선장은 배의 모든 활동을 관리하고, 관제선이랑 소통하면서 자기 배가 그룹에 합류하고 싶다고 알리고, 적재할 컨테이너에 대한 정보를 받아서 싣고, 배와 컨테이너의 상태를 관제선에 보고해. 이 선장이 바로 kubelet이야. kubelet은 클러스터의 각 노드에서 실행되는 에이전트로, kube-apiserver의 지시를 받아서 노드에 컨테이너를 배포하거나 삭제해. 그리고 kube-apiserver가 주기적으로 kubelet한테서 상태 보고서를 가져와서 노드와 컨테이너의 상태를 모니터링해.

근데 워커 노드에서 실행되는 애플리케이션들은 서로 통신할 수 있어야 해. 예를 들어 한 노드에서 웹 서버를 돌리고, 다른 노드에서 데이터베이스 서버를 돌린다고 해봐. 웹 서버가 다른 노드의 DB에 어떻게 접근하겠어? 이걸 가능하게 해주는 게 kube-proxy야. kube-proxy 서비스는 워커 노드에서 실행 중인 컨테이너들이 서로 연결될 수 있도록 필요한 네트워크 규칙을 적용해줘.

정리하면 이래. 마스터 노드에는 클러스터 정보를 저장하는 etcd, 컨테이너 배치를 결정하는 kube-scheduler, 노드 관리나 복제 보장 같은 다양한 기능을 처리하는 컨트롤러들, 그리고 이 모든 걸 오케스트레이션하는 kube-apiserver가 있어. 워커 노드에는 apiserver의 지시를 받아 컨테이너를 관리하는 kubelet과, 서비스 간 통신을 가능하게 해주는 kube-proxy가 있어. 이 전체 구조를 머릿속에 넣어두면 이후에 각 컴포넌트를 깊이 파고들 때 길을 잃지 않아.

그런데 위에서 Container Runtime Engine 얘기를 했잖아. Docker, ContainerD, 그리고 CLI 도구들(ctr, nerdctl, crictl)이 정확히 뭐가 다른 건지 짚어보자.

핵심부터 말하면, Kubernetes는 더 이상 Docker를 런타임으로 지원하지 않아. v1.24부터 Dockershim이 완전히 제거됐거든. 대신 ContainerD가 독립적인 컨테이너 런타임으로 자리 잡았어. 근데 Docker로 빌드한 이미지는 OCI 표준을 따르기 때문에 ContainerD에서도 문제없이 돌아가. 이미지가 안 되는 게 아니라, Docker "엔진" 자체가 런타임에서 빠진 거야.

왜 이렇게 됐냐면, 원래 Kubernetes는 Docker만 지원했어. 근데 다른 컨테이너 런타임(rkt 같은)도 쓰고 싶다는 요구가 생기면서 CRI(Container Runtime Interface)라는 표준 인터페이스를 만들었거든. CRI를 지원하는 런타임이면 뭐든 Kubernetes에서 쓸 수 있게 한 거야. 여기서 OCI(Open Container Initiative)라는 표준도 등장하는데, 이미지를 어떻게 빌드하고(이미지 사양), 런타임을 어떻게 만들어야 하는지(런타임 사양) 정의해놓은 거야. OCI 표준만 준수하면 누구든 컨테이너 런타임을 만들어서 Kubernetes에서 쓸 수 있어.

문제는 Docker가 CRI 이전에 만들어졌기 때문에 CRI를 지원하지 않았다는 거야. 그래서 Kubernetes는 Dockershim이라는 임시 방편을 만들어서 Docker를 CRI 바깥에서 지원했어. 근데 Docker 자체가 단순한 런타임이 아니라 CLI, API, 빌드 도구, 볼륨, 보안 등 여러 도구의 묶음이잖아. 그 안에 있는 실제 컨테이너 런타임이 바로 ContainerD(그리고 그 아래 runc)야. ContainerD는 CRI 호환이니까 Kubernetes에서 직접 쓸 수 있어. 그래서 굳이 Docker 전체를 끌고 다닐 필요가 없어진 거지. Dockershim 유지보수도 부담이었고.

이제 ContainerD를 좀 더 보자. ContainerD는 원래 Docker의 일부였지만 지금은 독립된 CNCF 졸업 프로젝트야. Docker 설치 없이 ContainerD만 따로 설치할 수 있어. Docker의 다른 기능이 굳이 필요 없다면 ContainerD만 설치하는 게 이상적이야.

CLI 도구는 세 가지가 있어.

ctr - ContainerD와 함께 제공되는 도구인데, 디버깅 전용이라 기능이 매우 제한적이고 사용자 친화적이지 않아. 이미지 풀이나 컨테이너 실행 같은 기본 작업은 할 수 있지만, 프로덕션용으로는 부적합해. 사실상 무시해도 돼.

ctr images pull docker.io/library/redis:latest
ctr run docker.io/library/redis:latest redis

nerdctl - ContainerD 커뮤니티에서 만든 도구로, Docker CLI와 거의 동일하게 작동해. Docker가 지원하는 옵션 대부분을 지원하고, 거기에 더해서 암호화된 컨테이너 이미지, 지연 풀링(lazy pulling), P2P 이미지 배포, 이미지 서명/검증, Kubernetes 네임스페이스 같은 추가 기능도 있어. Docker 대체용으로 가장 적합한 도구야.

nerdctl run --name webserver -p 8080:80 -d nginx

Docker 명령에서 dockernerdctl로 바꾸면 대부분 그대로 동작해. 포트 매핑(-p)이나 다른 옵션도 동일하게 쓸 수 있어.

crictl - Kubernetes 커뮤니티에서 만든 도구로, CRI 호환 런타임이면 뭐든 작동해. ContainerD뿐 아니라 CRI-O 같은 다른 런타임에서도 쓸 수 있어. 주로 디버깅 목적이야. crictl로 컨테이너를 만들 수는 있지만, Kubelet이 관리하지 않는 컨테이너를 발견하면 삭제해버리기 때문에 컨테이너 생성 용도로는 안 돼.

crictl pull busybox
crictl images
crictl ps
crictl exec -it <container-id> sh
crictl logs <container-id>
crictl pods

crictl은 pods 명령으로 파드를 인식할 수 있다는 게 Docker CLI와의 큰 차이점이야. docker 명령에서 쓰던 run, attach, images, inspect, logs, ps, stats, version 같은 명령은 crictl에서도 완전히 동일하게 작동하고, exec, create, pull, rm, start, stop 같은 명령도 유사하게 동작해.

crictl은 여러 런타임이 있을 때 어떤 런타임에 연결할지 지정할 수 있어. --runtime-endpoint 옵션이나 CONTAINER_RUNTIME_ENDPOINT 환경 변수로 설정하면 돼. 기본적으로는 Dockershim, ContainerD, CRI-O 순서로 연결을 시도해.

정리하면, ctr과 crictl은 디버깅용이고 nerdctl이 범용 도구야. ctr과 nerdctl은 ContainerD 커뮤니티에서 만들었고 ContainerD 전용인 반면, crictl은 Kubernetes 커뮤니티에서 만들었고 모든 CRI 호환 런타임에서 동작해. 예전에 워커 노드에서 Docker 명령으로 문제 해결하던 걸, 이제는 crictl 명령으로 하면 돼. 구문이 거의 비슷하니까 어렵지 않아.

그러면 "Docker 배울 필요 없는 거 아냐?"라고 생각할 수 있는데, 그건 아니야.

Kubernetes가 Docker "런타임"에 대한 지원을 중단한 거지, Docker 자체가 사라진 게 아니야. Docker는 여전히 가장 인기 있는 컨테이너 솔루션이고, 많은 사람들이 일상적인 개발과 빌드 프로세스에서 쓰고 있어.

Docker는 CLI, API, 빌드 도구, 볼륨, 인증, 보안, 그리고 컨테이너 런타임(ContainerD + runc) 이런 것들이 합쳐진 패키지야. 이 중에서 ContainerD가 CRI 호환이라 Kubernetes와 직접 작동할 수 있거든. Kubernetes 입장에서는 Docker가 제공하는 나머지 기능들(CLI, API, 빌드 등)이 필요 없어. 다 Kubernetes 자체에서 처리하니까. 그래서 Docker 지원을 끊을 수 있었던 거야.

이 강좌에서 컨테이너에 대해 이야기할 때 Docker를 예시로 드는 경우가 있는데, Kubernetes 오케스트레이션 세계로 들어가기 전에 컨테이너 자체가 어떻게 작동하는지 먼저 배우는 과정이니까 괜찮아. 컴퓨터에 Docker가 없고 ContainerD만 쓰는 경우에도, docker 명령을 nerdctl 명령으로 바꾸면 대부분 동일하게 작동해.


정리

2장 읽고 기억할 거 세 가지:

  1. 클러스터 = 컨트롤 플레인 + 워커 노드. 마스터에는 etcd(저장), scheduler(배치), controller(상태 유지), apiserver(허브)가, 워커에는 kubelet(실행)과 kube-proxy(네트워크)가 있어
  2. Docker 런타임은 빠졌지만 Docker 이미지는 살아있어. CRI 표준 → ContainerD가 직접 쓰이게 됐고, CLI는 nerdctl(범용)과 crictl(디버깅)로 대체
  3. Docker를 안 배워도 되는 건 아니야. 런타임에서 빠진 거지 빌드/개발 도구로서의 Docker는 여전히 표준이고, 이 강좌에서도 컨테이너 개념 설명에 Docker를 계속 써