pipeline { agent any options { timestamps() } parameters { string(name: 'BRANCH_NAME', defaultValue: '', description: '빌드할 브랜치명 (비우면 main)') string(name: 'GIT_COMMIT_HASH', defaultValue: '', description: '빌드할 커밋 해시 (비우면 최신 커밋)') } environment { NODE_ID = '{{ node_id }}' NODE_TYPE = '{{ node_type }}' } stages { // ────────────────────────────────────────────── // Stage 1. Checkout // - 브랜치 / 커밋 해시 기준으로 소스코드 체크아웃 // - 이후 stage 에서 사용할 환경변수 세팅 // ────────────────────────────────────────────── stage('Checkout') { steps { script { sh 'git fetch --all || true' def checkoutBranch = params.BRANCH_NAME?.trim() ?: 'main' def commitHash = params.GIT_COMMIT_HASH?.trim() if (commitHash) { sh "git fetch origin ${checkoutBranch}" sh "git checkout ${checkoutBranch}" sh "git checkout ${commitHash}" } else { sh "git fetch origin ${checkoutBranch}" sh "git checkout ${checkoutBranch}" commitHash = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim() } env.BRANCH_NAME = checkoutBranch env.GIT_COMMIT_HASH = commitHash env.COMPOSE_PROJECT = "${NODE_ID}-${checkoutBranch}" // .env 파일 생성 (docker compose 에서 참조) writeFile file: '.env', text: """\ BRANCH_NAME=${env.BRANCH_NAME} GIT_COMMIT_HASH=${env.GIT_COMMIT_HASH} NODE_ID=${NODE_ID} NODE_TYPE=${NODE_TYPE} """ echo "NODE_ID=${NODE_ID}" echo "BRANCH_NAME=${env.BRANCH_NAME}" echo "GIT_COMMIT_HASH=${env.GIT_COMMIT_HASH}" echo "COMPOSE_PROJECT=${env.COMPOSE_PROJECT}" } } } // ────────────────────────────────────────────── // Stage 2. Build // - Docker 이미지 빌드 // - 실패 시 로그에 pip install / SyntaxError 등 원인 출력 // ────────────────────────────────────────────── stage('Build') { steps { script { sh "docker compose -p ${env.COMPOSE_PROJECT} --env-file .env build --no-cache" } } post { failure { echo '[Build] ★ BUILD FAILED ★' echo '[Build] docker compose build 실패. 위 로그에서 원인을 확인하세요.' echo '[Build] 주요 원인: Dockerfile 오류 / pip install 실패 / SyntaxError / ModuleNotFoundError' } } } // ────────────────────────────────────────────── // Stage 3. Down // - 기존 실행 중인 컨테이너 종료 및 정리 // - 최초 배포 시 컨테이너가 없어도 오류 처리 안 함 // ────────────────────────────────────────────── stage('Down') { steps { script { sh "docker compose -p ${env.COMPOSE_PROJECT} --env-file .env down --rmi all || true" echo "[Down] 기존 컨테이너 정리 완료" } } } // ────────────────────────────────────────────── // Stage 4. Deploy // - 빌드된 이미지로 컨테이너 기동 // - 컨테이너 ID 출력 (watcher 가 추적에 사용) // ────────────────────────────────────────────── stage('Deploy') { steps { script { sh "docker compose -p ${env.COMPOSE_PROJECT} --env-file .env up --force-recreate --remove-orphans -d" // 컨테이너 ID 및 상태 출력 sh "docker compose -p ${env.COMPOSE_PROJECT} ps" sh "docker compose -p ${env.COMPOSE_PROJECT} ps -q | xargs docker inspect --format '{{.Id}} {{.State.Status}} {{.State.ExitCode}}' || true" } } post { failure { echo '[Deploy] ★ DEPLOY FAILED ★' sh "docker compose -p ${env.COMPOSE_PROJECT} ps || true" } } } // ────────────────────────────────────────────── // Stage 5. Verify // - 컨테이너 정상 기동 여부 검증 // - Running 상태 + exit code 확인 // - Docker HEALTHCHECK 결과 확인 (정의된 경우) // - 성공 시: 기동 로그 첫 30줄 출력 // - 실패 시: 컨테이너 로그 마지막 50줄 출력 → Poller 가 수집 // ────────────────────────────────────────────── stage('Verify') { steps { script { def maxWait = 60 // 최대 대기 시간 (초) def interval = 3 // 확인 간격 (초) def stabilize = 10 // 안정화 대기 시간 (초) def elapsed = 0 def containerId = sh( script: "docker compose -p ${env.COMPOSE_PROJECT} ps -q | head -1", returnStdout: true ).trim() if (!containerId) { error('[Verify] 컨테이너 ID 를 찾을 수 없습니다.') } echo "[Verify] 컨테이너 ID: ${containerId}" // ── Running 상태 대기 ────────────────── def isRunning = false while (elapsed < maxWait) { def status = sh( script: "docker inspect --format '{{.State.Status}}' ${containerId}", returnStdout: true ).trim() def exitCode = sh( script: "docker inspect --format '{{.State.ExitCode}}' ${containerId}", returnStdout: true ).trim() echo "[Verify] elapsed=${elapsed}s | status=${status} | exitCode=${exitCode}" if (status == 'exited' || exitCode != '0') { echo '[Verify] ★ VERIFY FAILED ★ 컨테이너가 비정상 종료되었습니다.' echo "[Verify] Status: ${status} | ExitCode: ${exitCode}" echo '[Verify] --- 컨테이너 로그 (마지막 50줄) ---' sh "docker logs --tail 50 ${containerId} || true" error("[Verify] 컨테이너 비정상 종료. ExitCode=${exitCode}") } if (status == 'running') { isRunning = true break } sleep(interval) elapsed += interval } if (!isRunning) { echo '[Verify] ★ VERIFY FAILED ★ 타임아웃: 컨테이너가 running 상태에 도달하지 못했습니다.' echo '[Verify] --- 컨테이너 로그 (마지막 50줄) ---' sh "docker logs --tail 50 ${containerId} || true" error("[Verify] 컨테이너 기동 타임아웃 (${maxWait}s 초과)") } // ── 안정화 대기 (바로 죽는 케이스 방지) ─ echo "[Verify] Running 확인. ${stabilize}초 안정화 대기 중..." sleep(stabilize) def finalStatus = sh( script: "docker inspect --format '{{.State.Status}}' ${containerId}", returnStdout: true ).trim() def finalExitCode = sh( script: "docker inspect --format '{{.State.ExitCode}}' ${containerId}", returnStdout: true ).trim() if (finalStatus != 'running' || finalExitCode != '0') { echo '[Verify] ★ VERIFY FAILED ★ 안정화 대기 후 컨테이너 비정상 종료' echo '[Verify] --- 컨테이너 로그 (마지막 50줄) ---' sh "docker logs --tail 50 ${containerId} || true" error("[Verify] 안정화 실패. Status=${finalStatus} ExitCode=${finalExitCode}") } // ── Docker HEALTHCHECK 확인 (정의된 경우) ─ def healthStatus = sh( script: "docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' ${containerId}", returnStdout: true ).trim() if (healthStatus != 'none') { echo "[Verify] HEALTHCHECK 감지됨. 상태: ${healthStatus}" def healthElapsed = 0 def healthMaxWait = 60 while (healthStatus == 'starting' && healthElapsed < healthMaxWait) { sleep(interval) healthElapsed += interval healthStatus = sh( script: "docker inspect --format '{{.State.Health.Status}}' ${containerId}", returnStdout: true ).trim() echo "[Verify] HEALTHCHECK elapsed=${healthElapsed}s | status=${healthStatus}" } if (healthStatus == 'unhealthy') { echo '[Verify] ★ VERIFY FAILED ★ Docker HEALTHCHECK 실패' echo '[Verify] --- 컨테이너 로그 (마지막 50줄) ---' sh "docker logs --tail 50 ${containerId} || true" error('[Verify] Docker HEALTHCHECK unhealthy') } if (healthStatus == 'starting') { echo "[Verify] ★ VERIFY FAILED ★ HEALTHCHECK 타임아웃 (${healthMaxWait}s 초과)" error("[Verify] HEALTHCHECK 타임아웃") } } // ── 최종 성공 ─────────────────────────── echo '[Verify] ✅ VERIFY SUCCESS' echo "[Verify] Status=${finalStatus} | ExitCode=${finalExitCode} | Health=${healthStatus}" echo '[Verify] --- 컨테이너 기동 로그 (첫 30줄) ---' sh "docker logs ${containerId} 2>&1 | head -30 || true" echo '[Verify] --- 컨테이너 상태 ---' sh "docker inspect --format 'ID={{.Id}}\nStatus={{.State.Status}}\nStartedAt={{.State.StartedAt}}\nImage={{.Config.Image}}' ${containerId} || true" } } post { failure { echo '[Verify] ★ VERIFY FAILED - post ★' script { def containerId = sh( script: "docker compose -p ${env.COMPOSE_PROJECT} ps -q | head -1 || true", returnStdout: true ).trim() if (containerId) { echo '[Verify] --- 최종 컨테이너 상태 ---' sh "docker inspect ${containerId} || true" } } } } } } post { success { echo '✅ DEPLOY SUCCESS' script { sh "docker compose -p ${env.COMPOSE_PROJECT} ps || true" } } failure { echo '★ DEPLOY FAILED ★' echo "NODE_ID=${NODE_ID} | BRANCH=${env.BRANCH_NAME} | COMMIT=${env.GIT_COMMIT_HASH}" } always { cleanWs() } } }