본문 바로가기
MSA (Micro Service Architecture)/Legacy to Domain-Driven Platform

[MSA 말고 Modular Monolith] 5편 — k3s Namespace 전략과 Traefik Ingress 구성

by kellis 2026. 7. 9.

 


이 시리즈는 글쓴이 본인이 수행한 레거시 서비스를 리뉴얼 전환한 과정에서 수행한 의사 결정 사항들을 정리한 글입니다. 
실제 운영 중인 PHP 레거시 에듀테크 플랫폼을 Python + React + K3S 환경으로 전환하는 과정에서 
아키텍처 설계와 인프라 구성에 대한 내용이 포함되어 있습니다. 

 

목차

 

 

 


 

 

이전 글에서 k3s 클러스터를 세웠다. 노드가 Ready 상태이고, Traefik까지 내장된 채로 돌아간다. 

이 비어 있는 클러스터에 서비스를 만들어 올리고 싶은 마음이 가득하지만, 그 전에 뼈대부터 세워야한다. 

격리(Namespace)    서비스들을 어떻게 나눠서 담을 것인가
라우팅(Ingress)        외부 요청을 어떻게 서비스로 흘려보낼 것인가

 

이 글에서는 위 두 가지, 즉 올릴 그릇과 트래픽이 흐를 길을 만든다. 실제 서비스(Platform-api, 프론트엔드 앱 등) 는 다음 글에서 만들어서 올리겠다. 

 

 


 

 

1. 먼저 알아야 할 쿠버네티스 오브젝트 

쿠버네티스 초급자들을 위해, 계속 등장할 쿠버네티스의 기본 오브젝트들을 정리하고 들어가겠다. 쿠버네티스는 결국 아래 네 가지 오브젝트들을 조합해서 애플리케이션을 굴리는 시스템이기 때문에 이들의 관계를 이해하면 훨씬 쉬워진다. 

 

Pod — 실행의 최소 단위

Pod는 쿠버네티스가 컨테이너를 실행하는 가장 작은 단위이다. 보통 컨테이너 하나가 Pod 하나에 담긴다. 

Pod는 언제든 사라지고 다시 생길 수 있다는 것이 중요한 특성이다. 노드에 문제가 생기거나, 배포를 새로 하거나, 부하 분산을 위해 옮겨질 때 Pod는 지워지고 새로 만들어진다. 그때 IP도 바뀐다. 그래서 Pod를 직접 상대하지 않고, 그 앞에 안정적인 층을 하나 두는데 이가 바로 Service이다. 

 

Deployment — Pod를 관리하는 관리자

Pod를 직접 만들면, 그게 죽었을 때 아무도 되살리지 않는다. 그래서 실무에서는 Pod를 직접 만들지 않고 Deployment로 만든다.

Deployment는 "이 이미지를 가진 Pod를 항상 N개 유지하라"고 선언하는 오브젝트다. Pod가 죽으면 자동으로 새로 띄우고, 개수를 늘리거나(스케일 아웃) 새 버전으로 무중단 교체 (롤링 업데이트) 하는 일도 Deployment가 한다. 

Deployment (선언: "이 이미지 Pod를 N개 유지")
      └── Pod × N (실제 실행)

 

platform-api를 여러 개 띄워 부하를 분산하는 것도, 결국 Deployment에 "몇 개 띄워라" 라고 선언하는 일이다.

 

Service — 변하지 않는 접속 주소

Pod는 언제든 사라질 수 있고, IP가 변경되기 때문에 다른 컴포넌트가 이 Pod에 안정적으로 접속할 방법이 필요하다. 그 답이 Service이다. 

Service는 여러 Pod 앞에 서서 고정된 하나의 접속 지점을 제공한다. Pod가 죽고 새로 뜨든, 개수가 바뀌든, 외부에서는 Service라는 하나의 주소로만 접근하면 된다. Service가 알아서 살아 있는 Pod로 요청을 분배한다. 

 

Ingress — 외부에서 들어오는 관문

Service까지 만들면 클러스터 내부에서는 서로 통신할 수 있다. 그러나 외부 사용자가 들어오려면 관문이 하나 더 필요하다. 그게 바로 Ingress 이다.

외부 요청 → Ingress → Service → Pod

 

 

