[Kubernetes] Naver Cloud Kubernetes Service(NKS)에서 3Tier Application 배포

프로젝트 개요

이번 프로젝트는 Naver Cloud Kubernetes Service(NKS)를 활용하여, 간단한 3-Tier 애플리케이션을 Kubernetes의 기본 리소스만으로 직접 배포하고, 이를 통해 Kubernetes 구성 요소에 대한 이해를 높이는 것에 중점을 두고 있다.

Frontend, Backend, Database 각 구성 요소는 개별적으로 컨테이너화되어 Kubernetes 클러스터에 배포되며, 서비스 간의 연결과 통신이 이루어지도록 하는것이 목표이다. 

아키텍처

애플리케이션 기술 스택

  • Frontend: React
  • Backend: Node.js(Express)
  • Database: MySQL

Kubernetes 리소스 

  • Deployment
  • Service
  • PersistentVolume (PV)
  • PersistentVolumeClaim (PVC)
  • Secret
  • Ingress

NCP 사용 리소스 

  • Naver Cloud Kubernetes Service(NKS) - 1.29.9 version
  • Container Registry
  • Applcation Load Balancer 
  • Global DNS

아키텍처 흐름 및 주요 작업 단계

간단한 게시판 형태의 애플리케이션 소스코드를 Kubernetes 환경에 배포할 예정이다. 일반적으로는 관리형 데이터베이스를 사용하는 경우가 많지만, 본 프로젝트에서는 MySQL을 컨테이너로 직접 구동하여 Kubernetes 리소스와 함께 구성할 예정이다. 

각 컴포넌트는 개별 Pod로 구성되며, 해당 Pod는 각각의 Service(SVC)와 연결된다. 이후, Ingress를 통해 외부와의 접점을 구성한다.

또한, PVC를 이용한 볼륨 관리, Secret을 통한 민감 정보 처리, DNS 설정을 통한 도메인 연결까지 진행하여, SSL 인증서를 제외한 모든 주요 구성 요소를 테스트해보는 것을 목표이다. Node는 하나만 띄워 테스트를 진행하였다. 

  • Docker Image build & Container Registry Push
    → 애플리케이션을 Docker 이미지 생성 
    → 이미지 저장소인 Container Registry에 생성한 이미지 Push 
  • Naver Cloud Kubernetes Service(NKS) 클러스터 생성
    → NCP에서 Kubernetes 클러스터 생성
  • kubectl 설치 및 NKS 접속 
    → 클러스터와 통신하기 위한 kubectl 설치 및 설정
  • Kubernetes 매니페스트 설정
    → Deployment, Service, Ingress 등 리소스 정의 파일 작성
  • 글로벌 DNS 도메인 연결
    → DNS매핑을 통한 도메인 연결

Github

https://github.com/rlaehdwn0105/k8s-3Tier

1. Docker Image build & Container Registry Push

Docker 이미지를 저장할 Container Registry를 먼저 생성한 후, 빌드한 이미지를 해당 레지스트리에 Push해야 한다.
NCP 서버 또는 로컬 환경에서 Dockerfile을 사용해 이미지를 빌드한 뒤, 생성한 Container Registry 주소로 태그를 지정하고 docker push 명령어를 통해 업로드하면 된다. 

<소스코드 설명은 오픈 소스코드를 가져온것이므로 생략하도록 하겠습니다>

Frontend Dockerfile

위 Dockerfile은 멀티 스테이지 빌드(Multi-stage Build) 방식을 사용하여 React 프론트엔드 애플리케이션의 이미지 크기를 줄이고 최적화하는 구조로 작성되어 있다. 

첫 번째 단계에서는 node:21-alpine3.17 이미지를 기반으로 애플리케이션을 빌드한다. /app 디렉토리에 소스를 복사한 후, npm install, npm update, npm run build를 실행하여 정적 파일을 생성한다. 

두 번째 단계에서는 동일한 이미지를 사용하되, 빌드된 결과물만 복사하여 실행 환경을 구성합니다. 이후 npm start 명령어를 통해 Node.js 서버를 실행한다. 

