도커 첫걸음 시리즈에서 만든 hello-api 이미지를 쿠버네티스에 올립니다. 로컬 클러스터 도구별로 내가 빌드한 이미지를 클러스터에 넣는 법, 디플로이먼트 YAML을 한 줄씩 읽는 법, 파드를 죽여도 되살아나는 것 확인하기, replicas로 개수 늘리기, 새 버전을 무중단으로 교체하는 롤링 업데이트와 한 줄 롤백을 실습합니다. 이어서 서비스가 라벨로 파드를 찾는 원리를 셀렉터 실험실 위젯으로 확인하고, ClusterIP·NodePort·LoadBalancer의 차이, ConfigMap·Secret으로 설정 분리, readinessProbe로 준비된 파드에만 트래픽 보내기까지 다룹니다.
3편에서 파드를 직접 만들어 봤고, 한 가지 약점을 확인했습니다. 직접 만든 파드는 죽으면 끝이라는 것. 실제 앱은 파드를 직접 만들지 않고 디플로이먼트에게 "이 이미지로 파드를 N개 유지해"라고 맡깁니다. 그리고 파드들 앞에 서비스를 세워 바뀌지 않는 주소를 줍니다. 이 두 개가 쿠버네티스에서 앱을 배포하는 기본 조합이고, 회사 저장소에서 가장 많이 보게 될 YAML입니다.
재료는 도커 첫걸음 4편에서 만든 파이썬 웹 API hello-api 이미지입니다. 아직 없다면 그 글의 1~3장을 따라 docker build -t hello-api:0.2 .까지 해 두세요.
도커 5편에서 말했듯 쿠버네티스는 빌드하지 않고, 이미지를 레지스트리에서 받아 옵니다. 운영에서는 GHCR 같은 레지스트리에 올리는 것이 정석이지만, 로컬 클러스터에서는 매번 올리기 번거로우니 도구마다 지름길이 있습니다.
로컬 도구
내 이미지를 쓰는 법
Rancher Desktop (엔진 dockerd)
그대로 된다 — docker build한 이미지를 쿠버네티스가 같은 엔진에서 본다
OrbStack
그대로 된다 — 빌드한 이미지가 바로 파드에서 보인다
Docker Desktop (kubeadm 방식)
그대로 된다. kind 방식은 레지스트리에 올리거나 아래 kind 방법
kind
kind load docker-image hello-api:0.2 --name dev
minikube
minikube image load hello-api:0.2
k3d
k3d image import hello-api:0.2 -c dev
⚠️
로컬 이미지에 latest 태그를 쓰지 마세요. 태그가 latest(또는 생략)이면 쿠버네티스는 기본적으로 매번 레지스트리에서 새로 받으려고 시도합니다(imagePullPolicy: Always). 레지스트리에 없는 로컬 이미지라 ImagePullBackOff가 납니다. 0.2처럼 버전 태그를 붙이면 기본값이 IfNotPresent(있으면 그대로 사용)가 되어 로컬 이미지를 씁니다. 도커 4편의 "latest 금지" 습관이 여기서 또 도움이 됩니다.
2. 첫 디플로이먼트
실습 폴더를 만들고 k8s/deployment.yaml을 작성합니다.
bash
mkdir -p ~/docker-lab/hello-api/k8s && cd ~/docker-lab/hello-api
찍어 낸 파드에 붙일 라벨. 반드시 selector와 일치해야 한다 (안 맞으면 apply가 거부된다)
디플로이먼트 YAML은 결국 "몇 개" + "누가 내 담당인지(라벨)" + "파드 틀"입니다. 적용합니다.
bash
kubectl apply -f k8s/deployment.yaml
kubectl get deployments
kubectl get pods -l app=hello-api
-l app=hello-api는 "이 라벨이 붙은 것만"이라는 필터입니다. 파드 이름이 hello-api-6f8c7d9b4-x2k9p처럼 생겼는데, 가운데 부분은 ReplicaSet의 이름입니다. 디플로이먼트는 버전마다 ReplicaSet을 하나씩 만들고, ReplicaSet이 실제로 파드 개수를 맞춥니다. 우리가 직접 만질 일은 없지만, 롤백이 가능한 이유가 이것입니다(5장).
bash
kubectl get replicasets
3. 자가 치유 확인 — 파드를 죽여 보자
1편 시뮬레이터에서 본 것을 진짜 클러스터에서 확인합니다. 터미널을 두 개 엽니다. 한쪽에서 지켜보고:
bash
kubectl get pods -l app=hello-api -w
다른 쪽에서 파드 하나를 지웁니다(이름은 목록에서 하나 골라 복사).
bash
kubectl delete pod <hello-api-로 시작하는 파드 이름>
지운 파드가 Terminating되는 사이 새 이름의 파드가 곧바로 생겨Running이 됩니다. 3편의 맨 파드와 정반대입니다. 디플로이먼트가 "3개여야 하는데 2개네?"를 알아채고 메운 것입니다.
4. 늘리고 줄이기 — scale
bash
kubectl scale deployment hello-api --replicas=5
kubectl get pods -l app=hello-api
5개가 됩니다. 이 명령은 편하지만, 팀에서 쓸 때는 주의가 필요합니다. YAML 파일에는 여전히 replicas: 3이라 적혀 있어서, 다음에 누군가 kubectl apply를 하면 3개로 돌아갑니다. 원칙은 YAML을 고치고 apply하는 것입니다(6편).
selector — 이 라벨이 붙은 파드에게 트래픽을 보낸다. 서비스는 파드 이름이 아니라 라벨로 대상을 찾습니다.
port: 80 — 서비스가 받는 포트. targetPort: 8000 — 파드(컨테이너)가 듣는 포트.
bash
kubectl apply -f k8s/service.yaml
kubectl get service hello-api
클러스터 안에서는 이제 http://hello-api(같은 네임스페이스) 또는 http://hello-api.default.svc.cluster.local로 접속됩니다. 도커 5편에서 compose가 db:5432처럼 서비스 이름으로 통신하게 해 준 것과 같은 원리입니다. 확인해 봅시다.
bash
kubectl run tmp --rm -it --image=busybox:1.37 -- wget -qO- http://hello-api
내 컴퓨터에서는 port-forward로 서비스에 연결합니다. 파드 하나가 아니라 서비스로 연결하는 것이 3편과 다릅니다.
bash
kubectl port-forward service/hello-api 8080:80
서비스가 라벨로 파드를 고른다는 것이 정확히 무슨 뜻인지, 아래 실험실에서 셀렉터를 바꾸고 파드의 준비 상태를 꺼 보며 확인해 보세요. 한 글자 오타가 어떤 결과를 내는지도요.
실제 클러스터에서 서비스가 어떤 파드에 연결됐는지는 이렇게 봅니다. 목록이 비어 있으면 셀렉터와 라벨이 안 맞는 것입니다.
bash
kubectl get endpointslices -l kubernetes.io/service-name=hello-api
웹 서비스를 도메인(api.example.com)으로 공개할 때는 보통 서비스 앞에 Ingress(또는 새 표준인 Gateway API)를 둡니다. 여러 서비스를 도메인·경로별로 나눠 주는 관문입니다. 입문 단계에서는 "ClusterIP 서비스 + 개발 중엔 port-forward, 공개는 Ingress" 정도로 기억하면 됩니다.
Secret은 암호화가 아닙니다. 기본 Secret은 값을 base64로 인코딩할 뿐이라 kubectl get secret -o yaml로 누구나 풀어 볼 수 있습니다. 그래서 ① Secret YAML은 Git에 커밋하지 않고 ② 네임스페이스 권한으로 볼 수 있는 사람을 제한하고(6편) ③ 운영에서는 Sealed Secrets, External Secrets(클라우드 비밀 저장소 연동) 같은 도구를 씁니다. 도커의 ".env 커밋 금지" 원칙 그대로입니다.
ConfigMap을 바꿔도 이미 떠 있는 파드의 환경변수는 바뀌지 않습니다. 파드를 새로 띄워야 반영됩니다.
bash
kubectl rollout restart deployment hello-api
8. 준비된 파드에만 트래픽 — readinessProbe
파드가 Running이어도 앱이 아직 시작 중일 수 있습니다(DB 연결, 모델 로딩 등). 그때 트래픽을 보내면 에러가 납니다. readinessProbe는 "이 주소가 응답해야 준비된 것"이라고 알려 줍니다. 위 셀렉터 실험실에서 NotReady 파드가 트래픽을 못 받던 것이 바로 이것입니다.
-f k8s/처럼 폴더를 주면 안의 YAML 전부에 적용됩니다. apply도 마찬가지 — kubectl apply -f k8s/ 한 줄로 디플로이먼트와 서비스를 함께 올립니다. 이 폴더를 Git에 커밋하는 것이 6편의 출발점입니다.
마치며
🧠
4편 요약
· 앱은 디플로이먼트로: replicas(몇 개) + selector(라벨) + template(파드 틀)
· 로컬 이미지는 도구별 지름길(Rancher·OrbStack은 그대로, kind는 kind load), 태그는 latest 금지
· 이미지 태그를 바꿔 apply → 롤링 업데이트, 문제 시 rollout undo 후 YAML도 되돌리기
· 서비스는 라벨로 파드를 찾아 고정 이름을 준다 — 클러스터 안에서 http://hello-api
· 설정은 ConfigMap, 비밀은 Secret(암호화 아님, Git 금지), 준비 판단은 readinessProbe
지금까지는 kubectl만 썼습니다. 그런데 파드가 수십 개가 되면 명령을 매번 치기가 버겁습니다. 다음 편에서는 kubectl을 왜 기본으로 익혀야 하는지, 그리고 k9s·Headlamp·Freelens·Lens·kubectx·stern·Helm 같은 도구가 각각 무엇을 더해 주는지 비교합니다.