Jenkins - Jenkins Multibranch pipeline(멀티브랜치 파이프라인)을 이용하여 Docker image build & push 하는 방법
오늘 다루어볼 포스팅은 Git과 Jenkins를 연동한 이후에 Jenkins Mulibranch pipeline으로 springboot web project를 docker image로 빌드 후 push하는 것까지 다루어볼 것이다. 이전까지는 오늘 다루어볼 포스팅의 연습 및 준비단계였고 본격적으로 CI/CD에 대해 다루어본다. 물론 오늘 다루어볼 포스팅 내용이 실무에서 그대로 활용하기 애매할 수는 있지만 포스팅을 이어나가면서 좀 더 보완해나갈 것이다.
1. Git & Jenkins 연동 후 Multibranch pipeline 작성
위의 내용에 Git & Jenkins 및 간단한 Multibranch pipeline을 연동하는 내용이 있다. 위의 링크를 참조하고, 위에 없는 내용중 보완할 내용만 추가로 다룬다. 젠킨스와 깃의 연동은 크게 다르지 않다. 하지만 Multibranch pipeline 설정이 조금 보완되었다.
1.1. Jenkins Multibranch pipeline 작성
General - Display Name과 Description을 작성해준다. (실행에 크게 영향 x)
*Branch Sources - 가장 핵심이 되는 configuration 영역이다. Github Credentials와 Repository HTTPS URL을 작성해준다. Credetials는 Jenkins Credentials에서 미리 만들어준다.(github의 인증정보 username/password로 작성하면 된다.) 그리고 깃헙의 https 주소를 넣어준다.
다음은 push & pull request event를 어떤 branch에 대해 인식시킬지에 대한 설정이다. 그리고 clean after checkout & clean before checkout을 넣어준다.
master & develop branch에 대해 push&pull request를 인식시키도록 하였다. 추후에는 일반 PR브랜치에 대해 인식시키면 feature branch등도 push&pull request 이벤트로 반응하도록 할 수 있을 것이다.
Clean after checkout & Clean before checkout은 .gitignore에 지정된 것을 포함하여 추적되지 않은 모든 파일과 디렉토리를 삭제하여 모든 체크 아웃 전후에 작업 공간을 정리하는 옵션이다.(테스트 결과 등의 파일정리) 또한 소스파일은 업데이트 된 경우 최신 변경사항을 가져오게 된다. Wipe out repository & force clone이라는 비슷한 옵션도 있지만, 이 옵션은 전체 프로젝트 작업 공간을 정리하고 빌드 전에 프로젝트를 다시 한번 클론하기 때문에 시간이 오래 걸린다.(프로젝트가 1기가라면 빌드 할 때마다 1기가를 pull받는다.)
마지막으로 push&pull request event가 발생시키면 젠킨스는 프로젝트 root에 있는 Jenkinsfile을 기준으로 pipeline을 기동시킨다는 설정이다.
2. Dockerfile & Jenkinsfile 작성
Jenkinsfile을 작성하기 전에 docker image build를 위해 우리는 Jenkins을 dood(docker out of docker)로 기동시켜야한다. 만약 준비가 되어 있지 않다면 아래 링크를 참조하자.
그리고 간단히 springboot web project를 생성해준다. 우리는 springboot web을 docker image로 빌드할 것이다.
2.1. Dockerfile 작성
간단히 우리가 만든 springboot web image를 빌드하기 위한 Dockerfile을 작성하자.
FROM adoptopenjdk/openjdk13:alpine-jre
MAINTAINER levi <1223yys@naver.com>
COPY ./build/libs/jenkins_sample-0.0.1-SNAPSHOT.jar /app/app.jar
EXPOSE 8080
CMD ["java", "-jar", "/app/app.jar"]
큰 내용은 없다. openjdk13을 base image로 하여 우리가 빌드한 springboot jar파일을 카피하여 실행시키는 image다.
2.2. Jenkinsfile 작성
pipeline {
agent none
environment {
SLACK_CHANNEL = '#jenkins_notification'
REGISTRY = '1223yys/spring-web-jenkins'
REGISTRYCREDENTIAL = '1223yys'
}
stages {
stage('Start') {
agent any
steps {
slackSend (channel: SLACK_CHANNEL, color: '#FFFF00', message: "STARTED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
}
}
stage('Test') {
agent {
docker {
image 'adoptopenjdk/openjdk13:alpine'
alwaysPull true
}
}
steps {
sh './gradlew test --no-daemon'
}
}
stage('Build') {
agent {
docker {
image 'adoptopenjdk/openjdk13:alpine'
alwaysPull true
}
}
steps {
sh './gradlew --no-daemon build -x test'
}
post {
success {
stash includes: '**/build/**', name: 'build'
}
}
}
stage('Docker image build & push') {
agent any
steps {
unstash 'build'
//docker username/password credential
sh 'docker login -u username -p password'
sh 'docker build -t $REGISTRY:latest .'
}
post {
success {
sh 'docker image ls | grep 1223yys'
sh 'docker push $REGISTRY:latest'
}
}
}
stage('Clean docker image') {
agent any
steps {
sh 'docker rmi $REGISTRY'
}
}
}
post {
success {
slackSend (channel: SLACK_CHANNEL, color: '#00FF00', message: "SUCCESSFUL: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
}
failure {
slackSend (channel: SLACK_CHANNEL, color: '#FF0000', message: "FAILED: Job '${env.JOB_NAME} [${env.BUILD_NUMBER}]' (${env.BUILD_URL})")
}
}
}
파이프라인의 모든 문법을 다루어보지는 못하지만 위에 작성된 문법정도는 다루어볼 것이다. 우선 항상 pipeline {} 가 root에 작성되어야하고 항상 해당 블록내에 파이프라인 스크립트를 작성한다. 그리고 바로 밑에 agent none이 보이는데, pipeline 바로 밑에 agent가 있으면 이것은 글로벌한 agent가 되기 때문에 각 steps마다 다른 agent 사용이 필요할 때는 사용하기 애매하기 때문에 none으로 지정하였다.
두번째는 environment {} 이다. 해당 설정에는 사용자 정의 환경설정 작성이 가능하지만 jenkins container 내부에서 사용하는 환경변수가 될수도 있다. 해당 설정은 중요하게 사용될 수 있다. 실제로 필자는 내부망 서버에 젠킨스를 올려 외부 파일을 받는 것이 되지 않아 environment에 proxy설정을 환경변수로 넣어놓아서 외부 파일을 다운로드 받을 수 있게 세팅하였다.
세번째는 slack에 파이프라인의 시작을 알림보내는 스크립트이다. 슬랙 알림에 대한 것은 아래 링크를 참고하자. agent any는 현재 젠킨스 인스턴스 중에 하나를 사용하겠다라는 설정이다.
네번째는 Test stage이다. git에서 checkout 받은 파일중 gradlew를 이용해 테스트 하기 위해서 agent를 openjdk13 base docker image에서 실행하겠다라는 스크립트이다. agent를 docker로 설정하고 사용할 이미지와 image pull 전략을 넣었다. 그리고 해당 docker image(openjdk13) 환경에서 실행할 스크립트를 "./gradlew test --no-daemon"를 작성했다. 참고로 docker agent로 실행시키기 때문에 이 환경은 특정 이미지 컨테이너의 환경이기 때문에 docker 명령이 먹지 않는다. 그 이유는 openjdk container에는 도커 엔진이 없기 때문에 ! 잘 유의하자.
다섯번째는 Build stage이다. 네번째 테스트 스테이지를 무사히 통과하였다면 이 단계로 넘어온다. 위 테스트 스테이지와 동일한 base 이미지로 build를 수행한다. 하지만 하나 추가된 설정이 있다.
post {
success {
stash includes: '**/build/**', name: 'build'
}
}
우리는 openjdk13을 볼륨마운트하는 설정을 어디에도 하지 않았고, 해당 이미지는 실행이 끝나면 docker container를 제거시킨다. 그렇기때문에 build된 jar파일이 없어질 것이다. 그래서 위와 같이 특정 파일을 stash(임시 저장)하여 필요할때 꺼내쓰기 위한 설정을 넣어주었다.
여섯번째는 도커 이미지 build 및 push 스테이지이다. 눈에 띄는 설정이 unstash 'build'이다. 도커 이미지를 빌드하기 위해 jar 파일이 필요하기 때문에 임시저장한 빌드 결과물을 꺼내고 있다. 그리고 해당 파일을 바탕으로 Dockerfile을 사용해 이미지를 빌드한다. agent any로 넣어줬기때문에 우리는 jenkins container로 빌드 잡을 수행한다. 이렇기 때문에 우리는 docker 명령 사용이 가능하다.
stage('Docker image build & push') {
agent any
steps {
unstash 'build'
sh 'ls -al'
sh 'docker login -u username -p password'
sh 'docker build -t $REGISTRY:latest .'
}
post {
success {
sh 'docker image ls | grep 1223yys'
sh 'docker push $REGISTRY:latest'
}
}
}
마지막 스테이지는 빌드한 이미지를 clean하는 스크립트이다. 이미지를 빌드하면 결국 로컬 디스크에 저장되므로 쌓이기 시작하면 어느샌가 디스크를 많이 먹고 있을 것이다. 그렇기 때문에 매 빌드마다 clean해준다.
stage('Clean docker image') {
agent any
steps {
sh 'docker rmi $REGISTRY'
}
}
slack에 빌드 성공 메시지가 갔는지 확인해보자.
마지막으로 dockerhub에 우리가 빌드한 이미지가 잘 올라갔는지 확인하자 !
오 ! 잘올라갔다 ㅎㅎ 여기까지 간단히 Jenkins Multibranch pipeline을 다루어보았다. 사실 필자는 내부망 장비를 할당받아서 실습했기 때문에 굉장히 삽질을 많이 했다.(외부에서 파일 다운로드 이슈 등) 아마 퍼플릭 존이라면 큰 이슈없이 실습이 가능할 것이다. 혹시나 내부망에서 실습을 진행하다 잘안되는 부분이 있다면 댓글을 남겨주길 바란다.
실습한 코드는 아래 깃헙링크를 참고하자.