diff --git a/deployment/Jenkinsfile b/deployment/Jenkinsfile new file mode 100644 index 0000000..097683b --- /dev/null +++ b/deployment/Jenkinsfile @@ -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 배포 중 오류가 발생했습니다." + } + } +} \ No newline at end of file diff --git a/deployment/Jenkinsfile_ArgoCD b/deployment/Jenkinsfile_ArgoCD new file mode 100644 index 0000000..099dcc5 --- /dev/null +++ b/deployment/Jenkinsfile_ArgoCD @@ -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 파이프라인 중 오류가 발생했습니다." + } + } +} \ No newline at end of file diff --git a/deployment/container/Dockerfile-smarketing-frontend b/deployment/container/Dockerfile-smarketing-frontend new file mode 100644 index 0000000..1f688b7 --- /dev/null +++ b/deployment/container/Dockerfile-smarketing-frontend @@ -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;"] \ No newline at end of file diff --git a/deployment/container/nginx.conf b/deployment/container/nginx.conf new file mode 100644 index 0000000..1ea0539 --- /dev/null +++ b/deployment/container/nginx.conf @@ -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; + } + } +} \ No newline at end of file diff --git a/deployment/deploy.yaml.template b/deployment/deploy.yaml.template new file mode 100644 index 0000000..d572c21 --- /dev/null +++ b/deployment/deploy.yaml.template @@ -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 \ No newline at end of file diff --git a/deployment/deploy_env_vars b/deployment/deploy_env_vars new file mode 100644 index 0000000..14d772a --- /dev/null +++ b/deployment/deploy_env_vars @@ -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 \ No newline at end of file diff --git a/deployment/manifest/deployment.yaml b/deployment/manifest/deployment.yaml new file mode 100644 index 0000000..272d253 --- /dev/null +++ b/deployment/manifest/deployment.yaml @@ -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 \ No newline at end of file diff --git a/deployment/manifest/frontend-configmap.yaml b/deployment/manifest/frontend-configmap.yaml new file mode 100644 index 0000000..d111f6e --- /dev/null +++ b/deployment/manifest/frontend-configmap.yaml @@ -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] 런타임 설정 로드 완료 (배포 환경)'); \ No newline at end of file diff --git a/deployment/manifest/service.yaml b/deployment/manifest/service.yaml new file mode 100644 index 0000000..fcb845a --- /dev/null +++ b/deployment/manifest/service.yaml @@ -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 \ No newline at end of file