Cloud in practice

From a monolith to NKS: rebuilding the stack in four layers

TL;DR — We moved a service that was locked into a single deployment — where small changes were scary — onto NKS (Naver Kubernetes Service) on Naver Cloud. Instead of a full rewrite, we split it into four layers: infrastructure, DB, API, frontend. Infrastructure became code with Terraform, the DB was pulled out into managed Cloud DB, and deployment moved to ArgoCD GitOps so Git is the single source of truth. The win isn't a performance number — it's being able to know again what changes when you touch something.

Aging systems share a symptom: small changes are frightening. To fix one line you redeploy the whole thing, and nobody is sure what else moves with it. Builds run inside an external SaaS console; deploys are stitched together by hand. This service was the same. It ran fine — but it was scary to change.

So the decision wasn't "make it faster." It was "make it changeable again." A full rewrite is high-risk and can't be paused midway. Instead we split the system into four layers — infrastructure, data, API, frontend — defined each as code, and rebuilt them on NKS on Naver Cloud.

Multi-app architecture on NCP NKS — Users, NCP ALB, NKS cluster (Frontend, API, Back-office), Cloud DB, and the GitLab/Jenkins/NCR/ArgoCD GitOps pipeline
The request flow (top) and the GitOps CI/CD pipeline (bottom) — on NCP NKS

Not all at once — layer by layer

The most common modernization mistake is "let's rewrite it in something modern" and change everything at once. That turns into a bet you can't stop. We went the other way. Draw the boundaries first, peel off one layer at a time.

Frontend is what the user sees, the API is business logic, the DB is state, infrastructure is the floor the other three run on. When those four responsibilities are tangled in one lump, touching anything shakes everything. So the whole job was setting boundaries where each layer builds independently, deploys independently, and can die and come back independently without taking the rest down.

Infrastructure: NKS as code

First, infrastructure moved from console clicks to code. Terraform declares the NKS cluster, networking, managed DB, and access control. The cluster runs Kubernetes 1.34, with worker nodes starting at four 8 vCPU / 16 GB machines.

Networking was secure by default. Every worker node sits in a Private Subnet with no public IP — there's no direct path from the outside into the cluster. User traffic enters only through an Ingress behind the NCP ALB.1 Inside the VPC, subnets are split by purpose — workers, load balancers, DB — and traffic between them is narrowed down to the port level with ACGs (security groups).

A single Ingress owns routing. Requests starting with /api go to the API service (8080); everything else goes to the frontend (3000). HTTP (80) auto-redirects to HTTPS (443). Those rules live in the manifest, so when routing changes, it shows up in a Git diff.

Once infrastructure was code, one thing changed: environments became reproducible. "How was this stood up again?" no longer depends on someone's memory — terraform apply is the answer.

Data: pulled out as managed

The DB doesn't live in the cluster. We pulled it out into Cloud DB for MySQL on Naver Cloud, separating state from compute.2 Pods should be disposable — free to die and come back anytime — and data must not be. Not binding the two to the same lifecycle is the starting point.

The production DB runs HA Multi-Zone so it survives a zone failure; the development DB is a lighter standalone instance. The DB sits in a dedicated DB subnet, with an ACG that opens port 3306 only inside the VPC. From outside, it's invisible.

The API connects using credentials injected as environment variables. Schema is locked so the app can't change it on a whim — Prisma migrations make every schema change an explicit step. No app quietly altering tables on startup.

Apps: rebuilt as containers

The API, the frontend, and a back-office for operations — three apps, each rebuilt as a container. Multi-stage Docker builds separate the build stage from the runtime stage, so the final image carries only what's needed to run. Every container runs as a non-root user.

For Kubernetes to judge an app's health correctly, the app has to report its state honestly. So we added readiness, liveness, and startup probes so Kubernetes checks pod state through a health endpoint. Apps run at two replicas by default and scale out under load with HPA. Secrets go in as Secrets, config as ConfigMaps — no settings baked into the image — so the same image boots in both development and production.

The API runs on Node with NestJS + Prisma. More than the framework choice, the point is that each app now has its own Dockerfile and its own deployment lifecycle.

Deployment, where Git is the truth

If a person pushes all these manifests with kubectl apply, we're back where we started — who changed what, when, scatters. So deployment moved to ArgoCD GitOps.3 A Git repository defines the cluster's desired state, and ArgoCD continuously reconciles the two.

Environment differences are handled with Kustomize base / overlays.4 Shared definitions live in base; only the deltas between dev and prod (namespace, domain, image tag, profile) get patched per overlay. Because both environments branch from the same base, "it worked in dev but not in prod" shrinks.

ArgoCD runs automated sync with self-heal and prune on. If someone changes the cluster by hand and drifts from Git, ArgoCD pulls it back to the Git state. Drift self-heals. The truth of the cluster lives in Git, not in the cluster.

Working on something similar?Get a free 30-min diagnosis

Bringing CI in-house

The other half was CI. Source used to live in an external SaaS and builds ran inside a GUI console — control sat outside our hands. We brought it in with self-hosted GitLab CE and Jenkins on NKS via Helm.5 Source and builds now run inside our own network.

The flow is simple. Push to main → Jenkins webhook trigger → build the image → push to the NCR (Container Registry) → update the image tag in the GitOps repo's kustomization.yaml to the new build → ArgoCD detects the change on roughly a 3-minute cycle and deploys automatically. The build number becomes the image tag, so there's no break in tracking what's running.

One thing we deliberately did not change: CD stayed on ArgoCD. We brought only CI (source and build) in-house and kept the deployment mechanism. Not changing everything at once — that's how you move without stopping.

What changed

The biggest shift is that Git history became deployment history. What changed, when, and why all live in one place. Infrastructure rebuilds with terraform apply, apps boot from the same image swapping only environment, and drift gets reverted by self-heal. The question "how is this running again?" moved from people's memory into a repository.

We left the honest limits in, too. The node pool is still a fixed four — no node-level autoscaling yet; pod autoscaling (HPA) is the current scope. Terraform state management is still simple. Modernization isn't a job that finishes — it's the job of getting the system into a state where you can touch the next thing. What we bought wasn't speed. It was that room.


This kind of NKS/NCP infrastructure modernization and GitOps shift is part of what we do in Cloud & Infrastructure and Product Engineering. The point isn't "we switched to something modern" — it's keeping it changeable afterward.

References

Platform behavior and configuration above follow the official docs below; cluster, DB, and node specs are the actual values from one project.

Footnotes

  1. Naver Cloud Platform — Kubernetes Service (NKS) guide. https://guide.ncloud-docs.com/docs/k8s-overview

  2. Naver Cloud Platform — Cloud DB for MySQL guide. https://guide.ncloud-docs.com/docs/clouddbformysql-overview

  3. Argo CD — Declarative GitOps CD for Kubernetes. https://argo-cd.readthedocs.io/en/stable/

  4. Kustomize — Kubernetes native configuration management. https://kubectl.docs.kubernetes.io/references/kustomize/

  5. Jenkins — Installing Jenkins on Kubernetes. https://www.jenkins.io/doc/book/installing/kubernetes/

Want this work done for you?

A free 30-minute consult to set the direction first.

Already trusted by 24 teams — finance · healthcare · media · public
Get a free 30-min diagnosis
Get a free 30-min diagnosis