[Part 3] Jenkins Pipeline 기초 - Groovy와 Pipeline 문법
CI/CD 학습 시리즈
- Part 1: Kotlin 문법 기초
- Part 2: Gradle Plugin 개발
- Part 3: Jenkins Pipeline + Groovy 기초 (현재 글)
- Part 4: App-Builder Plugin 실전 분석
Jenkins Pipeline 기초 학습
목차
- Jenkins Pipeline 개요
- Groovy 언어 소개
- Groovy 기초 문법
- 선언형 Pipeline (Declarative)
- 스크립트형 Pipeline (Scripted)
- 공유 라이브러리 (Shared Library)
- 실전 예제
- 자주 사용하는 패턴
- 디버깅 팁
- 실전 분석: app-builder-plugin의 build.groovy
- 참고 자료
1. Jenkins Pipeline 개요
1.1 Pipeline이란?
Jenkins Pipeline은 CI/CD 파이프라인을 코드로 정의하는 방법입니다.
기존 방식: Jenkins UI에서 클릭으로 설정
Pipeline: Jenkinsfile 코드로 정의 → 버전 관리 가능
1.2 왜 Pipeline인가?
| 장점 | 설명 |
|---|---|
| 코드로 관리 | Git에서 버전 관리 가능 |
| 재사용 | 공유 라이브러리로 재사용 |
| 리뷰 | PR로 파이프라인 변경 리뷰 가능 |
| 복구 | 이전 버전으로 쉽게 롤백 |
1.3 두 가지 문법
┌─────────────────────────────────────────────────────┐
│ Jenkins Pipeline │
├─────────────────────┬───────────────────────────────┤
│ 선언형 │ 스크립트형 │
│ (Declarative) │ (Scripted) │
├─────────────────────┼───────────────────────────────┤
│ pipeline { } │ node { } │
│ 구조화된 DSL │ 순수 Groovy │
│ 제한적이지만 간단 │ 유연하지만 복잡 │
└─────────────────────┴───────────────────────────────┘
1.4 Groovy 문법 vs Pipeline DSL
Jenkins Pipeline을 처음 접하면 “이게 Groovy야? Pipeline이야?” 헷갈릴 수 있습니다.
관계 이해하기
┌─────────────────────────────────────────┐
│ Jenkins Pipeline │
│ ┌───────────────────────────────────┐ │
│ │ Pipeline DSL (Jenkins 전용) │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ Groovy (언어 기반) │ │ │
│ │ └─────────────────────────────┘ │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
Jenkins Pipeline = Groovy 언어 + Jenkins 전용 DSL(키워드)
구분 예시
실제 Pipeline 코드에서 어떤 것이 Groovy이고, 어떤 것이 Jenkins DSL인지 구분해보면:
// ========== 순수 Groovy 문법 ==========
def config = [:] // Map 선언
def logLevel = args.logLevel ?: "warn" // Elvis 연산자
strSkipStages.split(',').each {} // 클로저, 컬렉션 메서드
return moduleDataMap // 함수 반환
camelCase.replaceAll('...', '...') // 문자열 메서드
// ========== Jenkins Pipeline DSL ==========
node('buildkit') {} // 빌드 실행할 노드(에이전트) 지정
stage("스테이지명") {} // 파이프라인 스테이지 정의
container("buildkit") {} // K8s Pod 내 컨테이너 선택
checkout scm // 소스 체크아웃
sh "gradle build" // 쉘 명령 실행
echo "메시지" // 콘솔 출력
withCredentials([]) {} // 인증정보 주입
properties([]) // Job 설정
parameters([]) // 빌드 파라미터 정의
readJSON text: "..." // JSON 파싱 (Pipeline Utility Steps 플러그인)
currentBuild // 현재 빌드 정보 객체
env.VARIABLE // Jenkins 환경변수 접근
비유로 이해하기
| 개념 | 비유 |
|---|---|
| Groovy | 한국어 (기본 언어) |
| Jenkins Pipeline DSL | 법률 용어 (특수 도메인 어휘) |
| Jenkinsfile | 법률 문서 (한국어 + 법률 용어로 작성) |
실제 코드로 보는 구분
def getTargetsTask(Map config) { // ← Groovy: 함수 정의
sh """ // ← Jenkins: 쉘 실행
gradle -p ${config.appBuilderConfig.appBuilderPath} GetTargets
""" // ← Groovy: String interpolation
def jsonContent = sh( // ← Jenkins: sh + returnStdout
script: "cat .../modules-data.json",
returnStdout: true
).trim() // ← Groovy: 문자열 메서드
def moduleDataMap = readJSON text: jsonContent // ← Jenkins: readJSON 스텝
return moduleDataMap // ← Groovy: return
}
핵심 Jenkins Pipeline 키워드 정리
| 키워드 | 용도 |
|---|---|
pipeline |
선언적 파이프라인 시작 |
node |
실행할 에이전트/노드 지정 |
stage |
시각적 스테이지 구분 |
steps |
선언적 문법에서 실행할 단계들 |
sh / bat |
쉘/배치 명령 실행 |
checkout |
SCM 체크아웃 |
withCredentials |
인증정보 바인딩 |
container |
K8s Pod 내 컨테이너 선택 |
echo |
로그 출력 |
env |
환경변수 접근 |
params |
빌드 파라미터 접근 |
currentBuild |
현재 빌드 정보 |
요약: Groovy 문법 위에 Jenkins가 제공하는 특수 함수/키워드(DSL)를 얹은 것이 Jenkins Pipeline입니다.
1.5 선언형 vs 스크립트형 상세 비교
역사와 배경
2016년: Jenkins Pipeline 출시
└─ 스크립트형 (Scripted) 먼저 등장
- 순수 Groovy 기반
- 유연하지만 복잡
2017년: 선언형 (Declarative) 추가
└─ 스크립트형의 복잡함을 해결
- 구조화된 DSL
- 초보자 친화적
현재 권장: 선언형 (Declarative)을 기본으로, 필요시 script { } 블록으로 스크립트형 혼합
기본 구조 비교
// ┌─────────────────────────────────────────────────────────┐
// │ 선언형 (Declarative) │
// └─────────────────────────────────────────────────────────┘
pipeline { // 반드시 pipeline으로 시작
agent any // 필수: 실행 환경 지정
stages { // 필수: stages 블록
stage('Build') {
steps { // 필수: steps 블록
echo 'Building...'
}
}
stage('Test') {
steps {
echo 'Testing...'
}
}
}
}
// ┌─────────────────────────────────────────────────────────┐
// │ 스크립트형 (Scripted) │
// └─────────────────────────────────────────────────────────┘
node { // node로 시작
stage('Build') { // stage만 있으면 됨 (steps 불필요)
echo 'Building...'
}
stage('Test') {
echo 'Testing...'
}
}
상세 비교표
| 비교 항목 | 선언형 (Declarative) | 스크립트형 (Scripted) |
|---|---|---|
| 시작 키워드 | pipeline { } |
node { } |
| 필수 구조 | agent, stages, steps 필수 | 자유로움 |
| Groovy 문법 | 제한적 (script { } 안에서만) |
완전히 자유로움 |
| 학습 곡선 | 낮음 (초보자 친화적) | 높음 (Groovy 지식 필요) |
| 가독성 | 높음 (구조화됨) | 낮을 수 있음 |
| 유연성 | 낮음 | 높음 |
| 에러 검출 | 빌드 전 구문 검사 | 런타임에 발견 |
| post 처리 | 내장 지원 (post { }) |
직접 try-catch 구현 |
| when 조건 | 내장 지원 (when { }) |
if문으로 직접 구현 |
| 병렬 실행 | parallel { } 블록 |
parallel() 메서드 |
| Blue Ocean | 완벽 지원 | 부분 지원 |
| 권장 대상 | 대부분의 경우 | 복잡한 로직 필요시 |
같은 작업, 다른 문법
예제 1: 기본 빌드
// 선언형
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'gradle build'
}
}
}
}
// 스크립트형
node {
stage('Build') {
sh 'gradle build'
}
}
예제 2: 조건부 실행
// 선언형 - when 블록 사용
pipeline {
agent any
stages {
stage('Deploy') {
when {
branch 'main'
}
steps {
sh 'deploy.sh'
}
}
}
}
// 스크립트형 - if문 사용
node {
stage('Deploy') {
if (env.BRANCH_NAME == 'main') {
sh 'deploy.sh'
}
}
}
예제 3: 후처리 (성공/실패)
// 선언형 - post 블록 사용
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'gradle build'
}
}
}
post {
success {
echo '빌드 성공!'
slackSend message: "성공"
}
failure {
echo '빌드 실패!'
slackSend message: "실패"
}
always {
cleanWs() // 워크스페이스 정리
}
}
}
// 스크립트형 - try-catch-finally 사용
node {
try {
stage('Build') {
sh 'gradle build'
}
echo '빌드 성공!'
slackSend message: "성공"
} catch (Exception e) {
echo '빌드 실패!'
slackSend message: "실패"
throw e
} finally {
cleanWs()
}
}
예제 4: 병렬 실행
// 선언형 - parallel 블록
pipeline {
agent any
stages {
stage('Test') {
parallel {
stage('Unit Test') {
steps {
sh 'npm run test:unit'
}
}
stage('E2E Test') {
steps {
sh 'npm run test:e2e'
}
}
}
}
}
}
// 스크립트형 - parallel 메서드
node {
stage('Test') {
parallel(
'Unit Test': {
sh 'npm run test:unit'
},
'E2E Test': {
sh 'npm run test:e2e'
}
)
}
}
예제 5: 동적 스테이지 생성
// 선언형 - 불가능! script 블록 필요
pipeline {
agent any
stages {
stage('Dynamic Stages') {
steps {
script {
// script 블록 안에서 Groovy 사용
def modules = ['api', 'web', 'batch']
modules.each { module ->
stage("Build ${module}") {
sh "gradle :${module}:build"
}
}
}
}
}
}
}
// 스크립트형 - 자연스럽게 가능
node {
def modules = ['api', 'web', 'batch']
stage('Checkout') {
checkout scm
}
// 동적으로 스테이지 생성
modules.each { module ->
stage("Build ${module}") {
sh "gradle :${module}:build"
}
}
}
언제 어떤 것을 사용해야 할까?
┌─────────────────────────────────────────────────────────────┐
│ 선언형을 사용하세요 │
├─────────────────────────────────────────────────────────────┤
│ 파이프라인이 단순하고 직관적인 경우 │
│ 팀에 Jenkins 초보자가 많은 경우 │
│ Blue Ocean UI를 적극 활용하는 경우 │
│ 구조화된 post 처리가 필요한 경우 │
│ when 조건이 많이 필요한 경우 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 스크립트형을 사용하세요 │
├─────────────────────────────────────────────────────────────┤
│ 복잡한 조건 분기가 필요한 경우 │
│ 동적으로 스테이지를 생성해야 하는 경우 │
│ 외부 API 호출, 복잡한 데이터 처리가 필요한 경우 │
│ 공유 라이브러리를 개발하는 경우 │
│ 완전한 Groovy 제어가 필요한 경우 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 혼합 사용 (권장) │
├─────────────────────────────────────────────────────────────┤
│ 기본 구조는 선언형으로 │
│ 복잡한 로직이 필요한 부분만 script { } 블록 사용 │
└─────────────────────────────────────────────────────────────┘
혼합 사용 예시 (권장 패턴)
pipeline {
agent any
environment {
DEPLOY_ENV = 'production'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh 'gradle build'
}
}
stage('Dynamic Tests') {
steps {
// 복잡한 로직이 필요한 부분만 script 블록
script {
def testTypes = ['unit', 'integration', 'e2e']
def parallelTests = [:]
testTypes.each { type ->
parallelTests[type] = {
sh "gradle test${type.capitalize()}"
}
}
parallel parallelTests
}
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
sh 'deploy.sh'
}
}
}
post {
success {
slackSend message: "빌드 성공: ${env.JOB_NAME}"
}
failure {
slackSend message: "빌드 실패: ${env.JOB_NAME}"
}
}
}
app-builder-plugin은 어떤 방식?
상세 분석: Part 3: App-Builder Plugin 실전 분석에서 전체 구조를 다룹니다.
현재 app-builder-plugin의 build.groovy는 스크립트형을 사용합니다:
// vars/build.groovy - 스크립트형
def call(Map args = [:]) {
node('buildkit') { // 스크립트형: node로 시작
checkout scm
stage("변경 감지") { // stage만 사용 (steps 없음)
container("buildkit") {
// Groovy 자유롭게 사용
buildData = getTargetsTask(config)
}
}
// 동적 스테이지 생성 - 스크립트형의 장점
buildData.modules.each { name, module ->
stage("Build: ${name}") {
if (module.changed) {
// 복잡한 로직
}
}
}
}
}
스크립트형을 선택한 이유:
- 동적 스테이지 생성 - 모듈 수에 따라 스테이지가 달라짐
- 복잡한 데이터 처리 - JSON 파싱, 조건 분기
- 공유 라이브러리 - 재사용 가능한 함수 정의
- 완전한 Groovy 제어 - Map, List, 클로저 자유롭게 사용
2. Groovy 언어 소개
2.1 Groovy란?
Groovy는 2003년 James Strachan이 만든 JVM 기반 동적 프로그래밍 언어입니다.
┌─────────────────────────────────────────────────────────┐
│ JVM │
├─────────────┬─────────────┬─────────────┬───────────────┤
│ Java │ Kotlin │ Scala │ Groovy │
│ (1995) │ (2011) │ (2004) │ (2003) │
└─────────────┴─────────────┴─────────────┴───────────────┘
모두 JVM 위에서 실행되고, 서로 호환됨
2.2 Groovy의 탄생 배경
Java의 불편함을 해결하기 위해 탄생:
// Java - 장황함
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("apple");
list.add("banana");
for (String item : list) {
System.out.println(item);
}
}
}
// Groovy - 간결함
def list = ['apple', 'banana']
list.each { println it }
2.3 Groovy의 특징
| 특징 | 설명 |
|---|---|
| Java 호환 | Java 코드를 그대로 사용 가능 |
| 동적 타입 | def 키워드로 타입 추론 |
| 간결한 문법 | 세미콜론, 괄호 생략 가능 |
| 클로저 지원 | 함수를 값처럼 전달 |
| DSL 친화적 | 도메인 특화 언어 작성에 적합 |
| 스크립트 지원 | 컴파일 없이 바로 실행 가능 |
2.4 DSL이란? (Domain Specific Language)
DSL의 개념
DSL (Domain Specific Language) = 도메인 특화 언어
특정 분야(도메인)의 문제를 해결하기 위해 설계된 언어입니다.
┌─────────────────────────────────────────────────────────────┐
│ 프로그래밍 언어 │
├─────────────────────────────┬───────────────────────────────┤
│ GPL (범용 언어) │ DSL (특화 언어) │
│ General Purpose │ Domain Specific │
├─────────────────────────────┼───────────────────────────────┤
│ - Java │ - SQL (데이터베이스) │
│ - Python │ - HTML/CSS (웹 페이지) │
│ - JavaScript │ - Regex (정규표현식) │
│ - Groovy ─────────────────────► Jenkinsfile (CI/CD) │
│ │ │ - Dockerfile (컨테이너) │
│ │ │ - build.gradle (빌드) │
│ │ │ │
│ └── DSL을 만들기 좋은 GPL │
│ │ │
│ "무엇이든 할 수 있음" │ "특정 일을 잘함" │
└─────────────────────────────┴───────────────────────────────┘
핵심 포인트:
- Groovy 자체는 GPL (범용 언어) - Java처럼 무엇이든 할 수 있음
- Groovy로 만든 것이 DSL - Jenkinsfile, build.gradle 등
- Groovy는 “DSL을 만들기에 최적화된 GPL”
Groovy로 작성하는 것들
┌─────────────────────────────────────────────────────────┐
│ Groovy 문법 │
│ (변수, 클로저, 맵, 리스트, 조건문 등) │
└─────────────────────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ Jenkinsfile │ │ build.gradle │
│ (Jenkins Pipeline) │ │ (Gradle) │
├─────────────────────┤ ├─────────────────────┤
│ pipeline { │ │ plugins { │
│ agent any │ │ id 'java' │
│ stages { │ │ } │
│ stage('Build'){ │ │ dependencies { │
│ steps { │ │ implementation '' │
│ sh 'make' │ │ } │
│ } │ │ │
│ } │ │ │
│ } │ │ │
│ } │ │ │
└─────────────────────┘ └─────────────────────┘
│ │
▼ ▼
Jenkins가 제공하는 Gradle이 제공하는
DSL 메서드들 DSL 메서드들
(pipeline, stage, (plugins, dependencies,
steps, sh, echo...) implementation, test...)
즉:
- Groovy 문법을 사용해서 작성
- Jenkins/Gradle이 제공하는 DSL 키워드(메서드)를 호출
그래서 Groovy 문법을 알면 → Jenkinsfile, build.gradle 둘 다 이해 가능!
// Jenkinsfile - Groovy 문법 + Jenkins DSL
def modules = ['api', 'web'] // Groovy: 변수, 리스트
pipeline { // Jenkins DSL: pipeline
agent any // Jenkins DSL: agent
stages { // Jenkins DSL: stages
stage('Build') { // Jenkins DSL: stage
steps { // Jenkins DSL: steps
modules.each { // Groovy: 클로저, 반복
sh "build ${it}" // Jenkins DSL: sh + Groovy: 문자열 보간
}
}
}
}
}
// build.gradle - Groovy 문법 + Gradle DSL
def springVersion = '3.0.0' // Groovy: 변수
plugins { // Gradle DSL: plugins
id 'java' // Gradle DSL: id
}
dependencies { // Gradle DSL: dependencies
// Groovy: 문자열 보간 + Gradle DSL: implementation
implementation "org.springframework:spring-core:${springVersion}"
// Groovy: 조건문
if (project.hasProperty('includeTest')) {
testImplementation 'junit:junit:4.13' // Gradle DSL
}
}
참고: Gradle은 Kotlin DSL (
build.gradle.kts)도 지원합니다. Kotlin DSL은 타입 안전성과 IDE 자동완성이 더 좋지만, 기존 프로젝트는 대부분 Groovy DSL을 사용합니다.
DSL의 종류
┌─────────────────────────────────────────────────────────────┐
│ DSL 종류 │
├─────────────────────────────┬───────────────────────────────┤
│ External DSL │ Internal DSL │
│ (외부 DSL) │ (내부 DSL) │
├─────────────────────────────┼───────────────────────────────┤
│ 독립적인 문법/파서 필요 │ 호스트 언어 문법 활용 │
│ │ │
│ 예시: │ 예시: │
│ - SQL │ - Jenkinsfile (Groovy) │
│ - HTML │ - build.gradle (Groovy) │
│ - Regex │ - RSpec (Ruby) │
│ - YAML │ - Kotest (Kotlin) │
└─────────────────────────────┴───────────────────────────────┘
Jenkins Pipeline = Groovy 기반 Internal DSL
왜 DSL을 사용하는가?
[일반 프로그래밍 언어로 CI/CD 작성]
BuildPipeline pipeline = new BuildPipeline();
pipeline.setAgent(new AnyAgent());
StageList stages = new StageList();
Stage buildStage = new Stage("Build");
StepList steps = new StepList();
steps.add(new ShellStep("make build"));
buildStage.setSteps(steps);
stages.add(buildStage);
pipeline.setStages(stages);
pipeline.execute();
[DSL로 CI/CD 작성]
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'make build'
}
}
}
}
| 비교 | 일반 언어 | DSL |
|---|---|---|
| 가독성 | 낮음 (장황함) | 높음 (의도가 명확) |
| 학습 곡선 | Java 문법 필요 | 도메인 용어만 알면 됨 |
| 생산성 | 낮음 | 높음 |
| 오류 가능성 | 높음 | 낮음 |
Groovy가 DSL에 적합한 이유
1. 괄호 생략 가능
// 일반 메서드 호출
stage('Build', { steps({ sh('make build') }) })
// Groovy DSL 스타일 (괄호 생략)
stage('Build') {
steps {
sh 'make build'
}
}
2. 클로저 (Closure) 지원
// 클로저 = 코드 블록을 값처럼 전달
def myBlock = {
echo 'Hello'
sh 'make build'
}
// 함수에 블록 전달
stage('Build', myBlock)
// 또는 직접 전달
stage('Build') {
echo 'Hello'
sh 'make build'
}
3. delegate 패턴
// delegate를 통해 블록 내부에서 메서드 호출 가능
pipeline {
// 여기서 agent, stages 등은 pipeline 객체의 메서드
agent any
stages {
// 여기서 stage는 stages 객체의 메서드
stage('Build') {
// 여기서 steps는 stage 객체의 메서드
steps {
// 여기서 sh, echo는 steps 객체의 메서드
sh 'make'
}
}
}
}
4. 메서드 체이닝과 빌더 패턴
// Groovy DSL 빌더 예시
html {
head {
title 'My Page'
}
body {
div(class: 'container') {
p 'Hello World'
}
}
}
// 결과:
// <html>
// <head><title>My Page</title></head>
// <body>
// <div class="container">
// <p>Hello World</p>
// </div>
// </body>
// </html>
실제 DSL 예시 비교
Jenkins Pipeline DSL:
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'gradle build'
}
}
stage('Test') {
steps {
sh 'gradle test'
}
}
}
}
Gradle Build DSL:
plugins {
id 'java'
}
dependencies {
implementation 'org.springframework:spring-core:5.3.0'
testImplementation 'junit:junit:4.13'
}
tasks.register('hello') {
doLast {
println 'Hello from Gradle!'
}
}
Spock Test DSL:
def "두 숫자를 더하면 합계가 나온다"() {
given: "두 숫자가 주어졌을 때"
def a = 1
def b = 2
when: "두 숫자를 더하면"
def result = a + b
then: "합계가 나온다"
result == 3
}
DSL의 장단점
| 장점 | 단점 |
|---|---|
| 도메인 전문가가 읽기 쉬움 | 학습이 필요함 |
| 코드가 간결해짐 | 디버깅이 어려울 수 있음 |
| 실수가 줄어듦 | 유연성이 제한될 수 있음 |
| 의도가 명확해짐 | DSL 설계가 어려움 |
2.5 Groovy가 사용되는 곳
┌─────────────────────────────────────────────────────────┐
│ Groovy 사용 사례 │
├─────────────────────────────────────────────────────────┤
│ │
│ 1. Jenkins Pipeline │
│ └─ CI/CD 파이프라인 정의 │
│ │
│ 2. Gradle 빌드 스크립트 │
│ └─ build.gradle 파일 │
│ │
│ 3. Spock Framework │
│ └─ Java/Groovy 테스트 프레임워크 │
│ │
│ 4. Grails Framework │
│ └─ 웹 애플리케이션 프레임워크 │
│ │
│ 5. Spring Boot 설정 │
│ └─ @Bean 정의, 동적 설정 │
│ │
└─────────────────────────────────────────────────────────┘
2.6 Jenkins와 Groovy의 관계
왜 Jenkins는 Groovy를 선택했는가?
2005년: Jenkins(Hudson) 탄생 - Java로 작성됨
│
▼
2011년: Jenkins로 이름 변경
│
▼
2014년: 문제 인식
│ - UI 클릭 기반 설정 → 버전 관리 불가
│ - 복잡한 파이프라인 구성 어려움
│ - 설정 재사용 어려움
│
▼
2016년: Jenkins Pipeline 출시 (Groovy 기반)
│
▼
왜 Groovy인가?
├─ 1. JVM 기반 → Jenkins(Java)와 완벽 호환
├─ 2. 스크립트 언어 → 컴파일 없이 바로 실행
├─ 3. DSL 친화적 → 읽기 쉬운 파이프라인 문법 구현
├─ 4. Java 문법 호환 → Java 개발자 진입 장벽 낮음
└─ 5. 동적 타입 → 유연한 설정 가능
Groovy가 Jenkins Pipeline에 적합한 이유
1. DSL (Domain Specific Language) 구현에 최적화
// Groovy의 DSL 기능으로 이런 문법이 가능해짐
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'make build'
}
}
}
}
이 코드는 사실 Groovy 메서드 호출의 연속입니다:
// 실제로는 이런 의미
pipeline({
agent('any')
stages({
stage('Build', {
steps({
sh('make build')
})
})
})
})
2. 클로저(Closure)가 핵심
// 클로저 덕분에 블록 {} 문법이 가능
stage('Build') {
// 이 블록이 클로저
steps {
// 이것도 클로저
echo 'Building...'
}
}
// 클로저 없이 Java 스타일이라면?
Stage buildStage = new Stage("Build");
Steps steps = new Steps();
steps.add(new EchoStep("Building..."));
buildStage.setSteps(steps);
// 훨씬 장황하고 읽기 어려움
3. 메서드 괄호 생략
// Groovy
echo 'Hello'
sh 'make build'
// Java 스타일이라면
echo('Hello');
sh('make build');
4. Java 라이브러리 직접 사용
// Jenkins Pipeline에서 Java 클래스 직접 사용 가능
import java.time.LocalDateTime
import java.nio.file.Files
node {
def now = LocalDateTime.now()
echo "Current time: ${now}"
}
2.7 Groovy vs Java 문법 비교
| 항목 | Java | Groovy |
|---|---|---|
| 세미콜론 | 필수 | 생략 가능 |
| 타입 선언 | 필수 | def로 생략 가능 |
| Getter/Setter | 직접 작성 | 자동 생성 |
| 문자열 보간 | "Hello " + name |
"Hello ${name}" |
| 리스트 생성 | Arrays.asList(...) |
[1, 2, 3] |
| 맵 생성 | new HashMap<>() |
[key: value] |
| 클로저 | 없음 (람다로 대체) | 기본 지원 |
| null 안전 | if (obj != null) |
obj?.method() |
| 메서드 괄호 | 필수 | 생략 가능 |
2.8 Gradle에서의 Groovy
Jenkins와 함께 Groovy를 많이 사용하는 곳이 Gradle입니다.
// build.gradle (Groovy DSL)
plugins {
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter:3.0.0'
testImplementation 'junit:junit:4.13.2'
}
// 이것도 사실 Groovy 메서드 호출
// plugins({ id('java') })
// dependencies({ implementation('...') })
// build.gradle.kts (Kotlin DSL) - 최근 대안
plugins {
java
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter:3.0.0")
testImplementation("junit:junit:4.13.2")
}
2.9 Jenkins Pipeline 작동 원리
┌─────────────────────────────────────────────────────────┐
│ Jenkinsfile │
│ ┌───────────────────────────────────────────────────┐ │
│ │ pipeline { │ │
│ │ agent any │ │
│ │ stages { │ │
│ │ stage('Build') { ... } │ │
│ │ } │ │
│ │ } │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Groovy Compiler/Interpreter │
│ Jenkinsfile을 Groovy로 파싱 및 실행 │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Jenkins Pipeline Plugin │
│ pipeline(), agent(), stage() 등 DSL 메서드 제공 │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Jenkins Core (Java) │
│ 실제 빌드 작업 실행 (노드, 에이전트) │
└─────────────────────────────────────────────────────────┘
2.10 알아두면 좋은 점
Jenkins Pipeline의 Groovy 제약사항:
// 1. CPS (Continuation Passing Style) 변환
// - Pipeline은 중단/재개가 가능해야 함
// - 일부 Groovy 문법이 제한됨
// 2. Serializable 요구
// - 변수가 직렬화 가능해야 함
// - 일부 Java 객체 사용 제한
// 3. Sandbox 보안
// - 위험한 메서드 호출 제한
// - Script Approval 필요할 수 있음
// 해결책: @NonCPS 어노테이션
@NonCPS
def processData(data) {
// CPS 변환 없이 실행
// 순수 Groovy 문법 사용 가능
// 단, 이 함수는 중단/재개 불가
return data.collect { it.toUpperCase() }
}
3. Groovy 기초 문법
이제 Groovy 문법을 하나씩 배워봅시다.
3.1 변수 선언
// def 키워드로 동적 타입 선언
def name = "Jenkins"
def count = 10
def isEnabled = true
// 타입 명시도 가능
String message = "Hello"
int number = 42
// 상수 (final)
final String VERSION = "1.0.0"
3.2 문자열
// 작은따옴표: 일반 문자열 (변수 치환 안 됨)
def str1 = 'Hello World'
// 큰따옴표: GString (변수 치환 됨)
def name = "Jenkins"
def str2 = "Hello ${name}" // "Hello Jenkins"
def str3 = "Count: ${1 + 2}" // "Count: 3"
// 여러 줄 문자열
def multiLine = """
이것은
여러 줄
문자열입니다.
"""
// 문자열 메서드
def text = "hello world"
text.toUpperCase() // "HELLO WORLD"
text.contains("world") // true
text.split(" ") // ["hello", "world"]
text.trim() // 앞뒤 공백 제거
3.3 리스트 (List)
// 리스트 생성
def fruits = ['apple', 'banana', 'orange']
def numbers = [1, 2, 3, 4, 5]
def empty = []
// 접근
fruits[0] // 'apple'
fruits[-1] // 'orange' (마지막)
fruits.first() // 'apple'
fruits.last() // 'orange'
// 추가/삭제
fruits.add('grape')
fruits << 'melon' // add와 동일
fruits.remove('banana')
// 크기
fruits.size() // 4
fruits.isEmpty() // false
// 포함 여부
fruits.contains('apple') // true
'apple' in fruits // true (Groovy 스타일)
// 반복
fruits.each { fruit ->
println fruit
}
// 필터링
def longNames = fruits.findAll { it.length() > 5 }
// 변환
def upperFruits = fruits.collect { it.toUpperCase() }
3.4 맵 (Map)
// 맵 생성
def person = [
name: 'Kim',
age : 30,
city: 'Seoul'
]
// 접근
person.name // 'Kim'
person['name'] // 'Kim'
person.get('name') // 'Kim'
// 추가/수정
person.job = 'Developer'
person['email'] = 'kim@example.com'
// 삭제
person.remove('age')
// 반복
person.each { key, value ->
println "${key}: ${value}"
}
// 키/값 목록
person.keySet() // [name, age, city]
person.values() // [Kim, 30, Seoul]
// 중첩 맵
def config = [
database: [
host: 'localhost',
port: 5432
],
cache : [
enabled: true
]
]
config.database.host // 'localhost'
3.5 조건문
// if-else
def score = 85
if (score >= 90) {
println "A"
} else if (score >= 80) {
println "B"
} else {
println "C"
}
// 삼항 연산자
def result = score >= 60 ? "Pass" : "Fail"
// Elvis 연산자 (?:) - null 체크
def name = null
def displayName = name ?: "Unknown" // "Unknown"
// Safe navigation (?.) - null-safe 접근
def user = null
def userName = user?.name // null (에러 안 남)
// switch
def day = 'Monday'
switch (day) {
case 'Monday':
case 'Tuesday':
println "Weekday"
break
case 'Saturday':
case 'Sunday':
println "Weekend"
break
default:
println "Unknown"
}
3.6 반복문
// for 루프
for (int i = 0; i < 5; i++) {
println i
}
// for-in 루프
for (item in [1, 2, 3]) {
println item
}
// 범위 (Range)
for (i in 1..5) {
println i // 1, 2, 3, 4, 5
}
for (i in 1..<5) {
println i // 1, 2, 3, 4 (5 제외)
}
// while
def count = 0
while (count < 3) {
println count
count++
}
// each (가장 Groovy스러운 방식)
[1, 2, 3].each { num ->
println num
}
// eachWithIndex
['a', 'b', 'c'].eachWithIndex { item, index ->
println "${index}: ${item}"
}
// times
5.times { i ->
println "반복 ${i}"
}
3.7 함수 (메서드)
// 기본 함수
def greet(name) {
return "Hello, ${name}!"
}
greet("Kim") // "Hello, Kim!"
// return 생략 가능 (마지막 표현식이 반환값)
def add(a, b) {
a + b
}
add(1, 2) // 3
// 기본값 파라미터
def sayHello(name = "World") {
"Hello, ${name}!"
}
sayHello() // "Hello, World!"
sayHello("Kim") // "Hello, Kim!"
// 가변 인자
def sum(int ... numbers) {
numbers.sum()
}
sum(1, 2, 3, 4) // 10
// Map 파라미터 (Jenkins에서 자주 사용)
def configure(Map args) {
println "Name: ${args.name}"
println "Port: ${args.port ?: 8080}"
}
configure(name: 'MyApp', port: 3000)
configure(name: 'MyApp') // port는 기본값 8080
3.8 클로저 (Closure)
클로저는 Groovy의 핵심 개념으로, Jenkins Pipeline에서 매우 중요합니다.
// 클로저 정의
def myClosure = { println "Hello!" }
myClosure() // "Hello!"
// 파라미터가 있는 클로저
def greet = { name -> println "Hello, ${name}!" }
greet("Kim")
// 암시적 파라미터 'it'
def double = {
it * 2
}
double(5) // 10
// 여러 파라미터
def add = { a, b -> a + b }
add(1, 2) // 3
// 클로저를 인자로 받는 함수
def doTwice(closure) {
closure()
closure()
}
doTwice { println "Hi!" }
// Hi!
// Hi!
// 클로저와 delegate (Jenkins Pipeline 핵심)
def config = {
name = "MyApp"
version = "1.0"
}
// Jenkins는 이 패턴을 사용해서 DSL을 구현함
3.9 예외 처리
// try-catch
try {
def result = 10 / 0
} catch (ArithmeticException e) {
println "수학 오류: ${e.message}"
} catch (Exception e) {
println "일반 오류: ${e.message}"
} finally {
println "항상 실행"
}
// throw
def divide(a, b) {
if (b == 0) {
throw new IllegalArgumentException("0으로 나눌 수 없습니다")
}
return a / b
}
4. 선언형 Pipeline (Declarative)
5.1 기본 구조
pipeline {
agent any // 필수: 실행 환경
stages { // 필수: 스테이지들
stage('Build') {
steps {
echo 'Building...'
}
}
}
}
4.2 핵심 요소
pipeline {
// ─────────────────────────────────────────────
// 1. agent: 어디서 실행할지
// ─────────────────────────────────────────────
agent any // 아무 노드에서나
// agent none // agent 없음 (stage별로 지정)
// agent { label 'linux' } // 특정 라벨 노드
// agent { docker 'node:18' } // Docker 컨테이너
// ─────────────────────────────────────────────
// 2. environment: 환경변수
// ─────────────────────────────────────────────
environment {
APP_NAME = 'my-app'
VERSION = '1.0.0'
// credentials 사용
DOCKER_CREDS = credentials('docker-hub-credentials')
}
// ─────────────────────────────────────────────
// 3. options: 파이프라인 옵션
// ─────────────────────────────────────────────
options {
timeout(time: 1, unit: 'HOURS') // 타임아웃
timestamps() // 타임스탬프 출력
disableConcurrentBuilds() // 동시 빌드 방지
buildDiscarder(logRotator(numToKeepStr: '10')) // 빌드 보관
}
// ─────────────────────────────────────────────
// 4. parameters: 빌드 파라미터
// ─────────────────────────────────────────────
parameters {
string(name: 'BRANCH', defaultValue: 'main', description: '브랜치')
choice(name: 'ENV', choices: ['dev', 'staging', 'prod'], description: '환경')
booleanParam(name: 'DEPLOY', defaultValue: false, description: '배포 여부')
}
// ─────────────────────────────────────────────
// 5. triggers: 자동 트리거
// ─────────────────────────────────────────────
triggers {
pollSCM('H/5 * * * *') // 5분마다 Git 폴링
cron('0 2 * * *') // 매일 새벽 2시
}
// ─────────────────────────────────────────────
// 6. stages: 스테이지 정의 (필수)
// ─────────────────────────────────────────────
stages {
stage('Build') {
steps {
echo "Building ${env.APP_NAME}..."
}
}
stage('Test') {
steps {
echo 'Testing...'
}
}
stage('Deploy') {
steps {
echo 'Deploying...'
}
}
}
// ─────────────────────────────────────────────
// 7. post: 후처리
// ─────────────────────────────────────────────
post {
always {
echo '항상 실행'
cleanWs() // 워크스페이스 정리
}
success {
echo '성공 시 실행'
}
failure {
echo '실패 시 실행'
}
unstable {
echo '불안정 시 실행'
}
changed {
echo '상태 변경 시 실행'
}
}
}
4.3 steps 상세
stages {
stage('Steps 예제') {
steps {
// ─────────────────────────────────────
// 기본 명령어
// ─────────────────────────────────────
echo 'Hello World' // 출력
sh 'echo "Shell command"' // 셸 명령 (Linux)
bat 'echo "Batch command"' // 배치 명령 (Windows)
// ─────────────────────────────────────
// 셸 명령 상세
// ─────────────────────────────────────
// 결과를 변수에 저장
script {
def output = sh(script: 'whoami', returnStdout: true).trim()
echo "User: ${output}"
}
// 종료 코드 확인
script {
def status = sh(script: 'exit 0', returnStatus: true)
echo "Exit code: ${status}"
}
// ─────────────────────────────────────
// 파일 작업
// ─────────────────────────────────────
writeFile file: 'output.txt', text: 'Hello'
def content = readFile 'output.txt'
// 파일 존재 확인
script {
if (fileExists('output.txt')) {
echo 'File exists'
}
}
// ─────────────────────────────────────
// 디렉토리 작업
// ─────────────────────────────────────
dir('subdir') {
// subdir 안에서 실행
sh 'pwd'
}
// ─────────────────────────────────────
// 아카이브
// ─────────────────────────────────────
archiveArtifacts artifacts: 'build/**/*.jar'
// ─────────────────────────────────────
// 빌드 중단
// ─────────────────────────────────────
error 'Build failed!' // 빌드 실패 처리
}
}
}
4.4 조건부 실행 (when)
stages {
stage('Deploy to Prod') {
when {
// 브랜치 조건
branch 'main'
// 환경변수 조건
environment name: 'DEPLOY', value: 'true'
// 표현식 조건
expression { return params.DEPLOY == true }
// 여러 조건 AND
allOf {
branch 'main'
environment name: 'DEPLOY', value: 'true'
}
// 여러 조건 OR
anyOf {
branch 'main'
branch 'release/*'
}
// NOT 조건
not {
branch 'develop'
}
// 변경된 파일 조건
changeset "src/**/*.java"
}
steps {
echo 'Deploying to production...'
}
}
}
5.5 병렬 실행 (parallel)
stages {
stage('Test') {
parallel {
stage('Unit Test') {
steps {
sh 'npm run test:unit'
}
}
stage('Integration Test') {
steps {
sh 'npm run test:integration'
}
}
stage('E2E Test') {
steps {
sh 'npm run test:e2e'
}
}
}
}
}
4.6 입력 대기 (input)
stages {
stage('Deploy Approval') {
steps {
input message: '프로덕션에 배포하시겠습니까?',
ok: '배포',
submitter: 'admin,deploy-team'
}
}
stage('Deploy with Parameters') {
steps {
script {
def userInput = input(
message: '배포 설정',
parameters: [
choice(name: 'ENV', choices: ['staging', 'prod']),
string(name: 'VERSION', defaultValue: '1.0.0')
]
)
echo "Deploying ${userInput.VERSION} to ${userInput.ENV}"
}
}
}
}
4.7 credentials 사용
pipeline {
agent any
environment {
// Username/Password
NEXUS_CREDS = credentials('nexus-credentials')
// → NEXUS_CREDS_USR, NEXUS_CREDS_PSW 자동 생성
// Secret text
API_KEY = credentials('api-key')
// Secret file
KUBECONFIG = credentials('kubeconfig-file')
}
stages {
stage('Use Credentials') {
steps {
// environment에서 정의한 경우
sh 'echo "User: ${NEXUS_CREDS_USR}"'
// withCredentials 블록 사용
withCredentials([
usernamePassword(
credentialsId: 'docker-hub',
usernameVariable: 'DOCKER_USER',
passwordVariable: 'DOCKER_PASS'
)
]) {
sh 'docker login -u ${DOCKER_USER} -p ${DOCKER_PASS}'
}
// SSH Key
withCredentials([
sshUserPrivateKey(
credentialsId: 'ssh-key',
keyFileVariable: 'SSH_KEY'
)
]) {
sh 'ssh -i ${SSH_KEY} user@server'
}
}
}
}
}
5. 스크립트형 Pipeline (Scripted)
5.1 기본 구조
node {
// 여기에 파이프라인 로직
stage('Build') {
echo 'Building...'
}
}
5.2 node와 stage
// 특정 노드에서 실행
node('linux') {
stage('Checkout') {
checkout scm
}
stage('Build') {
sh 'make build'
}
}
// 여러 노드 사용
node('build-node') {
stage('Build') {
sh 'make build'
stash name: 'build-output', includes: 'dist/**'
}
}
node('test-node') {
stage('Test') {
unstash 'build-output'
sh 'make test'
}
}
5.3 완전한 Groovy 제어
스크립트형의 장점은 완전한 Groovy 문법을 사용할 수 있다는 것입니다.
node {
// 변수 사용
def modules = ['api', 'web', 'batch']
def buildResults = [:]
stage('Checkout') {
checkout scm
}
// 동적 스테이지 생성
modules.each { module ->
stage("Build ${module}") {
try {
sh "gradle :${module}:build"
buildResults[module] = 'SUCCESS'
} catch (Exception e) {
buildResults[module] = 'FAILED'
currentBuild.result = 'UNSTABLE'
}
}
}
// 조건부 로직
stage('Deploy') {
if (env.BRANCH_NAME == 'main') {
def failedModules = buildResults.findAll { k, v -> v == 'FAILED' }
if (failedModules.isEmpty()) {
sh 'make deploy'
} else {
echo "Skipping deploy. Failed modules: ${failedModules.keySet()}"
}
}
}
// 결과 요약
stage('Summary') {
buildResults.each { module, result ->
echo "${module}: ${result}"
}
}
}
5.4 예외 처리
node {
stage('Build') {
try {
sh 'make build'
} catch (Exception e) {
echo "Build failed: ${e.message}"
currentBuild.result = 'FAILURE'
throw e // 다시 던지기
} finally {
echo 'Cleanup...'
}
}
// catchError: 실패해도 계속 진행
stage('Optional Step') {
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
sh 'optional-command'
}
}
}
5.5 병렬 실행
node {
stage('Parallel Tests') {
parallel(
'Unit Tests': {
sh 'npm run test:unit'
},
'Integration Tests': {
sh 'npm run test:integration'
},
'E2E Tests': {
node('browser-node') { // 다른 노드에서 실행
sh 'npm run test:e2e'
}
}
)
}
// 동적 병렬 빌드
stage('Build Modules') {
def modules = ['api', 'web', 'batch']
def parallelBuilds = [:]
modules.each { module ->
parallelBuilds[module] = {
sh "gradle :${module}:build"
}
}
parallel parallelBuilds
}
}
5.6 선언형과 스크립트형 혼합
pipeline {
agent any
stages {
stage('Build') {
steps {
// script 블록 안에서 스크립트형 문법 사용
script {
def modules = sh(
script: 'ls -d */',
returnStdout: true
).trim().split('\n')
modules.each { module ->
echo "Found module: ${module}"
}
}
}
}
}
}
6. 공유 라이브러리 (Shared Library)
6.1 개념
공유 라이브러리는 여러 파이프라인에서 공통 코드를 재사용하기 위한 방법입니다.
여러 Jenkinsfile에서 동일한 로직 반복
↓
공유 라이브러리로 추출
↓
@Library('my-library') _ 로 호출
6.2 디렉토리 구조
(shared-library-repo)/
├── vars/ # 전역 변수/함수 (Pipeline에서 직접 호출)
│ ├── build.groovy # build() 함수로 호출
│ ├── deploy.groovy # deploy() 함수로 호출
│ └── notify.groovy # notify() 함수로 호출
│
├── src/ # Groovy 클래스 (import 필요)
│ └── com/
│ └── example/
│ └── Utils.groovy
│
└── resources/ # 리소스 파일
└── templates/
└── email.html
6.3 vars/ 함수 작성
vars/build.groovy:
// call() 메서드가 기본 진입점
def call() {
echo 'Default build'
}
// 파라미터 받기 (Map 권장)
def call(Map config = [:]) {
def language = config.language ?: 'java'
def version = config.version ?: '11'
echo "Building ${language} project with version ${version}"
switch (language) {
case 'java':
sh 'gradle build'
break
case 'node':
sh 'npm install && npm run build'
break
default:
error "Unknown language: ${language}"
}
}
Jenkinsfile에서 사용:
@Library('my-shared-library') _
build() // 기본 호출
build(language: 'java', version: '17')
build(language: 'node')
6.4 복잡한 예제 (app-builder-plugin 스타일)
vars/build.groovy:
def call(Map args = [:]) {
// 설정 생성
def config = generateConfig(args)
// Pipeline 시작
node('buildkit') {
checkout scm
stage('변경 감지') {
def modules = detectChanges(config)
config.modules = modules
}
// 동적 스테이지 생성
config.modules.each { module ->
stage("Build: ${module.name}") {
if (module.changed) {
buildModule(config, module)
} else {
echo "Skipped: ${module.name}"
}
}
}
}
}
// Private 함수들
def generateConfig(Map args) {
return [
logLevel: args.logLevel ?: 'info',
timeout : args.timeout ?: 30
]
}
def detectChanges(Map config) {
// 변경 감지 로직
sh 'gradle detectChanges'
def output = readFile 'build/changes.json'
return readJSON(text: output)
}
def buildModule(Map config, Map module) {
sh "gradle build -p ${module.path}"
}
6.5 src/ 클래스 작성
src/com/example/GitUtils.groovy:
package com.example
class GitUtils implements Serializable {
def script // Pipeline script 참조
GitUtils(script) {
this.script = script
}
String getBranch() {
return script.sh(
script: 'git branch --show-current',
returnStdout: true
).trim()
}
String getCommitHash() {
return script.sh(
script: 'git rev-parse --short HEAD',
returnStdout: true
).trim()
}
List<String> getChangedFiles() {
def output = script.sh(
script: 'git diff --name-only HEAD~1 HEAD',
returnStdout: true
).trim()
return output ? output.split('\n').toList() : []
}
}
Jenkinsfile에서 사용:
@Library('my-shared-library') _
import com.example.GitUtils
node {
checkout scm
def git = new GitUtils(this)
echo "Branch: ${git.getBranch()}"
echo "Commit: ${git.getCommitHash()}"
echo "Changed files: ${git.getChangedFiles()}"
}
6.6 Jenkins에 라이브러리 등록
Jenkins 관리 > System > Global Pipeline Libraries:
| 설정 | 값 |
|---|---|
| Name | my-shared-library |
| Default version | main |
| Load implicitly | 체크 시 @Library 없이 사용 가능 |
| Allow default version to be overridden | 체크 |
| Include @Library changes in job recent changes | 체크 |
Source Code Management:
- Git
- Repository URL: https://github.com/your-org/shared-library.git
- Credentials: (선택)
7. 실전 예제
7.1 기본 Java 프로젝트 (선언형)
pipeline {
agent any
tools {
jdk 'JDK17'
gradle 'Gradle8'
}
environment {
GRADLE_OPTS = '-Dorg.gradle.daemon=false'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh 'gradle clean build -x test'
}
}
stage('Test') {
steps {
sh 'gradle test'
}
post {
always {
junit 'build/test-results/**/*.xml'
}
}
}
stage('Archive') {
steps {
archiveArtifacts artifacts: 'build/libs/*.jar'
}
}
}
post {
failure {
echo 'Build failed!'
}
}
}
7.2 멀티모듈 동적 빌드 (스크립트형)
node {
def changedModules = []
stage('Checkout') {
checkout scm
}
stage('Detect Changes') {
// 변경된 파일에서 모듈 추출
def changedFiles = sh(
script: 'git diff --name-only HEAD~1 HEAD',
returnStdout: true
).trim().split('\n')
changedFiles.each { file ->
def module = file.split('/')[0]
if (!changedModules.contains(module)) {
changedModules.add(module)
}
}
echo "Changed modules: ${changedModules}"
}
// 변경된 모듈만 빌드
changedModules.each { module ->
stage("Build: ${module}") {
dir(module) {
sh 'gradle build'
}
}
}
stage('Summary') {
echo "Built ${changedModules.size()} modules"
}
}
7.3 Docker 빌드 및 배포
pipeline {
agent any
environment {
DOCKER_REGISTRY = 'registry.example.com'
IMAGE_NAME = 'my-app'
IMAGE_TAG = "${env.BUILD_NUMBER}"
}
stages {
stage('Build Image') {
steps {
script {
docker.build("${DOCKER_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}")
}
}
}
stage('Push Image') {
steps {
script {
docker.withRegistry("https://${DOCKER_REGISTRY}", 'docker-credentials') {
docker.image("${DOCKER_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}").push()
docker.image("${DOCKER_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}").push('latest')
}
}
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
sh """
kubectl set image deployment/my-app \
my-app=${DOCKER_REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}
"""
}
}
}
}
7.4 Kubernetes에서 실행 (app-builder-plugin 스타일)
node('buildkit') { // Kubernetes Pod로 실행
checkout scm
// buildkit 컨테이너 안에서 실행
container('buildkit') {
stage('Build') {
sh 'gradle build'
}
stage('Docker Build') {
sh '''
buildctl build \
--frontend dockerfile.v0 \
--local context=. \
--local dockerfile=. \
--output type=image,name=my-image:latest,push=true
'''
}
}
// kubectl 컨테이너에서 배포
container('kubectl') {
stage('Deploy') {
sh 'kubectl apply -f k8s/'
}
}
}
8. 자주 사용하는 패턴
8.1 환경변수 활용
pipeline {
agent any
environment {
// 직접 정의
APP_NAME = 'my-app'
// Jenkins 내장 변수 사용
BUILD_ID = "${env.BUILD_NUMBER}"
BRANCH = "${env.BRANCH_NAME}"
// 스크립트로 값 설정
GIT_HASH = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
}
stages {
stage('Info') {
steps {
echo "App: ${env.APP_NAME}"
echo "Build: ${env.BUILD_ID}"
echo "Branch: ${env.BRANCH}"
echo "Commit: ${env.GIT_HASH}"
// Jenkins 내장 변수들
echo "Workspace: ${env.WORKSPACE}"
echo "Job Name: ${env.JOB_NAME}"
echo "Build URL: ${env.BUILD_URL}"
}
}
}
}
8.2 파라미터 활용
pipeline {
agent any
parameters {
string(name: 'VERSION', defaultValue: '1.0.0')
choice(name: 'ENVIRONMENT', choices: ['dev', 'staging', 'prod'])
booleanParam(name: 'RUN_TESTS', defaultValue: true)
text(name: 'RELEASE_NOTES', defaultValue: '')
}
stages {
stage('Build') {
steps {
echo "Version: ${params.VERSION}"
echo "Environment: ${params.ENVIRONMENT}"
}
}
stage('Test') {
when {
expression { params.RUN_TESTS }
}
steps {
sh 'gradle test'
}
}
}
}
8.3 조건부 스테이지
pipeline {
agent any
stages {
// main 브랜치에서만
stage('Deploy to Prod') {
when {
branch 'main'
}
steps {
echo 'Deploying to production'
}
}
// PR에서만
stage('PR Check') {
when {
changeRequest()
}
steps {
echo 'Running PR checks'
}
}
// 특정 파일 변경 시
stage('Build Docs') {
when {
changeset "docs/**"
}
steps {
echo 'Building documentation'
}
}
}
}
9. 디버깅 팁
9.1 로그 출력
// 일반 출력
echo 'Hello'
println 'Hello' // 스크립트형에서만
// 변수 확인
echo "Variable: ${myVar}"
echo "Type: ${myVar.getClass()}"
// 맵/리스트 예쁘게 출력
echo groovy.json.JsonOutput.prettyPrint(
groovy.json.JsonOutput.toJson(myMap)
)
9.2 환경 확인
stage('Debug') {
steps {
// 모든 환경변수 출력
sh 'printenv | sort'
// 현재 디렉토리
sh 'pwd'
// 파일 목록
sh 'ls -la'
// Git 상태
sh 'git status'
sh 'git log --oneline -5'
}
}
9.3 Replay 기능
Jenkins 빌드 페이지에서 Replay 버튼을 클릭하면:
- 파이프라인 코드를 수정해서 다시 실행 가능
- Git에 커밋하지 않고 테스트 가능
- 디버깅에 매우 유용
10. 실전 분석: app-builder-plugin의 build.groovy
지금까지 배운 내용을 바탕으로 실제 Jenkins Shared Library 파이프라인 스크립트를 분석해봅니다.
10.1 전체 구조 한눈에 보기
build.groovy
│
├── call() ← 메인 진입점 (Jenkinsfile에서 호출)
│ ├── generateBuildParameters()
│ ├── generateConfig()
│ └── node('buildkit') { } ← 실제 파이프라인
│ ├── stage("변경 감지")
│ │ └── getTargetsTask()
│ └── modules.each { stage(...) }
│ └── appBuilderTask()
│
├── generateBuildParameters() ← Jenkins UI 파라미터 정의
├── generateConfig() ← 설정값 병합
├── getTargetsTask() ← Gradle GetTargets 실행
├── parseSkipStages() ← "1,3,5" → [1,3,5] 변환
├── isSkipped() ← 스킵 여부 판단
├── skipStage() ← Jenkins UI에서 스킵 표시
├── appBuilderTask() ← Gradle AppBuilder 실행
├── camelToSnake() ← camelCase → SNAKE_CASE
└── getUser() ← 빌드 실행자 조회
10.2 메인 진입점: call()
@SuppressWarnings("GroovyAssignabilityCheck")
// IDE 경고 무시
def call(Map args = [:]) { // 기본값: 빈 Map
| 요소 | 설명 |
|---|---|
@SuppressWarnings |
Groovy 타입 체크 경고 무시 (동적 타입이라 IDE가 경고) |
def call(...) |
vars/ 폴더의 파일명이 함수명이 됨. build.groovy → build() |
Map args = [:] |
기본값 빈 Map. Jenkinsfile에서 인자 없이 build() 호출 가능 |
Jenkinsfile에서 호출 예시:
@Library('app-builder-gradle-plugin-pipeline') _
// 방법 1: 인자 없이
build()
// 방법 2: 설정 전달
build(
logLevel: 'debug',
appBuilderConfig: [
ecrRegistry: "111111111.dkr.ecr.ap-northeast-2.amazonaws.com"
]
)
10.3 빌드 파라미터 생성
def generateBuildParameters() {
return properties([ // ← Jenkins DSL: Job 속성 설정
parameters([ // ← Jenkins DSL: 빌드 파라미터
choice(
name: 'LOG_LEVEL',
choices: ['warn', 'debug', 'info', 'error'],
description: '...'
),
string(
name: 'SKIP_STAGES',
defaultValue: '',
description: '...'
)
])
])
}
Jenkins UI에서 이렇게 보임:
┌─────────────────────────────────────────────┐
│ Build with Parameters │
├─────────────────────────────────────────────┤
│ LOG_LEVEL: [warn ▼] │
│ ○ warn │
│ ○ debug │
│ ○ info │
│ ○ error │
├─────────────────────────────────────────────┤
│ SKIP_STAGES: [____________] │
│ 예: 1,3,5 │
└─────────────────────────────────────────────┘
10.4 설정값 생성 및 우선순위
def generateConfig(Map args, Map parameters) {
// 1. logLevel 결정 (우선순위: args > params > 기본값)
def logLevel = (args.logLevel ?: parameters.LOG_LEVEL ?: "warn").toLowerCase()
// │ │ │
// │ │ └─ 3순위: 기본값
// │ └─ 2순위: Jenkins UI 파라미터
// └─ 1순위: Jenkinsfile에서 전달한 값
// 2. skipStages 파싱
def strSkipStages = args.skipStages ?: parameters.SKIP_STAGES ?: ""
def skipStages = strSkipStages == "" ? [] : parseSkipStages(strSkipStages)
// 3. appBuilderConfig 처리 및 환경변수 내보내기
def appBuilderConfig = args.appBuilderConfig ?: [:]
appBuilderConfig.forEach { key, value ->
env[camelToSnake(key)] = value // ecrRegistry → ECR_REGISTRY
}
return [
logLevel : logLevel,
skipStages : skipStages,
appBuilderConfig: appBuilderConfig
]
}
설정 흐름도:
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Jenkinsfile │ │ Jenkins UI │ │ 환경변수 │
│ build(args) │ │ parameters │ │ env.* │
└────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘
│ │ │
└───────────────────────┼───────────────────────┘
▼
┌────────────────────────┐
│ generateConfig() │
│ 우선순위 병합 │
└────────────┬───────────┘
▼
┌────────────────────────┐
│ Jenkins 환경변수 │
├────────────────────────┤
│ ECR_REGISTRY=... │
│ HELM_VALUES_REPO=... │
└────────────────────────┘
10.5 파이프라인 본체
node('buildkit') { // K8s에서 'buildkit' 라벨 Pod 할당
checkout scm // Git 체크아웃
ansiColor('xterm') { // 터미널 색상 출력 활성화
withCredentials([ // 인증정보 주입
usernamePassword(credentialsId: 'nexus', ...),
usernamePassword(credentialsId: 'github', ...)
]) {
def buildData = [:] // 빌드 데이터 저장용
// Stage 1: 변경 감지
stage("🔍 변경 감지") { ... }
// Stage 2~N: 모듈별 빌드
buildData.modules.each { name, module ->
stage("[${module.language}] ${module.name}") { ... }
}
}
}
}
실행 환경 구조:
┌─────────────────────────────────────────────────────────┐
│ Kubernetes Cluster │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Pod (label: buildkit) │ │
│ │ ┌─────────────────┐ ┌─────────────────────────┐ │ │
│ │ │ jnlp 컨테이너 │ │ buildkit 컨테이너 │ │ │
│ │ │ (Jenkins │ │ (gradle, docker, │ │ │
│ │ │ agent) │ │ buildctl 등) │ │ │
│ │ └─────────────────┘ └─────────────────────────┘ │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
10.6 Stage 1: 변경 감지
stage("🔍 변경 감지") {
container("buildkit") {
buildData = getTargetsTask(config)
}
echo "📋Git 정보: { repo: ${buildData.commit.repo} ... }"
buildData.modules.each { name, module ->
log.append " ${module.changed ? "⭕" : "❌"} ${name}"
}
}
getTargetsTask() 상세:
def getTargetsTask(Map config) {
// Gradle 태스크 실행
sh "gradle -p ${config.appBuilderConfig.appBuilderPath} GetTargets --no-daemon"
// 결과 JSON 읽기
def jsonContent = sh(
script: "cat .../build/app-builder/modules-data.json",
returnStdout: true
).trim()
return readJSON(text: jsonContent)
}
modules-data.json 구조:
{
"commit": {
"repo": "my-project",
"branch": "feature/login",
"author": "kim",
"commitMessage": "feat: add login"
},
"modules": {
"edoc-api": {
"index": 0,
"name": "edoc-api",
"language": "JAVA",
"type": "app",
"changed": true,
"version": "1.0.0",
"status": null
}
}
}
10.7 Stage 2~N: 모듈별 동적 빌드
buildData.modules.each { name, module -> // 모듈 수만큼 반복
def stageName = "[${module.language}] ${module.name}"
stage(stageName) { // 동적 스테이지 생성!
if (isSkipped(config, module)) {
skipStage(stageName)
module.status = "SKIPPED"
} else {
container("buildkit") {
def afterModuleData = appBuilderTask(config, name, module)
module.status = afterModuleData.status
module.version = afterModuleData.version
}
}
echo """
📦모듈: { name: ${module.name}, language: ${module.language} }
🛠️결과: { status: ${module.status} }
"""
}
}
스킵 로직:
def isSkipped(Map config, Map module) {
return !module.changed // 변경 안됨
|| config.skipStages.contains(module.index + 1) // 수동 스킵
}
def skipStage(String stageName) {
script {
org.jenkinsci.plugins.pipeline.modeldefinition.Utils
.markStageSkippedForConditional(stageName)
}
}
10.8 유틸리티 함수들
// 문자열 파싱: "1, 3, 5" → [1, 3, 5]
def parseSkipStages(String strSkipStages) {
if (!strSkipStages?.trim()) return []
def skipStages = new LinkedHashSet<Integer>()
strSkipStages.split(',').each { part ->
if (part.trim()?.isInteger()) {
skipStages.add(part.trim() as Integer)
}
}
return skipStages
}
// 케이스 변환: ecrRegistry → ECR_REGISTRY
def camelToSnake(String camelCase) {
return camelCase.replaceAll('([a-z])([A-Z]+)', '$1_$2').toUpperCase()
}
// 빌드 실행자 조회
def getUser() {
def BuildCauses = currentBuild.getBuildCauses('hudson.model.Cause$UserIdCause')
return BuildCauses ? BuildCauses.userName : "Push event"
}
10.9 전체 실행 시퀀스 다이어그램
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Jenkinsfile │ │ build.groovy│ │ Gradle │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
│ build(args) │ │
│───────────────────────>│ │
│ │ │
│ │ generateBuildParameters()
│ │ generateConfig() │
│ │ │
│ │ stage("변경 감지") │
│ │────────────────────────>│
│ │ GetTargets 태스크 │
│ │<────────────────────────│
│ │ modules-data.json │
│ │ │
│ │ modules.each │
│ │ stage("모듈명") │
│ │────────────────────────>│
│ │ AppBuilder 태스크 │
│ │<────────────────────────│
│ │ │
│ 완료 │ │
│<───────────────────────│ │
10.10 Jenkins UI에서 보이는 모습
┌─────────────────────────────────────────────────────────────────┐
│ Pipeline: my-project #42 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 🔍 변경 │──▶│ [JAVA] │──▶│ [JAVA] │──▶│ [NODE] │ │
│ │ 감지 │ │ edoc-api │ │ common │ │ frontend │ │
│ │ ✅ 2s │ │ ✅ 45s │ │ ⏭️ skip │ │ ✅ 30s │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ Console Output: │
│ ───────────────────────────────────────────────────────────── │
│ 👨💼실행자: kim │
│ 📦대상 │
│ ⭕ edoc-api │
│ ❌ common │
│ ⭕ frontend │
│ │
└─────────────────────────────────────────────────────────────────┘
10.11 BuildKit 상세 설명
BuildKit이란?
BuildKit은 Docker의 차세대 빌드 엔진으로, 기존 docker build 대비 다음과 같은 장점을 제공합니다:
| 특징 | 설명 |
|---|---|
| 병렬 빌드 | 독립적인 빌드 스테이지를 동시에 처리 |
| 향상된 캐싱 | 레이어 캐싱 최적화로 빌드 시간 단축 |
| 분산 빌드 | 원격 캐시 및 분산 워커 지원 |
| 보안 | Rootless 빌드 지원 |
파이프라인에서 BuildKit 사용 위치
// 1. Jenkins Node 지정
node('buildkit') {
// 전체 파이프라인이 buildkit 라벨 Pod에서 실행
}
// 2. Container 지정
container("buildkit") {
// Kubernetes Pod 내 buildkit 컨테이너에서 작업 실행
buildData = getTargetsTask(config)
}
쉬운 비유: 사무실로 이해하기
🏢 회사 건물 (Kubernetes Cluster)
│
└── 🚪 사무실 (Pod)
│
├── 👔 관리자 (jnlp 컨테이너)
│ - "이번에 할 일은 A, B, C야"
│ - 일을 직접 안 하고 지시만 함
│
└── 👷 작업자 (buildkit 컨테이너)
- 실제로 코드 빌드함
- Docker 이미지 만듦
핵심 포인트:
jnlp= 지시하는 사람 (Jenkins Agent)buildkit= 실제 일하는 사람 (Gradle, Docker 등 도구 보유)
모듈 vs Dockerfile 스테이지 병렬
주의: BuildKit의 병렬 실행은 모듈 간 병렬이 아닙니다! 모듈은 순차 실행되고, 하나의 Dockerfile 내부 스테이지가 병렬로 실행됩니다.
// 모듈 실행은 순차적
buildData.modules.each { name, module -> // ← 하나씩 순차 실행!
stage(stageName) {
appBuilderTask(config, name, module) // 모듈 A 끝나야 B 시작
}
}
모듈 실행 순서 (순차)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[모듈 A 빌드] ──완료──▶ [모듈 B 빌드] ──완료──▶ [모듈 C 빌드]
BuildKit 병렬은 “Dockerfile 내부 스테이지”:
FROM gradle:8.0 AS builder
RUN gradle build
FROM node:18 AS frontend # ← builder와 독립적이면 동시 실행!
RUN npm run build
FROM openjdk:21
COPY --from=builder ...
COPY --from=frontend ...
기존 Docker Build BuildKit
(스테이지 순차 실행) (독립 스테이지 병렬 실행)
┌──────────┐ ┌──────────┐
│ builder │ │ builder │──┐
│ 30초 │ │ 30초 │ │ 동시에
└────┬─────┘ └──────────┘ │ 실행!
│ ┌──────────┐ │
▼ │ frontend │──┘
┌──────────┐ │ 20초 │
│ frontend │ └────┬─────┘
│ 20초 │ ▼
└────┬─────┘ ┌──────────┐
▼ │ final │
┌──────────┐ └──────────┘
│ final │
└──────────┘
총 55초 총 35초
10.12 핵심 포인트 요약
| 항목 | 설명 |
|---|---|
| 파이프라인 타입 | 스크립트형 (Scripted) - node { } 로 시작 |
| 실행 환경 | Kubernetes Pod (buildkit 라벨) |
| 동적 스테이지 | modules.each { stage(...) } 로 모듈 수만큼 생성 |
| 설정 우선순위 | Jenkinsfile args > Jenkins UI params > 환경변수 |
| Gradle 연동 | GetTargets, AppBuilder 태스크 호출 |
| 데이터 교환 | modules-data.json 파일을 통해 Gradle ↔ Jenkins 통신 |
댓글