이 글의 위치: 기술 블로그 → Infra/DevOps · OKE 인프라 학습 시리즈 실습 3편. 무료 클러스터 구축과 인그레스·HTTPS까지 왔다. 이제 진짜 서비스를 얹는다 — Go로 짠 채팅 백엔드를 arm64 OKE 위에 배포한다.
목표 구성은 소박하다. A1 단일 노드(aarch64, 4 OCPU/24GB) 위에 애플리케이션 pod 4~5개 + 스토어(Postgres·Redis·RabbitMQ) pod 3개, 도합 8개 pod를 전부 Running으로 올린다. Postgres·RabbitMQ는 OCI Block Volume(PVC)로 영속, Redis는 무영속. 그 과정에서 무료 티어 특유의 함정을 여럿 만났다.
함정 1 — arm64는 오히려 쉬웠다
A1 노드는 arm64라 "멀티아치 빌드가 번거롭겠다" 걱정부터 했는데, Apple Silicon(M칩)에서 빌드하면 기본이 arm64라 그대로 push하면 됐다. buildx --platform linux/arm64를 명시할 필요조차 없었다(x86 CI에서 빌드한다면 얘기가 다르다 — 그땐 buildx가 필수다).
# Apple Silicon 로컬 빌드 → OCIR push (arm64 그대로)
docker build -t yny.ocir.io/<namespace>/chat-platform/room-gateway-service:latest .
docker push yny.ocir.io/<namespace>/chat-platform/room-gateway-service:latest
OCIR 리포지토리를 public으로 만들어 imagePullSecret 없이 익명 pull이 되게 했다. 데모 규모에선 이게 제일 간단하다.
함정 2 — cri-o는 "짧은 이미지명"을 거부한다
OKE 노드는 Oracle Linux + cri-o 런타임이다. Docker Desktop에 익숙하면 postgres:16-alpine처럼 짧은 이름을 쓰는데, cri-o는 unqualified 이미지명을 거부한다. 퍼블릭 이미지도 레지스트리까지 붙인 fully-qualified 이름이어야 한다.
# ❌ cri-o 거부
image: rabbitmq:3.13-management-alpine
# ✅ fully-qualified
image: docker.io/library/rabbitmq:3.13-management-alpine
image: docker.io/library/postgres:16-alpine
함정 3 — RabbitMQ가 deprecated env로 부팅에 실패
RabbitMQ 매니페스트에 습관적으로 RABBITMQ_VM_MEMORY_HIGH_WATERMARK env를 넣었더니 부팅 자체가 실패했다. 최신 이미지에서 이 env가 deprecated 됐기 때문이다. 해결은 두 가지.
- 그 env를 제거하고, 메모리 상한은 컨테이너
resources.limits.memory로 대체. - readiness probe도 무거운
rabbitmq-diagnostics ping대신tcpSocket: 5672로 교체(느린 diagnostics가 준비 판정을 지연시켰다).
readinessProbe:
tcpSocket:
port: 5672 # rabbitmq-diagnostics ping 은 무겁고 느리다
initialDelaySeconds: 10
함정 4 — 단일 노드에 다 얹으려면 request를 짜야 한다
4 OCPU/24GB 하나에 8개 pod를 욱여넣으려면 스케줄러가 거절하지 않게 resource requests를 보수적으로 잡아야 한다. 실제로 쓴 값의 감을 표로 남긴다(limits는 별도).
| pod | CPU request | Mem request |
|---|---|---|
| 앱 서비스(각) | 50m | 64Mi |
| Postgres | 150m | 256Mi |
| RabbitMQ | 100m | 256Mi |
request는 "스케줄링 최소 보장"이고 limit은 "상한"이다. 이 둘을 낮게·명확히 나눠 잡아야 무료 노드 하나에 스토어와 앱이 공존한다. gateway의 limit은 300m/192Mi, RabbitMQ는 400m/384Mi 정도로 뒀다.
WebSocket을 위한 배포 설계
채팅 게이트웨이는 장수명 WebSocket을 유지한다. 일반 stateless 배포와 다르게 신경 쓸 게 있다.
- sticky + drain: Service에
sessionAffinity: ClientIP, Deployment에terminationGracePeriodSeconds: 60. 롤링 업데이트 때 연결을 천천히 빼기 위함. - 수평 확장 가능: pod 간 메시지 전달은 RabbitMQ fan-out이 담당하므로, HPA(min 1 / max 3, CPU 70%)로 게이트웨이를 늘려도 된다.
- graceful shutdown 순서: readiness=false(LB에서 제외) → fan-out consumer 취소 → 전 WS 강제 종료 → MQ close → HTTP drain. 예산 60초.
인그레스는 경로로 서비스를 나눈다. /api/ums(유저), /api/cms(채팅), /api/pms(게시글), /api/rgs(WebSocket 릴레이), /(프론트). WebSocket은 /api/rgs가 운반한다.
/ → front (React)
/api/ums → user-management-service:8081
/api/cms → chat-management-service:8082
/api/pms → post-management-service:8083
/api/rgs → room-gateway-service:8032 (WebSocket)
결과
배포 후 kubectl get pods -n chat-platform 에서 앱 4~5 + 스토어 3 = 8개 pod가 모두 Running이 됐고, 첫 테넌트 com.seongwon.myapp가 등록됐다. 라이브 URL은 앞 편에서 만든 인그레스 뒤의 https://134-185-107-236.sslip.io. 이 백엔드가 실제로 무엇을 하는지 — 왜 게이트웨이가 "멍청한 파이프(dumb pipe)"여야 하는지는 다음 채팅 백엔드 설계 편에서 이어간다.
참고로 무료 데모라 매니페스트엔 guest/guest·placeholder 시크릿이 남아 있다. 이건 "무료 티어 데모의 현실적 보안 부채"로 인정하고, 프로덕션 전환 시 env-driven 시크릿으로 교체할 지점으로 표시해 뒀다.
답글 남기기