feat: start deployment

This commit is contained in:
OhSeongRak 2025-06-17 16:51:42 +09:00
parent 9cbb1b63cc
commit 6d628427a6
9 changed files with 688 additions and 0 deletions

140
deployment/Jenkinsfile vendored Normal file
View File

@ -0,0 +1,140 @@
pipeline {
agent {
kubernetes {
yaml """
apiVersion: v1
kind: Pod
spec:
containers:
- name: podman
image: quay.io/podman/stable:latest
command:
- cat
tty: true
securityContext:
privileged: true
- name: envsubst
image: bhgedigital/envsubst
command:
- cat
tty: true
- name: azure-cli
image: mcr.microsoft.com/azure-cli:latest
command:
- cat
tty: true
"""
}
}
environment {
// 빌드 정보
imageTag = sh(script: "echo ${BUILD_NUMBER}", returnStdout: true).trim()
manifest = "deploy.yaml"
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Load Deploy Variables') {
steps {
script {
// deploy_env_vars 파일에서 환경변수 로드
if (fileExists('deployment/deploy_env_vars')) {
def envVars = readFile('deploy_env_vars').trim()
envVars.split('\n').each { line ->
if (line.contains('=')) {
def (key, value) = line.split('=', 2)
env."${key}" = value
echo "Loaded: ${key} = ${value}"
}
}
} else {
error "deploy_env_vars 파일이 없습니다!"
}
// 이미지 경로 설정
env.imagePath = "${env.REGISTRY}/${env.IMAGE_ORG}/smarketing-frontend:${imageTag}"
echo "Image Path: ${env.imagePath}"
}
}
}
stage('Build & Push Image') {
container('podman') {
steps {
script {
sh """
podman build \\
--build-arg PROJECT_FOLDER="." \\
--build-arg REACT_APP_AUTH_URL="${env.AUTH_URL}" \\
--build-arg REACT_APP_MEMBER_URL="${env.MEMBER_URL}" \\
--build-arg REACT_APP_STORE_URL="${env.STORE_URL}" \\
--build-arg REACT_APP_CONTENT_URL="${env.CONTENT_URL}" \\
--build-arg REACT_APP_RECOMMEND_URL="${env.RECOMMEND_URL}" \\
--build-arg BUILD_FOLDER="deployment/container" \\
--build-arg EXPORT_PORT="${env.EXPORT_PORT}" \\
-f deployment/container/Dockerfile-smarketing-frontend \\
-t ${env.imagePath} .
podman push ${env.imagePath}
"""
}
}
}
}
stage('Generate & Apply Manifest') {
container('envsubst') {
steps {
sh """
export namespace=${env.NAMESPACE}
export smarketing_frontend_image_path=${env.imagePath}
export replicas=${env.REPLICAS}
export export_port=${env.EXPORT_PORT}
export ingress_host=${env.INGRESS_HOST}
export resources_requests_cpu=${env.RESOURCES_REQUESTS_CPU}
export resources_requests_memory=${env.RESOURCES_REQUESTS_MEMORY}
export resources_limits_cpu=${env.RESOURCES_LIMITS_CPU}
export resources_limits_memory=${env.RESOURCES_LIMITS_MEMORY}
envsubst < deployment/${manifest}.template > deployment/${manifest}
echo "Generated manifest file:"
cat deployment/${manifest}
"""
}
}
container('azure-cli') {
steps {
sh """
kubectl apply -f deployment/${manifest}
echo "Waiting for deployment to be ready..."
kubectl -n ${env.NAMESPACE} wait --for=condition=available deployment/smarketing-frontend --timeout=300s
echo "Deployment completed successfully!"
kubectl -n ${env.NAMESPACE} get pods -l app=smarketing-frontend
kubectl -n ${env.NAMESPACE} get svc smarketing-frontend-service
"""
}
}
}
}
post {
always {
cleanWs()
}
success {
echo "✅ smarketing-frontend 배포가 성공적으로 완료되었습니다!"
}
failure {
echo "❌ smarketing-frontend 배포 중 오류가 발생했습니다."
}
}
}

View File

