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:
41
k8s/ingress-nginx.yaml
Normal file
41
k8s/ingress-nginx.yaml
Normal 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
|
||||
|
||||
135
k8s/local-path-provisioner.yaml
Normal file
135
k8s/local-path-provisioner.yaml
Normal 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
9
k8s/namespace.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
# vexplor 네임스페이스
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: vexplor
|
||||
labels:
|
||||
name: vexplor
|
||||
project: vexplor
|
||||
|
||||
133
k8s/vexplor-backend-deployment.yaml
Normal file
133
k8s/vexplor-backend-deployment.yaml
Normal 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
32
k8s/vexplor-config.yaml
Normal 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"
|
||||
|
||||
92
k8s/vexplor-frontend-deployment.yaml
Normal file
92
k8s/vexplor-frontend-deployment.yaml
Normal 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
58
k8s/vexplor-ingress.yaml
Normal 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
|
||||
|
||||
38
k8s/vexplor-secret.yaml.template
Normal file
38
k8s/vexplor-secret.yaml.template
Normal 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: ""
|
||||
|
||||
Reference in New Issue
Block a user