cmod.ify
[Technical Review] Wattup #4 VirtualBox 기반 K8s 클러스터 구축과 트러블슈팅 본문
728x90
반응형
DEPLOYMENT GUIDE
WattUp K8s 배포 전과정
VirtualBox · kubeadm · Calico · 트러블슈팅 포함
전체 배포 흐름
| 단계 | 내용 |
|---|---|
| 1VM 생성 | VirtualBox VM 4개 생성 (master + worker 3) |
| 2기본 설정 | hostname, /etc/hosts, swap 비활성화, 커널 모듈 |
| 3containerd | 컨테이너 런타임 설치 |
| 4K8s 바이너리 | kubeadm / kubelet / kubectl 설치 |
| 5클러스터 생성 | kubeadm init (master) |
| 6worker join | worker 노드 3대 클러스터 합류 |
| 7CNI 설치 | Calico 네트워크 플러그인 |
| 8StorageClass | local-path-provisioner (PVC용) |
| 9포트포워딩 | VirtualBox NAT 포트포워딩 설정 |
| 10이미지 빌드 | Debezium 커스텀 이미지 빌드 & 푸시 |
| 11yaml 수정 | ConfigMap, Secret 세팅 |
| 12배포 | 순서대로 kubectl apply |
| 13접속 확인 | curl / 브라우저로 서비스 확인 |
접근 구조
OCI (Frontend: React + Docker)
↓ Tailscale VPN
nginx (NodePort 30007) ← 유일한 외부 진입점
↓
backend-svc ← ClusterIP (내부 전용)
↓
postgres / mongo / kafka ← ClusterIP (내부 전용)
↑ ↓
debezium kafka
(CDC 캡처) ↓
mongo ← 싱크
VM 환경 구성
1
VirtualBox VM 생성
| 역할 | hostname | IP |
|---|---|---|
| master | master | 192.168.56.10 |
| worker1 | worker1 | 192.168.56.11 |
| worker2 | worker2 | 192.168.56.12 |
| worker3 | worker3 | 192.168.56.13 |
| 항목 | 값 |
|---|---|
| OS | Ubuntu 24.04 |
| CPU | 2 core |
| RAM | 8GB |
| Disk | 50GB |
네트워크 어댑터는 어댑터1: NAT (인터넷), 어댑터2: Host-Only (VM간 통신) 두 개로 설정한다.
netplan — 고정 IP 설정 (각 VM)
# /etc/netplan/00-installer-config.yaml
network:
version: 2
renderer: networkd
ethernets:
enp0s3:
dhcp4: true
enp0s8:
dhcp4: false
addresses:
- 192.168.56.10/24 # 각 VM에 맞게 변경
bash
sudo netplan apply
2
모든 VM 기본 설정 (master + worker 전부)
bash — hostname 설정
# 각 VM에서 자신의 역할에 맞게
sudo hostnamectl set-hostname master # master에서
sudo hostnamectl set-hostname worker1 # worker1에서
bash — /etc/hosts, swap 비활성화, 커널 모듈
# /etc/hosts에 추가
192.168.56.10 master
192.168.56.11 worker1
192.168.56.12 worker2
192.168.56.13 worker3
# swap 비활성화
sudo swapoff -a
sudo sed -i '/ swap / s/^/#/' /etc/fstab
# 커널 모듈
sudo modprobe overlay
sudo modprobe br_netfilter
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
sudo sysctl --system
3
containerd 설치 (모든 VM)
bash
sudo apt update
sudo apt install -y containerd
sudo mkdir -p /etc/containerd
containerd config default | sudo tee /etc/containerd/config.toml
# systemd cgroup 활성화 (필수!)
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
sudo systemctl restart containerd
sudo systemctl enable containerd
# 확인
sudo systemctl status containerd # active (running) 이어야 함
4
kubeadm / kubelet / kubectl 설치 (모든 VM)
bash
sudo apt-get update && sudo apt-get install -y apt-transport-https ca-certificates curl
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.29/deb/Release.key \
| sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] \
https://pkgs.k8s.io/core:/stable:/v1.29/deb/ /" \
| sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt update
sudo apt install -y kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl
5
클러스터 생성 (master에서만)
bash
sudo kubeadm init \
--apiserver-advertise-address=192.168.56.10 \
--pod-network-cidr=192.168.0.0/16
# kubeconfig 설정
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
# 확인 (CNI 설치 전이라 NotReady가 정상)
kubectl get nodes
⚠️ 주의완료 후 출력되는
kubeadm join ... 명령어를 반드시 복사해두세요. worker join에 필요합니다. 잃어버렸다면 master에서 kubeadm token create --print-join-command로 재발급 가능합니다.6
worker 노드 join
bash — worker1, worker2, worker3에서 각각 실행
sudo kubeadm join 192.168.56.10:6443 \
--token <token> \
--discovery-token-ca-cert-hash sha256:<hash>
bash — master에서 확인
kubectl get nodes # 4개 노드가 보이면 성공
7
CNI 설치 — Calico (master에서)
bash
kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml
# 모두 Ready 될 때까지 대기
kubectl get nodes -w
8
StorageClass 설치 (master에서)
PVC(PersistentVolumeClaim)를 자동으로 처리하기 위해 local-path-provisioner를 설치하고 기본 StorageClass로 지정한다.
bash
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml
kubectl patch storageclass local-path \
-p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
# 확인 — local-path (default) 표시되면 성공
kubectl get storageclass
9
VirtualBox 포트포워딩 설정
호스트 PC에서 K8s 서비스에 접근하기 위한 포트포워딩. VirtualBox → master VM → 설정 → 네트워크 → 어댑터1(NAT) → 고급 → 포트 포워딩에서 설정.
| 이름 | 호스트 포트 | 게스트 포트 | 용도 |
|---|---|---|---|
| nginx | 50007 | 30007 | 유일한 외부 진입점 |
10
Debezium 이미지 빌드 & 푸시 (호스트 PC)
PostgreSQL CDC 커넥터 플러그인이 포함된 커스텀 Debezium 이미지를 빌드해서 Docker Hub에 푸시한다.
bash
mkdir debezium && cd debezium
docker build -t agn705/wattup-debezium:v1.0 .
docker push agn705/wattup-debezium:v1.0
11
yaml 파일 수정
01-configmap.yaml에 schema.sql과 nginx.conf 내용을 붙여넣고, Secret은 별도로 생성한다.
📁 파일 구조
k8s-prod/
├── 00-namespace.yaml — Namespace
├── 01-configmap.yaml — schema.sql, nginx.conf
├── 02-databases.yaml — PostgreSQL, MongoDB
├── 03-kafka.yaml — Kafka 3-node KRaft
├── 04-debezium.yaml — Debezium CDC
├── 05-apps.yaml — importer, backend
└── 06-nginx.yaml — Nginx (외부 진입점)
환경변수 처리 기준
| 변수 | 처리 방식 |
|---|---|
| POSTGRES_PASSWORD | Secret (kubectl create) |
| MONGO_INITDB_ROOT_PASSWORD | Secret (kubectl create) |
| DATA_GO_KR_SERVICE_KEY | Secret (kubectl create) |
| KAFKA_BOOTSTRAP_SERVERS | yaml 하드코딩 (내부 FQDN) |
| GROUP_ID / *_TOPIC | yaml 하드코딩 (고정값) |
배포
12
배포 순서
bash — yaml 파일 master VM으로 복사
scp -r ./k8s-prod ubuntu@192.168.56.10:~/
bash — Secret 생성 후 순서대로 배포
# Namespace 먼저
kubectl apply -f k8s-prod/00-namespace.yaml
# Secret 생성
kubectl create secret generic ev-secret \
--from-literal=POSTGRES_USER=... \
--from-literal=POSTGRES_PASSWORD=... \
--from-literal=POSTGRES_DB=... \
--from-literal=MONGO_INITDB_ROOT_USERNAME=... \
--from-literal=MONGO_INITDB_ROOT_PASSWORD=... \
--from-literal=DATA_GO_KR_SERVICE_KEY=... \
-n ev-prod
# 순서대로 배포
kubectl apply -f k8s-prod/01-configmap.yaml
kubectl apply -f k8s-prod/02-databases.yaml
kubectl apply -f k8s-prod/03-kafka.yaml
# Kafka Ready 대기 (중요!)
kubectl wait --for=condition=Ready pod -l app=kafka -n ev-prod --timeout=180s
kubectl apply -f k8s-prod/04-debezium.yaml
kubectl apply -f k8s-prod/05-apps.yaml
kubectl apply -f k8s-prod/06-nginx.yaml
bash — 상태 확인
kubectl get pods -n ev-prod
kubectl get svc -n ev-prod
kubectl get pvc -n ev-prod
트러블슈팅 1 — Secret .env 파일 오류
문제
.env 파일 형식 오류로 Secret 생성 실패.
해결
파일 대신
--from-literal로 항목을 직접 넣어서 생성하면 형식 오류가 없다.트러블슈팅 2 — PostgreSQL command 무시 문제
문제
02-databases.yaml에서 command 필드가 씹혀서 DB가 제대로 초기화되지 않았다.
원인
PostgreSQL 공식 이미지는
command가 아니라 args로 인자를 받는다.
해결
command → args로 변경 후, PVC까지 삭제하고 재배포 (PVC를 안 지우면 빈 폴더가 남아서 같은 오류 반복).bash
kubectl delete -f 02-databases.yaml
kubectl delete pvc --all -n ev-prod # 꼭!
kubectl apply -f 02-databases.yaml
트러블슈팅 3 — PostgreSQL CrashLoopBackOff (권한 오류)
문제
chmod: changing permissions of '/var/lib/postgresql/data': Operation not permitted 에러로 postgres-0가 계속 죽었다.
원인
local-path-provisioner가 PVC 디렉토리를 root 소유로 생성하는데,
runAsUser: 999 설정을 같이 넣으면 오히려 권한이 없어진다.
해결
securityContext에서
runAsUser, runAsGroup을 제거하고 fsGroup: 999만 남긴다. K8s가 볼륨 소유권을 fsGroup으로 자동 처리해준다.yaml — 수정 후
securityContext:
fsGroup: 999 # runAsUser, runAsGroup 제거
트러블슈팅 4 — Kafka 데드락 (KRaft 쿼럼 실패)
문제
kafka-0 혼자 먼저 떠서 쿼럼을 못 맞춰 계속 죽는 데드락 상태.
원인
3-node KRaft는 과반수 노드가 동시에 떠야 리더 선출이 가능한데, StatefulSet 기본 정책이 순차 실행이라 0번이 먼저 뜨고 1, 2번을 기다리게 된다. 이때 0번 혼자서는 쿼럼 불가.
해결
headless Service에
publishNotReadyAddresses: true, StatefulSet에 podManagementPolicy: Parallel 추가해서 3개 동시에 뜨도록 변경.yaml
# kafka-headless Service
spec:
publishNotReadyAddresses: true
# Kafka StatefulSet
spec:
podManagementPolicy: Parallel
트러블슈팅 5 — Kafka init container (busybox) 문제
문제
busybox init container가 node.env 파일을 emptyDir에 써두고 메인 컨테이너로 넘겨주는 구조였는데, 파일 공유가 제대로 안 돼서 Kafka가 node ID를 못 읽었다.
해결
init container 통째로 제거. 메인 컨테이너에서 HOSTNAME을 보고 직접 node ID를 계산하도록 변경. 구조도 단순해지고 부팅 속도도 빨라졌다.
yaml — 수정 후 command
command:
- sh
- -c
- |
ORDINAL=${HOSTNAME##*-}
export KAFKA_NODE_ID=$((ORDINAL + 1))
export KAFKA_ADVERTISED_LISTENERS="PLAINTEXT://${HOSTNAME}.kafka-headless.ev-prod.svc.cluster.local:9092"
exec /etc/kafka/docker/run
트러블슈팅 6 — MongoDB readinessProbe timeout
문제
MongoDB Pod이 계속 Not Ready 상태. readinessProbe가 통과를 못 했다.
원인
readinessProbe timeout 기본값이 1초인데, mongosh 실행이 1초 안에 끝나지 않아서 계속 실패 처리됐다.
해결
timeoutSeconds: 5 추가.yaml
readinessProbe:
exec:
command: ["mongosh", "--eval", "db.adminCommand('ping')"]
initialDelaySeconds: 15
periodSeconds: 10
failureThreshold: 10
timeoutSeconds: 5 # 이거 추가
트러블슈팅 7 — importer CrashLoopBackOff (KeyError)
문제
importer가
KeyError: 'DATABASE_DSN'으로 바로 죽었다.
원인
코드가 요구하는 환경변수 키 이름과 yaml에 넣어준 키 이름이 달랐다.
해결
05-apps.yaml importer env에
DATABASE_DSN 추가.yaml
- name: DATABASE_DSN
value: "postgresql://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@postgres-svc:5432/$(POSTGRES_DB)"
트러블슈팅 8 — backend CrashLoopBackOff (pydantic ValidationError)
문제
backend가 pydantic_settings ValidationError로 죽었다. POSTGRES_URL, MONGO_URL, MONGO_DB가 없다고 했다.
원인
config.py가 요구하는 키 이름과 yaml env에 넣어준 키 이름이 달랐다.
해결
backend env에 코드가 실제로 요구하는 키로 추가.
yaml
- name: POSTGRES_URL
value: "postgresql://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@postgres-svc:5432/$(POSTGRES_DB)"
- name: MONGO_URL
value: "mongodb://$(MONGO_INITDB_ROOT_USERNAME):$(MONGO_INITDB_ROOT_PASSWORD)@mongo-svc:27017"
- name: MONGO_DB
value: "root"
트러블슈팅 9 — nginx readinessProbe 404
문제
nginx Pod이 계속 Not Ready.
GET /에 404를 반환했다.
원인
readinessProbe가
/로 요청하는데 nginx.conf에 / 경로 처리가 없었다.
해결
readinessProbe path를
/health로 변경하고, nginx.conf에 /api/와 /health location을 추가.nginx.conf
location /api/ {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /health {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
13
접속 확인 & 데이터 적재
| 주소 | 확인 내용 |
|---|---|
| http://localhost:50007 | 메인 서비스 |
| http://localhost:50007/api/docs | backend API 문서 |
bash — 공공 데이터 적재 (importer)
# importer에 port-forward로 임시 접근
kubectl port-forward --address 0.0.0.0 svc/importer-svc -n ev-prod 8081:8000
# 서울 충전소 데이터 수집 (호스트 PC에서)
curl -X POST "http://192.168.56.10:8081/import/seoul?confirm=true"
# → {"items_fetched":71800,"stations_upserted":12706,...}
bash — 서비스 확인
curl http://localhost:50007/api/wattup/map/강남구
# → {"regionName":"강남구","stations":[...]}
🛠 내부 서비스 임시 접근
kubectl port-forward svc/kafka-ui-svc 8080:8080 -n ev-prod
kubectl port-forward svc/debezium-svc 8083:8083 -n ev-prod
유용한 명령어 모음
bash — 상태 확인
kubectl get pods -n ev-prod
kubectl logs <pod명> -n ev-prod
kubectl describe pod <pod명> -n ev-prod
bash — 재시작
kubectl rollout restart deployment <서비스명> -n ev-prod
bash — 전부 날리고 재배포
kubectl delete statefulset --all -n ev-prod
kubectl delete deployment --all -n ev-prod
kubectl delete pvc --all -n ev-prod
kubectl apply -f k8s-prod/
bash — 무중단 배포
kubectl apply -f k8s-prod/05-apps.yaml
kubectl rollout restart deployment/backend -n ev-prod
728x90
반응형
'Project' 카테고리의 다른 글
| [Optimization] Wattup #5 K8s 클러스터 메모리 최적화 (1) | 2026.03.12 |
|---|---|
| [ARCHITECTURE] Wattup #3 OCI/VirtualBox-K8s | 하이브리드 네트워크 설계 (0) | 2026.03.11 |
| [Troubleshooting] Wattup #2 프론트엔드 트러블 슈팅 (0) | 2026.03.10 |
| [Project Intro] Wattup #1 프로젝트 소개 (2) | 2026.03.10 |
| [Technical Review] 말랑이 메이커 #3 AWS Resource Groups 및 태그 관리 방법 (0) | 2026.02.25 |