@ -0,0 +1,136 @@
pipeline {
agent {
kubernetes {
yaml """
apiVersion: v1
kind: Pod
spec:
containers:
- name: podman
image: quay.io/podman/stable:latest
command:
- cat
tty: true
securityContext:
privileged: true
- name: git
image: alpine/git:latest
command:
- cat
tty: true
"""
}
}
environment {
imageTag = sh(script: "echo ${BUILD_NUMBER}", returnStdout: true).trim()
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Load Deploy Variables') {
steps {
script {
// deploy_env_vars 파일에서 환경변수 로드
if (fileExists('deploy_env_vars')) {
def envVars = readFile('deploy_env_vars').trim()
envVars.split('\n').each { line ->
if (line.contains('=')) {
def (key, value) = line.split('=', 2)
env."${key}" = value
echo "Loaded: ${key} = ${value}"
}
}
} else {
error "deploy_env_vars 파일이 없습니다!"
}
// 이미지 경로 설정
env.imagePath = "${env.REGISTRY}/${env.IMAGE_ORG}/smarketing-frontend:${imageTag}"
echo "Image Path: ${env.imagePath}"
}
}
}
stage('Build & Push Image') {
container('podman') {
steps {
script {
sh """
podman build \\
--build-arg PROJECT_FOLDER="." \\
--build-arg REACT_APP_AUTH_URL="${env.AUTH_URL}" \\
--build-arg REACT_APP_MEMBER_URL="${env.MEMBER_URL}" \\
--build-arg REACT_APP_STORE_URL="${env.STORE_URL}" \\
--build-arg REACT_APP_CONTENT_URL="${env.CONTENT_URL}" \\
--build-arg REACT_APP_RECOMMEND_URL="${env.RECOMMEND_URL}" \\
--build-arg BUILD_FOLDER="deployment/container" \\
--build-arg EXPORT_PORT="${env.EXPORT_PORT}" \\
-f deployment/container/Dockerfile-smarketing-frontend \\
-t ${env.imagePath} .
podman push ${env.imagePath}
"""
}
}
}
}
stage('Update Manifest Repository') {
container('git') {
withCredentials([usernamePassword(
credentialsId: 'github-credentials-${env.TEAMID}',
usernameVariable: 'GIT_USERNAME',
passwordVariable: 'GIT_PASSWORD'
)]) {
steps {
sh """
# Git 설정
git config --global user.name "Jenkins"
git config --global user.email "jenkins@company.com"
# Manifest Repository Clone
git clone https://\$GIT_USERNAME:\$GIT_PASSWORD@github.com/${env.GITHUB_ORG}/smarketing-manifest.git
cd smarketing-manifest
# Frontend 이미지 태그 업데이트
echo "Updating smarketing-frontend deployment with image tag: ${imageTag}"
if [ -f "smarketing-frontend/deployment.yaml" ]; then
# 기존 이미지 태그를 새 태그로 교체
sed -i "s|image: ${env.REGISTRY}/${env.IMAGE_ORG}/smarketing-frontend:.*|image: ${env.imagePath}|g" smarketing-frontend/deployment.yaml
echo "Updated frontend deployment file:"
cat smarketing-frontend/deployment.yaml | grep "image:"
else
echo "Warning: Frontend deployment file not found"
fi
# 변경사항 커밋 및 푸시
git add .
git commit -m "Update smarketing-frontend image tag to ${imageTag}" || echo "No changes to commit"
git push origin main
"""
}
}
}
}
}
post {
always {
cleanWs()
}
success {
echo "✅ smarketing-frontend 이미지 빌드 및 Manifest 업데이트가 완료되었습니다!"
}
failure {
echo "❌ smarketing-frontend CI/CD 파이프라인 중 오류가 발생했습니다."
}
}
}

View File