이처럼 멀티 스테이지 빌드를 사용하면 이미지 크기를 줄이고 실행 환경을 구성할 수 있다. 

### Frontend 구조 ###
root@dj-master:~/kubernetes/Frontend# ls
Dockerfile  package.json  package-lock.json  public  README.md  src

### 멀티 스테이지 빌드를 통한 이미지 경량화 ### 
cat Dockerfile 
# 1단계: Node.js를 사용하여 애플리케이션 빌드
FROM node:21-alpine3.17 AS build
WORKDIR /app
COPY . .
ENV NODE_OPTIONS=--openssl-legacy-provider
RUN npm install
RUN npm update
RUN npm run build

# 2단계: 애플리케이션 실행
FROM node:21-alpine3.17
WORKDIR /app
COPY --from=build /app /app
ENV NODE_OPTIONS=--openssl-legacy-provider
CMD ["npm", "start"]

Backend Dockerfile

백엔드는 프론트엔드와 마찬가지로 Node.js 환경에서 동작하므로, node:14 이미지를 기반으로 구성된다. package*.json 파일을 먼저 복사한 뒤 npm install을 통해 필요한 모듈을 설치하고, 전체 소스를 복사한 후 node server.js 명령어를 통해 애플리케이션을 실행한다. 

데이터베이스는 Docker Hub에서 제공하는 mysql:latest를 기반으로 하며, 기본 포트인 3306을 외부에 노출한다. MySQL 데이터는 /var/lib/mysql 경로에 저장되고, 해당 디렉토리를 VOLUME)으로 지정하여 컨테이너 재시작 시에도 데이터가 유지될 수 있도록 구성하였다. 

### backend 구조 ### 
root@dj-master:~/kubernetes/backend# ls
Dockerfile  package.json  package-lock.json  server.js

### backend Dockerfile ###
root@dj-master:~/kubernetes/backend# cat Dockerfile
 FROM node:14 
 WORKDIR /app 
 COPY package*.json ./ 
 RUN npm install 
 COPY . . 
 CMD [ "node" , "server.js" ]

root@dj-master:~/kubernetes/Database# cat Dockerfile-Database 
FROM mysql:latest

# Expose the MySQL port
EXPOSE 3306

# Define a named volume for MySQL data
VOLUME /var/lib/mysql

# Command to run the MySQL server
CMD ["mysqld"]

2. Docker Build & Container Registry Push

이미지 저장소로는 Naver Cloud의 Container Registry를 사용하고 있으며, 이는 AWS의 ECR(Elastic Container Registry)와 같다고 보면 된다. 해당 레지스트리는 외부에서 직접 접근할 수 없도록 Private Registry로 구성되어 있다.

Container Registry에 Docker 이미지를 Push하기 위해서는 먼저 해당 Registry 주소로 로그인해야 한다. 이때 Access Key ID와 Secret Key를 사용하여 인증 과정을 거쳐야 한다. 인증이 완료되면, 빌드한 Docker 이미지에 Registry 주소를 포함한 Tag를 지정하고, docker push 명령어를 통해 이미지를 업로드할 수 있다.

아래 사진은 프론트엔드, 백엔드, 데이터베이스(MySQL)를 각각 개별 이미지로 구분하여 관리하고 있는 모습이다.

root@dj-master:~/kubernetes/# docker login dj-kubetest-container.kr.ncr.ntruss.com
Authenticating with existing credentials...
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credential-stores

Login Succeeded

root@dj-master:~/kubernetes/# cd frontend

root@dj-master:~/kubernetes/frontend/# docker build -t dj-kubetest-container.kr.ncr.ntruss.com/backend-image:latest .
root@dj-master:~/kubernetes/frontend# cd ..
root@dj-master:~/kubernetes/# cd backend/
root@dj-master:~/kubernetes/backend# docker build -t dj-kubetest-container.kr.ncr.ntruss.com/backend-image:latest . 
root@dj-master:~/kubernetes/frontend# cd ..
root@dj-master:~/kubernetes/# cd database/
root@dj-master:~/kubernetes/database# docker build -t dj-kubetest-container.kr.ncr.ntruss.com/mysql-image:latest .

