
저는 지금 팀 내부에서 작은 프로젝트를 진행하고 있습니다. 개발은 어느 정도 완료되었고, 팀 알파 서버에 배포하는 단계가 남았죠.
이 프로젝트로 사내 오픈소스와 기술 발표를 노리고 있기 때문에 지속적으로 발전시켜나갈 예정입니다.
그러기 위해 필수적인 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 |