@ -0,0 +1,82 @@
# Build stage
FROM node:20-slim AS builder
ARG PROJECT_FOLDER
ENV NODE_ENV=production
WORKDIR /app
# Install dependencies
COPY ${PROJECT_FOLDER}/package*.json ./
RUN npm ci --only=production
# Build application
COPY ${PROJECT_FOLDER} .
RUN npm run build
# Run stage
FROM nginx:stable-alpine
ARG BUILD_FOLDER
ARG EXPORT_PORT
ARG REACT_APP_AUTH_URL
ARG REACT_APP_MEMBER_URL
ARG REACT_APP_STORE_URL
ARG REACT_APP_CONTENT_URL
ARG REACT_APP_RECOMMEND_URL
# Create nginx user if it doesn't exist
RUN adduser -S nginx || true
# Copy build files
COPY --from=builder /app/build /usr/share/nginx/html
# Create runtime config with all smarketing APIs
# index.html의 헤더에서 이 값을 읽어 환경변수를 생성함
# api.js에서 이 환경변수를 이용함
RUN echo "console.log('=== RUNTIME-ENV.JS 로드됨 (Docker 빌드) ==='); \
window.__runtime_config__ = { \
AUTH_URL: '${REACT_APP_AUTH_URL}', \
MEMBER_URL: '${REACT_APP_MEMBER_URL}', \
STORE_URL: '${REACT_APP_STORE_URL}', \
CONTENT_URL: '${REACT_APP_CONTENT_URL}', \
RECOMMEND_URL: '${REACT_APP_RECOMMEND_URL}', \
ENV: 'production', \
DEBUG: false, \
API_TIMEOUT: 30000, \
RETRY_ATTEMPTS: 3, \
RETRY_DELAY: 1000, \
VERSION: '1.0.0', \
BUILD_DATE: new Date().toISOString() \
}; \
window.getApiConfig = () => window.__runtime_config__; \
window.getApiUrl = (serviceName) => { \
const config = window.__runtime_config__; \
const urlKey = \`\${serviceName.toUpperCase()}_URL\`; \
return config[urlKey] || null; \
}; \
console.log('✅ [RUNTIME] Docker 빌드 런타임 설정 로드 완료');" > /usr/share/nginx/html/runtime-env.js
# Copy and process nginx configuration
COPY ${BUILD_FOLDER}/nginx.conf /etc/nginx/templates/default.conf.template
# Add custom nginx settings
RUN echo "client_max_body_size 100M;" > /etc/nginx/conf.d/client_max_body_size.conf
RUN echo "proxy_buffer_size 128k;" > /etc/nginx/conf.d/proxy_buffer_size.conf
RUN echo "proxy_buffers 4 256k;" > /etc/nginx/conf.d/proxy_buffers.conf
RUN echo "proxy_busy_buffers_size 256k;" > /etc/nginx/conf.d/proxy_busy_buffers_size.conf
# Set permissions
RUN chown -R nginx:nginx /usr/share/nginx/html && \
chmod -R 755 /usr/share/nginx/html && \
chown -R nginx:nginx /var/cache/nginx && \
chown -R nginx:nginx /var/log/nginx && \
chown -R nginx:nginx /etc/nginx/conf.d && \
touch /var/run/nginx.pid && \
chown -R nginx:nginx /var/run/nginx.pid
USER nginx
EXPOSE ${EXPORT_PORT}
CMD ["nginx", "-g", "daemon off;"]

View File

@ -0,0 +1,69 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
# Basic settings
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 10240;
gzip_proxied expired no-cache no-store private must-revalidate;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/x-javascript
application/xml+rss
application/javascript
application/json;
server {
listen 18080;
server_name localhost;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
}

View File

@ -0,0 +1,86 @@
# ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: smarketing-frontend-config
namespace: ${namespace}
data:
runtime-env.js: |
window.__runtime_config__ = {
AUTH_URL: 'http://${ingress_host}/auth',
CONTENT_URL: 'http://${ingress_host}/content',
AI_URL: 'http://${ingress_host}/ai'
};
---
# Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: smarketing-frontend
namespace: ${namespace}
labels:
app: smarketing-frontend
spec:
replicas: ${replicas}
selector:
matchLabels:
app: smarketing-frontend
template:
metadata:
labels:
app: smarketing-frontend
spec:
imagePullSecrets:
- name: acr-secret
containers:
- name: smarketing-frontend
image: ${smarketing_frontend_image_path}
imagePullPolicy: Always
ports:
- containerPort: ${export_port}
resources:
requests:
cpu: ${resources_requests_cpu}
memory: ${resources_requests_memory}
limits:
cpu: ${resources_limits_cpu}
memory: ${resources_limits_memory}
volumeMounts:
- name: runtime-config
mountPath: /usr/share/nginx/html/runtime-env.js
subPath: runtime-env.js
livenessProbe:
httpGet:
path: /health
port: ${export_port}
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: ${export_port}
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: runtime-config
configMap:
name: smarketing-frontend-config
---
# Service
apiVersion: v1
kind: Service
metadata:
name: smarketing-frontend-service
namespace: ${namespace}
labels:
app: smarketing-frontend
spec:
type: ClusterIP
ports:
- port: 80
targetPort: ${export_port}
protocol: TCP
selector:
app: smarketing-frontend

View File

