Add Kubernetes deployment and CI/CD workflow

Introduce Kubernetes manifests for backend, frontend, ingress, storage, and namespace setup under k8s/. Add Gitea Actions workflow for automated build and deployment to Kubernetes. Provide deployment and cluster setup guides in docs/ and project root. Update .gitignore to exclude Kubernetes secret files.
This commit is contained in:
Johngreen
2025-12-22 15:33:24 +09:00
parent 1856017bde
commit 47ac3dcaf1
12 changed files with 1549 additions and 0 deletions

41
k8s/ingress-nginx.yaml Normal file
View File

@@ -0,0 +1,41 @@
# Nginx Ingress Controller 설치
# 단일 노드 클러스터용 설정 (NodePort 사용)
#
# 설치 명령어:
# kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.9.5/deploy/static/provider/baremetal/deploy.yaml
#
# 또는 이 파일로 커스텀 설치:
# kubectl apply -f k8s/ingress-nginx.yaml
# NodePort를 80, 443으로 고정하는 패치용 설정
apiVersion: v1
kind: Service
metadata:
name: ingress-nginx-controller
namespace: ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/component: controller
spec:
type: NodePort
externalTrafficPolicy: Local
ipFamilyPolicy: SingleStack
ipFamilies:
- IPv4
ports:
- name: http
port: 80
protocol: TCP
targetPort: http
nodePort: 30080
- name: https
port: 443
protocol: TCP
targetPort: https
nodePort: 30443
selector:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/instance: ingress-nginx
app.kubernetes.io/component: controller

View File

