도커와 쿠버네티스 첫걸음
- 2.1 컨테이너 이미지 만들고 돌리고 공유하기
- 2.2 쿠버네티스 클러스터 세팅
- 2.3 첫 앱을 쿠버네티스에 올리기
2장은 개념 설명을 잠깐 멈추고 손으로 직접 해보는 장이야. Node.js 앱 하나 짜서 Docker 이미지로 빌드하고, 레지스트리에 푸시하고, 쿠버네티스에 배포하고, 외부에 노출하고, 스케일링까지 한 번에 훑어. 이 흐름을 한 번 돌려보고 나면 뒤에 나올 Pod, ReplicationController, Service 같은 개념들이 추상적으로 안 느껴지거든.
먼저 Docker를 깔고 docker run busybox echo "Hello world" 같은 거부터 돌려봐. 이 한 줄짜리 명령이 내부적으로는 이미지 pull, 컨테이너 생성, 실행, 종료를 다 해내. 중요한 건 이 프로세스가 호스트의 다른 프로세스와 완전히 격리된 채로 돌았다는 거야. 그 다음에 진짜 앱을 만들어 — Node.js로 HTTP 서버 하나 짜는데, 요청이 들어오면 자기 호스트명을 응답에 실어 보내게 만들어. 이게 왜 중요하냐면, 나중에 스케일 아웃했을 때 요청이 어느 인스턴스에 꽂히는지 눈으로 확인할 수 있게 하기 위한 장치거든.
이 앱을 이미지로 만들려면 Dockerfile이 필요해. FROM node:7로 베이스 이미지를 깔고, ADD app.js /app.js로 파일을 복사하고, ENTRYPOINT ["node", "app.js"]로 컨테이너 시작 시 실행할 명령을 지정해. docker build -t kubia .로 빌드하면 한 가지 알아둘 게 있어 — 이때 현재 디렉터리 내용이 통째로 Docker 데몬으로 업로드되고 거기서 빌드가 일어나. 클라이언트에서 빌드하는 게 아니야. 그래서 빌드 디렉터리에 쓸데없는 큰 파일을 넣어두면 안 돼. 그리고 여기서 이미지 레이어 개념도 짚어둘 만한데, Dockerfile의 명령 하나가 레이어 하나를 만들고, 같은 베이스를 쓰는 이미지들은 그 레이어를 공유해. 그래서 node:7 기반 이미지 여러 개를 같은 머신에 받아도 베이스 부분은 한 번만 저장돼.
빌드가 끝나면 docker run --name kubia-container -p 8080:8080 -d kubia로 실행해. -p 8080:8080은 호스트 포트 8080을 컨테이너의 8080으로 매핑하는 거야. curl로 찔러보면 컨테이너 ID가 호스트명으로 돌아와 — 컨테이너 안의 프로세스는 자기만의 UTS 네임스페이스를 갖기 때문이야. 컨테이너 안을 들여다보고 싶으면 docker exec -it kubia-container bash로 셸을 띄우고 ps aux를 찍어봐. 그러면 node 프로세스랑 bash 두어 개 정도만 보여. 호스트의 다른 프로세스는 전혀 안 보이지. PID 네임스페이스 덕분이야. 근데 호스트에서 ps aux | grep app.js를 해보면 같은 node 프로세스가 다른 PID로 보여 — 컨테이너 프로세스는 호스트 OS에서 진짜로 돌고 있고, 단지 자기 네임스페이스 안에서 PID 1로 보일 뿐이라는 증거야. 마지막으로 docker tag kubia luksa/kubia로 태그를 다시 달고 docker push luksa/kubia로 Docker Hub 같은 레지스트리에 푸시해. 이제 이 이미지는 전 세계 어디서든 pull 받아 돌릴 수 있어.
쿠버네티스 클러스터를 직접 세팅하는 건 원래 쉽지 않아. 네트워킹, 인증, etcd HA 같은 걸 다 고려해야 하니까. 그래서 책은 초보자용 두 가지 옵션만 소개해. Minikube는 로컬 머신에 단일 노드 클러스터를 VM 안에 띄워주는 도구인데, minikube start 한 줄이면 끝나. 다중 노드 기능은 못 보지만 대부분의 개념을 학습하기엔 충분해. **Google Kubernetes Engine(GKE)**는 관리형 멀티 노드 클러스터인데, gcloud container clusters create kubia --num-nodes 3으로 워커 3대짜리 클러스터를 띄울 수 있어. 책이 3노드를 권장하는 이유는 스케줄링이 여러 노드에 어떻게 퍼지는지 보여주기 위해서야. 둘 중 뭘 쓰든 kubectl CLI가 필요해. kubectl은 API 서버에 REST 요청을 보내는 클라이언트야. kubectl cluster-info로 클러스터가 살아있는지 확인하고, kubectl get nodes로 노드 목록을, kubectl describe node <name>으로 특정 노드의 상세 정보를 볼 수 있어. 팁 하나 — alias k=kubectl 걸어두고 탭 자동완성 켜두면 삶의 질이 확 올라가.
이제 첫 앱을 쿠버네티스에 올려봐. 보통은 YAML 매니페스트를 쓰지만 2장에선 아직 개념을 배우기 전이니까 한 줄 명령으로 가. kubectl run kubia --image=luksa/kubia --port=8080 --generator=run/v1. 이 한 줄이 뭘 하냐면, ReplicationController를 하나 만들고, 그게 Pod를 하나 생성하고, Pod 안에 컨테이너가 떠. --generator=run/v1은 "Deployment 말고 ReplicationController로 만들어"라는 지시인데, 9장 가기 전까진 Deployment를 안 다루기 때문에 일부러 이걸 쓴 거야.
여기서 쿠버네티스의 첫 번째 중요한 개념이 나와 — Pod. 쿠버네티스는 컨테이너를 직접 다루지 않아. 대신 같이 떠야 하는 컨테이너들을 묶은 그룹, 그러니까 Pod를 다뤄. 한 Pod 안의 컨테이너들은 항상 같은 워커 노드, 같은 Linux 네임스페이스에서 돌아. 그래서 서로 localhost로 통신할 수 있어. Pod는 자기만의 IP를 갖는 논리적인 머신이라고 생각하면 돼. 대부분은 Pod 하나에 컨테이너 하나지만, 사이드카 같은 걸 붙일 땐 여러 개가 될 수도 있어. kubectl get pods로 Pod 목록을 확인하면 처음엔 Pending 상태야. 스케줄러가 노드를 할당하고 그 노드가 이미지를 pull하는 중이거든. 끝나면 Running으로 바뀌어.
kubectl run 한 줄로 뒤에서 뭐가 일어났냐면, 먼저 kubectl이 API 서버에 REST 요청을 보내서 ReplicationController 객체를 만들어. 그 ReplicationController가 "Pod 하나가 필요해" 라고 etcd에 기록하고, Scheduler가 그걸 보고 어느 노드로 갈지 결정해. 그러면 그 노드의 Kubelet이 "내 노드에 Pod가 배정됐네" 하고 Docker에게 이미지 pull과 실행을 지시해. 이게 1장에서 그림으로만 봤던 API Server → etcd → Scheduler → Kubelet → Docker 파이프라인이야.
Pod는 자기 IP를 갖긴 하지만 그건 클러스터 내부용이야. 외부에서 접속하려면 Service 객체를 만들어야 해. kubectl expose rc kubia --type=LoadBalancer --name=kubia-http. type=LoadBalancer로 하면 클라우드 프로바이더가 외부 IP가 붙은 로드밸런서를 프로비저닝해줘 (Minikube는 이걸 지원 안 해서 다른 방법이 필요해). kubectl get services로 잠시 후에 external IP가 뜨고, curl로 찔러보면 응답이 와. Service가 왜 필요하냐면, Pod는 죽고 다시 뜨면 IP가 바뀌거든. 그러면 클라이언트 입장에서는 누구한테 얘기해야 하는지 매번 바뀌는 셈이야. Service는 그 앞에 안정적인 가상 IP를 하나 놓고 뒤의 Pod들로 로드밸런싱해줘. Pod가 교체돼도 Service IP는 유지되고.
마지막으로 수평 확장. Pod를 늘리고 싶으면 kubectl scale rc kubia --replicas=3 한 줄이면 끝이야. ReplicationController의 desired state가 3으로 바뀌고, 컨트롤러가 현재 1개와 비교해서 2개를 더 만들어. kubectl get pods로 보면 3개가 떠 있고, curl을 여러 번 찔러보면 매번 다른 Pod의 hostname이 응답으로 나와 (Service가 라운드로빈 중인 거지). 이게 쿠버네티스의 힘이야. "5개로 늘려줘"라고 말하면 되지, 어느 노드에 띄울지 네트워크를 어떻게 연결할지를 사람이 결정하지 않아.
정리
2장 읽고 기억할 거 세 가지:
- 컨테이너 이미지는 Dockerfile로 빌드하고 레지스트리를 통해 배포한다. 빌드는 호스트가 아니라 Docker 데몬에서 일어나고, 이미지는 레이어로 나뉘어 재사용·공유된다. 컨테이너 안 프로세스는 호스트에서 진짜로 돌지만 자기만의 네임스페이스 때문에 격리돼 보인다.
- 쿠버네티스에서 가장 작은 배포 단위는 컨테이너가 아니라 Pod다. Pod는 같이 떠야 하는 컨테이너들의 묶음이고, 같은 노드·같은 네임스페이스에서 돌며 자기 IP를 갖는다. 대부분은 Pod 하나에 컨테이너 하나.
- Pod를 외부에 노출하려면 Service, 늘리려면 replicas만 바꾸면 된다. Pod IP는 불안정하니까 Service가 앞에서 안정적인 VIP를 제공하고, ReplicationController는 desired state를 감시하며 Pod 수를 맞춘다. 사람은 "뭘 얼마나"만 선언하고 나머지는 시스템이 한다.