@ -0,0 +1,34 @@
# smarketing-frontend 배포 환경변수 설정
# 이 파일을 실제 환경에 맞게 수정하세요
# Container Registry 설정
REGISTRY=acrdigitalgarage02.azurecr.io
IMAGE_ORG=smarketing
# Kubernetes 설정
NAMESPACE=smarketing
REPLICAS=1
EXPORT_PORT=18080
# Gateway/Ingress 설정 (⭐ smarketing-backend와 동일한 IP 사용)
INGRESS_HOST=smarketing.20.249.184.228.nip.io
# 리소스 설정
RESOURCES_REQUESTS_CPU=256m
RESOURCES_REQUESTS_MEMORY=256Mi
RESOURCES_LIMITS_CPU=1024m
RESOURCES_LIMITS_MEMORY=1024Mi
# API URLs (⭐ smarketing-backend ingress를 통해 라우팅)
# 현재 설정된 백엔드 API들과 일치
AUTH_URL=http://smarketing.20.249.184.228.nip.io/api/auth
MEMBER_URL=http://smarketing.20.249.184.228.nip.io/api/member
STORE_URL=http://smarketing.20.249.184.228.nip.io/api/store
MENU_URL=http://smarketing.20.249.184.228.nip.io/api/menu
SALES_URL=http://smarketing.20.249.184.228.nip.io/api/sales
CONTENT_URL=http://smarketing.20.249.184.228.nip.io/api/content
RECOMMEND_URL=http://smarketing.20.249.184.228.nip.io/api/recommend
# GitHub 설정 (실제 팀 설정으로 변경)
GITHUB_ORG=won-ktds
TEAMID=smarketing

View File

@ -0,0 +1,52 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: smarketing-frontend
namespace: ${namespace}
labels:
app: smarketing-frontend
spec:
replicas: ${replicas}
selector:
matchLabels:
app: smarketing-frontend
template:
metadata:
labels:
app: smarketing-frontend
spec:
imagePullSecrets:
- name: acr-secret
containers:
- name: smarketing-frontend
image: ${smarketing_frontend_image_path}
imagePullPolicy: Always
ports:
- containerPort: ${export_port}
resources:
requests:
cpu: ${resources_requests_cpu}
memory: ${resources_requests_memory}
limits:
cpu: ${resources_limits_cpu}
memory: ${resources_limits_memory}
volumeMounts:
- name: runtime-config
mountPath: /usr/share/nginx/html/runtime-env.js
subPath: runtime-env.js
livenessProbe:
httpGet:
path: /health
port: ${export_port}
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: ${export_port}
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: runtime-config
configMap:
name: smarketing-frontend-config

View File

@ -0,0 +1,74 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: smarketing-frontend-config
namespace: ${namespace}
data:
runtime-env.js: |
console.log('=== RUNTIME-ENV.JS 로드됨 (배포 환경) ===');
window.__runtime_config__ = {
// 백엔드 API 구조에 맞게 URL 설정
AUTH_URL: 'http://${ingress_host}/api/auth',
MEMBER_URL: 'http://${ingress_host}/api/member',
STORE_URL: 'http://${ingress_host}/api/store',
MENU_URL: 'http://${ingress_host}/api/menu',
SALES_URL: 'http://${ingress_host}/api/sales',
CONTENT_URL: 'http://${ingress_host}/api/content',
RECOMMEND_URL: 'http://${ingress_host}/api/recommendations',
// Gateway URL (운영 환경)
GATEWAY_URL: 'http://${ingress_host}',
// 기능 플래그
FEATURES: {
ANALYTICS: true,
PUSH_NOTIFICATIONS: true,
SOCIAL_LOGIN: false,
MULTI_LANGUAGE: false,
API_HEALTH_CHECK: true,
},
// 환경 설정 (배포 환경)
ENV: 'production',
DEBUG: false,
// API 타임아웃 설정
API_TIMEOUT: 30000,
// 재시도 설정
RETRY_ATTEMPTS: 3,
RETRY_DELAY: 1000,
// 버전 정보
VERSION: '1.0.0',
BUILD_DATE: new Date().toISOString()
};
// 설정 검증 함수
const validateConfig = () => {
const config = window.__runtime_config__;
const requiredUrls = ['AUTH_URL', 'STORE_URL', 'SALES_URL', 'RECOMMEND_URL'];
for (const url of requiredUrls) {
if (!config[url]) {
console.error(`❌ [CONFIG] 필수 URL 누락: ${url}`);
return false;
}
}
console.log('✅ [CONFIG] 설정 검증 완료');
return true;
};
// 전역 설정 접근 함수
window.getApiConfig = () => window.__runtime_config__;
window.getApiUrl = (serviceName) => {
const config = window.__runtime_config__;
const urlKey = `${serviceName.toUpperCase()}_URL`;
return config[urlKey] || null;
};
// 설정 검증 실행
validateConfig();
console.log('✅ [RUNTIME] 런타임 설정 로드 완료 (배포 환경)');

View File

@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: smarketing-frontend-service
namespace: ${namespace}
labels:
app: smarketing-frontend
spec:
type: ClusterIP
ports:
- port: 80
targetPort: ${export_port}
protocol: TCP
selector:
app: smarketing-frontend