Jenkinsfile은 Jenkins Pipeline(코드가 올라온 뒤 빌드·테스트·배포까지 거치는 자동화된 작업 순서)을 코드로 정의하는 파일이다. 프로젝트 저장소의 루트 디렉토리(그 프로젝트의 가장 바깥 폴더)에 두고, 빌드, 테스트, 배포 같은 자동화 작업을 Groovy(자바와 비슷하지만 세미콜론이나 괄호를 생략할 수 있는 더 자유로운 문법을 가진 언어) 기반의 선언형(Declarative) 또는 스크립트형(Scripted) 문법으로 적는다.
Jenkins Pipeline을 쓰면 이런 점이 좋다.
- 코드로 관리 - Jenkinsfile도 코드라서 Git 같은 버전 관리 시스템으로 누가 언제 바꿨는지 추적할 수 있다
- 재사용성 - Shared Libraries(여러 Jenkinsfile이 공통으로 가져다 쓰는, 미리 만들어 둔 Groovy 코드 모음)로 공통 코드를 재사용한다
- 가독성 - 선언형 문법으로 직관적으로 정의한다
- 확장성 - 다양한 플러그인을 붙여 기능을 늘린다
이 글의 예제에는 sh 'make build', sh 'npm install'처럼 셸 명령이 많이 나온다. 이 명령들은 Jenkins 문법이 아니라 Jenkins가 대신 실행해 주는 “할 일”의 예시다. 처음 나올 때 한 줄씩 풀어 두었고, 나머지는 Jenkinsfile 문법에 집중한다.
기본 구문: 선언형 vs 스크립트형 파이프라인
선언형 파이프라인은 pipeline 블록 안에 정해진 칸을 채운다
선언형 파이프라인은 더 간단하고 직관적인 문법을 제공한다.
| 특징 | 설명 |
|---|---|
| 구조 | pipeline 블록 사용 |
| 가독성 | 높음 - 선언적이고 이해하기 쉬움 |
| 커스터마이징 | 낮음 - 기본 기능으로 제한적 |
| 권장 | 대부분의 일반적인 CI/CD 작업 |
pipeline {
agent any
environment {
BUILD_VERSION = '1.0.0'
}
stages {
stage('Build') {
steps {
echo "Building version ${BUILD_VERSION}"
sh 'echo "Build step"'
}
}
stage('Test') {
steps {
echo "Testing"
sh 'echo "Test step"'
}
}
}
post {
always {
echo "Pipeline completed"
}
}
}
한 줄씩 따라가기
agent any— 이 파이프라인을 어느 컴퓨터(에이전트)에서 실행할지 정하는 줄이다.any는 “사용 가능한 아무 에이전트에서나 실행해도 된다”는 뜻이다. 이 줄이 없으면 Jenkins가 어디서 실행할지 몰라 멈춘다.environment { BUILD_VERSION = '1.0.0' }— 파이프라인 전체에서 쓸 환경변수를 정의하는 블록이다. 자바의 변수 선언과 비슷하지만, 여기 적은 값은 Groovy 코드뿐 아니라sh로 실행하는 셸 명령에서도 읽을 수 있는 “이름=값” 쌍이 된다.{ }로 묶어서 “이 안에 적은 건 전부 환경변수다”라고 Jenkins에게 알려준다.stages { }— 이 파이프라인이 거칠 단계들을 묶는 블록이다. 그 안의stage('Build'),stage('Test')처럼 이름이 붙은 덩어리 하나하나가 단계 하나다.steps { }— 한 단계(stage) 안에서 실제로 실행할 명령들을 묶는 블록이다.echo "Building version ${BUILD_VERSION}"— 화면(콘솔 로그)에 글자를 출력하는 명령이다. 문자열 안의${BUILD_VERSION}은 위에서 정의한 변수 값을 그 자리에 끼워 넣으라는 뜻으로, 자바의 문자열 연결("Building version " + BUILD_VERSION)과 같은 효과를 낸다. 이 끼워 넣기는 큰따옴표("...") 문자열에서만 된다. 작은따옴표('...') 문자열은${...}를 글자 그대로 둔다.sh 'echo "Build step"'—sh는 셸(운영체제의 명령어 실행 창) 명령을 실행하라는 뜻이다. 작은따옴표 안의echo "Build step"은 Jenkins가 리눅스에 실제로 넘기는 명령어이고, 바로 위 줄의 Groovyecho(로그 출력)와는 다른, 셸 프로그램의echo명령이다.post { always { ... } }— 파이프라인이 끝난 뒤에 실행할 처리를 정의하는 블록이다.always는 성공하든 실패하든 항상 실행된다는 뜻이다.
중괄호({ })로 묶인 블록이 계속 중첩되는 이유는, Jenkinsfile이 “파이프라인 안에 단계들이 있고, 단계 안에 명령들이 있다”는 구조를 그대로 코드로 옮겨 적기 때문이다. 바깥쪽 블록부터 읽고 그 안쪽 블록을 순서대로 읽어 내려가면 된다.
스크립트형 파이프라인은 node 블록 안에 Groovy 코드를 자유롭게 쓴다
스크립트형 파이프라인은 Groovy 언어의 모든 기능을 활용할 수 있다.
| 특징 | 설명 |
|---|---|
| 구조 | node 블록 사용 |
| 가독성 | 낮음 - 코드 스타일 자유로움 |
| 커스터마이징 | 높음 - 완전한 프로그래밍 가능 |
| 권장 | 복잡한 로직이 필요한 경우 |
node {
stage('Build') {
echo "Building"
sh 'echo "Build step"'
}
stage('Test') {
echo "Testing"
try {
sh 'echo "Test step"'
} catch (Exception e) {
echo "Test failed: ${e.message}"
throw e
}
}
}
node { }— 선언형의agent any에 해당한다. Jenkins가 실행할 컴퓨터(노드) 하나를 잡고, 그 컴퓨터의 작업 폴더에서 중괄호 안의 코드를 실행한다. 괄호 안에 아무것도 적지 않으면 비어 있는 아무 노드나 쓴다.stage('Build') { ... }— 선언형과 달리stages·steps칸 없이stage안에 바로 명령을 적는다.try { } catch (Exception e) { }— 자바의try/catch와 똑같다. 셸 명령이 실패하면sh가 예외를 던지므로 여기서 잡을 수 있다.
catch (Exception e)로 잡은 예외 e에서 메시지를 꺼낼 때 자바처럼 e.getMessage()를 호출하지 않고 e.message라고 적은 점이 다르다. Groovy는 get으로 시작하는 메서드를 필드처럼 점(.) 뒤에 이름만 적어도 자동으로 호출해 주는 문법(프로퍼티 접근)을 지원해서, e.message가 e.getMessage()와 같은 값을 돌려준다.
스테이지 및 단계 구조
선언형 파이프라인의 바깥 뼈대는 agent · environment · stages · post 넷이다
pipeline {
agent any // 실행 환경 지정
environment { // 전역 환경 변수
VERSION = '1.0.0'
}
stages { // 스테이지 정의
stage('Phase 1') {
steps { // 단계별 명령
echo "Phase 1"
}
}
}
post { // 파이프라인 완료 후 처리
always {
echo "Complete"
}
}
}
주석으로 붙은 설명처럼, agent·environment·stages·post는 모두 파이프라인을 이루는 블록 이름이다. 앞서 “한 줄씩 따라가기”에서 본 것처럼 바깥쪽 pipeline { } 안에 이 블록들이 나란히 들어가고, stages 블록 안에는 다시 stage 블록들이 들어가는 중첩 구조다.
Groovy의 주석은 자바와 같이 //(한 줄)과 /* */(여러 줄)이다. #은 파일 맨 첫 줄의 셰뱅(shebang) 줄에만 쓴다. 셰뱅은 유닉스 계열 운영체제가 읽는 특수한 줄로, 스크립트 파일을 명령줄에서 바로 실행할 수 있게 해 준다. 코드 중간에 #으로 주석을 달면 안 된다.
스테이지마다 agent와 when으로 실행 장소와 조건을 따로 정한다
| 요소 | 설명 | 예제 |
|---|---|---|
| stage | 파이프라인 단계 이름 | stage('Build') |
| steps | 해당 스테이지에서 수행할 작업 | sh 'npm install' |
| agent | 스테이지별 실행 환경 지정 | agent { label 'ubuntu' } |
| when | 조건부 실행 | when { branch 'main' } |
agent { label 'ubuntu' }의 label(라벨)은 사람이 에이전트에 붙여 둔 이름표다. 'ubuntu'라는 이름표가 붙은 에이전트 중 하나에서 이 스테이지를 돌리라는 뜻이다.
pipeline {
agent any
stages {
stage('Build') {
agent { label 'linux' } // 스테이지별 에이전트
when { // 조건부 실행
branch 'main'
}
steps {
echo "Building on Linux"
sh 'make build'
}
}
stage('Deploy') {
when {
expression {
currentBuild.result == null || currentBuild.result == 'SUCCESS'
}
}
steps {
echo "Deploying"
sh 'make deploy'
}
}
}
}
agent { label 'linux' }— 파이프라인 전체는agent any로 아무 데서나 돌지만, 이 스테이지만은linux이름표가 붙은 에이전트에서 돌린다.when { branch 'main' }— 지금 빌드하는 코드가main브랜치일 때만 이 스테이지를 실행한다. Jenkins 문서에 따르면branch조건은 브랜치마다 파이프라인을 따로 돌리는 Multibranch Pipeline에서만 동작한다.sh 'make build'— make는 프로젝트의 makefile에 적어 둔 일을 이름으로 실행하는 도구다.make build·make test·make deploy·make lint는 그 프로젝트의 makefile에 각각 build·test·deploy·lint라는 이름으로 적힌 일을 돌린다(예시라서 makefile 내용은 생략했다).currentBuild.result == null || currentBuild.result == 'SUCCESS'—currentBuild는 Jenkins가 모든 파이프라인에 미리 넣어 주는 객체로, 지금 돌고 있는 빌드를 가리킨다. 그result는 보통SUCCESS(성공)·UNSTABLE(불안정, 주로 테스트 실패. 아래 「Post 섹션」에서 다룬다)·FAILURE(실패) 중 하나인데, 빌드가 아직 진행 중이라 결과가 정해지지 않았으면null이다. 그래서 “아직 아무도 실패로 표시하지 않았거나(null) 성공으로 표시된 경우에만 배포하라”는 조건이 된다.
환경 변수 설정 및 활용
전역 환경 변수는 pipeline 바로 아래 environment에 둔다
파이프라인 전체에서 사용 가능한 변수를 정의한다.
pipeline {
agent any
environment {
// 정적 변수
APP_NAME = 'my-app'
ENVIRONMENT = 'production'
// Jenkins가 이미 주는 값을 같은 이름에 다시 담는다
BUILD_NUMBER = "${BUILD_NUMBER}"
// 명령어 출력값을 변수로 설정
GIT_COMMIT = "${sh(returnStdout: true, script: 'git rev-parse HEAD').trim()}"
}
stages {
stage('Print Variables') {
steps {
echo "App: ${APP_NAME}"
echo "Env: ${ENVIRONMENT}"
echo "Commit: ${GIT_COMMIT}"
}
}
}
}
BUILD_NUMBER = "${BUILD_NUMBER}"는 명령어를 실행하는 줄이 아니다. Jenkins가 빌드마다 넣어 주는 빌드 번호(아래 「시스템 환경 변수」 표)를 같은 이름에 다시 담는 줄이라, 자기 자신을 자기에게 넣는 것처럼 보이는 게 맞다. 이 줄을 지웠을 때 무엇이 달라지는지는 확인하지 않았다.
GIT_COMMIT = "${sh(returnStdout: true, script: 'git rev-parse HEAD').trim()}"은 Git 명령을 실행해서 그 결과를 변수에 담는 줄이다. 앞에서 ${...}에 변수 이름을 넣었는데, Groovy는 ${ } 안에 변수뿐 아니라 메서드 호출 같은 식(expression)도 넣을 수 있다. 그 식을 실행한 결과가 문자열 안에 끼워진다.
git rev-parse HEAD— “지금 체크아웃된 커밋의 이름표(커밋 해시, 40자리 영문·숫자)를 보여줘”라는 Git 명령이다.sh(returnStdout: true, script: '...')— 괄호 안에이름: 값을 늘어놓는 것은 Groovy의 이름 붙은 인자(named argument)다. 자바처럼 순서로 인자를 넘기지 않고 이름으로 넘긴다(아래 「Post 섹션」에서mail로 한 번 더 본다).script에 실행할 명령을,returnStdout: true로 “결과를 로그에 찍지 말고 문자열로 돌려 달라“를 넘긴다..trim()— 그 결과 끝에 붙는 줄바꿈 문자를 지운다.
스테이지별 환경 변수는 그 stage 안의 environment에 둔다
특정 스테이지에서만 사용 가능한 변수를 정의한다.
pipeline {
agent any
environment {
GLOBAL_VAR = 'global'
}
stages {
stage('Development') {
environment {
ENV_TYPE = 'dev'
DB_HOST = 'dev-db.example.com'
}
steps {
echo "Global: ${GLOBAL_VAR}"
echo "Stage: ${ENV_TYPE}"
echo "DB: ${DB_HOST}"
}
}
stage('Production') {
environment {
ENV_TYPE = 'prod'
DB_HOST = 'prod-db.example.com'
}
steps {
echo "Global: ${GLOBAL_VAR}"
echo "Stage: ${ENV_TYPE}"
echo "DB: ${DB_HOST}"
}
}
}
}
DB_HOST는 DB가 돌고 있는 컴퓨터의 이름(호스트)이다. 같은 이름의 변수가 개발 단계에서는 개발용 DB를, 운영 단계에서는 운영용 DB를 가리키게 해서, 명령은 그대로 두고 값만 스테이지마다 바꾼다. GLOBAL_VAR는 바깥 environment에 있으므로 두 스테이지 모두에서 읽힌다.
동적 환경 변수는 실행 중에 명령 결과로 값을 정한다
런타임(파이프라인이 실제로 실행되는 동안)에 명령어 실행 결과를 변수로 설정한다.
pipeline {
agent any
environment {
// 명령어 출력값 캡처
TIMESTAMP = "${sh(returnStdout: true, script: 'date +%Y%m%d_%H%M%S').trim()}"
// 종료 상태 캡처
EXIT_CODE = "${sh(returnStatus: true, script: 'ls /tmp')}"
// 조건에 따른 변수
DEPLOY_ENV = "${env.BRANCH_NAME == 'main' ? 'production' : 'staging'}"
}
stages {
stage('Info') {
steps {
echo "Build Time: ${TIMESTAMP}"
echo "Deploy to: ${DEPLOY_ENV}"
}
}
}
}
date +%Y%m%d_%H%M%S는 지금 시각을 정해진 모양으로 찍는 셸 명령이다. %Y는 연도 네 자리, %m은 월, %d는 일, %H·%M·%S는 시·분·초라서 20261020_050508 같은 글자가 나온다.
EXIT_CODE = "${sh(returnStatus: true, script: 'ls /tmp')}"에서 ls /tmp는 리눅스에서 /tmp 디렉터리 안의 파일 목록을 보여주는 명령이고, returnStatus: true는 그 명령의 출력 내용이 아니라 명령이 성공했는지 실패했는지를 나타내는 종료 코드(0이면 성공)만 돌려받겠다는 뜻이다. 앞의 GIT_COMMIT 예제가 returnStdout(출력 내용)을 받은 것과 대조된다.
env.BRANCH_NAME은 지금 빌드 중인 브랜치 이름이다. 아래 시스템 변수 표의 값들과 달리, 브랜치마다 파이프라인을 따로 돌리는 Multibranch Pipeline에서 Jenkins가 넣어 주는 값이다.
DEPLOY_ENV = "${env.BRANCH_NAME == 'main' ? ... }"에서 env.BRANCH_NAME처럼 env.을 붙이는 쪽과, 앞에서 본 ${BUILD_VERSION}처럼 바로 이름만 쓰는 쪽이 둘 다 있다. 둘 다 같은 환경 변수를 가리키지만, env.VAR는 “이건 환경 변수다”라고 명시적으로 적는 표기이고 ${VAR}는 그 변수가 지금 스코프(블록 범위) 안에서 환경 변수로 자동으로 인식될 때 쓰는 줄임 표기다. 조건식(? :)처럼 변수인지 아닌지 헷갈릴 수 있는 자리에서는 env.를 붙여 분명히 밝히는 경우가 많다. 이름만 쓴 쪽이 어디까지 자동으로 인식되는지 정확한 기준은 확인하지 않았다. 헷갈리면 env.를 붙인다 — Jenkins 문서도 Multibranch가 넣어 주는 BRANCH_NAME 같은 값을 env 전역 변수로 읽는다고 적는다. 'staging'은 운영에 올리기 전에 마지막으로 확인하는 스테이징 서버를 가리킨다.
시스템 환경 변수는 Jenkins가 빌드마다 넣어 준다
Jenkins에서 제공하는 기본 환경 변수들이다.
| 변수 | 설명 | 예제 |
|---|---|---|
${BUILD_NUMBER} |
빌드 번호 | Build #123 |
${BUILD_ID} |
빌드 ID | 2026-01-04_10-30-45 |
${BUILD_URL} |
이 빌드 결과를 볼 수 있는 Jenkins 화면 주소 | .../job/my-pipeline/17/ |
${JOB_NAME} |
작업 이름 | my-pipeline |
${WORKSPACE} |
작업 디렉토리 | /var/jenkins_home/workspace/my-pipeline |
${GIT_BRANCH} |
현재 브랜치 | origin/main |
${GIT_COMMIT} |
현재 커밋 SHA | abc123def456... |
origin/main의 origin은 내가 복제해 온 원격 저장소의 기본 이름이다. 그래서 origin/main은 “원격 저장소의 main 브랜치”라는 뜻이다. 표의 “커밋 SHA”는 위에서 본 커밋 해시를 가리킨다.
pipeline {
agent any
stages {
stage('Show System Variables') {
steps {
echo "Job: ${JOB_NAME}"
echo "Build #: ${BUILD_NUMBER}"
echo "Branch: ${GIT_BRANCH}"
echo "Commit: ${GIT_COMMIT}"
echo "Workspace: ${WORKSPACE}"
}
}
}
}
에러 핸들링
Post 섹션은 빌드 결과에 따라 실행할 처리를 고른다
선언형 파이프라인에서는 post 섹션으로 결과에 따른 처리를 정의한다.
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'echo "Building..."'
sh 'make build'
}
}
stage('Test') {
steps {
sh 'make test'
}
}
}
post {
// 항상 실행
always {
echo "Cleaning up..."
sh 'rm -rf build/tmp/*'
}
// 성공 시만 실행
success {
echo "Pipeline succeeded!"
mail to: 'team@example.com',
subject: 'Build Success',
body: "Build ${BUILD_NUMBER} completed successfully."
}
// 실패 시만 실행
failure {
echo "Pipeline failed!"
mail to: 'team@example.com',
subject: 'Build Failed',
body: "Build ${BUILD_NUMBER} failed. Check logs."
}
// 불안정한 상태
unstable {
echo "Build is unstable"
}
// 상태 변경 시
changed {
echo "Build status changed"
}
}
}
sh 'rm -rf build/tmp/*'—rm은 파일을 지우는 셸 명령이다.-r은 폴더와 그 안의 내용까지 통째로,-f는 없는 파일이 있어도 묻지 않고 넘어가라는 옵션이다.build/tmp/아래의 파일을 전부 지운다.body: "Build ${BUILD_NUMBER} ..."— 메일 본문은 큰따옴표로 감쌌다. 작은따옴표로 감싸면${BUILD_NUMBER}가 빌드 번호로 바뀌지 않고 글자 그대로 메일에 찍힌다.unstable— 빌드가 “불안정”으로 표시됐을 때만 돈다. Jenkins 문서는 보통 테스트 실패나 코드 규칙 위반 때문에 이 상태가 된다고 설명하고, 화면에는 노란색으로 표시된다. 명령 자체가 실패해서(셸 명령이 0이 아닌 종료 코드로 끝나 예외가 나서) 빌드가 멈춘failure와 다르다.changed— 이번 빌드의 결과가 바로 이전 빌드의 결과와 다를 때만 돈다. 지난번엔 실패했는데 이번엔 성공한 경우 같은 때다.
mail to: 'team@example.com', subject: 'Build Success', body: '...'처럼 괄호도 =도 없이 이름: 값 쌍을 나열하는 문법이 자바에는 없어서 낯설다. 이것도 Groovy 문법으로, mail(to: '...', subject: '...', body: '...')에서 괄호와 쉼표 뒤의 줄바꿈을 생략할 수 있게 허용한 모양이다. mail은 메서드 이름이고, to·subject·body는 그 메서드에 전달하는 이름 붙은 인자(named argument)다 — 자바에서 sendMail(new MailOptions().to("...").subject("..."))라고 쓰던 것을 한 줄로 줄인 것과 비슷한 효과라고 보면 된다.
| Post 조건 | 설명 | 사용 시기 |
|---|---|---|
always |
항상 실행 | 정리 작업, 로그 수집 |
success |
성공 시만 | 배포, 알림 전송 |
failure |
실패 시만 | 에러 리포팅, 알림 |
unstable |
불안정(주로 테스트 실패) | 테스트 결과 처리 |
changed |
이전 빌드와 결과가 다를 때 | 상태 변경 알림 |
Try-Catch는 script 블록 안에서 예외를 직접 잡는다
스크립트형 파이프라인이나 스크립트 블록에서 Groovy의 try-catch를 사용한다.
선언형 파이프라인의 steps 안에는 원래 정해진 명령(step)만 적을 수 있다. try/catch나 while 같은 Groovy 코드를 쓰려면 script { } 블록으로 감싼다. Jenkins 문서는 이 블록을 선언형 안에서 Groovy 코드를 쓰게 해 주는 “비상구”라고 부른다.
pipeline {
agent any
stages {
stage('Build') {
steps {
script {
try {
sh 'make build'
echo "Build succeeded"
} catch (Exception e) {
echo "Build failed: ${e.message}"
currentBuild.result = 'FAILURE'
}
}
}
}
stage('Test') {
steps {
script {
try {
sh 'make test'
} catch (Exception e) {
echo "Tests failed"
throw e // 예외 재발생
} finally {
echo "Test cleanup"
junit '**/test-results.xml'
}
}
}
}
}
}
Build스테이지 — 예외를 잡아서 빌드 결과만FAILURE로 표시하고 다시 던지지 않는다. 그래서 이 스테이지 자체는 예외 없이 끝난다. 이렇게 했을 때 다음Test스테이지가 이어서 도는지는 확인하지 않았다.Test스테이지 — 예외를 다시 던지므로(throw e) 파이프라인이 실패로 멈춘다. 그래도finally는 자바처럼 항상 돌아서 테스트 결과는 남긴다.
junit '**/test-results.xml'은 JUnit 같은 테스트 도구가 만들어 낸 결과 파일(XML)을 읽어서 Jenkins 화면에 “몇 개 중 몇 개 통과”로 보여주는 전용 명령(step)이다. '**/test-results.xml'은 그 결과 파일을 찾을 경로 패턴(글로브)으로, **는 하위 폴더를 몇 단계든 내려가며 찾으라는 뜻이다.
When 조건문은 조건이 맞을 때만 스테이지를 실행한다
특정 조건에서만 스테이지를 실행한다.
pipeline {
agent any
stages {
stage('Build') {
steps {
echo "Building..."
}
}
stage('Deploy to Staging') {
when {
// 특정 브랜치에서만
branch 'develop'
}
steps {
echo "Deploying to staging"
}
}
stage('Deploy to Production') {
when {
// 메인 브랜치이고 성공한 경우
allOf {
branch 'main'
expression { currentBuild.result == null }
}
}
steps {
echo "Deploying to production"
}
}
stage('Tagged Release') {
when {
// 태그가 붙은 커밋
tag "release-*"
}
steps {
echo "Tagged release"
}
}
}
}
branch 'develop'—develop은 개발 중인 코드를 모으는 브랜치 이름이다. 이 브랜치면 스테이징 서버에,main이면 실제 사용자가 쓰는 운영 환경(production)에 배포하도록 나눈 예다.expression { currentBuild.result == null }— 앞에서 본 것처럼null은 “아직 아무도 실패로 표시하지 않았다”는 뜻이다. 주석의 “성공한 경우”는 이 뜻이다.tag "release-*"— 빌드하는 커밋에 붙은 태그(버전 이름표) 이름이release-로 시작할 때만 실행한다.*는 그 뒤에 무엇이 오든 상관없다는 뜻이다.
| When 옵션 | 설명 | 예제 |
|---|---|---|
branch |
특정 브랜치 | branch 'main' |
tag |
태그 패턴 | tag 'release-*' |
expression |
조건식 | expression { BUILD_NUMBER.toInteger() % 2 == 0 } |
allOf |
AND 조건 | allOf { branch 'main'; expression {...} } |
anyOf |
OR 조건 | anyOf { branch 'main'; branch 'develop' } |
BUILD_NUMBER는 환경 변수라서 숫자가 아니라 "153" 같은 문자열이다. Groovy의 문자열에는 %(나머지) 연산이 없으므로 .toInteger()로 정수로 바꾼 뒤 나눈다.
병렬 실행
선언형 파이프라인은 parallel 안에 stage를 나란히 둔다
pipeline {
agent any
stages {
stage('Build') {
steps {
echo "Building..."
}
}
stage('Parallel Testing') {
parallel {
stage('Unit Tests') {
steps {
echo "Running unit tests"
sh 'make test:unit'
}
}
stage('Integration Tests') {
steps {
echo "Running integration tests"
sh 'make test:integration'
}
}
stage('Linting') {
steps {
echo "Linting code"
sh 'make lint'
}
}
}
}
stage('Deploy') {
steps {
echo "Deploying"
}
}
}
}
parallel { } 안의 세 스테이지는 차례로 돌지 않고 동시에 돈다. 셋은 서로 결과를 기다릴 필요가 없는 검사들이다 — 단위 테스트(메서드 하나씩 떼어 검증), 통합 테스트(여러 부품을 연결해 검증), 린트(코드를 실행하지 않고 규칙 위반을 찾는 검사). 셋이 다 끝나야 다음 Deploy 스테이지로 넘어간다.
스크립트형 파이프라인은 parallel에 이름과 코드 덩어리를 넘긴다
node {
stage('Build') {
sh 'echo "Building..."'
}
stage('Parallel Tests') {
parallel linux: {
node('linux') {
echo "Testing on Linux"
sh 'make test'
}
},
windows: {
node('windows') {
echo "Testing on Windows"
bat 'make test'
}
},
macos: {
node('macos') {
echo "Testing on macOS"
sh 'make test'
}
}
}
stage('Deploy') {
echo "Deploying"
}
}
parallel linux: { ... }, windows: { ... }, macos: { ... }— 앞에서 본 이름 붙은 인자 문법이다.linux·windows·macos가 이름이고, 각각의 값이 중괄호로 감싼 코드 덩어리(클로저)다. 자바의 람다처럼 “지금 실행하지 말고 넘겨 둘 코드”이고,parallel이 받은 덩어리들을 동시에 실행한다.node('linux') { }— 바깥node와 별개로 노드를 하나 더 잡는다. 괄호 안의'linux'는 라벨이라서,linux이름표가 붙은 컴퓨터에서 이 덩어리를 돌린다. 운영체제마다 다른 컴퓨터에서 같은 테스트를 돌리려는 것이다.bat 'make test'—sh는 리눅스·유닉스 계열을 전제한 명령이라 Windows 컴퓨터에서는bat을 쓴다.bat은 Windows의 배치 스크립트(Windows 명령 프롬프트가 읽는 명령)를 실행한다. 리눅스의sh에 해당하는 Windows 쪽 짝이다.
노드끼리 파일을 넘길 때는 stash와 unstash로 맡겼다 찾는다
node {
stage('Build') {
node('master') {
checkout scm
sh 'make build'
// 다른 노드에서 사용할 아티팩트 저장
stash includes: 'build/**', name: 'app-build'
}
}
stage('Test') {
parallel linux: {
node('linux') {
checkout scm
// 저장된 아티팩트 복원
unstash 'app-build'
try {
sh 'make test'
} finally {
junit '**/test-results.xml'
}
}
},
windows: {
node('windows') {
checkout scm
unstash 'app-build'
try {
bat 'make test'
} finally {
junit '**/test-results.xml'
}
}
}
}
}
node('master')— Jenkins 서버 자신(컨트롤러)을 가리키는 이름이다.checkout scm— SCM(Git 같은 소스 관리 시스템)에서 이 파이프라인을 실행하게 만든 바로 그 판의 코드를 이 노드의 작업 폴더로 받아 오는 명령이다. 노드마다 작업 폴더가 따로라서 노드를 옮길 때마다 다시 받는다.- 주석의 아티팩트는 빌드 결과물(빌드가 만든 jar·zip 같은 파일)이다.
이 예제는 서로 다른 컴퓨터(노드)를 오가며 실행되므로, 한 노드에서 만든 빌드 결과물을 다른 노드로 옮겨야 한다. stash includes: 'build/**', name: 'app-build'는 build/ 폴더 아래의 파일들을 app-build라는 이름표를 붙여 Jenkins 서버에 잠깐 보관하는 명령이고, unstash 'app-build'는 다른 노드에서 그 이름표로 보관해 둔 파일을 다시 꺼내 오는 명령이다. 노드끼리는 파일 시스템을 공유하지 않기 때문에, 이렇게 보관(stash)하고 복원(unstash)하는 절차가 필요하다. 보관한 파일은 파이프라인이 끝나면 Jenkins 서버에서 지워진다.
트리거 (Webhooks 및 실행 조건)
트리거는 triggers 블록 하나에 모아 적는다
트리거는 사람이 버튼을 누르지 않아도 파이프라인을 시작하게 만드는 조건이다. triggers 블록은 pipeline 안에 한 번만 둔다.
pipeline {
agent any
triggers {
// 트리거 정의
}
stages {
stage('Build') {
steps {
echo "Building"
}
}
}
}
Cron 트리거는 정해진 시각에 파이프라인을 실행한다
정해진 시간에 자동으로 파이프라인을 실행한다.
pipeline {
agent any
triggers {
// 매주 월-금요일 8시마다 실행
cron('0 8 * * 1-5')
}
stages {
stage('Scheduled Build') {
steps {
echo "Scheduled build running"
}
}
}
}
cron('0 8 * * 1-5')의 작은따옴표 안 다섯 자리는 순서대로 분 · 시 · 일(월 중) · 월 · 요일을 뜻하고, 별표(*)는 “아무 값이나 상관없다”는 뜻이다. 0 8 * * 1-5는 “분은 0, 시는 8, 일·월은 아무 때나, 요일은 1-5(월요일~금요일)”이므로 “평일 매일 8시 0분”이 된다. 요일 자리에서 0과 7은 일요일이다.
아래 표에 나오는 H는 Jenkins가 cron 자리에 쓸 수 있게 해 둔 특수 기호로, “그 자리의 숫자를 Jenkins가 골라라”라는 뜻이다. 무작위로 고르는 게 아니라 작업 이름으로 계산한 값이라서, 같은 작업이면 매번 같은 숫자가 나온다. 모든 파이프라인이 똑같이 0 8 * * *(정각)로 걸려 있으면 그 순간 Jenkins 서버에 요청이 몰리는데, H 8 * * *라고 적으면 파이프라인마다 8시대의 서로 다른 분(0~59 중 하나)에 흩어져 실행되어 부하가 분산된다.
| Cron 표현식 | 설명 | 예제 |
|---|---|---|
H * * * * |
매 시간 한 번 | 분은 Jenkins가 고른 값 |
H 8 * * * |
매일 8시대 | cron('H 8 * * *') |
H 8 * * 1-5 |
평일 8시대 | cron('H 8 * * 1-5') |
H */4 * * * |
4시간마다 | cron('H */4 * * *') |
H 0 * * 0 |
매주 일요일 0시대 | cron('H 0 * * 0') |
*/4—/는 “몇 칸씩 건너뛰며”라는 뜻이다. 시 자리의*/4는 0시부터 4시간 간격(0·4·8·12·16·20시)이다.H 0 * * 0— 시가 0이고 분이H이므로 정확히 자정 0시 0분이 아니라 0시대의 어느 분이다.
pipeline {
agent any
triggers {
// 다양한 Cron 예제
cron('H * * * *') // 매 시간
cron('H 8,14 * * *') // 8시, 14시
}
stages {
stage('Build') {
steps {
echo "Cron triggered build"
}
}
}
}
8,14처럼 쉼표로 값을 늘어놓으면 “8시 또는 14시”다. 시각 두 개를 쓰려고 cron을 두 번 적을 필요 없이 한 줄에 쉼표로 적으면 된다. 이 예제처럼 한 triggers 블록에 cron을 두 번 적어도 둘 다 동작하는지는 확인하지 않았다.
pollSCM 트리거는 주기마다 저장소 변경을 확인한다
저장소의 변경을 감지하여 파이프라인을 실행한다.
pipeline {
agent any
triggers {
// 평일 4시간마다 변경사항 확인
pollSCM('H */4 * * 1-5')
}
stages {
stage('Build') {
steps {
echo "SCM change detected"
sh 'git log -1 --oneline'
}
}
}
}
cron이 정해진 시각에 무조건 파이프라인을 돌린다면, pollSCM은 정해진 시각에 Jenkins가 SCM(저장소)을 들여다보고 새 변경이 있을 때만 돌린다. 시각을 적는 문법은 cron과 같다.
git log -1 --oneline은 git log(커밋 기록을 보여 주는 명령)에 옵션을 붙인 것이다. -1은 가장 최근 커밋 하나만, --oneline은 그것을 b394a25eed Fix: ...처럼 짧은 해시와 메시지 한 줄로 보여 달라는 뜻이다. 어떤 커밋을 빌드하는지 로그에 남긴다.
GitHub Webhook 트리거는 push가 오면 바로 실행한다
GitHub(Git 저장소를 인터넷에 올려 두고 함께 쓰는 서비스) 저장소와 연동하여 push(커밋을 원격 저장소에 올림)나 PR(내 브랜치를 합쳐 달라는 요청) 때 자동 실행한다.
pipeline {
agent any
triggers {
githubPush() // GitHub Webhook 활성화
}
stages {
stage('Build') {
when {
branch 'main'
}
steps {
echo "Building from GitHub webhook"
}
}
}
}
GitHub Webhook(어떤 일이 일어났을 때 GitHub가 먼저 Jenkins로 요청을 보내 알려주는 방식) 설정은 GitHub 웹사이트에서 해당 저장소 페이지를 열고 한다.
- Repository → Settings → Webhooks
- “Add webhook” 클릭
- Payload URL:
https://jenkins.example.com/github-webhook/ - Events: “Push events” 또는 “Pull requests” 선택
Payload URL은 GitHub가 “무슨 일이 있었다”는 내용(payload)을 실어 보낼 Jenkins의 주소다. githubPush()는 GitHub 플러그인이 이 주소로 들어온 push 알림을 받아 파이프라인을 시작하게 한다.
Upstream 트리거는 다른 파이프라인이 끝나면 실행한다
다른 파이프라인이 완료되면 현재 파이프라인을 실행한다.
pipeline {
agent any
triggers {
// 'build-dependency' 파이프라인 성공 시 실행
upstream(upstreamProjects: 'build-dependency',
threshold: hudson.model.Result.SUCCESS)
}
stages {
stage('Build') {
steps {
echo "Triggered by upstream pipeline"
}
}
}
}
upstreamProjects에는 기다릴 앞 파이프라인 이름을, threshold(우리말로 “문턱”)에는 앞 파이프라인의 결과가 어떠해야 이 파이프라인을 띄울지를 적는다. 이 예제는 SUCCESS를 넣어 주석대로 “앞 파이프라인이 성공했을 때”를 조건으로 삼는다. 다른 값을 넣었을 때 정확히 어떻게 갈리는지는 확인하지 않았다.
hudson.model.Result.SUCCESS처럼 점으로 이어진 긴 이름은 자바의 패키지 경로와 똑같은 표기다. hudson.model이 패키지 이름이고 Result가 그 안의 클래스, SUCCESS가 그 클래스 안에 미리 정의된 값(상수)이다. Jenkins가 내부적으로 Hudson이라는 이름의 예전 프로젝트에서 갈라져 나온 역사 때문에 패키지 이름에 지금도 hudson이 남아 있다. Result.SUCCESS는 “앞 파이프라인이 성공으로 끝났을 때”를 가리키는 값이다.
실전 예제
예제 1 — Node.js 프로젝트를 빌드하고 승인을 받아 운영에 배포한다
Node.js는 브라우저 밖에서 자바스크립트를 돌리는 실행 환경이고, npm은 그 라이브러리를 설치하고 package.json에 이름 붙여 둔 명령을 npm run <이름>으로 돌려 주는 도구다.
pipeline {
agent any
environment {
NODE_ENV = 'production'
APP_PORT = '3000'
}
triggers {
githubPush()
pollSCM('H * * * *') // 매 시간 확인
}
stages {
stage('Checkout') {
steps {
checkout scm
sh 'git log -1 --oneline'
}
}
stage('Install Dependencies') {
steps {
sh 'npm install'
}
}
stage('Lint') {
steps {
sh 'npm run lint || true'
}
}
stage('Build') {
steps {
sh 'npm run build'
}
}
stage('Unit Tests') {
steps {
sh 'npm run test:unit'
}
}
stage('Integration Tests') {
steps {
sh 'npm run test:integration'
}
}
stage('Deploy to Staging') {
when {
branch 'develop'
}
steps {
echo "Deploying to staging environment"
sh 'npm run deploy:staging'
}
}
stage('Deploy to Production') {
when {
allOf {
branch 'main'
expression { currentBuild.result == null }
}
}
steps {
input message: 'Deploy to production?', ok: 'Deploy'
echo "Deploying to production environment"
sh 'npm run deploy:prod'
}
}
}
post {
always {
junit 'test-results/**/*.xml'
echo "Cleaning workspace"
cleanWs()
}
success {
echo "Build successful!"
mail to: 'team@example.com',
subject: "Build Success: ${JOB_NAME} #${BUILD_NUMBER}",
body: "Build completed successfully.\nCheck: ${BUILD_URL}"
}
failure {
echo "Build failed!"
mail to: 'team@example.com',
subject: "Build Failed: ${JOB_NAME} #${BUILD_NUMBER}",
body: "Build failed. Check logs: ${BUILD_URL}"
}
}
}
NODE_ENV·APP_PORT— 환경변수로 넘겨 두는 값인데, 이 예제의 명령들 중에 이 값을 읽는 줄은 보이지 않는다. 읽는 쪽은npm run이 실행하는 프로젝트 코드이고, 예시라서 그 코드는 생략됐다.APP_PORT의 3000은 앱이 요청을 받을 포트 번호로 보인다.githubPush()와pollSCM('H * * * *')— 웹훅은 GitHub가 알려 줄 때 돌고,pollSCM은 Jenkins가 매시간 직접 저장소를 확인한다. 이 글을 처음 쓸 때 둘을 같이 건 이유를 적어 두지 않았다. 같은 변경으로 두 번 돌지는 않는지도 확인하지 않았다.sh 'npm install'— 이 프로젝트가 쓰는 남의 라이브러리(의존성)를 내려받아 설치한다.sh 'npm run lint || true'— 셸의||는 “앞 명령이 실패하면 뒤 명령을 실행하라”는 뜻이고,true는 아무 일도 하지 않고 성공(종료 코드 0)으로 끝나는 명령이다. 그래서 린트가 규칙 위반을 찾아 실패해도 전체는 성공으로 끝나, 린트 결과가 파이프라인을 멈추지 않는다.|| true는 셸 명령의 일부라서 작은따옴표 안에 넣어야 한다.junit 'test-results/**/*.xml'—**는 하위 폴더 몇 겹이든,*.xml의*는 이름이 무엇이든이라는 뜻이다.test-results아래의 모든 XML 결과 파일을 읽는다.${BUILD_URL}— 이 빌드 결과를 볼 수 있는 Jenkins 화면 주소다(시스템 환경 변수 표 참고). 메일을 받은 사람이 눌러서 바로 로그를 보게 한다.
input message: 'Deploy to production?', ok: 'Deploy'는 파이프라인을 그 자리에서 멈추고, Jenkins 화면에 “Deploy to production?”이라는 메시지와 “Deploy” 버튼을 띄워 사람이 직접 승인 버튼을 눌러야 다음 줄로 넘어가는 단계다. 운영 환경 배포처럼 자동으로 진행하면 위험한 단계 앞에 넣어, 사람이 한 번 더 확인하게 만드는 장치다.
cleanWs()는 “clean workspace”의 줄임으로, 이 파이프라인이 쓴 작업 디렉터리(빌드 산출물·체크아웃한 소스 등)를 통째로 비우는 명령이다. 다음 빌드가 이전 빌드의 찌꺼기 파일 때문에 꼬이지 않도록 끝날 때 청소하는 용도다.
예제 2 — 자바 · 파이썬 · 자바스크립트 테스트를 다른 에이전트에서 동시에 돌린다
pipeline {
agent any
environment {
ARTIFACT_DIR = "${WORKSPACE}/artifacts"
TEST_REPORT = "${WORKSPACE}/test-reports"
}
stages {
stage('Prepare') {
steps {
sh 'mkdir -p ${ARTIFACT_DIR} ${TEST_REPORT}'
sh 'echo "Build started at $(date)" > ${ARTIFACT_DIR}/build.log'
}
}
stage('Parallel Tests') {
parallel {
stage('Java Tests') {
agent { label 'java' }
steps {
sh 'mvn clean test'
sh 'cp target/test-results/* ${TEST_REPORT}/'
}
}
stage('Python Tests') {
agent { label 'python' }
steps {
sh 'pip install -r requirements.txt'
sh 'pytest tests/ --junit-xml=${TEST_REPORT}/python-tests.xml'
}
}
stage('JavaScript Tests') {
agent { label 'nodejs' }
steps {
sh 'npm ci'
sh 'npm run test:ci'
sh 'cp coverage/junit.xml ${TEST_REPORT}/js-tests.xml'
}
}
}
}
stage('Collect Results') {
steps {
echo "Collecting test results"
junit "${TEST_REPORT}/**/*.xml"
archiveArtifacts artifacts: "${ARTIFACT_DIR}/**", allowEmptyArchive: true
}
}
}
post {
always {
sh 'echo "Build finished at $(date)"'
}
}
}
sh 'mkdir -p ${ARTIFACT_DIR} ${TEST_REPORT}'—mkdir은 폴더를 만드는 셸 명령이고,-p는 중간 폴더가 없으면 같이 만들고 이미 있어도 오류를 내지 않는다는 옵션이다. 작은따옴표라 Groovy가${...}를 바꾸지 않고, 셸이 환경변수로 읽어 바꾼다.echo "Build started at $(date)" > ${ARTIFACT_DIR}/build.log— 셸에서$(date)는date명령을 실행한 결과(지금 시각)를 그 자리에 끼워 넣고,>는 화면에 찍을 내용을 대신 파일에 쓰라는 뜻이다. 시작 시각을build.log파일에 남긴다.sh 'mvn clean test'— Maven(자바 빌드 도구)에게 이전 빌드가 만든 파일을 지우고(clean) 컴파일부터 테스트까지 돌리라고(test) 시킨다.sh 'cp target/test-results/* ${TEST_REPORT}/'—cp는 파일을 복사하는 셸 명령이다. 테스트 결과 파일을 한 폴더로 모은다.sh 'pip install -r requirements.txt'—pip은 Python 라이브러리를 설치하는 명령이고,-r은requirements.txt파일에 적힌 목록대로 설치하라는 옵션이다. 자바 쪽의 Maven이 라이브러리를 받는 일에 해당한다.sh 'npm ci'— npm ci는 lock 파일에 적힌 버전 그대로 새로 설치하는 명령이다. 앞의npm install과 달리 매번 똑같은 설치가 나와서 CI에서 쓴다.cp coverage/junit.xml ...—coverage는 이 예제의 자바스크립트 테스트가 결과 파일을 두는 폴더 이름이다.archiveArtifacts artifacts: "${ARTIFACT_DIR}/**", allowEmptyArchive: true—artifacts폴더 아래 파일을 빌드 결과물로 Jenkins에 보관해 나중에 내려받게 한다.allowEmptyArchive: true는 보관할 파일이 하나도 없어도 빌드를 실패시키지 말라는 뜻이다.sh 'echo "Build finished at $(date)"'—$(date)는 셸 문법이라sh로 감싸야 한다. Groovy의echo "...$(date)"로 쓰면 Groovy 큰따옴표 문자열에서$(가 문법 오류가 된다.
이 예제에는 빈틈이 하나 있다. 세 테스트는 java·python·nodejs 이름표가 붙은 서로 다른 에이전트에서 돌고, 결과 파일을 각자의 ${TEST_REPORT}에 복사한다. 그런데 앞의 「stash와 unstash」에서 본 것처럼 노드끼리는 파일 시스템을 공유하지 않는다. 그러면 Collect Results가 도는 노드에는 그 파일들이 없을 수 있다. 앞 예제처럼 각 스테이지에서 stash하고 Collect Results에서 unstash해야 할 것으로 보이지만, 이 예제를 실제로 돌려 확인하지는 않았다.
pytest tests/ --junit-xml=...에서 pytest는 Python으로 짠 테스트를 실행하는 도구다. 자바의 JUnit과 같은 역할을 Python 세계에서 담당한다고 보면 되고, --junit-xml=... 옵션은 그 결과를 JUnit과 같은 형식의 XML 파일로 남겨서, 언어가 달라도 Jenkins의 junit 단계가 똑같이 읽어 보여줄 수 있게 해 준다.
예제 3 — 테스트는 세 번까지 다시 돌리고 배포가 실패하면 되돌린다
pipeline {
agent any
stages {
stage('Build') {
steps {
script {
try {
sh 'npm install'
sh 'npm run build'
} catch (Exception e) {
echo "Build failed: ${e.message}"
currentBuild.result = 'FAILURE'
throw e
}
}
}
}
stage('Test with Retry') {
steps {
script {
def retryCount = 0
def maxRetries = 3
def success = false
while (retryCount < maxRetries && !success) {
try {
sh 'npm run test'
success = true
} catch (Exception e) {
retryCount++
if (retryCount < maxRetries) {
echo "Test failed, retrying... (${retryCount}/${maxRetries})"
sleep(time: 5, unit: 'SECONDS')
} else {
throw e
}
}
}
}
}
}
stage('Deploy') {
when {
expression { currentBuild.result == null || currentBuild.result == 'SUCCESS' }
}
steps {
script {
try {
sh 'npm run deploy'
} catch (Exception e) {
echo "Deployment failed, rolling back"
sh 'npm run rollback'
throw e
}
}
}
}
}
post {
failure {
script {
echo "Pipeline failed at stage: ${env.STAGE_NAME}"
// 실패 알림
mail to: 'team@example.com',
subject: "Pipeline Failed: ${JOB_NAME}",
body: "Failed stage: ${env.STAGE_NAME}\n\nCheck: ${BUILD_URL}"
}
}
unstable {
echo "Pipeline is unstable"
}
}
}
def retryCount = 0—def는 Groovy에서 타입 이름 대신 쓰는 자리 표시다. 자바의var처럼 타입을 직접 적지 않고 변수를 만든다.while (...)반복 — 테스트가 성공하면success = true로 빠져나오고, 실패하면 5초 쉬었다가(sleep) 다시 돌린다. 세 번째도 실패하면 예외를 다시 던져 파이프라인을 실패시킨다.sh 'npm run rollback'— 배포가 실패하면 롤백(직전의 멀쩡한 판으로 되돌리기)을 한 뒤 예외를 다시 던진다.rollback은 이 프로젝트의package.json에 그 이름으로 적어 둔 명령이라고 가정한 예시다.${env.STAGE_NAME}— 지금 도는 스테이지 이름이 담기는 변수다. 이 예제는 모든 스테이지가 끝난 뒤 도는post에서 이 값을 “실패한 스테이지”로 쓰는데,post안에서 실제로 실패한 스테이지 이름이 들어 있는지는 확인하지 않았다.
예제 4 — Docker 이미지를 만들어 레지스트리에 올리고 쿠버네티스에 배포한다
Docker는 프로그램을 실행 환경째 포장하는 도구이고, 그 포장이 이미지, 이미지를 올려 두는 저장소가 레지스트리다.
pipeline {
agent any
environment {
DOCKER_REGISTRY = 'docker.io'
IMAGE_NAME = 'myapp'
IMAGE_TAG = "${BUILD_NUMBER}"
}
triggers {
githubPush()
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build Image') {
steps {
script {
sh 'docker build -t ${DOCKER_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} .'
sh 'docker tag ${DOCKER_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} ${DOCKER_REGISTRY}/${IMAGE_NAME}:latest'
}
}
}
stage('Test Image') {
steps {
script {
sh '''
docker run --rm ${DOCKER_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} \
npm run test
'''
}
}
}
stage('Push Image') {
when {
branch 'main'
}
steps {
script {
withCredentials([usernamePassword(credentialsId: 'docker-hub',
usernameVariable: 'DOCKER_USER',
passwordVariable: 'DOCKER_PASS')]) {
sh '''
docker login -u $DOCKER_USER -p $DOCKER_PASS
docker push ${DOCKER_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}
docker push ${DOCKER_REGISTRY}/${IMAGE_NAME}:latest
'''
}
}
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
script {
sh '''
kubectl set image deployment/myapp \
myapp=${DOCKER_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG} \
--record
'''
}
}
}
}
post {
always {
sh 'docker image prune -f'
}
}
}
DOCKER_REGISTRY = 'docker.io'—docker.io는 Docker Hub의 주소다. 아래credentialsId: 'docker-hub'는 그 Docker Hub에 로그인할 계정을 Jenkins에 저장해 둔 이름이다.${DOCKER_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}— 이미지 이름은레지스트리주소/이름:태그모양이다. 맨 앞의 레지스트리 주소가 “이 이미지가 있는(올라갈) 곳”이다. 주소를 빼면 Docker는 Docker Hub(docker.io)로 여기고, 다른 레지스트리에 올리려면 그 주소를 반드시 붙인다.docker build -t 이름:태그 .— 이미지를 만드는 명령이다.-t로 이미지 이름과 태그(여기서는 빌드 번호)를 붙이고, 끝의 점(.)은 “지금 폴더를 재료로 써라”는 뜻이다. 따로 지정하지 않으면 그 폴더의Dockerfile을 읽는다. Dockerfile은 이미지를 만들 때 실행할 명령을 차례로 적어 둔 글자 파일이다.docker tag ... :latest— 같은 이미지에latest라는 이름표를 하나 더 붙인다. 이미지를 내려받을 때 태그를 적지 않으면 Docker는latest를 쓰므로, “가장 최근 판”을 태그 없이 받게 하려는 것이다.sh ''' ... '''— 작은따옴표 세 개는 여러 줄에 걸친 문자열이다. 작은따옴표 하나짜리처럼 Groovy가${...}를 바꾸지 않고 셸에 그대로 넘긴다. 줄 끝의\는 셸에게 “명령이 다음 줄로 이어진다”고 알리는 표시다.docker run --rm 이미지 npm run test— 방금 만든 이미지로 컨테이너를 띄워 그 안에서 테스트를 돌린다.--rm은 컨테이너가 끝나면 자동으로 지우라는 옵션이다.docker login -u $DOCKER_USER -p $DOCKER_PASS— 레지스트리에 아이디·비밀번호로 로그인한다. 셸에서$DOCKER_USER와${DOCKER_USER}는 같은 뜻이라, 여기만 중괄호가 없는 것은 차이가 없다.docker push 이름:태그— 이미지를 레지스트리에 올린다. 빌드 번호 태그와latest태그 둘 다 올린다.--record— 이 변경을 배포 기록에 남기라는 옵션이다. 다만 내가 가진 kubectl v1.32.2로 돌려 보니 “Flag –record has been deprecated, –record will be removed in the future”라는 경고가 나왔다. 앞으로 없어질 옵션이다.sh 'docker image prune -f'— 쓰지 않는 이미지를 지우는 명령이고,-f는 “정말 지울까요?”를 묻지 않게 한다. 빌드할 때마다 이미지가 쌓여 디스크를 채우지 않게 끝날 때 청소한다.
withCredentials([usernamePassword(credentialsId: 'docker-hub', usernameVariable: 'DOCKER_USER', passwordVariable: 'DOCKER_PASS')]) { ... }는 Jenkins에 미리 등록해 둔 아이디·비밀번호(credentialsId: 'docker-hub'로 저장된 것)를 꺼내서, 그 블록 안에서만 쓸 수 있는 임시 변수(DOCKER_USER, DOCKER_PASS)에 담아 주는 구조다. 비밀번호를 파이프라인 코드에 직접 적지 않고도 로그인 명령에 쓸 수 있게 해 주고, 블록을 벗어나면 그 변수는 사라진다. 바깥의 대괄호 [ ]는 Groovy의 리스트(자바의 List.of(...)에 해당)다. 꺼낼 자격증명 설정을 리스트에 담아 withCredentials에 넘기는 모양이다.
kubectl set image deployment/myapp myapp=...:${IMAGE_TAG} --record는 Kubernetes(여러 서버에 컨테이너를 나눠 띄우고 관리해 주는 도구)에게 내리는 명령이다. kubectl은 Kubernetes를 조작하는 명령줄 도구이고, 이 명령은 “myapp이라는 배포(deployment)가 쓰는 Docker 이미지를 방금 만든 새 태그(${IMAGE_TAG})의 이미지로 바꿔라”는 뜻이다. deployment/myapp의 myapp은 배포 이름이고, myapp=이미지의 myapp은 그 배포 안의 컨테이너 이름이다(컨테이너이름=새이미지 모양). 이 예제는 둘에 같은 이름을 붙였다. 이 한 줄이 실행되면 Kubernetes가 기존 컨테이너를 새 이미지로 띄운 컨테이너로 순서대로 교체한다.
팁 & 주의사항
주의할 점은 비밀 값 · 스테이지 이름 · post 순서 · 병렬 자원 넷이다
환경 변수에 민감 정보 노출 금지
- API 키, 패스워드 등은 Jenkins Credentials 사용
withCredentials([string(credentialsId: 'api-key', variable: 'API_KEY')]) {
sh 'echo ${API_KEY}'
}
string(credentialsId: 'api-key', variable: 'API_KEY')는 Jenkins에 “비밀 글자(Secret text)” 하나로 저장해 둔 값을 API_KEY 변수에 담는다. 예제 4의 usernamePassword(...)가 아이디·비밀번호 두 값을 변수 두 개에 담는 것과 달리, 이쪽은 API 키처럼 값이 하나인 비밀에 쓴다.
이 예제는 일부러 키를 echo로 찍는다. Jenkins는 withCredentials로 꺼낸 값이 로그에 찍히면 ****로 가려서 보여 준다. 다만 Jenkins 문서는 이것이 실수로 새는 위험을 줄일 뿐이라고 적는다. 그리고 비밀 값은 작은따옴표 안에 두어 셸이 읽게 해야 한다. 큰따옴표로 감싸면 셸에 넘기기 전에 Groovy가 비밀 값을 명령 글자 안에 직접 박아 넣는다. 문서는 이 방식이 덜 안전하다고 경고하고, 비밀 값에는 Groovy의 끼워 넣기를 절대 쓰지 말라고 적는다.
스테이지 이름 중복 피하기
- 파이프라인 가독성과 로깅을 위해 고유한 스테이지 이름 사용
Post 섹션 순서
post안의 조건들은 내가 적은 순서가 아니라 Jenkins가 정한 순서대로 실행된다. Jenkins 문서의 순서는always→changed→fixed→regression→aborted→failure→success→unstable→unsuccessful→cleanup이다- 앞에서 안 본 다섯 개의 뜻은 이렇다 —
fixed(이전 빌드는 실패·불안정이었는데 이번에 성공),regression(이전 빌드는 성공했는데 이번에 실패·불안정·중단),aborted(이번 빌드가 중단됨),unsuccessful(이번 빌드가 성공이 아님),cleanup(다른 조건을 다 처리한 뒤 결과와 상관없이 마지막에 실행) always,success,failure등을 동시에 사용 가능
병렬 실행의 리소스
- 병렬로 실행되는 단계가 많을수록 Jenkins 리소스 사용 증가
- 에이전트 수와 작업 부하를 고려하여 설계
알아 두면 편한 문법은 script · when · Shared Library · 시각화 · 디버깅이다
script 블록 활용
- 선언형 파이프라인에서도 Groovy 코드 실행 가능
script {
def version = sh(returnStdout: true, script: 'cat version.txt').trim()
}
cat은 파일 내용을 그대로 출력하는 셸 명령이다. version.txt에 적힌 버전 글자를 읽어 version 변수에 담는다.
조건부 단계 실행
when블록으로 불필요한 단계 스킵
when {
expression { BUILD_NUMBER.toInteger() % 2 == 0 }
}
빌드 번호가 짝수일 때만 실행하는 예다. 앞의 When 표에서 본 것처럼 BUILD_NUMBER는 문자열이라 .toInteger()로 바꾼 뒤 나눈다.
재사용 가능한 파이프라인 만들기
- Shared Libraries 사용으로 코드 중복 제거
@Library('common') _
commonBuild()
@Library('common')은 “Jenkins에 common이라는 이름으로 등록해 둔 Shared Library(여러 Jenkinsfile이 공유하는 Groovy 코드 모음)를 이 파이프라인에서 쓰겠다”고 선언하는 줄이다. 바로 뒤의 밑줄(_)은 특별한 의미가 있는 게 아니라, @Library가 문법적으로 그 뒤에 무언가가 와야 하는데 따로 변수를 쓸 일이 없을 때 자리만 채우는 더미 값이다. 그다음 줄의 commonBuild()가 그 라이브러리 안에 미리 정의해 둔, 여러 줄의 파이프라인 로직을 담은 함수를 호출하는 부분이다.
파이프라인 시각화
- Blue Ocean 플러그인 설치로 직관적인 파이프라인 시각화
디버깅
echo문으로 변수값 확인- Jenkins 콘솔 로그에서 상세 정보 확인
echo "DEBUG: Variable = ${VARIABLE}"
관련 문서
자신만의 철학을 만들어가는 중입니다.
댓글남기기