모놀리식을 NKS로 — 네 개의 계층으로 다시 세우기

오래된 시스템에는 공통된 증상이 있다. 작은 변경이 무섭다. 한 줄을 고치려면 전체를 다시 올려야 하고, 무엇이 같이 움직일지 아무도 자신하지 못한다. 빌드는 외부 SaaS 화면 안에서 돌고, 배포는 손으로 이어 붙였다. 이 서비스도 그랬다. 돌아가긴 하는데 바꾸기가 두려운 상태였다.
그래서 목표는 "더 빠르게"가 아니라 **"다시 손댈 수 있게"**였다. 통째로 갈아엎는 재작성은 위험이 크고 중간에 멈출 수도 없다. 그래서 시스템을 인프라, 데이터, API, 프론트엔드 네 계층으로 나누고, 각 계층을 코드로 정의해 NKS 위에 다시 세우기로 했다.
이 사례에 앞서 무엇을 유지하고 리플랫폼·리팩터·리아키텍처할지 정하는 과정은 애플리케이션 현대화 진단과 전환 로드맵에서 따로 정리했다.

통째로가 아니라, 계층으로
모던화에서 흔히 하는 실수는 "현대적인 걸로 다시 짜자"며 모든 걸 한 번에 바꾸려 드는 일이다. 그러면 멈출 수 없는 큰 베팅이 된다. 우리는 반대로 갔다. 경계를 먼저 긋고, 계층별로 떼어냈다.
프론트엔드는 사용자에게 보이는 화면, API는 비즈니스 로직, DB는 상태, 인프라는 그 셋이 돌아가는 바닥이다. 이 넷의 책임이 한 덩어리에 섞이면 어디를 고쳐도 전부가 흔들린다. 이 작업은 각 계층이 독립적으로 빌드되고 배포되고, 혼자 죽었다 살아나도 나머지가 멀쩡하도록 경계를 다시 세우는 일이었다.
인프라: 코드로 만든 NKS
가장 먼저, 인프라를 화면 클릭이 아니라 코드로 옮겼다. Terraform으로 NKS 클러스터, 네트워크, 관리형 DB, 접근 제어를 전부 선언했다. 클러스터는 Kubernetes 1.34, 워커 노드는 8 vCPU / 16GB짜리 4대로 시작했다.
네트워크는 보안을 기본값으로 깔았다. 워커 노드는 전부 Private Subnet에 두고 공인 IP를 주지 않았다. 따라서 워커의 공인 IP를 통한 직접 인바운드 경로는 없다. 사용자 트래픽은 이 구성에서 NCP ALB를 거친 Ingress로만 들어온다.1 VPC 안은 워커용, 로드밸런서용, DB용으로 서브넷을 나눴고, 그 사이 통신은 ACG(보안 그룹)로 포트 단위까지 좁혔다.
라우팅은 Ingress 한 곳이 책임진다. /api로 시작하는 요청은 API 서비스(8080)로, 나머지는 프론트엔드(3000)로 보낸다. HTTP(80)로 들어오면 HTTPS(443)로 자동 리다이렉트한다. 이 규칙이 매니페스트 안에 적혀 있으니 라우팅이 바뀌면 Git diff에 남는다.
ALB 앞에 CDN과 TLS 계층이 더해지면 관찰·차단 지점도 달라진다. SNI·ECH와 CDN 통제 지점에서 그 경계를 따로 정리했다.
인프라가 코드가 되자 환경을 다시 만들 수 있게 됐다. "이게 어떻게 떠 있더라"를 더는 사람 기억에 의존하지 않는다. terraform apply가 답이다.
데이터: 관리형으로 떼어내다
DB는 클러스터 안에 두지 않았다. NCP의 Cloud DB for MySQL로 떼어내 상태를 컴퓨트와 분리했다.2 파드는 언제든 죽였다 다시 띄워도 되는 일회용이어야 하고, 데이터는 그래선 안 된다. 둘을 같은 생애주기에 묶지 않는 것이 출발점이다.
운영 DB는 HA Multi-Zone으로 Master와 Standby를 다른 존에 배치했다. 장애 시 자동 Failover를 지원하지만 완료까지 수분이 걸릴 수 있으므로 애플리케이션 재연결도 함께 검증해야 한다.2 개발 DB는 단독 인스턴스로 가볍게 뒀다. DB는 전용 DB 서브넷에 두고, ACG로 3306 포트를 VPC 내부에서만 열었다. 바깥에서는 보이지 않는다.
API는 환경 변수로 주입한 접속 정보로 DB에 붙는다. 스키마는 애플리케이션이 마음대로 바꾸지 못하게 잠갔다. Prisma 마이그레이션으로 스키마 변경을 명시적인 단계로 묶었으니, 앱이 뜰 때 테이블을 슬쩍 고치는 일은 없다.
앱: 컨테이너로 다시 빌드
API, 프론트엔드, 그리고 운영용 백오피스까지 세 앱을 각각 컨테이너로 다시 빌드했다. 멀티스테이지 Docker 빌드로 빌드 단계와 런타임 단계를 분리해, 최종 이미지에는 실행에 필요한 것만 남겼다. 모든 컨테이너는 non-root 사용자로 돈다.
쿠버네티스가 앱의 생사를 제대로 판단하려면 앱이 자기 상태를 정직하게 말해야 한다. 그래서 readiness·liveness·startup 프로브를 붙여 헬스 엔드포인트로 파드 상태를 확인하게 했다. 기본 2 레플리카로 띄우고, 부하가 오르면 HPA가 파드를 늘린다. 비밀값은 Secret, 환경 설정은 ConfigMap으로 주입해 이미지에 설정을 굽지 않았다. 그래서 같은 이미지가 개발에서도 운영에서도 그대로 뜬다.
API는 Node 기반 NestJS + Prisma로 짰다. 다만 프레임워크 선택보다 각 앱이 자기 Dockerfile과 자기 배포 수명주기를 갖게 된 것이 본질이다.
배포의 기준을 Git으로 옮긴 GitOps
여기까지 만든 매니페스트를 사람이 kubectl apply로 밀면 다시 원점이다. 누가 무엇을 언제 바꿨는지 흩어진다. 그래서 배포를 ArgoCD GitOps로 옮겼다.3 클러스터의 목표 상태는 Git 저장소가 정의하고, ArgoCD가 실제 상태를 계속 거기에 맞춘다.
환경 차이는 Kustomize의 base / overlay로 다뤘다.4 공통 정의는 base에 두고, 개발·운영 차이(네임스페이스, 도메인, 이미지 태그, 프로파일)만 각 overlay가 덮어쓴다. 두 환경이 같은 base에서 갈라지니 "개발에선 됐는데 운영에선 안 돼"가 줄어든다.
자동 동기화에는 self-heal과 prune을 켰다. 누가 클러스터를 손으로 바꿔 Git과 어긋나면 ArgoCD가 도로 Git 상태로 되돌린다. 드리프트가 스스로 복구되는 셈이다. 클러스터의 진실은 클러스터가 아니라 Git에 있다.
CI를 우리 안으로
남은 절반은 CI였다. 기존에는 소스가 외부 SaaS에 있었고 빌드는 GUI 화면 안에서 돌았다. 통제권이 우리 손 밖에 있었다. 이걸 self-hosted GitLab CE와 NKS 위에 Helm으로 띄운 Jenkins로 가져왔다.5 이제 소스도 빌드도 우리 네트워크 안에서 돈다.
GitHub가 소스의 중심인 자사 애플리케이션에서는 러너를 직접 운영하는 대신 Blacksmith로 GitHub Actions의 실행 계층을 바꿨다. CI 도구는 하나로 통일하기보다 소스 저장소와 네트워크 경계에 맞춰 고른다.
흐름은 단순하다. main 브랜치에 푸시 → Jenkins 웹훅 트리거 → 이미지 빌드 → NCR(Container Registry)에 푸시 → GitOps 저장소의 kustomization.yaml에서 이미지 태그를 새 빌드로 갱신 → ArgoCD가 약 3분 주기로 변경을 감지해 자동 배포. 빌드 번호가 곧 이미지 태그이니 무엇이 떠 있는지 추적이 끊기지 않는다.
의도적으로 손대지 않은 게 하나 있다. CD는 그대로 ArgoCD로 뒀다. CI(소스·빌드)만 우리 안으로 가져오고 배포 메커니즘은 유지했다. 한 번에 다 바꾸지 않는 것, 그게 멈추지 않고 옮기는 방법이다.
무엇이 달라졌나
가장 큰 변화는 Git 히스토리가 곧 배포 히스토리가 됐다는 점이다. 무엇이 언제 왜 바뀌었는지가 한곳에 남는다. 인프라는 terraform apply로 다시 만들고, 앱은 같은 이미지에 환경만 갈아끼워 뜨고, 드리프트는 self-heal이 되돌린다. "이거 어떻게 떠 있더라"라는 질문이 사람 기억에서 저장소로 옮겨갔다.
솔직한 한계도 남겨 둔다. 노드 풀은 아직 고정 4대이고 노드 단위 오토스케일은 적용하지 않았다. 파드 오토스케일(HPA)까지가 현재 범위다. Terraform 상태도 아직 단순하게 관리한다. 모던화는 끝나는 작업이 아니라 다음 단계를 손댈 수 있는 상태로 만드는 작업이다. 우리가 산 건 속도가 아니라 그 여지다.
이런 NKS·NCP 기반 전환 사례는 애플리케이션 현대화 인사이트에서 더 볼 수 있다. 구축과 전환은 애플리케이션 현대화와 클라우드 & 인프라에서 다룬다. 전환 뒤에도 계속 손댈 수 있는 구조를 남기는 것이 목표다.
참고 자료
본문의 플랫폼 동작·구성은 아래 공식 문서를 기준으로 했고, 클러스터·DB·노드 스펙은 한 프로젝트의 실제 구성값이다.
출처와 각주5개펼치기접기
Footnotes
-
Naver Cloud Platform, ALB Ingress Controller 설정. Ingress와 Load Balancer 연동·라우팅 구성. ↩
-
Naver Cloud Platform, Cloud DB for MySQL 사용 준비. HA·Standby Master·자동 Failover와 소요 시간. ↩ ↩2
-
Argo CD, Automated Sync Policy. 자동 동기화·prune·self-heal 동작. ↩
-
Kustomize — Kubernetes native configuration management. https://kubectl.docs.kubernetes.io/references/kustomize/ ↩
-
Jenkins — Installing Jenkins on Kubernetes. https://www.jenkins.io/doc/book/installing/kubernetes/ ↩