### 생성된 Docker Image ### 
root@dj-master:~/kubernetes/database# docker images
REPOSITORY                                               TAG          IMAGE ID       CREATED         SIZE
dj-kubetest-container.kr.ncr.ntruss.com/frontend-image   latest       e388be37cf5c   5 days ago      253MB
dj-kubetest-container.kr.ncr.ntruss.com/backend-image    latest       b488de0e29d6   5 days ago      694MB
dj-kubetest-container.kr.ncr.ntruss.com/mysql-image      latest       6dca13361869   5 days ago      463MB

### Docker login ### 
root@dj-master:~/kubernetes/database# docker login dj-kubetest-container.kr.ncr.ntruss.com
Authenticating with existing credentials...
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credential-stores

Login Succeeded

API 인증키가 필요하며, Username에 Access Key Id를 Password에 Secret Key를 사용해주어야한다. 

### Docker Push ### 
root@dj-master:~/kubernetes/database# docker push dj-kubetest-container.kr.ncr.ntruss.com/frontend-image:latest
root@dj-master:~/kubernetes/database# docker push dj-kubetest-container.kr.ncr.ntruss.com/backend-image:latest
root@dj-master:~/kubernetes/database# docker push dj-kubetest-container.kr.ncr.ntruss.com/mysql-image:latest

Docker 빌드와 푸시 작업을 완료한 후, Container Registry에서 이미지를 확인해보니 정상적으로 업로드된 것을 확인할 수 있었다. 이제 이 이미지를 사용하여 Deployment를 통해 Kubernetes에 배포할 예정이다.

 

3. Naver Cloud Kubernetes Service(NKS) 클러스터 생성

NKS를 생성하기 위해서는 NKS 클러스터를 배치할 Private Subnet, 로드밸런서를 위한 Public Subnet, 그리고 애플리케이션이 위치할 또 다른 Private Subnet이 각각 하나씩 필요하다. 이번 구성에서는 ALB를 Public Subnet에 배치하고, Ingress를 통해 Private Subnet에 위치한 애플리케이션과 외부 간의 통신을 처리할 예정입니다. 테스트 목적이기 때문에 ACG는 모든 포트를 오픈해주었지만, 추후에는 필요한 포트만 선택적으로 오픈하여 보안에 신경써야한다. 

kubectl 설치 및 NKS 접속 

NKS 클러스터에 접속하려면 먼저 kubectl을 설치하고, kubeconfig를 설정한 다음 NKS의 UUID 경로를 통해 접속해야 한다.
이 작업은 NCP 서버에 해도 되고, 로컬 환경에 해도 상관없다. 현재 편의성을 고려해 로컬 환경에서 설정을 진행하였다.

kubectl 설치 방법은 쿠버네티스 공식 문서에 운영체제별로 정리되어 있으니 참고하면 된다.

https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/#install-using-native-package-management

ncp-iam-authenticator 설치

NKS는 ncp-iam-authenticator를 통해 AWS와 비슷한 구조로 IAM 기반 인증을 제공한다.
kubectl 명령어를 사용하려면 ncp-iam-authenticator를 먼저 설치하고, 이를 인증에 사용할 수 있도록 kubeconfig를 수정해줘야 한다.

# ncp-iam-authenticator다운로드
$ curl -o ncp-iam-authenticator -L https://github.com/NaverCloudPlatform/ncp-iam-authenticator/releases/latest/download/ncp-iam-authenticator_linux_amd64

# SHA-256 다운로드
$ curl -o ncp-iam-authenticator.sha256 -L https://github.com/NaverCloudPlatform/ncp-iam-authenticator/releases/latest/download/ncp-iam-authenticator_SHA256SUMS

# 권한 수정 
$ chmod +x ./ncp-iam-authenticator

# PATH 추가 (예시)
$ export PATH=$PATH:$HOME/bin

# 동작 테스트
root@dj-master:~/kubernetes# ncp-iam-authenticator
cli written to authenticate with iam in ncloud kubernetes service

