K8s 기초 #1 쿠버네티스란 — 왜 컨테이너 오케스트레이터가 필요한가
도커 기초 시리즈 6편을 마치면 한 컨테이너를 빌드하고, 실행하고, 데이터를 보존하고, 레지스트리에 올리는 흐름까지 익히게 됩니다. 그러면 자연스럽게 다음 질문이 따라옵니다 — 컨테이너 한 대는 띄울 줄 안다. 그렇다면 컨테이너 100대를, 그것도 죽으면 자동으로 다시 살리고 트래픽에 따라 늘렸다 줄였다 하면서 어떻게 운영할까? 그 질문에 대한 사실상 표준 해답이 쿠버네티스(Kubernetes, K8s) 입니다. 이 시리즈는 그 K8s를 처음부터 따라갑니다.
이번 시리즈는 K8s 기초 7편입니다.
- #1 쿠버네티스란 — 왜 컨테이너 오케스트레이터가 필요한가 ← 이번 글
- #2 로컬 환경 — minikube / kind / Docker Desktop k8s
- #3 kubectl과 첫 Pod
- #4 Deployment / ReplicaSet
- #5 Service — ClusterIP / NodePort / LoadBalancer
- #6 ConfigMap / Secret
- #7 Namespace와 라벨
이번 글에서는 단일 컨테이너 도구의 한계와 K8s가 해결하는 문제를 먼저 짚고, 이어서 desired state / reconcile loop와 control plane / worker node의 큰 그림을 정리합니다. 실습은 #2부터 시작합니다.
kubectl apply 가 의도와 다른 에러를 뱉어, 원인을 클러스터 단에서 거꾸로 짚어 가게 됩니다. 매니페스트를 적용하기 전에 utilrepo 의 YAML 검증기 에 한 번 붙여 넣으면 구문 에러를 줄,열 번호로 짚어 줍니다. utilrepo 는 브라우저에서 동작하는 가벼운 웹 유틸리티 모음으로, 비밀 정보가 외부로 나가지 않고 --- 로 묶인 다중 문서 매니페스트와 탭,스페이스 혼용 같은 자주 만나는 함정까지 함께 잡아 줍니다.docker run만으로 부족해지는 순간
#
도커 기초 시리즈 마지막 #6에서 한 컨테이너를 운영용으로 띄우는 명령을 정리했습니다. 이런 모양이었습니다.
docker run -d --name myapp \
--restart unless-stopped \
-p 127.0.0.1:8000:8000 \
-v myapp-data:/app/data \
ghcr.io/curtis/myapp:1.0.0호스트 한 대에 컨테이너 하나, 또는 Compose로 같은 호스트에 web + db + cache를 묶는 정도까지는 이걸로 충분합니다. 그러나 트래픽이 늘고, 무중단 배포가 요구되고, 서버 한 대가 죽어도 서비스는 살아 있어야 하는 단계가 오면 같은 도구로는 풀리지 않는 문제들이 생깁니다.
1) 노드 한 대가 죽으면 서비스도 같이 죽는다. docker run으로 띄운 컨테이너는 그 호스트에 묶여 있습니다. 호스트가 재부팅되거나 디스크가 망가지면, 컨테이너를 다른 머신으로 옮길 표준 방법이 없습니다. 사람이 새 머신에 SSH로 들어가 같은 명령을 다시 쳐야 합니다.
2) 컨테이너가 죽었을 때의 자동 복구가 약하다. --restart unless-stopped는 같은 호스트 안에서만 통합니다. 호스트 자체가 살아 있다면 도커 데몬이 컨테이너를 다시 띄우지만, 호스트가 죽으면 거기까지입니다. 그리고 “프로세스는 살아 있는데 응답을 못 한다” 같은 부분 장애는 잡지 못합니다.
3) 인스턴스 수를 트래픽에 맞춰 조절하는 일이 전부 수동이다. 평소 2대로 충분한 API 서버를 캠페인 시간 동안 10대로 늘리려면, 누군가 새 머신을 마련하고, 같은 이미지를 받아 같은 명령을 10번 실행한 다음, 앞단의 로드밸런서 설정까지 손으로 바꿔야 합니다. 끝나면 다시 줄입니다. 수작업이 많이 필요합니다.
4) 무중단 배포(롤링 업데이트)를 직접 짜야 합니다. v1.0.0에서 v1.1.0으로 옮길 때, 10대 중 한 대씩 새 버전으로 교체하면서 헬스체크가 통과한 컨테이너로만 트래픽을 보내는 흐름은 손으로 짜기 까다롭습니다. 실패 시 자동 롤백까지 포함하면 더 복잡합니다.
5) 설정,비밀 값,서비스 디스커버리에 표준이 없습니다. DB 접속 정보를 환경변수로 줘야 하는데, 환경(stage/prod)마다 다른 값을 안전하게 주입하는 표준적인 방법이 도커 기본 도구에는 없습니다. 컨테이너 A가 컨테이너 B를 어떤 이름으로 찾을지(서비스 디스커버리)도 직접 정해야 합니다.
이 다섯 가지가 한 호스트, 한 컨테이너 단위 도구의 한계입니다. 그리고 이 한계를 푸는 것이 컨테이너 오케스트레이터의 역할입니다.
컨테이너 오케스트레이터가 푸는 문제 #
위의 다섯 항목을 한 줄로 추상화하면 이렇습니다.
여러 머신을 한 덩어리(클러스터)로 묶고, “어떤 모양으로 돌아야 하는가"만 선언하면, 시스템이 알아서 그 모양을 유지해 줍니다.
이를 부르는 이름은 선언형 desired state와 **reconcile loop(맞추기 루프)**입니다.
- 선언형 (declarative) — “어떻게 만들지(how)“가 아니라 **“무엇이어야 하는지(what)”**만 적습니다. 예: “myapp 컨테이너 3개가 항상 떠 있어야 합니다.”
- reconcile loop — 시스템이 끊임없이 현재 상태(actual state)와 원하는 상태(desired state)를 비교하고, 차이가 있으면 그 차이를 줄이는 방향으로 움직입니다. 컨테이너 한 개가 죽어 2개가 됐다면, 시스템이 알아서 한 개를 더 띄워 다시 3개로 맞춥니다.
┌──────────────────┐ ┌──────────────────┐
│ desired state │ │ actual state │
│ "myapp 3 replicas"│ ◀───▶ │ 현재 떠 있는 2개 │
└──────────────────┘ compare └──────────────────┘
│ │
└────────┬──────────────┘
▼
┌─────────────┐
│ controller │ → 차이 → "한 개 더 띄워라"
└─────────────┘이 모델이 K8s의 핵심 발상입니다. 사람은 “이렇게 돼 있어야 한다"만 선언하고, 그 선언을 계속 지키는 일은 시스템이 합니다. 노드 하나가 죽어도, 새 버전을 점진적으로 굴려도, 트래픽 따라 5개를 8개로 늘려도 — 모두 같은 모델 안에서 해결됩니다.
쿠버네티스가 어디서 왔나 #
이 모델은 K8s가 처음 만든 것이 아닙니다. 구글이 사내에서 10년 이상 운영하던 Borg라는 클러스터 관리 시스템이 그 뿌리입니다. 구글의 검색,지메일,맵 같은 서비스는 Borg 위에서 운영되었고, 그 경험을 바탕으로 다음 세대 시스템인 Omega가 나왔습니다. 이후 그 발상을 오픈소스로 다시 구현한 것이 Kubernetes입니다.
대략의 흐름은 다음과 같습니다.
- 2014년 6월 — 구글이 Kubernetes의 초기 버전을 오픈소스로 공개
- 2015년 7월 — Kubernetes 1.0 출시. 같은 시점에 **CNCF(Cloud Native Computing Foundation)**가 발족되며 K8s가 첫 프로젝트로 기증됨
- 이후 — 주요 클라우드 사업자(AWS EKS, GCP GKE, Azure AKS)가 모두 매니지드 K8s를 제공하고, 사실상 컨테이너 오케스트레이션의 표준이 됨
“Kubernetes"는 그리스어로 키잡이(helmsman)를 뜻합니다. 약자 K8s는 K와 s 사이의 8글자를 줄여 쓴 것입니다. 두 표기는 자주 섞여 쓰이며, 같은 대상을 가리킵니다.
K8s 클러스터의 큰 그림 #
K8s 클러스터는 두 종류의 노드로 구성됩니다 — control plane과 worker node.
┌───────────────────────────────────┐
│ Control Plane │
│ │
kubectl ───▶ │ kube-apiserver ◀──────┐ │
│ │ │ │
│ ▼ │ │
│ etcd │ │
│ │ │
│ kube-scheduler ───────┤ │
│ kube-controller-manager ─┤ │
│ cloud-controller-manager ┘ │
└────────────┬───────────────────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ Worker Node 1 │ │ Worker Node 2 │ │ Worker Node 3 │
│ │ │ │ │ │
│ kubelet │ │ kubelet │ │ kubelet │
│ kube-proxy │ │ kube-proxy │ │ kube-proxy │
│ containerd │ │ containerd │ │ containerd │
│ │ │ │ │ │
│ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │
│ │ Pod │ ... │ │ │ Pod │ ... │ │ │ Pod │ ... │
│ └──────┘ │ │ └──────┘ │ │ └──────┘ │
└────────────────┘ └────────────────┘ └────────────────┘Control plane — 클러스터의 뇌 #
control plane은 클러스터의 desired state를 받아 보관하고, 그 상태가 실제로 유지되도록 결정을 내리는 부분입니다.
- kube-apiserver — 클러스터의 정문.
kubectl을 비롯한 모든 도구는 이 API 서버에 REST로 요청을 보냅니다. 인증,인가,검증을 거친 뒤 결과를 etcd에 기록합니다. - etcd — 클러스터의 모든 상태(어떤 Pod가 어디에 있어야 하는지, 어떤 Service가 정의돼 있는지 등)를 보관하는 분산 키-값 저장소. 클러스터의 단일 진실 공급원(source of truth) 입니다.
- kube-scheduler — 새로 만들어진 Pod에게 “어느 worker node에 갈지"를 정해 줍니다. 노드의 가용 자원, 라벨, 제약 조건을 보고 결정합니다.
- kube-controller-manager — 여러 컨트롤러를 한 프로세스로 묶어 돌립니다. 위에서 본 reconcile loop를 실제로 수행하는 주체입니다. 예: ReplicaSet 컨트롤러는 “Pod 3개가 떠 있어야 한다"는 선언과 실제 떠 있는 수를 끊임없이 비교합니다.
- cloud-controller-manager — 클라우드(AWS/GCP/Azure 등) 위에 올라간 클러스터에서, 그 클라우드의 로드밸런서,디스크,노드 같은 자원을 K8s 리소스와 연결해 줍니다. 자체 데이터센터에 올린 클러스터에는 없을 수 있습니다.
Worker node — 일을 하는 곳 #
worker node는 실제로 컨테이너가 실행되는 머신입니다. 보통 노드 한 대에 다음 세 가지가 설치되어 있습니다.
- kubelet — 노드의 에이전트. apiserver로부터 “이 노드에 이 Pod를 띄우라"는 지시를 받아, 컨테이너 런타임에 실제 실행을 요청합니다. 컨테이너의 헬스를 살피고 상태를 다시 apiserver에 보고하는 일도 합니다.
- kube-proxy — 노드의 네트워크 규칙(iptables/IPVS 등)을 관리해 Service(#5)의 가상 IP가 실제 Pod로 전달되도록 합니다.
- 컨테이너 런타임 — 실제로 컨테이너를 실행하는 계층. 현재 표준은 containerd이고, CRI-O도 자주 쓰입니다. 과거에 K8s가 도커 데몬을 직접 쓰던 어댑터(dockershim)는 Kubernetes 1.24(2022)부터 제거되었습니다. 그래서 “K8s가 도커를 쓴다"는 말은 더 이상 정확하지 않습니다. K8s는 OCI 호환 런타임을 쓰고, 도커가 만든 이미지(OCI 이미지)는 그대로 동작합니다.
흐름 한 번에 #
kubectl apply -f deployment.yaml을 친 순간 일어나는 일을 줄여 보면 이렇습니다.
kubectl이 apiserver에 “이 Deployment가 있어야 한다"는 요청을 보냄- apiserver가 인증,검증 후 그 사실을 etcd에 기록
- controller-manager 안의 Deployment 컨트롤러가 “Pod 3개가 더 필요하다"고 판단해 Pod 객체 3개를 만듦
- scheduler가 그 Pod 각각에 노드를 할당
- 해당 노드의 kubelet이 그 정보를 보고 containerd에게 컨테이너 실행을 지시
- kubelet이 결과를 다시 apiserver에 보고 → 사용자는
kubectl get pods로 그 상태를 확인
전체가 apiserver를 거쳐 etcd에 적힌 desired state를, 각 컴포넌트가 자기 영역에서 reconcile하는 한 가지 패턴으로 흐릅니다.
핵심 리소스 한 표 #
K8s에서 사람이 다루는 단위는 컨테이너가 아니라 리소스(resource, object) 입니다. Pod, Deployment, Service 같은 이름들입니다. 시리즈 다음 편들에서 하나씩 깊게 다루지만, 미리 한 표로 모양을 잡아 두겠습니다.
| 리소스 | 한 줄 정의 | 다루는 회차 |
|---|---|---|
| Pod | 컨테이너 1개 또는 같이 동작하는 몇 개를 묶은 K8s의 최소 실행 단위. IP를 공유함 | #3 |
| ReplicaSet | “이 Pod 템플릿이 N개 떠 있어야 한다"를 보장 | #4 |
| Deployment | ReplicaSet 위에서 롤링 업데이트,롤백을 다루는 상위 리소스. 사람이 가장 자주 만지는 단위 | #4 |
| Service | 여러 Pod 앞단에 안정적인 가상 IP/DNS 이름을 붙여 주는 네트워크 추상화 | #5 |
| ConfigMap | 설정값(환경변수, 설정 파일)을 코드와 분리해 주입 | #6 |
| Secret | 비밀 값(비밀번호, 토큰, 인증서)을 분리해 주입. base64 인코딩 + 접근 제어 | #6 |
| Namespace | 클러스터 내부의 논리 구획. 권한,리소스 한도,이름 충돌을 분리 | #7 |
이 여섯 가지를 손에 익히면 일상적으로 다루는 일의 상당 부분을 처리할 수 있습니다. 더 깊은 리소스(Job/CronJob, StatefulSet, Ingress, PersistentVolume 등)는 K8s 중급에서 다루겠습니다.
도커 컴포즈와 무엇이 다른가 #
도커 기초 시리즈 독자에게 가장 먼저 떠오르는 비교 대상은 도커 컴포즈(Docker Compose) 입니다. Compose도 YAML 한 파일에 여러 컨테이너를 선언적으로 정의한다는 점에서 K8s와 닮았기 때문입니다. 그러나 둘은 푸는 문제의 범위가 다릅니다.
| Docker Compose | Kubernetes | |
|---|---|---|
| 다루는 범위 | 한 호스트 안의 멀티 컨테이너 | 여러 노드(클러스터) 위의 컨테이너 묶음 |
| 자가 복구 | 같은 호스트에서 재시작 정도 | 노드가 죽어도 다른 노드로 자동 이전 |
| 스케일링 | --scale로 수동 | desired state로 선언 → 자동 유지, 오토스케일링 가능 |
| 무중단 배포 | 직접 짜야 함 | Deployment의 롤링 업데이트가 기본 |
| 설정/비밀 | .env, secrets 정도 | ConfigMap / Secret이 1급 리소스 |
| 적합한 곳 | 로컬 개발, 단일 서버 운영 | 여러 머신 위 운영 / 본격 SLA 환경 |
Compose는 한 호스트의 개발,운영을 단순하게 만드는 도구이고, K8s는 그 한계를 넘어서는 운영 환경에서 사용됩니다. 둘은 경쟁 관계라기보다 서로 다른 단계의 문제를 풉니다. 로컬 개발은 여전히 Compose가 가볍고, 본 운영은 K8s 또는 그 매니지드 서비스가 자연스럽습니다.
이번 시리즈에서 다룰 범위 #
K8s는 넓은 주제입니다. 이 7편에서 어디까지 다룰지 미리 정해 두겠습니다.
이 K8s 기초 시리즈가 다루는 것:
- 로컬에 K8s 클러스터 한 대 띄우기 (#2)
kubectl로 첫 Pod를 띄우고 살펴보기 (#3)- Deployment로 여러 개의 Pod를 안정적으로 굴리기, 새 버전 배포 (#4)
- Service로 내,외부 접근 만들기 (#5)
- ConfigMap/Secret으로 설정,비밀 다루기 (#6)
- Namespace와 라벨로 클러스터를 정리하기 (#7)
이 7편을 마치면 로컬 클러스터 한 대에 작은 웹 앱을 K8s 방식으로 배포하고 운영할 수 있는 수준에 도달합니다.
다루지 않는 부분은 다음 시리즈에서 이어 가겠습니다.
| 주제 | 시리즈 |
|---|---|
| Ingress, PersistentVolume, StatefulSet, Job/CronJob, RBAC | K8s 중급 |
| Helm, kustomize, GitOps(Argo CD/Flux), 멀티 클러스터 | K8s 실전 |
| 클러스터 직접 부트스트랩(kubeadm), 업그레이드, 백업/복구, 보안 하드닝 | K8s 운영 |
| EKS / GKE / AKS 같은 매니지드 클러스터 운영 | 클라우드 K8s 시리즈 |
기초 시리즈에서는 YAML로 리소스를 정의하고, kubectl로 그 리소스를 클러스터에 적용하고, 결과를 들여다보는 한 사이클을 손에 익히는 데 집중하겠습니다.
정리 #
이번 글에서 잡은 그림:
- 단일 컨테이너 도구만으로는 노드 장애,자동 복구,스케일링,무중단 배포,설정 관리 다섯 가지가 풀리지 않는다. 이 지점이 컨테이너 오케스트레이터가 등장한 배경이다.
- K8s의 핵심 발상은 선언형 desired state + reconcile loop입니다. “무엇이어야 하는지"만 적으면 시스템이 그 모양을 끊임없이 유지합니다.
- K8s는 구글의 사내 시스템 Borg의 발상을 오픈소스로 다시 구현한 것이며, 2015년 1.0 출시 이후 CNCF로 이관되어 사실상의 표준이 되었습니다.
- 클러스터는 control plane(kube-apiserver / etcd / kube-scheduler / kube-controller-manager / cloud-controller-manager)과 worker node(kubelet / kube-proxy / containerd)로 나뉩니다. 모든 흐름은 apiserver를 통해 etcd에 기록된 desired state를 각 컴포넌트가 reconcile하는 패턴으로 정리할 수 있습니다.
- 일상적으로 다루는 핵심 리소스는 Pod / Deployment / Service / ConfigMap / Secret / Namespace 여섯 가지입니다. 이 시리즈가 그 여섯 가지를 한 편씩 다룹니다.
- Docker Compose는 한 호스트, K8s는 여러 노드를 다룹니다. 비슷해 보이지만 서로 다른 단계의 문제를 풉니다.
다음 — 로컬에서 K8s 한 대 띄우기 #
이번 글에서는 K8s가 왜 필요한지와 큰 그림을 정리했습니다. 다음 글에서는 로컬 환경에서 실제 클러스터를 띄우는 방법을 다루겠습니다.
#2 로컬 환경 — minikube / kind / Docker Desktop k8s에서는 (1) 로컬에서 K8s를 시험하는 세 가지 방법(minikube / kind / Docker Desktop의 내장 K8s), (2) 각자의 장단점과 어떤 경우에 무엇을 선택할지, (3) kubectl 설치와 첫 컨텍스트 전환, (4) kubectl get nodes로 첫 클러스터를 확인하는 방법까지 다루겠습니다. 로컬 클러스터를 준비한 뒤 #3으로 넘어가겠습니다.