From d3ea978375a36fa9b8fcfdb25b61e1919ceeba0a Mon Sep 17 00:00:00 2001 From: admin Date: Thu, 19 Mar 2026 13:22:38 +0900 Subject: [PATCH] Update common/Jenkinsfile.docker.j2 --- common/Jenkinsfile.docker.j2 | 262 ++++++++++++++++++++++++++++++++--- 1 file changed, 242 insertions(+), 20 deletions(-) diff --git a/common/Jenkinsfile.docker.j2 b/common/Jenkinsfile.docker.j2 index 6eadef8..d08741d 100644 --- a/common/Jenkinsfile.docker.j2 +++ b/common/Jenkinsfile.docker.j2 @@ -1,53 +1,275 @@ pipeline { agent any + + options { timestamps() } + parameters { - string(name: 'BRANCH_NAME', defaultValue: '', description: '빌드할 브랜치명(비우면 main)') - string(name: 'GIT_COMMIT_HASH', defaultValue: '', description: '빌드할 커밋 해시(비우면 최신 커밋 기준)') + 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' - def checkoutBranch = params.BRANCH_NAME?.trim() ? params.BRANCH_NAME.trim() : 'main' - def commitHash = params.GIT_COMMIT_HASH?.trim() + sh 'git fetch --all || true' + + def checkoutBranch = params.BRANCH_NAME?.trim() ?: 'main' + def commitHash = params.GIT_COMMIT_HASH?.trim() + if (commitHash) { - echo "브랜치(${checkoutBranch})의 커밋 해시(${commitHash})로 체크아웃합니다." sh "git fetch origin ${checkoutBranch}" sh "git checkout ${checkoutBranch}" sh "git checkout ${commitHash}" } else { - echo "브랜치(${checkoutBranch})의 최신 커밋으로 체크아웃합니다." 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.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('Build & Deploy') { + + // ────────────────────────────────────────────── + // Stage 2. Build + // - Docker 이미지 빌드 + // - 실패 시 로그에 pip install / SyntaxError 등 원인 출력 + // ────────────────────────────────────────────── + stage('Build') { steps { script { - def composeProject = "{{ node_id }}-" + (env.BRANCH_NAME ?: "") + "{{ node_type }}" + 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' + } + } + } - writeFile file: '.env', text: """ -BRANCH_NAME=${env.BRANCH_NAME} -GIT_COMMIT_HASH=${env.GIT_COMMIT_HASH} -""" - sh "cat .env" - sh "cat docker-compose.yml" - sh "docker compose -p ${composeProject} --env-file .env down --rmi all" - sh "docker compose -p ${composeProject} build --no-cache" - sh "docker compose -p ${composeProject} --env-file .env up --force-recreate --remove-orphans -d" + // ────────────────────────────────────────────── + // 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() } } -} +} \ No newline at end of file