Usage:
  ncp-iam-authenticator [command]

Available Commands:
  create-kubeconfig Get Kubeconfig to access kubernetes
  help              Help about any command
  token             Authenticate using SubAccount and get token for Kubernetes
  update-kubeconfig update Kubeconfig to access kubernetes
  version           Show the version info of the ncp-iam-authenticator

Flags:
      --credentialConfig string   credential config path (default : ~/.ncloud/configure)
      --debug                     debug option
  -h, --help                      help for ncp-iam-authenticator
      --profile string            profile

Use "ncp-iam-authenticator [command] --help" for more information about a command.
root@dj-master:~/kubernetes/MERN-Stack-Project/DevOps/docker# 

Kubeconfig 설정

ncp-iam-authenticator를 사용하면 IAM 인증이 적용된 kubeconfig를 통해 클러스터에 접근할 수 있다.
Access Key와 Secret Key는 마이페이지 → 인증키 관리 화면에서 확인 가능하다.

# OS 환경변수나 configure파일에 API 키를 설정.
$ export NCLOUD_ACCESS_KEY=ACCESSKEYIDACCESSKEY
$ export NCLOUD_SECRET_KEY=SECRETACCESSKEYSECRETACCESSKEYSECRETACCE
$ export NCLOUD_API_GW=https://ncloud.apigw.ntruss.com

Cluster 접속

생성된 kubeconfig 파일을 이용해 NKS 클러스터 UUID를 기반으로 클러스터에 접속할 수 있다. 이때 클러스터에 접속할 때마다 해당 명령어를 반복해서 입력해야 하므로, 편의상 alias로 등록해두는 것이 좋다. 

# kubeconfig파일 생성
$ ncp-iam-authenticator create-kubeconfig --region KR --clusterUuid <cluster-uuid> --output kubeconfig.yaml

정상적으로 Node가 확인되는 것을 볼 수 있다.

root@dj-master:~/kubernetes# kubectl get nodes
NAME         STATUS   ROLES    AGE   VERSION
kdj-w-5xni   Ready    <none>   12d   v1.29.9

4. Database Kubernetes Manifest

namespace.yaml

애플리케이션 리소스를 논리적으로 격리하기 위해 별도의 namespace를 구성하였다. (mern stack)
현재는 프론트엔드, 백엔드, 데이터베이스를 모두 하나의 네임스페이스에 배포했지만, 프로젝트를 마무리하고 보니 각 구성 요소를 별도의 네임스페이스에 배포하는 방식이 관리 측면에서 더 효율적일 것 같다는 생각이 들었다.

apiVersion: v1
kind: Namespace
metadata:
  name: mern
  labels:
    name: mern

pv.yaml

PersistentVolume은 Kubernetes에서 데이터를 지속적으로 저장할 수 있도록 해주는 리소스로, 파드(Pod)가 삭제되더라도 데이터를 유지할 수 있게 해준다. 하지만 단독으로는 사용할 수 없으며, 반드시 PersistentVolumeClaim(PVC)와 함께 사용해야 한다. PVC는 사용자가 필요한 저장소 용량과 접근 모드를 정의하여 PV에 요청을 보내고, 실제 저장소를 바인딩해 사용할 수 있게 해준다.

/mnt/data 경로를 실제 디스크 위치로 사용하는 hostPath 타입이다.

  • accessModes: ReadWriteOnce는 하나의 노드에서만 읽기/쓰기가 가능하다는 의미이다.
  • persistentVolumeReclaimPolicy: Retain은 PVC가 삭제되더라도 PV 자체는 삭제하지 않고 보존하도록 설정하는 정책이다.

이렇게 구성된 PV는 이후 PVC에 의해 바인딩되어 데이터 저장에 활용된다.

apiVersion: v1
kind: PersistentVolume
metadata:
  name: db-pv
  namespace: mern
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: standard
  hostPath:
    path: /mnt/data

pvc.yaml

