HealthSync_FE/Jenkinsfile
2025-06-20 07:30:03 +00:00

1020 lines
44 KiB
Groovy
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

pipeline {
agent any
// 환경 변수 설정 (실제 환경 정보 반영)
environment {
// ACR 정보
ACR_REGISTRY = 'acrhealthsync01.azurecr.io'
ACR_REPOSITORY = 'healthsync-frontend'
RESOURCE_GROUP = 'rg-digitalgarage-01'
// SonarQube 정보
SONARQUBE_SERVER = 'http://20.249.161.128:9000'
SONAR_PROJECT_KEY = 'Healthsync-front'
// Docker 이미지 관련
IMAGE_TAG = "${BUILD_NUMBER}-${GIT_COMMIT.take(7)}"
IMAGE_NAME = "${ACR_REGISTRY}/${ACR_REPOSITORY}:${IMAGE_TAG}"
LATEST_IMAGE = "${ACR_REGISTRY}/${ACR_REPOSITORY}:latest"
// Node.js 환경 설정
NODE_VERSION = '18'
NODE_OPTIONS = '--max-old-space-size=4096'
// 빌드 환경 설정
CI = 'true'
GENERATE_SOURCEMAP = 'false'
INLINE_RUNTIME_CHUNK = 'false'
// 타임스탬프
BUILD_TIMESTAMP = new Date().format("yyyy-MM-dd_HH-mm-ss")
// 프로젝트 정보
PROJECT_NAME = 'HealthSync Frontend'
BUILD_USER = "${env.BUILD_USER ?: 'Jenkins'}"
}
options {
// 빌드 기록 보관 (최근 15개)
buildDiscarder(logRotator(numToKeepStr: '15', daysToKeepStr: '30'))
// 타임아웃 설정 (30분)
timeout(time: 30, unit: 'MINUTES')
// 동시 실행 방지
disableConcurrentBuilds()
// 기본 체크아웃 건너뛰기 (수동으로 처리)
skipDefaultCheckout()
}
stages {
stage('🔍 Checkout & Validation') {
steps {
script {
echo "🚀 === ${PROJECT_NAME} CI/CD 파이프라인 시작 ==="
echo "📋 빌드 정보:"
echo " • 빌드 번호: #${BUILD_NUMBER}"
echo " • 브랜치: ${env.BRANCH_NAME ?: 'N/A'}"
echo " • 시작 시간: ${BUILD_TIMESTAMP}"
echo " • 트리거: ${BUILD_USER}"
}
// 소스코드 체크아웃
checkout scm
script {
// Git 정보 확인
env.GIT_COMMIT = sh(returnStdout: true, script: 'git rev-parse HEAD').trim()
env.GIT_BRANCH = sh(returnStdout: true, script: 'git rev-parse --abbrev-ref HEAD').trim()
env.GIT_AUTHOR = sh(returnStdout: true, script: 'git log -1 --pretty=format:"%an"').trim()
echo "📝 Git 정보:"
echo " • Commit: ${env.GIT_COMMIT}"
echo " • Branch: ${env.GIT_BRANCH}"
echo " • Author: ${env.GIT_AUTHOR}"
// 필수 파일 존재 확인
def requiredFiles = ['package.json', 'src/index.js', 'public/index.html']
requiredFiles.each { file ->
if (!fileExists(file)) {
error "❌ 필수 파일이 없습니다: ${file}"
}
}
echo "✅ 프로젝트 구조 검증 완료"
}
}
}
stage('⚙️ Setup Environment') {
steps {
script {
echo "🔧 === Node.js 환경 설정 ==="
// Node.js 버전 확인
def nodeVersion = sh(returnStdout: true, script: 'node --version').trim()
def npmVersion = sh(returnStdout: true, script: 'npm --version').trim()
echo "📋 환경 정보:"
echo " • Node.js: ${nodeVersion}"
echo " • npm: ${npmVersion}"
echo " • 작업 디렉토리: ${WORKSPACE}"
echo " • NODE_OPTIONS: ${NODE_OPTIONS}"
// Node.js 버전 검증 (v18 이상 필요)
def nodeVersionNumber = nodeVersion.replaceAll(/^v/, '').split('\\.')[0] as Integer
if (nodeVersionNumber < 18) {
error "❌ Node.js 18 이상이 필요합니다. 현재 버전: ${nodeVersion}"
}
echo "✅ Node.js 버전 검증 완료"
// 프로젝트 정보 확인
def packageJson = readJSON file: 'package.json'
echo "📦 프로젝트 정보:"
echo " • 이름: ${packageJson.name}"
echo " • 버전: ${packageJson.version}"
echo " • React: ${packageJson.dependencies?.react ?: 'N/A'}"
echo " • Scripts: ${packageJson.scripts?.keySet()?.join(', ')}"
}
}
}
stage('📦 Install Dependencies') {
steps {
script {
echo "📥 === 종속성 설치 시작 ==="
// npm 캐시 설정
sh '''
echo "🔧 npm 캐시 설정..."
npm config set cache ${WORKSPACE}/.npm-cache
npm config set prefer-offline true
npm config set audit-level moderate
'''
// package-lock.json 존재 여부에 따른 설치 방법 선택
def packageLockExists = fileExists('package-lock.json')
if (packageLockExists) {
echo "📋 package-lock.json 발견 - npm ci 실행"
sh '''
echo "⏱️ 설치 시작 시간: $(date)"
npm ci --prefer-offline --no-audit --no-fund
echo "✅ 설치 완료 시간: $(date)"
'''
} else {
echo "📋 package-lock.json 없음 - npm install 실행"
sh '''
echo "⏱️ 설치 시작 시간: $(date)"
npm install --prefer-offline --no-audit --no-fund
echo "✅ 설치 완료 시간: $(date)"
'''
}
// 설치된 패키지 정보 확인
sh '''
echo "📊 설치 완료 통계:"
if [ -d node_modules ]; then
echo " • node_modules 크기: $(du -sh node_modules 2>/dev/null || echo 'N/A')"
echo " • 설치된 패키지 수: $(ls node_modules 2>/dev/null | wc -l || echo '0')"
else
echo " • node_modules 디렉토리 없음"
fi
'''
}
}
post {
success {
echo "✅ 종속성 설치 성공"
}
failure {
echo "❌ 종속성 설치 실패"
}
}
}
stage('🔍 Code Quality Analysis') {
parallel {
stage('ESLint') {
steps {
script {
echo "🔍 === ESLint 정적 분석 시작 ==="
try {
// package.json에서 lint 스크립트 확인
def packageJson = readJSON file: 'package.json'
def hasLintScript = packageJson.scripts?.containsKey('lint')
if (hasLintScript) {
echo "📋 package.json에 lint 스크립트 발견"
sh 'npm run lint 2>&1 || echo "ESLint 검사 완료 (경고 포함)"'
} else {
echo "📋 package.json에 lint 스크립트 없음 - 직접 ESLint 실행"
// ESLint가 설치되어 있는지 확인 후 실행
sh '''
if [ -f node_modules/.bin/eslint ]; then
echo "🔍 직접 ESLint 실행 중..."
npx eslint src --ext .js,.jsx,.ts,.tsx --format table 2>&1 || echo "ESLint 검사 완료 (경고 포함)"
else
echo "⚠️ ESLint가 설치되지 않았습니다. React App의 기본 설정을 사용합니다."
fi
'''
}
echo "✅ ESLint 분석 완료"
} catch (Exception e) {
echo "⚠️ ESLint 실행 중 오류: ${e.getMessage()}"
echo " 빌드를 계속 진행합니다"
}
}
}
}
stage('Security Audit') {
steps {
script {
echo "🔒 === npm 보안 감사 시작 ==="
try {
// 보안 취약점 검사
def auditResult = sh(
returnStatus: true,
script: '''
echo "🔍 보안 취약점 검사 실행 중..."
npm audit --audit-level=moderate --json > audit-results.json 2>/dev/null || true
npm audit --audit-level=moderate
'''
)
if (auditResult == 0) {
echo "✅ 보안 취약점 없음"
} else {
echo "⚠️ 보안 취약점 발견 - 검토 필요"
echo " 자세한 내용은 'npm audit' 결과를 확인하세요"
}
// 감사 결과 아카이브 (파일이 존재하는 경우에만)
if (fileExists('audit-results.json')) {
archiveArtifacts artifacts: 'audit-results.json', allowEmptyArchive: true
}
} catch (Exception e) {
echo "⚠️ 보안 감사 중 오류: ${e.getMessage()}"
}
}
}
}
stage('Dependency Check') {
steps {
script {
echo "📋 === 종속성 검사 시작 ==="
sh '''
echo "📊 종속성 분석..."
npm ls --depth=0 2>/dev/null || echo "일부 종속성 문제 발견"
echo ""
echo "📋 주요 종속성 버전:"
npm list react react-dom react-scripts 2>/dev/null || echo "React 정보 확인 불가"
echo ""
echo "🔍 outdated 패키지 확인:"
npm outdated 2>/dev/null || echo "모든 패키지가 최신 상태이거나 확인 불가"
'''
}
}
}
}
}
stage('🧪 Run Tests') {
steps {
script {
echo "🧪 === 유닛 테스트 실행 시작 ==="
try {
// React 테스트 실행
sh '''
echo "🔧 테스트 환경 설정..."
export CI=true
export NODE_ENV=test
export WATCHMAN_DISABLE_RECRAWL=true
echo "🧪 테스트 실행 중..."
npm test -- --coverage --watchAll=false --verbose --testResultsProcessor=jest-junit --coverageReporters=text-lcov --coverageReporters=html --testTimeout=30000 2>&1
'''
// 테스트 결과 분석
sh '''
echo "📊 테스트 결과 분석..."
if [ -d coverage ]; then
echo "✅ 커버리지 리포트 생성 완료"
echo "📋 커버리지 요약:"
ls -la coverage/
# 커버리지 요약 정보 출력
if [ -f coverage/lcov.info ]; then
echo "📊 LCOV 리포트 생성됨"
fi
if [ -f coverage/lcov-report/index.html ]; then
echo "📊 HTML 리포트 생성됨"
fi
else
echo "⚠️ 커버리지 리포트 생성 실패"
fi
'''
echo "✅ 테스트 실행 완료"
} catch (Exception e) {
echo "❌ 테스트 실행 실패: ${e.getMessage()}"
currentBuild.result = 'UNSTABLE'
echo " 빌드는 계속 진행됩니다 (UNSTABLE)"
}
}
}
post {
always {
script {
// 테스트 결과 발행
if (fileExists('coverage/lcov-report/index.html')) {
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'coverage/lcov-report',
reportFiles: 'index.html',
reportName: 'Test Coverage Report',
reportTitles: 'Coverage Report'
])
echo "📊 커버리지 리포트 발행 완료"
}
// JUnit 테스트 결과 발행 (파일 존재 시)
if (fileExists('junit.xml')) {
publishTestResults testResultsPattern: 'junit.xml'
echo "📋 테스트 결과 발행 완료"
} else if (fileExists('test-results.xml')) {
publishTestResults testResultsPattern: 'test-results.xml'
echo "📋 테스트 결과 발행 완료"
}
}
}
}
}
stage('📊 SonarQube Analysis') {
when {
anyOf {
branch 'main'
branch 'develop'
branch 'master'
}
}
steps {
script {
echo "📊 === SonarQube 코드 품질 분석 시작 ==="
// SonarQube 토큰을 Jenkins Secret으로 사용 (수정된 크리덴셜 ID)
withCredentials([string(credentialsId: 'sonarqube_access_token', variable: 'SONAR_TOKEN')]) {
sh '''
echo "🔧 SonarQube 분석 설정..."
echo " • 프로젝트 키: ${SONAR_PROJECT_KEY}"
echo " • 서버: ${SONARQUBE_SERVER}"
echo " • 브랜치: ${GIT_BRANCH}"
# SonarQube Scanner 실행 가능 여부 확인
if command -v sonar-scanner &> /dev/null; then
echo "📊 SonarQube Scanner 발견 - 분석 시작..."
sonar-scanner \\
-Dsonar.projectKey=${SONAR_PROJECT_KEY} \\
-Dsonar.projectName="HealthSync Frontend" \\
-Dsonar.projectVersion=${BUILD_NUMBER} \\
-Dsonar.sources=src \\
-Dsonar.tests=src \\
-Dsonar.test.inclusions="**/*.test.js,**/*.test.jsx,**/*.spec.js,**/*.spec.jsx" \\
-Dsonar.host.url=${SONARQUBE_SERVER} \\
-Dsonar.login=${SONAR_TOKEN} \\
-Dsonar.javascript.lcov.reportPaths=coverage/lcov.info \\
-Dsonar.coverage.exclusions="**/*.test.js,**/*.test.jsx,**/*.spec.js,**/*.spec.jsx,**/node_modules/**,**/public/**,**/build/**" \\
-Dsonar.cpd.exclusions="**/node_modules/**,**/build/**,**/coverage/**" \\
-Dsonar.exclusions="**/node_modules/**,**/build/**,**/coverage/**,**/*.min.js,**/serviceWorker.js,**/reportWebVitals.js" \\
-Dsonar.sourceEncoding=UTF-8 \\
-Dsonar.scm.provider=git \\
-Dsonar.working.directory=${WORKSPACE}/.scannerwork \\
-Dsonar.qualitygate.wait=true
echo "✅ SonarQube 분석 완료"
else
echo "❌ SonarQube Scanner를 찾을 수 없습니다"
echo "Jenkins Global Tool Configuration에서 SonarQube Scanner를 설정해주세요"
error "SonarQube Scanner 설정 필요"
fi
'''
}
}
}
post {
success {
echo "✅ SonarQube 분석 성공"
}
failure {
echo "❌ SonarQube 분석 실패"
}
}
}
stage('🚪 Quality Gate') {
when {
anyOf {
branch 'main'
branch 'develop'
branch 'master'
}
}
steps {
script {
echo "🚪 === SonarQube 품질 게이트 확인 시작 ==="
timeout(time: 10, unit: 'MINUTES') {
echo "⏳ 품질 게이트 결과 대기 중..."
def qg = waitForQualityGate()
echo "📋 품질 게이트 결과: ${qg.status}"
if (qg.status != 'OK') {
echo "❌ 품질 게이트 실패!"
echo "📄 실패 상세:"
qg.conditions?.each { condition ->
echo " • ${condition.metricKey}: ${condition.actualValue} (기준: ${condition.operator} ${condition.threshold})"
}
error "SonarQube 품질 게이트를 통과하지 못했습니다: ${qg.status}"
} else {
echo "✅ 품질 게이트 통과!"
echo "🎉 코드 품질 기준을 만족합니다"
}
}
}
}
}
stage('🏗️ Build Application') {
steps {
script {
echo "🏗️ === React 애플리케이션 빌드 시작 ==="
try {
// 빌드 환경 설정
sh '''
echo "🔧 빌드 환경 설정..."
export NODE_ENV=production
export GENERATE_SOURCEMAP=false
export INLINE_RUNTIME_CHUNK=false
export BUILD_PATH=build
echo "📋 빌드 설정:"
echo " • NODE_ENV: $NODE_ENV"
echo " • 소스맵 생성: $GENERATE_SOURCEMAP"
echo " • 런타임 청크 인라인: $INLINE_RUNTIME_CHUNK"
echo " • 빌드 경로: $BUILD_PATH"
'''
// React 프로덕션 빌드
sh '''
echo "🏗️ 프로덕션 빌드 시작..."
echo "⏱️ 빌드 시작 시간: $(date)"
npm run build
echo "⏱️ 빌드 완료 시간: $(date)"
echo "✅ 빌드 성공!"
'''
// 빌드 결과 분석
sh '''
echo "📊 빌드 결과 분석..."
if [ -d build ]; then
echo "📁 빌드 디렉토리 내용:"
ls -la build/
echo ""
echo "📏 빌드 크기 분석:"
du -sh build/
if [ -d build/static ]; then
echo " • 정적 파일: $(du -sh build/static 2>/dev/null || echo 'N/A')"
if [ -d build/static/js ]; then
echo " • JS 파일: $(find build/static/js -name '*.js' -exec du -ch {} + 2>/dev/null | tail -1 || echo 'N/A')"
fi
if [ -d build/static/css ]; then
echo " • CSS 파일: $(find build/static/css -name '*.css' -exec du -ch {} + 2>/dev/null | tail -1 || echo 'N/A')"
fi
fi
echo ""
echo "📋 주요 파일 목록:"
find build -name '*.js' -o -name '*.css' -o -name '*.html' | head -10
else
echo "❌ 빌드 디렉토리가 생성되지 않았습니다"
exit 1
fi
'''
// 빌드 아티팩트 아카이브
archiveArtifacts artifacts: 'build/**/*', fingerprint: true, allowEmptyArchive: false
echo "📦 빌드 아티팩트 아카이브 완료"
} catch (Exception e) {
error "❌ React 빌드 실패: ${e.getMessage()}"
}
}
}
post {
success {
echo "✅ 애플리케이션 빌드 성공"
}
failure {
echo "❌ 애플리케이션 빌드 실패"
}
}
}
stage('🐳 Docker Build') {
steps {
script {
echo "🐳 === Docker 이미지 빌드 시작 ==="
// 최적화된 Dockerfile 생성
writeFile file: 'Dockerfile', text: '''
# 멀티스테이지 빌드: 프로덕션 스테이지 (빌드는 Jenkins에서 완료)
FROM nginx:1.25-alpine
# 메타데이터 추가
LABEL maintainer="HealthSync Team" \\
version="1.0" \\
description="HealthSync Frontend Application"
# 보안을 위한 사용자 생성
RUN addgroup -g 1001 -S healthsync && \\
adduser -S healthsync -u 1001 -G healthsync
# 빌드된 React 애플리케이션 복사
COPY --chown=healthsync:healthsync build/ /usr/share/nginx/html/
# 최적화된 Nginx 설정 복사
COPY --chown=healthsync:healthsync nginx.conf /etc/nginx/nginx.conf
# 헬스체크용 curl 설치 및 권한 설정
RUN apk add --no-cache curl && \\
mkdir -p /var/log/nginx /var/cache/nginx /var/run && \\
chown -R healthsync:healthsync /usr/share/nginx/html && \\
chown -R healthsync:healthsync /var/cache/nginx && \\
chown -R healthsync:healthsync /var/log/nginx && \\
chown -R healthsync:healthsync /etc/nginx/conf.d && \\
touch /var/run/nginx.pid && \\
chown healthsync:healthsync /var/run/nginx.pid
# 포트 노출
EXPOSE 80
# 헬스체크 추가
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \\
CMD curl -f http://localhost:80/ || exit 1
# 사용자 변경
USER healthsync
# Nginx 실행
CMD ["nginx", "-g", "daemon off;"]
'''
// 최적화된 nginx.conf 생성
writeFile file: 'nginx.conf', text: '''
# Nginx 설정 - HealthSync Frontend 최적화
user healthsync;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 로그 포맷
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time"';
access_log /var/log/nginx/access.log main;
# 성능 최적화
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 10m;
# Gzip 압축 설정
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
# 서버 설정
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html index.htm;
# 보안 헤더
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 "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https:" always;
# React Router 지원 (SPA)
location / {
try_files $uri $uri/ /index.html;
# 캐시 설정 (HTML은 캐시 안함)
location ~* \\.html$ {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
}
# 정적 파일 캐싱
location ~* \\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
# API 프록시 (필요시)
location /api/ {
# API Gateway로 프록시 설정 (실제 환경에 맞게 수정)
proxy_pass http://healthsync-api-gateway:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
}
# 헬스체크 엔드포인트
location /health {
access_log off;
return 200 "healthy\\n";
add_header Content-Type text/plain;
}
# 에러 페이지
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}
'''
// Docker 이미지 빌드
sh '''
echo "🔧 Docker 이미지 빌드 준비..."
echo " • 이미지 이름: ${IMAGE_NAME}"
echo " • Latest 이미지: ${LATEST_IMAGE}"
echo " • 빌드 컨텍스트: $(pwd)"
echo "🐳 Docker 이미지 빌드 중..."
docker build \\
--tag ${IMAGE_NAME} \\
--tag ${LATEST_IMAGE} \\
--build-arg BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \\
--build-arg VCS_REF=${GIT_COMMIT} \\
--build-arg BUILD_VERSION=${BUILD_NUMBER} \\
.
echo "📊 이미지 정보 확인..."
docker images | grep ${ACR_REPOSITORY} | head -5
echo "📏 이미지 크기:"
docker image inspect ${IMAGE_NAME} --format='Size: {{.Size}} bytes ({{div .Size 1048576}} MB)'
echo "✅ Docker 이미지 빌드 완료"
'''
}
}
}
stage('🔒 Security Scan') {
steps {
script {
echo "🔒 === Docker 이미지 보안 스캔 시작 ==="
try {
// Docker 이미지 보안 스캔
sh '''
echo "🔍 Docker 이미지 취약점 스캔..."
# Trivy 보안 스캔 (설치되어 있는 경우)
if command -v trivy &> /dev/null; then
echo "🔍 Trivy로 취약점 스캔 중..."
trivy image --exit-code 0 --severity HIGH,CRITICAL --format table ${IMAGE_NAME}
trivy image --exit-code 0 --severity HIGH,CRITICAL --format json -o trivy-report.json ${IMAGE_NAME} || true
else
echo "⚠️ Trivy가 설치되지 않아 보안 스캔을 건너뜁니다"
fi
# Docker Scout 스캔 (Docker Desktop에 포함)
if command -v docker && docker scout version &> /dev/null; then
echo "🔍 Docker Scout로 취약점 스캔 중..."
docker scout cves ${IMAGE_NAME} || echo "Docker Scout 스캔 완료 (경고 포함)"
else
echo " Docker Scout를 사용할 수 없습니다"
fi
echo "✅ 보안 스캔 완료"
'''
// 스캔 결과 아카이브
if (fileExists('trivy-report.json')) {
archiveArtifacts artifacts: 'trivy-report.json', allowEmptyArchive: true
echo "📋 보안 스캔 리포트 아카이브 완료"
}
} catch (Exception e) {
echo "⚠️ 보안 스캔 중 오류: ${e.getMessage()}"
echo " 빌드는 계속 진행됩니다"
}
}
}
}
stage('🚀 Push to ACR') {
steps {
script {
echo "🚀 === Azure Container Registry에 이미지 푸시 시작 ==="
// Azure 크리덴셜로 로그인 및 푸시
withCredentials([azureServicePrincipal('Azure-Credential')]) {
sh '''
echo "🔑 Azure 서비스 프린시펄로 로그인..."
az login --service-principal \\
-u ${AZURE_CLIENT_ID} \\
-p ${AZURE_CLIENT_SECRET} \\
--tenant ${AZURE_TENANT_ID}
echo "🔑 ACR 로그인..."
az acr login --name acrhealthsync01
echo "📤 이미지 푸시 중..."
echo " • 태그별 이미지: ${IMAGE_NAME}"
docker push ${IMAGE_NAME}
echo " • Latest 이미지: ${LATEST_IMAGE}"
docker push ${LATEST_IMAGE}
echo "📊 푸시된 이미지 확인..."
az acr repository show-tags --name acrhealthsync01 --repository ${ACR_REPOSITORY} --top 5 --orderby time_desc
echo "✅ 이미지 푸시 완료!"
echo "🎯 푸시된 이미지:"
echo " • ${IMAGE_NAME}"
echo " • ${LATEST_IMAGE}"
'''
}
}
}
post {
success {
echo "✅ ACR 푸시 성공"
}
failure {
echo "❌ ACR 푸시 실패"
}
}
}
stage('📋 Update Deployment') {
when {
branch 'main'
}
steps {
script {
echo "📋 === Kubernetes 배포 매니페스트 업데이트 ==="
// GitOps를 위한 매니페스트 업데이트 또는 직접 배포
sh '''
echo "🔄 배포 매니페스트 업데이트..."
echo " • 새로운 이미지: ${IMAGE_NAME}"
echo " • 빌드 번호: ${BUILD_NUMBER}"
echo " • Git 커밋: ${GIT_COMMIT}"
# ArgoCD 또는 GitOps를 위한 배포 정보 생성
echo "📝 배포 정보 생성..."
cat > deployment-info.yaml << EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: healthsync-frontend
annotations:
deployment.jenkins.build: '${BUILD_NUMBER}'
deployment.jenkins.commit: '${GIT_COMMIT}'
deployment.jenkins.timestamp: '${BUILD_TIMESTAMP}'
deployment.jenkins.image: '${IMAGE_NAME}'
spec:
template:
spec:
containers:
- name: healthsync-frontend
image: ${IMAGE_NAME}
imagePullPolicy: Always
EOF
echo "📄 생성된 배포 정보:"
cat deployment-info.yaml
'''
// 배포 정보 아카이브
archiveArtifacts artifacts: 'deployment-info.yaml', allowEmptyArchive: true
}
}
}
}
post {
always {
script {
echo "🧹 === 파이프라인 정리 작업 시작 ==="
// 빌드 통계 출력
def buildDuration = currentBuild.duration ? "${(currentBuild.duration / 1000).intValue()}초" : "계산 중"
echo "📊 빌드 통계:"
echo " • 총 소요 시간: ${buildDuration}"
echo " • 최종 상태: ${currentBuild.result ?: 'SUCCESS'}"
echo " • 빌드 URL: ${env.BUILD_URL}"
echo " • 워크스페이스: ${WORKSPACE}"
// Docker 이미지 정리 (디스크 공간 확보)
sh '''
echo "🐳 Docker 정리..."
docker system prune -f --volumes || echo "Docker 정리 완료"
echo "📊 디스크 사용량:"
df -h ${WORKSPACE} || echo "디스크 정보 확인 불가"
'''
// 워크스페이스 정리
try {
cleanWs(
cleanWhenNotBuilt: false,
deleteDirs: true,
disableDeferredWipeout: true,
notFailBuild: true,
patterns: [
[pattern: 'node_modules', type: 'INCLUDE'],
[pattern: '.npm-cache', type: 'INCLUDE'],
[pattern: 'coverage', type: 'INCLUDE'],
[pattern: '.scannerwork', type: 'INCLUDE'],
[pattern: 'build', type: 'INCLUDE']
]
)
echo "✅ 워크스페이스 정리 완료"
} catch (Exception e) {
echo "⚠️ 워크스페이스 정리 중 오류: ${e.getMessage()}"
}
}
}
success {
script {
echo "🎉 === 파이프라인 성공적으로 완료! ==="
echo "✅ 모든 단계가 성공했습니다"
echo "🚀 배포 준비 완료: ${IMAGE_NAME}"
def buildDurationMinutes = currentBuild.duration ? "${(currentBuild.duration / 60000).intValue()}분 ${((currentBuild.duration % 60000) / 1000).intValue()}초" : "계산 중"
// 성공 알림 (Slack, Teams 등)
try {
def message = """
🎉 *HealthSync Frontend 빌드 성공!*
📋 *빌드 정보:*
• 브랜치: `${env.BRANCH_NAME ?: 'N/A'}`
• 빌드: `#${env.BUILD_NUMBER}`
• 커밋: `${env.GIT_COMMIT?.take(8) ?: 'N/A'}`
• 작성자: `${env.GIT_AUTHOR ?: 'N/A'}`
🐳 *이미지 정보:*
• 태그: `${IMAGE_TAG}`
• 레지스트리: `${ACR_REGISTRY}`
⏱️ *소요 시간:* ${buildDurationMinutes}
🔗 *빌드 링크:* ${env.BUILD_URL}
✅ 프로덕션 배포 준비 완료!
"""
// Slack 알림 (환경 변수 설정 시)
if (env.SLACK_CHANNEL) {
slackSend(
channel: env.SLACK_CHANNEL,
color: 'good',
message: message
)
} else {
echo "📢 Slack 채널이 설정되지 않아 알림을 건너뜁니다"
}
} catch (Exception e) {
echo "📢 알림 전송 실패: ${e.getMessage()}"
}
}
}
failure {
script {
echo "❌ === 파이프라인 실패! ==="
echo "💥 빌드가 실패했습니다"
echo "🔍 실패 단계: ${env.STAGE_NAME ?: '알 수 없음'}"
def buildDurationMinutes = currentBuild.duration ? "${(currentBuild.duration / 60000).intValue()}분 ${((currentBuild.duration % 60000) / 1000).intValue()}초" : "계산 중"
// 실패 알림
try {
def message = """
❌ *HealthSync Frontend 빌드 실패!*
📋 *빌드 정보:*
• 브랜치: `${env.BRANCH_NAME ?: 'N/A'}`
• 빌드: `#${env.BUILD_NUMBER}`
• 실패 단계: `${env.STAGE_NAME ?: '알 수 없음'}`
• 커밋: `${env.GIT_COMMIT?.take(8) ?: 'N/A'}`
⏱️ *소요 시간:* ${buildDurationMinutes}
🔗 *로그 확인:* ${env.BUILD_URL}console
🛠️ 개발팀 확인 필요!
"""
// Slack 알림 (환경 변수 설정 시)
if (env.SLACK_CHANNEL) {
slackSend(
channel: env.SLACK_CHANNEL,
color: 'danger',
message: message
)
} else {
echo "📢 Slack 채널이 설정되지 않아 알림을 건너뜁니다"
}
} catch (Exception e) {
echo "📢 실패 알림 전송 실패: ${e.getMessage()}"
}
}
}
unstable {
script {
echo "⚠️ === 파이프라인이 불안정한 상태로 완료 ==="
echo "🔶 일부 테스트 실패 또는 경고 발생"
def buildDurationMinutes = currentBuild.duration ? "${(currentBuild.duration / 60000).intValue()}분 ${((currentBuild.duration % 60000) / 1000).intValue()}초" : "계산 중"
// 불안정 상태 알림
try {
def message = """
⚠️ *HealthSync Frontend 빌드 불안정*
📋 *빌드 정보:*
• 브랜치: `${env.BRANCH_NAME ?: 'N/A'}`
• 빌드: `#${env.BUILD_NUMBER}`
• 상태: `UNSTABLE`
💡 *가능한 원인:*
• 일부 테스트 실패
• 코드 품질 경고
• 보안 스캔 경고
⏱️ *소요 시간:* ${buildDurationMinutes}
🔗 *상세 확인:* ${env.BUILD_URL}
🔍 검토 후 조치 필요
"""
// Slack 알림 (환경 변수 설정 시)
if (env.SLACK_CHANNEL) {
slackSend(
channel: env.SLACK_CHANNEL,
color: 'warning',
message: message
)
} else {
echo "📢 Slack 채널이 설정되지 않아 알림을 건너뜁니다"
}
} catch (Exception e) {
echo "📢 불안정 알림 전송 실패: ${e.getMessage()}"
}
}
}
}
}