SSAFY에서 제공하는 배포 환경이 EC2 인스턴스 하나인 데다가 GitLab도 레포 하나를 프론트/백이 같이 쓰다보니 CI/CD를 파트별로 분리하는 데에 어려움이 많았다. 프론트↔백의 경우 2개의 develop 브랜치를 사용함으로써 브랜치명으로 파이프라인 트리거를 설정해서 나누는 것이 어렵지는 않았지만, 백엔드 내에서 여러 기능 서버를 띄울 경우는 서버별로 브랜치를 나누는 데에 제한이 있었다.
우리 프로젝트의 경우도 Spring/Play(추천) 두 개의 서버를 운영하는데 기능 개발은 두 서버가 다른 브랜치에서 수행한다고 할지라도 결국은 하나의 develop 브랜치에서 합쳐져야 하기에 기존에 사용하던 방식으로는 Spring 서버에 대한 변경이 생기든 Play 서버에 대한 변경이 생기든 두 서버를 모두 빌드/배포해야하는 문제가 있었다.
각 서버의 작업 주기가 다른데 Spring에서 변경 사항이 생겼다고 멀쩡히 동작하던 Play 서버가 다시 빌드되는건 말이 안된다 싶었고, 당연히 반대 경우도 마찬가지여서 이 과정을 분리해야겠다고 느꼈다.
이 상황을 해결하기 위해서는 이벤트가 일어난 target branch의 이름 뿐만 아니라 merge의 source branch의 이름을 파이프라인 트리거에서 활용할 수 있어야 했다. 기존에 사용하던 GitLab 플러그인을 사용할 경우는 source branch 정보를 얻어올 수가 없어서 다른 방법을 찾아야 했고, 또다시 숱한 구글링+GPT 끝에 Jenkins의 Generic Webhook Trigger 플러그인(이제 GWT라고 부르겠음)에 대해 알게 되었다.
GWT는 GitLab을 포함한 다양한 소스로부터 오는 webhook 요청을 처리할 수 있는 보다 범용적인 플러그인으로 webhook 요청 내의 특정 데이터를 분석하여 Jenkins 파이프라인의 변수로 사용할 수 있다. 범용 플러그인여서 그런지 GitLab 플러그인과 달리 webhook 요청 url을 Jenkins pipeline 별로 설정할 수 없고 모든 파이프라인이 http://JENKINS_URL/generic-webhook-trigger/invoke
을 요청 url로 사용해야한다. 요청 source 별로 다른 파이프라인을 지정하고 싶다면 GWT의 token
필드를 이용하면 된다. token
필드는 webhook 요청의 쿼리 파라미터로 전달되며, 파이프라인별로 다른 token
문자열을 사용해 어떤 source로부터의 webhook인지 구분할 수 있다.
GitLab Webhook 트리거를 설정할 때, push event와 달리 merge request의 경우 webhook이 발생할 브랜치를 지정할 수 없기 때문에 해당 repository에서 발생하는 모든 MR 이벤트에 대한 webhook이 발생하고, Jenkins에서 webhook 요청 데이터를 활용해서 파이프라인의 트리거 여부를 판단해줘야한다.
GitLab Webhook 요청의 데이터는 json 형태이고, 현 상황에 유용한 데이터는 아래와 같다.
{
"object_kind": String, // Webhook 이벤트 종류(푸시 이벤트: "push", MR 이벤트: "merge_request")
...,
"object_attributes": {
...,
"state": String, // Merge Request 상태(opened, closed, **merged**, ..etc), 여기서는 merged를 사용.
"source_branch": String, // merge source branch 이름
"target_branch": String, // merge target branch 이름
}
}
해당 데이터는 아래와 같이 GWT의 Post content parameters
섹션에서 받아와 환경 변수로 저장하 pipeline script나 이어 설명할 Optional filter 섹션에서 활용할 수 있다.
빌드/배포 파이프라인을 트리거하기 위한 요청 데이터의 요구사항은 아래와 같았다.
EVENT_TYPE
은 merge_request
이다.MR_STATE
는 merged
이다.MR_SOURCE_BRANCH
는 <spring-서버-작업-브랜치명>
또는 <play-서버-작업-브랜치명>
이다.MR_TARGET_BRANCH
는 <백엔드-develop-브랜치명>
이다.이를 처리하기 위한 방법으로는 ①pipeline script에서 조건문을 통해 필터링해주는 방법과 ②Optional filter 섹션을 사용하는 방법이 있는데 ①의 경우 필터링 조건에 걸릴 때마다 강제로 error step을 발생시키는 방식으로 MR의 merged를 제외한 매 이벤트마다 Jenkins 빌드 히스토리에 빌드 실패 기록이 남아 보기가 좀 안 좋아서 ② 방식을 사용하기로 했다.
Optional filter 섹션에서는 정규표현식을 활용한 필터링을 통해 파이프라인 실행 여부 자체를 결정할 수 있어 빌드 히스토리에 실패 기록을 남기지 않을 수 있다. Optional filter에서는 Expression 필드와 Text 필드를 설정할 수 있는데, Expression은 필터링에 사용될 정규표현식이고 Text는 필터링될 문자열이다. 앞서 저장한 환경 변수는 아래와 같이 prefix에 ‘$’를 붙여서 Text에서 사용할 수 있다.
사용한 Optional filter
Expression: merge_request merged (<spring-서버-작업-브랜치명>|<play-서버-작업-브랜치명>) <백엔드-develop-브랜치명>
Text: $EVENT_TYPE $MR_STATE $MR_SOURCE_BRANCH $MR_TARGET_BRANCH
pipeline script에서는 아래와 같이 환경 변수를 사용할 수 있다.
pipeline {
agent any
environment {
// Optional filter를 통해 트리거 조건은 모두 만족시킨 시점이므로,
// 파이프라인에는 source branch만 필요
SOURCE_BRANCH = "${env.MR_SOURCE_BRANCH ?: 'manual-build'}"
}
stages {
stage('build') {
steps {
script {
if (SOURCE_BRANCH == '<spring-서버-작업-브랜치명>') {
buildSpring()
} else if (SOURCE_BRANCH == '<play-서버-작업-브랜치명>') {
buildPlay()
} else { // 수동 빌드 시 전체 빌드 (parallel 블록으로 병렬 처리)
parallel springBuild: {
buildSpring()
}, playBuild: {
buildPlay()
}
}
}
...
}