앞서 설명한 것처럼, PVC는 위에서 정의한 PV에 저장소를 요청하는 리소스이다. 파드가 필요로 하는 저장소의 사양을 정의하면, 해당 조건에 맞는 PV가 존재할 경우 자동으로 바인딩되어 연결된다. 이번에는 테스트 목적으로 최소 용량인 1Gi로 설정하여 테스트를 진행하였다.
또한, storageClassName은 PV와 PVC가 동일한 스토리지 클래스를 사용할 수 있도록 반드시 일치시켜야 한다.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: db-pvc
  namespace: mern
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  storageClassName: standard

Secret.yml

Secret은 민감한 정보를 안전하게 저장하고 관리하는 데 사용되는 Kubernetes 리소스이다. 주로 데이터베이스의 사용자명, 비밀번호, 데이터베이스 이름 등의 정보에 대해 사용할 수 있다. 아래는 DB 설정에 필요한 변수 값들을 base64로 인코딩하여 저장한 것이다. 

apiVersion: v1
kind: Secret
metadata: 
  name: db-credentials
  namespace: mern
type: Opaque
data:
  host: ZGF0YWJhc2U= #database (base64 인코딩된 값)
  user: cm9vdA== #root (base64 인코딩된 값)
  password: a2ltZG9uZ2p1MTIz #kimdongju123 (base64 인코딩된 새 비밀번호)
  database: a2ltZG9uZ2p1 #kimdongju (base64 인코딩된 값)

또한, DB뿐만 아니라 Container Registry에서 이미지를 가져올 때도 인증 정보(Access Key / Secret Key)가 필요하기 때문에,
다음과 같이 별도의 Secret을 하나 더 생성해주었다.

kubectl create secret docker-registry myregistrykey \
  --docker-server=<"자신의 컨테이너 Registry 주소" \
  --docker-username= <"자신의 Accessekey"> \
  --docker-password=<"자신의 Secretekey"> \
  --namespace=mern

Deployment.yaml

Deployment는 Kubernetes에서 애플리케이션의 배포, 업데이트, 파드 복제 및 자동 복구 등을 관리하는 핵심 리소스이다.
아래는 데이터베이스를 위한 Deployment manifest이다.

먼저, 데이터베이스의 비밀번호와 이름은 db-credentials라는 Secret을 통해 관리되며, 해당 값들은 환경 변수로 주입된다.
또한, Private Container Registry에서 이미지를 가져오기 위해 myregistrykey라는 시크릿을 imagePullSecrets에 등록하였다.
데이터는 db-pvc라는 PersistentVolumeClaim을 통해 /var/lib/mysql 경로에 영구적으로 저장되도록 구성하였다.

이러한 PV/PVC 설정을 통해 파드가 재시작되더라도 데이터가 유지될 수 있다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: database
  namespace: mern
  labels:
    role: database
    env: dev
spec:
  replicas: 1
  selector:
    matchLabels:
      role: database
  template:
    metadata:
      labels:
        role: database
    spec:
      imagePullSecrets:
      - name: myregistrykey  # NCP 인증 Secret 값
      containers:
      - name: database
        image: dj-kubetest-container.kr.ncr.ntruss.com/mysql-image:latest
        imagePullPolicy: Always
        ports:
        - containerPort: 3306
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: password
        - name: MYSQL_DATABASE
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: database
        volumeMounts:
        - name: db-storage
          mountPath: /var/lib/mysql
      volumes:
      - name: db-storage
        persistentVolumeClaim:
          claimName: db-pvc

Service.yaml

Service 리소스는 Kubernetes에서 파드 간의 네트워크 통신을 관리하는 역할을 한다.
클러스터 내에서 파드들이 서로 쉽게 연결될 수 있도록 도와주며, 여러 파드를 하나의 고정된 IP 주소와 DNS 이름으로 접근할 수 있게 해준다.

백엔드와 프론트엔드 서비스는 외부 접근이 하도록 NodePort 타입을 사용하였고, 데이터베이스 서비스는 ClusterIP 타입을 사용하였다. 데이터베이스가 외부에서 직접 접근될 필요가 없는 내부 전용 서비스이기 때문에, 클러스터 내부에서만 접근 가능하도록 제한하였다. 