네 가지의 관계

 

 


 

 

2. Namespace 전략 

Namespace란?

 

kubernetes의 Namespace는 하나의 클러스터 안을 논리적으로 나누는 경계다. 앞서 설명한 Pod, Service, Deployment, Ingress 같은 리소스를 그룹으로 묶어서 구분한다. 물리적으로 서버를 나누는게 아니라, 같은 클러스터 위에서 이름표로 영역을 가르는 것이다. 

 

무엇을 기준으로 나눌 것인가

Namespace를 나누는 기준은 여러 가지가 있다. 

환경별         dev / staging / prod
계층별         app / data / monitoring
도메인별      identity / academy / learning ...

 

이 글에서는 계층/역할별로 구분할 것이다. 

 

  • 환경별로 나누지 않는 이유 
    • 환경으로 나누는 방법은 두 가지이다. 하나는 클러스터 안에서 namespace로 나누는 것(dev-platform, prod-platform...), 다른 하나는 클러스터 자체를 환경별로 따로 두는 것이다. 글에서 다루고 있는 환경은 후자다. 현재 세팅중인 환경은 개발클러스터와 운영 클러스터가 이미 환경적으로 분리되어 있다. 따라서 클러스터 안에서 환경별 namespace를 만들 필요가 없다.
  • 도메인별로 나누지 않는 이유 
    • platform-api가 하나의 배포 단위인 modular monolith 구조이기 때문이다. 이는 중요한 내용이므로 아래에서 하나의 섹션으로 다루겠다. 

서로 성격이 다른 배포 단위들을 계층으로 나눈다. 

platform                애플리케이션 계층 (platform-api, 프론트엔드, svc-c/d)
data                      데이터 계층 (redis, 그리고 dev에선 MySQL·BDB)
monitoring            관측 계층 (Prometheus, Grafana, Loki )
kube-system         k3s 기본 (이미 존재, 건드리지 않음)

 

이렇게 구분하면 관리 단위가 명확해진다. 

kubectl get pods -n platform으로 애플리케이션만 골라 볼 수 있고, 추후에 namespace별로 리소스 제한(ResourceQuota)이나 네트워크 정책을 걸기도 쉽다. 무엇보다 이 논리적 구분이 운영계의 물리적 분리(app/db)로 자연스럽게 이어진다. 

 

 

 개발과 운영, namespace 구조는 같다

 

여기서 짚고 넘어가야 할 부분은 "운영으로 넘어가면 이 namespace 구조가 바뀌나?" 하는 것이다. 답은 아니다. 

namespace 구조는 개발과 운영이 동일하다. platform / data / monitoring 이라는 계층 구분은 그대로다.

 

바뀌는 건 namespace 구조가 아니라, 각 namespace를 어느 노드에 배치하느냐이다.

dev (단일 노드)
     한 노드 안에 platform / data / monitoring 이 모두 올라감
     → data namespace의 redis, MySQL도 이 노드에서 돈다
운영 (다중 노드)
     app01 노드 ── platform (platform-api, 프론트, svc-c/d)
                        └─ monitoring
     db01 노드 ── data (MySQL, redis, Berkeley DB)

 

개발 서버는 단일 노드라 모든 namespace가 한 서버에 몰려 있다. 

운영 서버는 data namespace만 db 노드로 분리하고 나머지는 app 노드에 둔다. 즉 data(데이터 계층)만 물리적으로 빠지는 것이지, namespace를 나누는 전략 자체는 바뀌지 않는다.

 

이런 노드 배치는 쿠버네티스의 nodeSelector 나 taint/toleration 으로 지정한다. "data namespace의 Pod는 db 노드에만 뜨게하라"고 선언하는 식이다. 

 

계층별로 namespace를 나눠둔 것이 여기서 빛을 발한다. 처음부터 data를 별도 namespace로 떼어놨기 때문에, data만 db  서버로 옮기는 게 깔끔하게 된다. 

 

 

왜 도메인별로 Namespace를 나누지 않는가

만약 MSA였다면 도메인마다 namespace를 나누었을 것이다. 그러나 이 글에서는 그렇게 하지 않는다. 

 

이유는 심플하다. platform-api가 하나의 배포 단위이기 때문이다.

 

