cmod.ify

[Technical Review] Wattup #4 VirtualBox 기반 K8s 클러스터 구축과 트러블슈팅 본문

Project

[Technical Review] Wattup #4 VirtualBox 기반 K8s 클러스터 구축과 트러블슈팅

modifyC 2026. 3. 11. 00:35
728x90
반응형
WattUp K8s 배포 가이드
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 joinworker 노드 3대 클러스터 합류
7CNI 설치Calico 네트워크 플러그인
8StorageClasslocal-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 생성
역할hostnameIP
mastermaster192.168.56.10
worker1worker1192.168.56.11
worker2worker2192.168.56.12
worker3worker3192.168.56.13
항목
OSUbuntu 24.04
CPU2 core
RAM8GB
Disk50GB

네트워크 어댑터는 어댑터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) → 고급 → 포트 포워딩에서 설정.

이름호스트 포트게스트 포트용도
nginx5000730007유일한 외부 진입점
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_PASSWORDSecret (kubectl create)
MONGO_INITDB_ROOT_PASSWORDSecret (kubectl create)
DATA_GO_KR_SERVICE_KEYSecret (kubectl create)
KAFKA_BOOTSTRAP_SERVERSyaml 하드코딩 (내부 FQDN)
GROUP_ID / *_TOPICyaml 하드코딩 (고정값)
배포
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로 인자를 받는다.
해결
commandargs로 변경 후, 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/docsbackend 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
반응형