apiVersion: v1
kind: Service
metadata:
  name: database
  namespace: mern
spec:
  ports:
  - port: 3306
    protocol: TCP
  type: ClusterIP
  selector:
    role: database

5. Backend Kubernetes Manifest

Deployment.yaml

데이터베이스와 마찬가지로 백엔드에서도 Secret 리소스를 통해 데이터베이스 접속에 필요한 환경 변수를 주입받아 DB와 연결되도록 구성되어 있다. 또한, 3500번 포트를 외부에 노출시켜 외부 요청을 받을 수 있도록 설정하였다.

애플리케이션의 상태를 지속적으로 점검하기 위해 livenessProbe를 설정하였으며, /backend/ 경로에 HTTP 요청을 주기적으로 보내 컨테이너의 정상 동작 여부를 확인한다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
  namespace: mern
  labels:
    role: backend
    env: dev
spec:
  replicas: 1
  selector:
    matchLabels:
      role: backend
  template:
    metadata:
      labels:
        role: backend
    spec:
      imagePullSecrets:
      - name: myregistrykey  # NCP 레지스트리에 대한 시크릿 이름
      containers:
      - name: backend
        image: dj-kubetest-container.kr.ncr.ntruss.com/backend-image:latest  # 이미지 경로 변경
        imagePullPolicy: Always
        ports:
        - containerPort: 3500
        livenessProbe:
          httpGet:
            path: backend/
            port: 3500
          initialDelaySeconds: 3
          periodSeconds: 10
        env:
        - name: host
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: host
        - name: user
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: user
        - name: password
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: password
        - name: database
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: database

service.yaml

외부에서 백엔드 서비스에 접근할 수 있도록 NodePort 타입의 Service를 구성하였다. 사용자가 80번 포트로 요청을 보내면 Ingress Controller가 이를 수신하고, 내부적으로 NodePort(31001)로 전달한 뒤 해당 요청은 백엔드 컨테이너의 3500번 포트로 연결된다.

Service는 role: backend 레이블을 가진 파드를 선택하고, 해당 파드의 3500번 포트와 연결되어 통신이 이루어진다.

apiVersion: v1
kind: Service
metadata:
  name: backend
  namespace: mern
spec:
  type: NodePort                  
  ports:
  - name: backend-port
    port: 3500                     
    targetPort: 3500               
    nodePort: 31001             
    protocol: TCP
  selector:
    role: backend

6. Frontend Kubernetes Manifest

Deployment.yaml

Frontend는 3000번 포트를 통해 외부와 통신하며, 환경 변수로 API 서버 URL을 설정하여 백엔드와의 연결을 가능하게 하였다. 
REACT_APP_API_BASE_URL 환경 변수에 http://kimdongju.site/backend 값을 설정함으로써, 프론트엔드 애플리케이션은 해당 백엔드 주소를 통해 API 요청을 보낼 수 있다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
  namespace: mern
  labels:
    role: frontend
    env: dev
spec:
  replicas: 1
  selector:
    matchLabels:
      role: frontend
  template:
    metadata:
      labels:
        role: frontend
    spec:
      imagePullSecrets:
      - name: myregistrykey 
      containers:
      - name: frontend
        image: dj-kubetest-container.kr.ncr.ntruss.com/frontend-image:latest  # 이미지 경로
        imagePullPolicy: Always
        ports:
        - containerPort: 3000
        env:
        - name: REACT_APP_API_BASE_URL
          value: "http://kimdongju.site/backend"  # 자신의 도메인이름 적어준다 API 경로를 명세해주는 작업 
        - name: NODE_OPTIONS
          value: "--openssl-legacy-provider"

service.yaml

프론트엔드 서비스는 백엔드와 마찬가지로 NodePort 방식을 사용하였다. 외부에서 80번 포트로 들어온 요청은 Ingress Controller를 통해 NodePort(31000)로 전달되며, 이는 다시 프론트엔드 컨테이너의 3000번 포트로 연결된다.

Service는 role: frontend 레이블을 가진 파드를 선택하여, 해당 파드의 3000번 포트로 트래픽을 안정적으로 전달하도록 구성되어 있다.

