1020 lines
44 KiB
Groovy
1020 lines
44 KiB
Groovy
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()}"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} |