저는 지금 팀 내부에서 작은 프로젝트를 진행하고 있습니다. 개발은 어느 정도 완료되었고, 팀 알파 서버에 배포하는 단계가 남았죠.
이 프로젝트로 사내 오픈소스와 기술 발표를 노리고 있기 때문에 지속적으로 발전시켜나갈 예정입니다.
그러기 위해 필수적인 CI/CD(지속적인 통합, 지속적인 제공) 환경을 구축하게 되었는데, 만만치 않았습니다. 사소한 문제는 공식 가이드나 레퍼런스로 해결이 가능했지만, 결정적인 부분에서 관련 가이드나 레퍼런스가 부실해 결국 우회하게 되었습니다.
제가 이 포스팅을 작성하는 이유는 저와 같은 문제가 발생해 이런저런 시도를 해보았는데도 해결하지 못한 분께 해결책은 아니지만 우회 방법을 공유하고자 함에 있습니다.
Jenkins 파이프라인 설계
우선 이 프로젝트는 팀 전용 파일럿 Github 리파지터리에서 관리되고 있습니다. 파일럿 리파지터리는 멀티 모듈 프로젝트로 세팅되어있으며, 한 개의 리파지토리에 여러 개의 파일럿 프로젝트가 존재하고 있었습니다.
리파지토리를 통째로 파이프라인으로 구성한다면?
- Github 릴리즈 태그가 발행되어 빌드될 경우, 모든 모듈이 같은 버전으로 빌드됩니다. (다른 모듈은 변경사항이 없었음에도)
- 개발 중인 모듈에서 빌드 혹은 테스트가 실패할 경우 전체 빌드가 실패합니다.
- 한 개 모듈만 빌드하면 되는데 전체가 빌드되어 빌드 시간이 불필요하게 길어집니다.
위 이유로 Github 릴리즈 태그 발행 시 한 개 모듈만 빌드되도록 파이프라인을 설계했습니다.
- 파일럿 리파지터리에서
모듈명
+/
+버전
형태로 릴리즈 태그를 발행합니다. - 젠킨스에서 태그 발행을 감지하고, 릴리즈 태그명에서 모듈명을 추출합니다.
- 파이프라인의 빌드 스테이지를 순회하면서 빌드 스테이지에 사전에 정의된 모듈명과 추출한 모듈명이 일치하는지 검사합니다.
- 일치할 경우 해당 스테이지를 수행하고, 일치하지 않을 경우 해당 스테이지를 스킵합니다.
Jenkinsfile 작성
앞선 설계 내용을 바탕으로 jenkinsfile.groovy
를 작성했습니다.
pipeline {
stages {
stage('SCM') {
steps {
checkout scm
}
}
stage('BUILD MODULE A') {
when {
tag 'module-a/*'
}
stages {
stage('BUILD') {
...
}
stage('TEST') {
...
}
stage('DOCKER PUSH') {
...
}
}
}
stage('BUILD MODULE B') {
when {
tag 'module-b/*'
}
stages {
stage('BUILD') {
...
}
stage('TEST') {
...
}
stage('DOCKER PUSH') {
...
}
}
}
}
environment {
TAG_NAME = getCurrentTag()
}
}
def getCurrentTag() {
script {
return sh(regurnStdout: true, script: "git tag --sort=creatordate | tail -1").trim();
}
}
Jenkins Pipeline Syntax 중 when
을 사용하면 표현식 결과가 참일 경우에만 해당 스테이지를 실행합니다.
when
의 표현식 중 tag
는 TAG_NAME
환경변수 값이 GLOB 스타일(Ant Path)의 정규표현식을 만족하면 true를 반환한다고 합니다.
자, 이렇게 작성하면 생각한대로 동작할 줄 알았습니다. 그리고 결과는... 이 포스팅의 이유가 되었습니다.😭
릴리즈 태그명과 상관없이 모든 스테이지가 동작했습니다!!
위 jenkinsfile.groovy
를 리파지터리에 푸시하고 릴리즈 태그를 module-a/0.0.1
로 발행했습니다. 그런데 태그명과 상관없이 모든 스테이지가 수행돼버렸습니다. 이게 뭔...
표현식이 문제가 있는 것일까요? 표현식이 정상적인지 확인하는 스크립트를 각 모듈별 스테이지의 BUILD 스테이지에 추가했습니다.
stage("BUILD") {
steps {
script {
def isMatchA = (env.TAG_NAME ==~ /^module-a\/.*/)
if (isMatchA) {
echo 'isMatchA = true'
} else {
echo 'isMatchA = false'
}
def isMatchB = (env.TAG_NAME ==~ /^module-b\/.*/)
if (isMatchB) {
echo 'isMatchB = true'
} else {
echo 'isMatchB = false'
}
...
}
}
}
그리고 다시 릴리즈 태그를 module-a/0.0.2
로 발행했는데, 결과는 아래와 같았습니다.
isMatchA = true
isMatchB = false
표현식은 정상적인 것이 분명해졌습니다. 그래서 tag의 comparator를 GLOB
에서 REGEXP
로도 변경해보았으나 결과는 마찬가지였습니다. Jenkins Pipeline Syntax 공식 가이드와 여러 사이트의 레퍼런스를 뒤져보았지만, 제가 위에서 작성한 대로 한 사람들은 모두 잘 되었다는 글뿐이었습니다.
결국은 우회했습니다
열세 번의 수정과 배포를 거친 뒤 우회하기로 결정했습니다. 이 망할 놈의 젠킨스는 제가 맘에 안 드는 게 분명합니다.
그래도 아래 두 가지 상황이 우회를 가능하게 해 주었습니다. 고오맙다.
TAG_NAME
환경 변수에는getCurrentTag()
메서드를 통해 유효한 태그명이 세팅되었습니다.when
에서tag
는 정상적으로 동작하지 않았지만,expression
은 정상적으로 동작했습니다.
그렇다면 태그명과 매개변수를 비교해 일치 여부를 반환하는 메서드를 선언하고, when에서 expression으로 호출해주면 되는 일이었습니다.
stage("BUILD MODULE A") {
when {
expression {isTagMatchModuleName("module-a") == true}
}
stages {
...
}
}
stage("BUILD MODULE B") {
when {
expression {isTagMatchModuleName("module-b") == true}
}
stages {
...
}
}
...
def isTagMatchModuleName(moduleName) {
return env.TAG_NAME ==~ /^${moduleName}\/.*/
}
결과는 대.성.공이었습니다. 이 포스팅에서는 생략했지만 각 모듈의 빌드 스테이지는 병렬로 구성했으며, 릴리즈 태그의 모듈명에 따라 해당 모듈만이 빌드되고 나머지는 스킵되었습니다.
'📦 ETC > 그냥 쓰고 싶어서요' 카테고리의 다른 글
NHN FORWARD 22 발표 후기 (4) | 2022.11.25 |
---|---|
막쓰는 기록 (0) | 2022.04.17 |
2021년 개발자 컨퍼런스 (0) | 2021.12.05 |
최근에 포스팅을 못한 이유 (0) | 2021.10.31 |
원티드 New Start Package를 받았습니다 (0) | 2021.08.28 |