apiVersion: v1
kind: Service
metadata:
  name: frontend
  namespace: mern
spec:
  type: NodePort                
  ports:
  - port: 3000                 # 클러스터 내부에서 사용할 포트
    targetPort: 3000           # 컨테이너에서 사용할 포트
    nodePort: 31000            # 외부에서 접근할 포트 (예시)
    protocol: TCP
  selector:
    role: frontend

Ingress.yaml

Ingress 리소스는 kimdongju.site 도메인으로 들어오는 HTTP 요청을 처리하며, ALB를 통해 외부에서 접근할 수 있도록 설정되어 있다. 사용자가 브라우저에서 http://kimdongju.site로 요청을 보내면, ALB가 80번 포트로 이를 수신하고 Ingress Controller에 전달한다.

Ingress는 두 가지 경로 규칙을 사용한다.  /backend* 경로는 backend 서비스로, 그 외 모든 경로(/*)는 frontend 서비스로 전달된다. 두 서비스 모두 NodePort 타입으로 구성되어 있으며,

  • backend는 NodePort 31001 → 내부 포트 3500
  • frontend는 NodePort 31000 → 내부 포트 3000

으로 각각 매핑된다.

Ingress 설정에서는 NodePort 번호를 직접 명시하지 않지만, Ingress Controller는 Kubernetes API를 통해 이를 자동으로 인식하고 ALB Target Group을 생성하여 트래픽을 라우팅한다.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: mern-ingress
  namespace: mern
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTP": 80}]'
    alb.ingress.kubernetes.io/rewrite-target: /$2
spec:
  rules:
  - host: kimdongju.site # 사용할 도메인
    http:
      paths:
      - path: /backend*
        pathType: Prefix
        backend:
          service:
            name: backend
            port:
              number: 3500   # backend 서비스의 포트
      - path: /*
        pathType: Prefix
        backend:
          service:
            name: frontend
            port:
              number: 3000 # frontend 서비스의 포트

7. Kubernetes manifest 배포 결과 

모든 Kubernetes 리소스는 정상적으로 배포되었으며, 실무에서는 Argo CD와 같은 GitOps 도구를 사용하는 것이 바람직하지만, 이번에는 직접 명령어를 이용하여 수동으로 배포를 진행하였다.

### Deployment, Service 정상 배포 확인 ### 
root@dj-master:~/kubernetes/Kubernetes-Manifests# kubectl get all -n mern
NAME                            READY   STATUS    RESTARTS   AGE
pod/backend-5888cb94b5-wmk4r    1/1     Running   0          5d6h
pod/database-58d656c7ff-n2sp5   1/1     Running   0          12d
pod/frontend-85785769f8-vj4n7   1/1     Running   0          18m

NAME               TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
service/backend    NodePort    198.19.185.88    <none>        3500:31001/TCP   10d
service/database   ClusterIP   198.19.189.238   <none>        3306/TCP         12d
service/frontend   NodePort    198.19.204.218   <none>        3000:31000/TCP   10d

NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/backend    1/1     1            1           12d
deployment.apps/database   1/1     1            1           12d
deployment.apps/frontend   1/1     1            1           18m

NAME                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/backend-5888cb94b5    1         1         1       10d
replicaset.apps/database-58d656c7ff   1         1         1       12d
replicaset.apps/frontend-85785769f8   1         1         1       18m

### Secret 정상 배포 확인 ### 
root@dj-master:~/kubernetes/Kubernetes-Manifests# kubectl get secret -n mern
NAME             TYPE                             DATA   AGE
db-credentials   Opaque                           4      12d
myregistrykey    kubernetes.io/dockerconfigjson   1      12d

### PV, PVC 정상 배포 확인 ### 
root@dj-master:~/kubernetes/Kubernetes-Manifests# kubectl get pv -n mern
NAME    CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                    STORAGECLASS   VOLUMEATTRIBUTESCLASS   REASON   AGE
db-pv   1Gi        RWO            Retain           Bound    mern/db-pvc      standard       <unset>                 -        12d
root@dj-master:~/kubernetes/Kubernetes-Manifests# kubectl get pvc -n mern
NAME     STATUS   VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
db-pvc   Bound    db-pv    1Gi        RWO            standard       <unset>                 12d

### ingress 정상 배포 확인 ### 
root@dj-master:~/kubernetes/Kubernetes-Manifests# kubectl get ingress -n mern
NAME      CLASS   HOSTS            ADDRESS                                                                PORTS   AGE
ingress   alb     kimdongju.site   ing-mern-merningress-27dd8-100409304-3669ec287cc4.kr.lb.naverncp.com        80      17m

root@dj-master:~/kubernetes/DevOps/Kubernetes-Manifests# kubectl describe ingress mern-ingress -n mern
Name:             ingress
Labels:           <none>
Namespace:        mern
Address:          ing-mern-merningress-27dd8-100409304-3669ec287cc4.kr.lb.naverncp.com
Ingress Class:    alb
Default backend:  <default>
Rules:
  Host            Path  Backends
  ----            ----  --------
  kimdongju.site  
                  /backend*   backend:3500 (198.18.0.69:3500)
                  /*          frontend:3000 (198.18.0.87:3000)
Annotations:      alb.ingress.kubernetes.io/listen-ports: [{"HTTP": 80}]
                  alb.ingress.kubernetes.io/rewrite-target: /$2
                  alb.ingress.kubernetes.io/scheme: internet-facing
Events:
  Type    Reason              Age   From     Message
  ----    ------              ----  ----     -------
  Normal  CreateLoadBalancer  18m   ingress  no load balancer found, load balancer created (ingress: mern-ingress, instanceNo: 100409304)
  Normal  UpdateLoadBalancer  17m   ingress  load balancer updated (ingress: mern-ingress, instanceNo: 100409304)
  Normal  CreateTargetGroup   17m   ingress  target group created (no: 2919545, name: svc-mern-backend-22ad5, nodePort: 31001)
  Normal  CreateTargetGroup   17m   ingress  target group created (no: 2919546, name: svc-mern-frontend-85211, nodePort: 31000)
  Normal  CreateRule          17m   ingress  rule created (listenerPort: 80, rulePriority: 1)
  Normal  CreateRule          17m   ingress  rule created (listenerPort: 80, rulePriority: 2)

Ingress와 Service 구성을 기반으로 로드 밸런서와 타겟 그룹이 정상적으로 생성된 모습을 확인할 수 있다. 

백엔드의 Pod에 livenessProbe 경로를 /backend/로 지정하였기 때문에, ALB에서 생성된 백엔드 Target Group의 헬스 체크 경로 또한 동일하게 /backend/로 설정해야 정상적으로 통신이 이루어진다.

8. Global DNS Domain Mapping

마지막으로 Global DNS를 통해 도메인과 ALB 주소를 매핑시켜주시면 된다. 

도메인 접속

애플리케이션이 외부에서 정상적으로 접속되고 있는 것을 확인할 수 있다.

개선할 점 & 느낀점 

  • 현재는 GitOps 도구를 사용하지 않고 수동으로 배포를 진행하였기 때문에, 테스트나 반복적인 배포 과정에서 시간이 소요되었다. 따라서 향후에는 CI/CD 기반의 GitOps 방식을 도입하여 배포 자동화와 관리 효율성을 높일 필요가 있다.
  • 직접 애플리케이션을 배포하는 과정을 경험하며 Kubernetes 리소스에 대해 전반적인 이해도가 높아진 것 같다. 기존에는 Deployment에서 직접 변수를 지정했지만, ConfigMap 리소스를 활용해 설정 값을 분리하여야겠다. 

'kubernetes' 카테고리의 다른 글

[Kubernetes] Karpenter  (0) 2025.04.25
[Kubernetes] EKS(Elastic Kubernetes Service)  (0) 2025.04.24
[kubernetes] Security Context  (0) 2024.12.16
[kubernetes] CSR  (0) 2024.12.14
[Kubernetes] etcd member  (0) 2024.12.14