namespace는 여러 개의 독립적인 배포 단위를 격리하는 도구다. 그런데 우리의 platform-api는 도메인은 여러개지만 하나의 프로세스, 하나의 컨테이너 이미지, 하나의 Deployment로 돌아간다. 나눌 대상 자체가 없다. 

 

MSA였다면
     Identity Service → identity namespace (독립 배포)
     Academy Service → academy namespace (독립 배포)
     Learning Service → learning namespace (독립 배포)
     ... 도메인마다 별도 배포 단위 → namespace로 격리 필요
Modular Monolith (우리)
     platform-api 하나
     ├── identity (코드 안의 모듈)
     ├── academy (모듈)
     ├── learning (모듈)
     └── ...
     → 하나의 배포 단위 → namespace로 나눌 게 없음

 

도메인 경계는 이미 코드 안에 있다. Bounded Context 는 platform-api 내부의 모듈 구조로 표현된다. 이 경계는 쿠버네티스가 아니라 코드가 지킨다. 

 

비유하자면 이러하다. 

namespace로 나누기  = 건물을 여러 채로 나누기 (MSA)
모듈로 나누기              = 한 건물 안을 방으로 나누기 (Modular Monolith)

 

우리는 한 건물(platform-api) 안을 방(모듈)로 나눴다. 건물을 여러 채로 지을 이유가 없다. 오히려 그것이 MSA 의 복잡성(네트워크 통신, 분산 트랜잭션, 배포 오케스트레이션) 을 불러오기 때문이다. 

 

정리하면 층위가 다르다

도메인 경계          → 코드 안 모듈로 (platform-api 내부)
배포 단위 격리      → namespace로 (platform / data / monitoring)

 

Namespace는 도메인을 나누는 게 아니라 계층을 나누는 것이다. platform-api는 그 안에서 하나의 배포 단위로 통째로 들어간다.

 

 

 


 

3. Namespace 생성

이제 3개의 namespace를 만든다.

kubectl create namespace platform
kubectl create namespace data
kubectl create namespace monitoring

 

생성 후 전체 목록을 확인해보면 아래와 같다

$ kubectl get namespaces
NAME              STATUS   AGE
data              Active   11s
default           Active   19h
kube-node-lease   Active   19h
kube-public       Active   19h
kube-system       Active   19h
monitoring        Active   4s
platform          Active   16s

 

생성한 namespace 세개가 active 상태로 잘 올라와 있는 것을 확인할 수 있다. (나머지는 기본 namespace들이다.)

 

 


 

4. Traefik Ingress

 

k3s 내장 Traefik

 

Ingress는 "외부에서 들어오는 관문"이다. 단, Ingress는 규칙일 뿐, 그 규칙을 실제로 실행하는 주체Ingress 컨트롤러다. Ingress 규칙을 아무리 정의해도 컨트롤러가 없으면 아무 일도 일어나지 않는다.

k3s에는 Ingress 컨트롤러가 이미 들어 있는데 이 것이 바로 Traefik이다.

외부 요청 → Traefik (Ingress 컨트롤러) → Service → Pod

 

정식 쿠버네티스라면 Nginx Ingress Controller 같은 것을 직접 설치해야 한다. 그러나 k3s는 부팅되면서 Traefik을 자동으로 띄워주기 때문에 "어떤 요청을 어디로 보낼지" 규칙(Ingress)만 정의하면 된다. 

 

 

테스트 전략

 

실제 서비스(platform-api, 프론트엔드 앱들)는 아직 만들지 않았다. 그러나 "외부 요청이 Ingress를 거쳐 서비스까지 도달하는 경로"가 제대로 뚫리는지 검증은 미리 해두고 싶다. 따라서 테스트용 nginx를 만들어서 Ingress → Service → Pod 경로가 동작하는지 확인해 보겠다. (테스트가 끝나면 삭제)

미리 체크해두면 추후에 진짜 서비스를 올릴때 오류가 발생해도 Ingress가 문제인지 내가 올린 앱이 문제인지 헷갈릴 일이 없다.

 

 


 

 

5. Ingress 동작 검증 (테스트 앱)

 

5.1 테스트 앱 배포

 

nginx를 platform namespace에 배포하고 Service로 노출한다.

kubectl create deployment test-nginx --image=nginx -n platform
kubectl expose deployment test-nginx --port=80 -n platform

 

