Jenkins의 Multibranch-pipeline을 이용한 CD 설정에 대해 정리해봅니다.
CD의 필요성
인간 젠킨스 라는 표현에 대해 들어보셨나요?
자바 애플리케이션은 빌드 결과물로 .jar 파일이 생성되는데요,
이를 배포하려는 서버에서 실행시키는 것이 곧 배포에 해당합니다.
즉, 새로운 버전이 배포되어야 할 때마다 jar파일을 빌드해서 서버에 옮겨서, 배포해야 하는 것이죠.
위와 같은 배포 과정을 사람이 수동으로 하는 경우를 익살스럽게 인간 젠킨스 라고 표현하기도 합니다.
쉽게 말하면 수동 배포죠.
이러한 수동 배포는 당연하게도 여러 가지 문제가 있습니다.
번거로움, 생산성, 실수 가능성 등등 말이죠.
이러한 배포 작업을 특정 이벤트에 따라 자동으로 트리거되어
Jenkins가 대신해주게 설정할 수 있습니다.
가령 develop 브랜치에 merge가 발생하면, 개발 서버에 신규 버전을 빌드해서 배포해주고,
main 브랜치에 merge가 발생하면, 운영 서버에 신규 버전을 빌드해서 배포해주는 겁니다.
Jenkins vs TeamCity
CD툴의 핵심 기능은 SCM으로부터 소스코드를 체크아웃하고 빌드하고,
이를 배포하고자 하는 곳으로 이동시킨 뒤, 스크립트를 실행하여 배포를 실행하는 것입니다.
따라서 Jenkins이외에도 여러 CD 툴이 있는데요,
Travis 와 TeamCity가 가장 많이 언급되는 것 같습니다.
그러나 Travis는 유료여서 선택지에서 가장 먼저 제외되었고,
Jetbrains 사의 TeamCity와 오픈 소스인 Jenkins를 고려하게 되었습니다.
레퍼런스가 가장 많은 Jenkins를 통해 우선 CD툴에 입문하자는 생각으로
Jenkins를 선택했고, 학습 과정에서 무수한 트러블 슈팅을 겪으며
Jetbrains 사가 만들었다면 뭔가 다르지 않을까 싶어 TeamCity도 설치해서 잠시 경험해봤습니다.
TeamCity는 일부 기능의 제한이 있긴 하지만, 직접 서버를 호스팅한다면 상업용으로도 무료입니다.
그래도 Jenkins 보다는 설정 등이 좀 더 편리하고 친절하지 않을까 싶어 Linux용으로 설치해서 사용해봤는데요,
t4g.micro 인스턴스에서 TeamCity는 무리라고 느껴졌습니다.
1GB의 메모리에, 2GB의 스왑메모리 정도로는 TeamCity를 사용하기에 턱없이 부족한 것 같습니다.
물론 UI도 깔끔하고 러닝커브가 있겠지만 JetBrains 사의 제품이라는 신뢰도가 큰 메리트였습니다만
아무래도 레퍼런스가 Jenkins에 비해서 부족하고 협업하게 될 동료들에게 다시 러닝 커브를 유발한다는 점과
무엇보다 Jenkins 보다 훨씬 무겁다는 점이 낙제 사유였습니다.
만약 성능이 뛰어난 인스턴스를 사용하거나, 온 프레미스 상황에서
협업할 동료들이 모두 TeamCity에 익숙한 상황이라면 꼭 한 번 나중에라도 써보고 싶네요.
Jenkins로 돌아오고 나니 Jenkins가 정말 가벼운 거였다는 걸 새삼 느꼈습니다.
Multibranch Pipeline
The Multibranch Pipeline project type enables you to implement different Jenkinsfiles
for different branches of the same project.
In a Multibranch Pipeline project,
Jenkins automatically discovers, manages and executes Pipelines
for branches which contain a Jenkinsfile in source control.
https://www.jenkins.io/doc/book/pipeline/multibranch/
멀티 브랜치 파이프라인은 Jenkins에서 제공하는 Item의 종류 중 하나입니다.
Item은 배포 자동화를 위한 설정의 종류 중 한 가지입니다.
멀티 브랜치 파이프라인은 하나의 프로젝트의 여러 브랜치를 한 번에 대응할 수 있는 장점이 있습니다.
브랜치마다 새로운 아이템이 생기는 것이 아니라, 하나의 프로젝트에 여러 브랜치를 동적으로 관리해줍니다.
동적으로 관리한다 라는 의미는 다음의 사례를 보시면 쉽게 이해되실 겁니다.
main 브랜치에 merge가 발생되면 main 브랜치에 해당하는 job이 수행됩니다.
release/1.0.1 브랜치가 새롭게 생성될 경우, release 브랜치에 해당하는 job이 수행됩니다.
같은 프로젝트임에도 개발, 운영 환경에 따라 새로운 Item을 설정해줘야 한다면,
공통으로 적용되어야 하는 CD 과정 중 일부 문법의 수정이 발생한다면,
각각의 Item을 모두 찾아가서 각각의 Jenkinsfile을 수정해야 하는 일이 발생할 수도 있습니다.
그러나 멀티브랜치 파이프라인은 하나의 Jenkinsfile을 사용하기에
관리 포인트가 한 곳으로 응집됩니다.
위 이미지처럼 하나의 backend-frankarrot 이라는 Item 내부에
develop, main 등 여러 브랜치들이 한 곳에서 응집도 있게 관리되게 됩니다.
요구사항 정리
Jenkins를 이용한 CD를 지금부터 진행해볼텐데요,
요구사항을 정리해보겠습니다.
- Github 리포지토리에 있는 프로젝트를 checkout 해온다.
- 서브 모듈이 포함되어 있다면, Github Token을 제공받아 함께 checkout 해올 수 있어야 한다.
- 한 프로젝트에 백엔드, 프론트엔드 코드가 함께 담겨있을 경우, 이를 분기할 수 있어야 한다.
- develop, main 등 브랜치별로 서로 다른 서버에 배포할 수 있어야 한다.
- 브랜치 별로 다른 설정이 적용되어야 하지만 하나의 Jenkinsfile로 관리되어야 한다.
- Jenkinsfile이 빌드 대상 리포지토리 내부가 아닌 Jenkins 내부에서 관리되어야 한다.
- gradle 명령어로 build를 수행하고 jar파일을 만들어내야 한다.
- 빌드 결과물을 서버에 배포하기 위해 private Key를 전달 받아 서버에 전송할 수 있어야 한다.
- 서버에 있는 쉘 파일을 실행시킬 수 있어야 한다.
- 빌드가 실패할 경우 알림을 보낼 수 있어야 한다.
Jenkinsfile은 어디에 있어야 할까
Jenkinsfile을 빌드 대상이 되는 프로젝트 내부에 같이 두기
이는 가장 기본적인 설정입니다.
그러나 이러한 설정은 다음과 같은 단점이 있습니다.
- Jenkinsfile이 대상 프로젝트와 같이 형상관리되어 Jenkinsfile의 수정이 커밋에 남게 된다.
- 배포 설정을 수정하려면 checkout -> 수정 -> PR -> merge 의 절차를 거쳐야 한다.
- 이론적인 관점에서 관심사가 분리되지 않은 모습이다.
- 이론적인 관점에서 Jenkins가 자신의 책임을 수행하기 위한 충분한 정보를 가지고 있지 않다.
- Jenkins가 자신의 핵심 책임을 외부에 의존하고 있다.
- public 리포지토리일 경우, Jenkinfile이 외부에 노출된다.
Jenkinsfile을 원격 private 리포지토리에 관리하기
이 옵션도 선택할 수 있는 하나의 옵션입니다.
이 경우에는 Jenkinsfile을 외부로부터 숨길 수 있다는 장점이 있지만,
빌드 대상 프로젝트 내부에 두는 것의 단점을 대부분 고스란히 답습합니다.
Jenkinsfile을 Jenkins 내부에 관리하기
마지막으로 소개할 옵션입니다.
이 경우에는 다음과 같은 장단점이 있습니다.
- Jenkins가 자신의 책임을 수행하기 위한 모든 정보를 가지고 있게 된다.
- 배포 설정 수정이 필요할 경우, Jenkins 내에서 모두 처리할 수 있다.
- 개발을 위한 리포지토리와 devops를 위한 CD 툴의 책임이 완전히 분리된다.
- Jenkinsfile이 형상관리되지 않기에 유실될 수도 있다.
저는 형상관리가 되지 않아 유실될 수 있다는 단점에 대해
극복 가능하거나, 다른 더 큰 유익을 위해 희생할 수 있는 정도의 단점이라고 판단했습니다.
그리고 Jenkins가 자신의 책임을 다하기 위한 정보를 모두 가지고 있어야 한다는 이론적인 측면과
배포 설정 수정, 관리의 용이성이라는 측면에 가점을 주어
Jenkinsfile을 Jenkins 내부에서 관리하는 쪽을 택했습니다.
JDK 11, Jenkins 설치하기
https://www.jenkins.io/doc/book/installing/linux/#debianubuntu
sudo apt install openjdk-11-jdk -y
curl -fsSL https://pkg.jenkins.io/debian-stable/jenkins.io.key | sudo tee \
/usr/share/keyrings/jenkins-keyring.asc > /dev/null
echo deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] \
https://pkg.jenkins.io/debian-stable binary/ | sudo tee \
/etc/apt/sources.list.d/jenkins.list > /dev/null
sudo apt-get update && sudo apt-get install jenkins -y
(필요시) Jenkins 포트 변경, Swap 메모리 할당, 포트 개방
Jenkins 포트 변경을 위한 설정 변경
# 수정 권한 열기
sudo chmod 777 /usr/lib/systemd/system/jenkins.service
Environment="JENKINS_PORT={수정하고자 하는 포트}"
# 수정 권한 닫기
sudo chmod 444 /usr/lib/systemd/system/jenkins.service
# 재시작
sudo systemctl daemon-reload && sudo service jenkins restart
Swap 메모리 할당을 위한 설정
touch ./swapfile
sudo fallocate -l 2G ./swapfile
sudo chmod 600 ./swapfile
sudo mkswap ./swapfile
sudo swapon ./swapfile
sudo vi /etc/fstab
swapfile none swap sw 0 0 # 이 내용을 맨 아레에 추가하고 저장
free -h
특정 포트 개방을 위한 설정
오라클 클라우드 프리 티어를 사용할 경우,
Ingress Rules 에서 개방하더라도 인스턴스에서 추가로 설정해주어야 합니다.
sudo iptables -I INPUT 1 -p tcp --dport {개방하고자 하는 포트} -j ACCEPT
Jenkins 첫 로그인 및 플러그인 설치
{서버 아이피}:{설정한 포트} 로 접속합니다.
최초에는 임시 비밀번호를 입력해야 하는데, cat 명령어로 확인하고 로그인 합니다.
저는 Install Suggested Plugins를 설치하는 방법을 택했습니다.
# 최초 임시 비밀번호 확인을 위한 명령어
sudo cat /var/lib/jenkins/secrets/initialAdminPassword
이후 Jenkins 관리 -> 플러그인 관리 -> 설치 가능 에서 다음과 같은 플러그인을 설치했습니다.
- https://plugins.jenkins.io/multibranch-scan-webhook-trigger/
- 멀티브랜치 아이템이 깃헙 webhook을 받아 동작할 수 있게 해주는 도구
- https://plugins.jenkins.io/pipeline-multibranch-defaults/
- 멀티브랜치 아이템이 리포지토리 내부 Jenkinsfile이 아닌 젠킨스 내부 스크립트로 빌드할 수 있게 해주는 도구
- https://plugins.jenkins.io/remote-file/
- 별도 리포지토리에 저장된 Jenkinsfile로 빌드할 수 있게 해주는 도구
- https://plugins.jenkins.io/multibranch-job-tear-down/
- 특정 브랜치가 삭제되었을 때 특정 동작을 수행할 수 있게 해주는 도구
- https://plugins.jenkins.io/blueocean/
- 파이프라인 시각화 도구
- https://plugins.jenkins.io/slack/
- 빌드 결과 슬랙 알림 도구
- https://plugins.jenkins.io/multibranch-build-strategy-extension/
- 특정 경로 이하 파일이 수정 됐을 때만 빌드 혹은 무시 설정을 위한 도구
크레덴셜 추가
크레덴셜은 총 세 가지를 추가했습니다.
- private 리포지토리인 서브모듈을 함께 checkout 해오기 위한 Github 토큰 Username and Password
- 별도 서버에 배포하기 위한 SSH username and private key
- Slack 알림을 위한 Secret Text
Jenkins 관리에서 Manage Credentials 에서 (global) 을 선택하고 Add Credentials를 클릭하여 생성합니다.
Credential 추가 관련 자세한 설명은 아래 링크를 참고해주세요
https://www.jenkins.io/doc/book/using/using-credentials/
https://slack.com/apps/A0F7VRFKN-jenkins-ci?tab=more_info
Jenkins 전역 변수 추가
Jenkins 관리 -> 시스템 설정 -> Global Properties 에서 Add를 눌러
Key-Value 형식으로 전역 변수를 설정할 수 있습니다.
이렇게 설정해둔 전역변수는 이후 Jenkinsfile에서 활용할 수 있습니다.
가령 저는 개발 서버의 IP주소, 운영 서버의 IP주소 등은 변수 처리해두었습니다.
따라서 여러 Item의 Jenkinsfile에서 개발, 운영 서버의 IP주소를 사용해야 하고,
만약 이들 IP 주소가 바뀌더라도, 전역 변수에서만 수정해주면 모든 수정이 완료되게 됩니다.
추가로 Jenkinsfile을 Jenkins 내부에 관리하지 않고
public 리포지토리에 함께 관리된다 하더라도 민감 정보를 숨길 수 있다는 장점도 있습니다.
(그럼에도 불구하고 배포 관련 스크립트 자체를 노출하는 것에 조금 감점을 주고 싶긴 합니다 ㅎ)
Slack 알림을 위한 시스템 설정
Slack 알림을 위한 플러그인을 설치하셨다면, Jenkins 관리 -> 시스템 설정 최하단에서
Slack 섹션을 발견하실 수 있을 겁니다.
여기에서 앞서 만들어두었던 Slack 연동을 위한 Secret Text Credential을 연동해줍니다.
Multibranch-pipeline 아이템 생성
Item 생성
플러그인을 설치하셨다면 새로운 Item 버튼을 눌렀을 때 Multibranch Pipeline with defaults 가 있을 겁니다.
이 Item을 선택하면 Jenkins 내부에 설정된 Jenkinsfile을 통해 빌드를 수행할 수 있습니다.
Branch Source 추가
Add Source 를 누르고 사용하는 SCM을 선택합니다.
저는 Github를 선택했고, Credential로 미리 만들어두었던 private 서브모듈 리포지토리를
checkout 하기 위한 토큰을 설정해주었습니다.
추가로 원격 리포지토리의 주소도 추가해주었습니다.
Filter by name (with wildcards)
Multibranch-pipeline은 해당 프로젝트 내 브랜치들을 동적으로 관리해주기 때문에,
관리 대상에 해당하는 브랜치들을 브랜치이름으로 설정해줄 수 있습니다.
Branch Source 하단의 Add 버튼을 눌러 여러 옵션을 추가로 설정할 수 있는데요,
이 중 Filter by name (with wildcards)를 선택해서,
main develop release* 라고 입력해줍니다.
이렇게 입력하면 main, develop 브랜치와 release로 시작하는 브랜치들에 트리거링됩니다.
즉 release/1.0.0, release/1.0.1 등에 대해 모두 트리거링 되는 거죠.
Git Flow 를 따를 경우 release 브랜치의 생명주기는 생성과 소멸을 반복하기에
이렇게 정규식으로 설정해주게 됩니다.
서브 모듈 Checkout 설정
앞서 진행하던 Branch Source 옵션에 추가로 설정합니다.
Add 를 누르고 Advanced sub-modules behaviours 를 선택합니다.
체크박스 네 개중 아래의 세 개를 체크해줍니다.
이렇게 설정해주면 대상 리포지토리를 체크해올 때, .submodules 파일을 이용해
서브모듈들도 함께 checkout 해오게 되는데,
이때 앞서 설정했던 크레덴셜을 이용해 가져오게 됩니다.
Build Strategy
https://plugins.jenkins.io/multibranch-build-strategy-extension/ 플러그인을 정상 설치했다면
Branch Source 설정 최하단에 Build strategies 옵션이 보입니다.
Add 를 누르고 Build included regions strategy를 누릅니다.
src/** 라고 입력하게 되면 src 경로 이하에 수정이 일어났을 경우에만 트리거링 됩니다.
즉, src 이하 경로가 아닌 루트 경로에 있는 README.md 파일이 변경된 경우엔
Github webhook이 Jenkins에 도달하더라도 새롭게 빌드하지 않고 해당 이벤트를 무시합니다.
한 프로젝트의 백엔드와 프론트엔드를 한 리포지토리에서 관리할 경우,
멀티 브랜치 파이프라인 아이템을 백엔드용 프론트엔드용으로 나누어 생성한 뒤에,
각각 해당 경로에 수정되었을 경우에만 대응하도록 처리하기에 유용합니다.
이하 설정들을 보시면 특정 경로 수정이 있을 경우 무시하는 옵션도 가능합니다.
이 플러그인 관련해선 다소 호기심이 생겨 리포지토리를 살펴봤는데요,
관심이 생기신 분들은 이 파일 정도만 살펴보셔도 좋을 것 같습니다.
들여쓰기가 안 좋아서 읽기가 다소 불편하긴 하지만,
Jenkins의 의존성을 import 하고, Build 관련 메서드를 Override 하여,
빌드가 발생하는 트리거를 동작시키는 boolean을 리턴하는 메서드를
입력된 옵션에 따라 동작하게 만드는 내용을 확인할 수 있습니다.
Build Configuration
드디어 빌드 설정입니다.
by default Jenkinsfile을 선택하고, 설정파일 식별자를 입력해줍니다.
아직 만들지 않았더라도 우선 이름을 입력하고 추후 해당 이름으로 파일을 만들어도 됩니다.
Run default Jenkinsfile within Groovy sandbox 는 체크를 해주셔야 정상 동작합니다.
Scan Repository Triggers
Scan by webhook 을 체크하고, Trigger token을 입력합니다.
해당 토큰값은 Github의 Webhook 설정에서 추가해서 이벤트를 식별할 값입니다.
Github Repository 에서는 Settings -> Webhooks -> Add a webhook 을 이용해 추가해줍니다.
이 설정을 추가해주면, merge, push 이벤트 발생 시, Jenkins로 해당 이벤트가 전달됩니다.
저는 PR에 대해선 CI를 Github Actions로 수행하고, merge, push에 대해서만
Jenkins가 이벤트를 받아 CD를 수행하기 원했기에 Just the push event 만 Github Webhook으로 받았습니다.
JENKINS_URL/multibranch-webhook-trigger/invoke?token=[Trigger token]
default Jenkinsfile 생성
드디어 Multibranch-pipeline Item 생성 및 설정이 완료되었습니다.
이제는 Jenkinsfile 만 잘 작성하면 끝입니다!
Jenkins 관리 -> Managed Files -> Add a new Config -> Groovy File + 앞서 입력한 default Jenkinsfile ID를 선택합니다.
그리고 스크립트를 작성하면 되는데요,
제가 작성한 스크립트는 다음과 같습니다.
이벤트가 발생한 브랜치를 체크아웃 해오고,
gradle 빌드한 뒤, 빌드 결과물을 브랜치에 따라 다른 서버로 전송시킨뒤,
그 서버에 있는 쉘 파일을 실행하는 겁니다.
그리고 모든 작업이 성공적이었을 경우와 실패했을 경우에 따라 Slack 알림을 전송합니다.
pipeline {
agent any
stages {
stage('Checkout') {
steps {
git branch: env.GIT_BRANCH, url: 'https://github.com/Frankarrot/backend.git'
}
}
stage('Build') {
steps {
sh '''
chmod +x ./gradlew
./gradlew build
'''
}
}
stage('develop 브랜치는 dev서버에 배포한다') {
when {
expression { env.GIT_BRANCH == 'develop' }
}
steps {
withCredentials([sshUserPrivateKey(credentialsId: 'Frankarrot-Oracle-Cloud', keyFileVariable: 'privateKey', usernameVariable: 'username')]) {
sh '''
echo "trying to deploy on develop server"
scp -i ${privateKey} -o StrictHostKeyChecking=no ./build/libs/*.jar ${username}@${DEV_SERVER_IP}:/home/ubuntu
ssh -i ${privateKey} -o StrictHostKeyChecking=no ${username}@${DEV_SERVER_IP} "sh /home/ubuntu/script/server_start.sh" &
'''
}
}
}
stage('main 브랜치는 prod서버에 배포한다') {
when {
expression { env.GIT_BRANCH == 'main' }
}
steps {
withCredentials([sshUserPrivateKey(credentialsId: 'Frankarrot-Oracle-Cloud', keyFileVariable: 'privateKey', usernameVariable: 'username')]) {
sh '''
echo "trying to deploy on prod server"
scp -i ${privateKey} -o StrictHostKeyChecking=no ./build/libs/*.jar ${username}@${PROD_SERVER_IP}:/home/ubuntu
ssh -i ${privateKey} -o StrictHostKeyChecking=no ${username}@${PROD_SERVER_IP} "sh /home/ubuntu/script/server_start.sh" &
'''
}
}
}
}
post {
success {
sh 'echo "success"'
slackSend(color: '#00FF00', message: "SUCCESS: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
}
unsuccessful {
sh 'echo "fail"'
slackSend(color: '#FF0000', message: "FAIL: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
}
}
}
서버에 있는 배포 스크립트는 아주 단순하게 작성했습니다.
kill -15 $(lsof -t -i:8080) || true
nohup java -jar frankarrot-0.0.1-SNAPSHOT.jar &
마무리
한 달 이상을 Jenkins의 멀티 브랜치 파이프라인을 이용한 CD에 도전한 것 같습니다.
핵심은 Jenkinsfile을 여러 브랜치에 동적으로 대응할 수 있게 작성하는 것이었는데요,
evn.GIT_BRANCH와 같이 Jenkins에서 기본 제공하는 변수가 있다는 점,
when, expression을 이용해 조건문을 적용할 수 있다는 점을 알게된 게 트러블 슈터였습니다.
최종적으로는 main 브랜치에 merge, 또는 push가 발생했을 경우
운영 서버에 배포하고 실행되도록 구성이 완료되었습니다.
'Server & Infra' 카테고리의 다른 글
AWS 의도하지 않은 요금 환불받기 (2) | 2022.09.29 |
---|---|
AWS CloudWatch 구축기 (EC2를 On-Premise로) (0) | 2022.09.29 |
Git에 잘못 올린 파일 삭제하기 (0) | 2022.08.29 |
🔒HTTPS 한 스푼 (대칭키, 개인키, 공개키) (2) | 2022.08.22 |
SonarQube with Github Actions (PR Decoration) (0) | 2022.08.17 |