@@ -0,0 +1,135 @@
# Local Path Provisioner - 단일 노드 클러스터용 스토리지
# Rancher의 Local Path Provisioner 사용
# 참고: https://github.com/rancher/local-path-provisioner
apiVersion: v1
kind: Namespace
metadata:
name: local-path-storage
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: local-path-provisioner-service-account
namespace: local-path-storage
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: local-path-provisioner-role
rules:
- apiGroups: [""]
resources: ["nodes", "persistentvolumeclaims", "configmaps"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["endpoints", "persistentvolumes", "pods"]
verbs: ["*"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "patch"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: local-path-provisioner-bind
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: local-path-provisioner-role
subjects:
- kind: ServiceAccount
name: local-path-provisioner-service-account
namespace: local-path-storage
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: local-path-provisioner
namespace: local-path-storage
spec:
replicas: 1
selector:
matchLabels:
app: local-path-provisioner
template:
metadata:
labels:
app: local-path-provisioner
spec:
serviceAccountName: local-path-provisioner-service-account
containers:
- name: local-path-provisioner
image: rancher/local-path-provisioner:v0.0.26
imagePullPolicy: IfNotPresent
command:
- local-path-provisioner
- --debug
- start
- --config
- /etc/config/config.json
volumeMounts:
- name: config-volume
mountPath: /etc/config/
env:
- name: POD_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
volumes:
- name: config-volume
configMap:
name: local-path-config
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-path
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: rancher.io/local-path
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete
---
apiVersion: v1
kind: ConfigMap
metadata:
name: local-path-config
namespace: local-path-storage
data:
config.json: |-
{
"nodePathMap": [
{
"node": "DEFAULT_PATH_FOR_NON_LISTED_NODES",
"paths": ["/opt/local-path-provisioner"]
}
]
}
setup: |-
#!/bin/sh
set -eu
mkdir -m 0777 -p "$VOL_DIR"
teardown: |-
#!/bin/sh
set -eu
rm -rf "$VOL_DIR"
helperPod.yaml: |-
apiVersion: v1
kind: Pod
metadata:
name: helper-pod
spec:
containers:
- name: helper-pod
image: busybox:latest
imagePullPolicy: IfNotPresent

9
k8s/namespace.yaml Normal file
View File

@@ -0,0 +1,9 @@
# vexplor 네임스페이스
apiVersion: v1
kind: Namespace
metadata:
name: vexplor
labels:
name: vexplor
project: vexplor

View File

@@ -0,0 +1,133 @@
# vexplor Backend Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: vexplor-backend
namespace: vexplor
labels:
app: vexplor-backend
component: backend
spec:
replicas: 2
selector:
matchLabels:
app: vexplor-backend
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: vexplor-backend
component: backend
spec:
imagePullSecrets:
- name: harbor-registry
containers:
- name: vexplor-backend
image: harbor.wace.me/vexplor/vexplor-backend:latest
imagePullPolicy: Always
ports:
- containerPort: 3001
protocol: TCP
envFrom:
- configMapRef:
name: vexplor-config
- secretRef:
name: vexplor-secret
env:
- name: PORT
value: "3001"
- name: HOST
value: "0.0.0.0"
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
httpGet:
path: /api/health
port: 3001
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /api/health
port: 3001
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
volumeMounts:
- name: uploads
mountPath: /app/uploads
- name: data
mountPath: /app/data
- name: logs
mountPath: /app/logs
volumes:
- name: uploads
persistentVolumeClaim:
claimName: vexplor-backend-uploads-pvc
- name: data
persistentVolumeClaim:
claimName: vexplor-backend-data-pvc
- name: logs
emptyDir: {}
---
# Backend Service
apiVersion: v1
kind: Service
metadata:
name: vexplor-backend-service
namespace: vexplor
labels:
app: vexplor-backend
spec:
type: ClusterIP
selector:
app: vexplor-backend
ports:
- name: http
port: 3001
targetPort: 3001
protocol: TCP
---
# Backend PVC - Uploads
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: vexplor-backend-uploads-pvc
namespace: vexplor
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storageClassName: local-path
---
# Backend PVC - Data
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: vexplor-backend-data-pvc
namespace: vexplor
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: local-path

32
k8s/vexplor-config.yaml Normal file
View File

@@ -0,0 +1,32 @@
# vexplor ConfigMap - 환경 설정
apiVersion: v1
kind: ConfigMap
metadata:
name: vexplor-config
namespace: vexplor
labels:
app: vexplor
data:
# 공통 설정
NODE_ENV: "production"
TZ: "Asia/Seoul"
# Backend 설정
BACKEND_PORT: "3001"
BACKEND_HOST: "0.0.0.0"
JWT_EXPIRES_IN: "24h"
LOG_LEVEL: "info"
CORS_CREDENTIALS: "true"
# Frontend 설정
FRONTEND_PORT: "3000"
FRONTEND_HOSTNAME: "0.0.0.0"
NEXT_TELEMETRY_DISABLED: "1"
# 내부 서비스 URL (클러스터 내부 통신)
INTERNAL_BACKEND_URL: "http://vexplor-backend-service:3001"
# 외부 URL (클라이언트 접근용)
NEXT_PUBLIC_API_URL: "https://api.vexplor.com/api"
CORS_ORIGIN: "https://v1.vexplor.com,https://api.vexplor.com"

View File

@@ -0,0 +1,92 @@
# vexplor Frontend Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: vexplor-frontend
namespace: vexplor
labels:
app: vexplor-frontend
component: frontend
spec:
replicas: 2
selector:
matchLabels:
app: vexplor-frontend
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: vexplor-frontend
component: frontend
spec:
imagePullSecrets:
- name: harbor-registry
containers:
- name: vexplor-frontend
image: harbor.wace.me/vexplor/vexplor-frontend:latest
imagePullPolicy: Always
ports:
- containerPort: 3000
protocol: TCP
envFrom:
- configMapRef:
name: vexplor-config
env:
- name: PORT
value: "3000"
- name: HOSTNAME
value: "0.0.0.0"
- name: NODE_ENV
value: "production"
- name: NEXT_PUBLIC_API_URL
value: "https://api.vexplor.com/api"
# 서버사이드 렌더링시 내부 백엔드 호출용
- name: SERVER_API_URL
value: "http://vexplor-backend-service:3001"
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
httpGet:
path: /
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /
port: 3000
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
---
# Frontend Service
apiVersion: v1
kind: Service
metadata:
name: vexplor-frontend-service
namespace: vexplor
labels:
app: vexplor-frontend
spec:
type: ClusterIP
selector:
app: vexplor-frontend
ports:
- name: http
port: 3000
targetPort: 3000
protocol: TCP

58
k8s/vexplor-ingress.yaml Normal file
View File

@@ -0,0 +1,58 @@
# vexplor Ingress - Nginx Ingress Controller 기반
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: vexplor-ingress
namespace: vexplor
labels:
app: vexplor
annotations:
# Nginx Ingress Controller 설정
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
nginx.ingress.kubernetes.io/proxy-connect-timeout: "60"
# WebSocket 지원
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
nginx.ingress.kubernetes.io/upstream-hash-by: "$remote_addr"
# SSL Redirect
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
# Cert-Manager (Let's Encrypt)
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: nginx
tls:
- hosts:
- v1.vexplor.com
- api.vexplor.com
secretName: vexplor-tls
rules:
# Frontend 도메인
- host: v1.vexplor.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: vexplor-frontend-service
port:
number: 3000
# Backend API 도메인
- host: api.vexplor.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: vexplor-backend-service
port:
number: 3001

View File

@@ -0,0 +1,38 @@
# vexplor Secret 템플릿
# 이 파일은 템플릿입니다. 실제 값으로 채운 후 vexplor-secret.yaml로 저장하세요.
# 주의: vexplor-secret.yaml은 .gitignore에 추가되어야 합니다!
#
# Secret 값은 base64로 인코딩해야 합니다:
# echo -n "your-value" | base64
#
apiVersion: v1
kind: Secret
metadata:
name: vexplor-secret
namespace: vexplor
labels:
app: vexplor
type: Opaque
data:
# 데이터베이스 연결 정보 (base64 인코딩 필요)
# echo -n "postgresql://postgres:password@211.115.91.141:11134/plm" | base64
DATABASE_URL: "cG9zdGdyZXNxbDovL3Bvc3RncmVzOnZleHBsb3IwOTA5ISFAMjExLjExNS45MS4xNDE6MTExMzQvcGxt"
# JWT 시크릿
# echo -n "your-jwt-secret" | base64
JWT_SECRET: "aWxzaGluLXBsbS1zdXBlci1zZWNyZXQtand0LWtleS0yMDI0"
# 메일 암호화 키
# echo -n "your-encryption-key" | base64
ENCRYPTION_KEY: "aWxzaGluLXBsbS1tYWlsLWVuY3J5cHRpb24ta2V5LTMyY2hhcmFjdGVycy0yMDI0LXNlY3VyZQ=="
# API 키들
# echo -n "your-kma-api-key" | base64
KMA_API_KEY: "b2dkWHIyZTlUNGlIVjY5bnZWLUl3QQ=="
# echo -n "your-its-api-key" | base64
ITS_API_KEY: "ZDZiOWJlZmVjMzExNGQ2NDgyODQ2NzRiOGZkZGNjMzI="
# echo -n "your-expressway-api-key" | base64
EXPRESSWAY_API_KEY: ""