상태를 확인한다.

$ kubectl get pods,svc -n platform
NAME                              READY   STATUS    RESTARTS   AGE
pod/test-nginx-5946d7969f-kcldf   1/1     Running   0          10s

NAME                 TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
service/test-nginx   ClusterIP   10.43.36.143   <none>        80/TCP    4s

 

Pod가 Running 상태이고, Service가 생성됐다. 여기서 보아야 하는 것이 Service의 Type이다. 

TYPE                    ClusterIP
CLUSTER-IP       10.43.36.143   ← 클러스터 내부 전용 주소
EXTERNAL-IP     <none>            ← 외부 접근 불가

 

ClusterIP 타입은 클러스터 내부에서만 접근 가능하다. EXTERNAL-IP 이 <none>인 것이 그 증거이다. 지금 외부에서 이 주소로 접속이 불가능하다. 이 서비스를 외부로 열어주는 것이 Ingress의 역할이다. 

 

 

5.2 Ingress 규칙 생성

 

Ingress를 YAML로 정의해서 적용하다.

cat <<'EOF' | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: test-nginx
  namespace: platform
spec:
  ingressClassName: traefik
  rules:
    - http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: test-nginx
                port:
                  number: 80
EOF

 

핵심 라인은 이러하다. 

ingressClassName: traefik                     k3s 내장 Traefik이 이 규칙을 처리하도록 지정
rules → paths → path: /                         루트 경로(/)로 들어온 요청을
backend → service: test-nginx:80          test-nginx Service의 80포트로 전달

 

"외부에서 / 로 들어오는 HTTP 요청을 test-nginx로 보내라" 는 규칙이다.

 

생성된 Ingress를 확인해보자

$ kubectl get ingress -n platform
NAME         CLASS     HOSTS   ADDRESS		PORTS   AGE
test-nginx   traefik   *       공인IP		   80      5s

 

여기서 중요한 것은 ADDRESS에 서버의 공인IP가 잡혔다는 점이다. Traefik이 Ingress 규칙을 인식하고, 외부에서 이 IP의 80 포트로 들어오는 요청을 받을 준비가 됐다는 뜻이다. 이제 이 Traefik은 실제로 트래픽을 받는다. 

CLASS가 traefik, HOST가 *(호스트 미지정이라 모든 요청 허용)인 것도 확인된다. 

 

 

5.3 외부 접근 확인

 

실제로 접근되는지 검증해보자

$ curl -s http://localhost | head -5
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
$ curl -s http://115.68.195.184 | head -5
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>

 

localhost와 공인IP 둘 다 nginx 기본 페이지가 나타난다. 특히 공인IP로 접근이 된 게 핵심이다. 외부에서 오는 것도 동일한 경로로 요청이 클러스터 안까지 도달했다는 뜻이다. 

응답 헤더도 확인한다. 

$ curl -sI http://localhost
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: 896
Content-Type: text/html
Date: Thu, 02 Jul 2026 03:27:13 GMT
Server: nginx/1.31.2

 

200 OK, 그리고 Server: nginx/1.32.2 요청이Traefik을 거쳐 nginx Pod까지 제대로 도달해 응답이 돌아온 것이다. 

전체 경로가 검증됐다. 

외부 요청 (115.68.195.184:80)
   → Traefik Ingress (규칙 매칭: path /)
      → test-nginx Service (ClusterIP 10.43.36.143:80)
         → test-nginx Pod (nginx 컨테이너)
            → 200 OK + HTML 응답

 

 

5.4 테스트 리소스 정리

 

검증이 끝났으니 테스트 앱은 지운다. 실제 서비스는 6편 이후로 구성하는 내용을 작성해보겠다. 

kubectl delete ingress test-nginx -n platform
kubectl delete service test-nginx -n platform
kubectl delete deployment test-nginx -n platform

 

platform namespace가 깨끗이 비었는지 확인한다.

$ kubectl get all -n platform
No resources found in platform namespace.

 

리소스는 지워졌지만 namespace 자체(platform, data, mornitoring)는 그대로 남는다. 이제 그릇과 길은 준비되었으니, 내용물만 채우면 된다. 

 

 


 

 

6. 서비스는 어떻게 올리는가

