Update common/Jenkinsfile.docker.j2

This commit is contained in:
2026-03-19 13:22:38 +09:00
parent b6c3c098dc
commit d3ea978375

View File

@@ -1,53 +1,275 @@
pipeline { pipeline {
agent any agent any
options { timestamps() }
parameters { parameters {
string(name: 'BRANCH_NAME', defaultValue: '', description: '빌드할 브랜치명(비우면 main)') string(name: 'BRANCH_NAME', defaultValue: '', description: '빌드할 브랜치명 (비우면 main)')
string(name: 'GIT_COMMIT_HASH', defaultValue: '', description: '빌드할 커밋 해시(비우면 최신 커밋 기준)') string(name: 'GIT_COMMIT_HASH', defaultValue: '', description: '빌드할 커밋 해시 (비우면 최신 커밋)')
} }
environment {
NODE_ID = '{{ node_id }}'
NODE_TYPE = '{{ node_type }}'
}
stages { stages {
// ──────────────────────────────────────────────
// Stage 1. Checkout
// - 브랜치 / 커밋 해시 기준으로 소스코드 체크아웃
// - 이후 stage 에서 사용할 환경변수 세팅
// ──────────────────────────────────────────────
stage('Checkout') { stage('Checkout') {
steps { steps {
script { script {
sh 'git fetch --all' sh 'git fetch --all || true'
def checkoutBranch = params.BRANCH_NAME?.trim() ? params.BRANCH_NAME.trim() : 'main'
def commitHash = params.GIT_COMMIT_HASH?.trim() def checkoutBranch = params.BRANCH_NAME?.trim() ?: 'main'
def commitHash = params.GIT_COMMIT_HASH?.trim()
if (commitHash) { if (commitHash) {
echo "브랜치(${checkoutBranch})의 커밋 해시(${commitHash})로 체크아웃합니다."
sh "git fetch origin ${checkoutBranch}" sh "git fetch origin ${checkoutBranch}"
sh "git checkout ${checkoutBranch}" sh "git checkout ${checkoutBranch}"
sh "git checkout ${commitHash}" sh "git checkout ${commitHash}"
} else { } else {
echo "브랜치(${checkoutBranch})의 최신 커밋으로 체크아웃합니다."
sh "git fetch origin ${checkoutBranch}" sh "git fetch origin ${checkoutBranch}"
sh "git checkout ${checkoutBranch}" sh "git checkout ${checkoutBranch}"
commitHash = sh(script: "git rev-parse --short HEAD", returnStdout: true).trim() 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.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 { steps {
script { 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} // Stage 3. Down
GIT_COMMIT_HASH=${env.GIT_COMMIT_HASH} // - 기존 실행 중인 컨테이너 종료 및 정리
""" // - 최초 배포 시 컨테이너가 없어도 오류 처리 안 함
sh "cat .env" // ──────────────────────────────────────────────
sh "cat docker-compose.yml" stage('Down') {
sh "docker compose -p ${composeProject} --env-file .env down --rmi all" steps {
sh "docker compose -p ${composeProject} build --no-cache" script {
sh "docker compose -p ${composeProject} --env-file .env up --force-recreate --remove-orphans -d" 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 { 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 { always {
cleanWs() cleanWs()
} }
} }
} }