이제 자연스러운 질문이 생긴다. "nginx는 기성 이미지라 바로 올렸는데, 우리가 만들 platform-api 또는 프론트엔드 앱들은 어떻게 올리지?"

 

핵심은 직접 만든 서비스든 기성품이든, k3s에 올리는 방식은 똑같다는 것이다. 컨테이너 이미지로 만들어서 올린다.

① 소스코드                platform-api(FastAPI), Frontend(React) 코드를 작성
② Docker 빌드          각 코드를 Dockerfile로 이미지화 (docker build)
③ 레지스트리            빌드한 이미지를 저장소에 push (k3s가 가져갈 수 있게)
④ k3s 배포                Deployment로 "이 이미지를 띄워라" 선언 → Pod 실행 → Service → Ingress

 

nginx 테스트와 실제 서비스의 차이는 딱 하나다. nginx는 이미 만들어진 이미지라 ②③을 건너뛰고 ④만 했다. 우리 서비스는 ①부터 코드를 짜서 ②에서 이미지로 만들어야 한다. 하지만 ④(k3s에 올리는 방식)는 완전히 동일하다.

 

현재 ④의 경로(Deployment → Pod → Service → Ingress)를 미리 검증해뒀으니, 나중에 진짜 platform-api를 올릴 때 그 자리에 이미지만 바꿔 끼우면 된다.

 

각 서비스가 실제로 만들어지는 시점은 이렇게 될 것이다.

 

platform-api    6편에서 FastAPI 구조 설계 → 이후 이미지화·배포
Frontend        9편에서 React Monorepo 구성 → 10편에서 빌드·배포(CI/CD)
svc-c / svc-d   12편에서 PHP 레거시를 리팩토링 없이 이미지화

 

 


 

마치며

 

이번 글에서 한 일들은 이러하다.

 

✓  쿠버네티스 기본 오브젝트 이해 — Pod / Deployment / Service / Ingress

✓  Namespace를 계층/역할별로 설계 (platform / data / monitoring)

✓  도메인별로 나누지 않는 이유 — platform-api는 하나의 배포 단위

✓  세 개의 namespace 생성

✓  Ingress 개념과 k3s 내장 Traefik의 역할 이해

  테스트 앱으로 외부 접근 경로 검증 (공인 IP로 200 OK 확인)

  테스트 리소스 정리 

 

중요 한 것은 

Namespace는 도메인이 아니라 계층을 나눈다. 도메인 경계는 코드 안 모듈이 지킨다.
k3s 내장 Traefik 덕분에, Ingress 규칙만 정의하면 외부 트래픽이 서비스까지 흐른다.

 

서비스를 담을 namespace가 있고, 외부 트래픽이 흐를 Ingress 경로가 검증되었다. 이제 다음 화부터는 platform-api를 만들면서 FastAPI 프로젝트를 어떻게 구성할지, 도메인을 Vertical Slice 구조로 어떻게 코드로 담을지를 다루어보겠다. 

 

 

 

 

**참고 자료

 

Kubernetes Namespaces: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/

 

Namespaces

In Kubernetes, namespaces provide a mechanism for isolating groups of resources within a single cluster. Names of resources need to be unique within a namespace, but not across namespaces. Namespace-based scoping is applicable only for namespaced objects (

kubernetes.io

 

Kubernetes Ingress: https://kubernetes.io/docs/concepts/services-networking/ingress/

 

Ingress

Make your HTTP (or HTTPS) network service available using a protocol-aware configuration mechanism, that understands web concepts like URIs, hostnames, paths, and more. The Ingress concept lets you map traffic to different backends based on rules you defin

kubernetes.io

 

k3s Networking (Traefik): https://docs.k3s.io/networking/networking-services

 

Networking Services | K3s

This page explains how CoreDNS, Traefik Ingress controller, Network Policy controller, and ServiceLB load balancer controller work within K3s.

docs.k3s.io

 

Traefik 공식 문서: https://doc.traefik.io/traefik/

 

Traefik Proxy Documentation - Traefik

What is Traefik? Traefik is an open-source Application Proxy and the core of the Traefik Hub Runtime Platform. If you start with Traefik for service discovery and routing, you can seamlessly add API management, API gateway, AI gateway, and API mocking capa

doc.traefik.io

 

댓글