[CI/CD] (6) 배포 자동화 - 2
💡 CICD 과정은 LINK의 개요에 따라 작성되었습니다.
순서를 따르면 순조롭게 진행할 수 있습니다.
Jenkins에서 Docker Image Build & Push하기
이제 build
된 jar
파일을 Docker Image로 만들고 Docker Hub에 push
해보자.
Docker를 사용하는 이유는 프로젝트를 항상 같은 환경에서 구동하기 위함이다. 즉, 구동에 필요한 여러 dependency를 쉽게 관리하기 위함이다.
현재는 java
가 프로젝트의 dependency라고 말할 수 있다. 만약 서버에 다른 버전의 java
가 설치되어 있다면 실행에 문제가 생길 수 있기 때문에 Docker를 이용해 dependency를 쉽게 관리하기 위함이다.
또한 Docker Hub를 이용하는 이유는 이미지의 버전을 쉽게 관리하기 위함이다. 만약 새로 배포한 프로젝트에 문제가 생겼을 때, Docker Hub의 버전관리를 이용하면 쉽게 이전 버전으로 돌아갈 수 있게된다.
이제 여러 작업을 하며 Jenkins가 Docker Image를 빌드하고 Docker Hub에 push
할 수 있도록 해보자.
Docker Pipeline Plugin 설치
가장 먼저 Jenkins에서 Docker 명령어를 쉽게 입력하기 위해 Docker Pipeline Plugin
을 설치해주자.
Jenkins에서 [Jenkins 관리] - [플러그인 관리] - [Available Plugins]에서 Docker Pipeline
플러그인을 설치해준다.
Docker Pipeline Plugin 공식문서는 LINK에서 확인할 수 있다. 해당 플러그인은 Docker 이미지를 빌드하고, Docker Registry에 push하는 것을 지원한다.
Dockerfile 작성
해당 플러그인 공식문서를 살펴보면 Dockerfile
을 기반으로 이미지를 빌드한다. 따라서 Dockerfile
을 작성할 필요가 있다.
1
2
3
4
5
6
7
8
9
10
11
FROM eclipse-temurin:11.0.18_10-jre-focal
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENV TZ=Asia/Seoul
RUN ln -snf /us/share/zoneinfo/STZ /etc/localtime && echo STZ > /etc/timezone
ENTRYPOINT ["java", "-jar", "/app.jar", "-Duser.timezone=Asia/Seoul"]
Dockerfile의 내용은 간단하다. 원하는 java
버전을 기반으로 build/libs/*.jar
파일을 app.jar
로 복사한 후 timezone설정을 마치고 jar
파일을 실행하는 이미지이다. (여기서 *.jar
로 파일을 복사하기 위해 bootJar 설정을 하였다.)
나는 eclipse-temurin-jre-11이미지를 기반으로 Dockerfile을 작성했지만 사용하는 java
버전이 존재하면 Docker Hub에서 검색해 사용하면 된다.
프로젝트 루트에 해당 Dockerfile
을 작성한다.
DockerHub 접속 Credential 생성
Docker Hub에 이미지를 올릴 예정이므로 Docker Hub에 접속 가능한 Credential을 Jenkins에 생성한다. 그러면 stage에서 credential을 이용해 docker hub에 접속해 이미지를 push할 수 있다.
가장 먼저 Docker Hub에 가입한다.
Jenkins에서 [Jenkins 관리] - [Manage Credentials]에 접속한다. Docker Hub 접속은 다음 프로젝트에서 필요할 수 있으므로 Global Credential로 생성해 보겠다.
System에 접속한다.
Global credentials
에 접속한다. [Add Credentials]를 눌러 크레덴셜을 추가할 수 있다.
위와 같이 docker hub ID와 PW를 입력해주고 Credential을 추가한다. 이제 Jenkinsfile에서 Credential의 ID로 Credential을 불러올 수 있다.
Stage 작성
dev브랜치에서 push가 일어났을 때는 dev-cicd-test
로 이미지를 업로드 하고 main브랜치에서 push가 일어났을 때는 prod-cicd-test
로 이미지를 업로드 하고 싶다.
따라서 브랜치의 이름에 따라 변하는 변수를 만드는 Set Variables stage
를 추가해 보자. Set Variables stage
를 작성해 놓고 모든 변수를 해당 stage
에서 관리하면 다른 프로젝트에 적용 시에 해당 stage
만 변경하면 되므로 재사용성이 높아진다.
Stage들의 가장 앞에 다음 Stage를 추가한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
stage('Set Variables') {
steps {
echo 'Set Variables'
script {
// BASIC
PROJECT_NAME = 'cicd-test'
REPOSITORY_URL = '[원격 REPOSITORTY URL]'
PROD_BRANCH = 'main'
DEV_BRANCH = 'develop'
BRANCH_NAME = env.BRANCH_NAME
OPERATION_ENV = BRANCH_NAME.equals(PROD_BRANCH) ? 'prod' : 'dev'
// DOCKER
DOCKER_HUB_URL = 'registry.hub.docker.com'
DOCKER_HUB_FULL_URL = 'https://' + DOCKER_HUB_URL
DOCKER_HUB_CREDENTIAL_ID = 'DOCKER_HUB_CREDENTIAL'
DOCKER_IMAGE_NAME = OPERATION_ENV + '-' + PROJECT_NAME
}
}
}
위 처럼 작성하면 DOCKER_IMAGE_NAME
에 main
브랜치 일 때 prod-cicd-test
, develop
브랜치 일때 dev-cicd-test
가 담기게 된다.
하는 김에 checkout stage
의 repository url
도 변수로 담아주자.
1
2
3
4
5
6
7
stage('Git Checkout') {
steps {
echo 'Checkout Remote Repository'
git branch: "${env.BRANCH_NAME}",
url: REPOSITORY_URL
}
}
위와 같이 chekout stage
를 변경한다.
이제 실제로 Build & Push Docker Image stage
를 작성해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
stage('Build & Push Docker Image') {
steps {
echo 'Build & Push Docker Image'
withCredentials([usernamePassword(
credentialsId: DOCKER_HUB_CREDENTIAL_ID,
usernameVariable: 'DOCKER_HUB_ID',
passwordVariable: 'DOCKER_HUB_PW')]) {
script {
docker.withRegistry(DOCKER_HUB_FULL_URL,
DOCKER_HUB_CREDENTIAL_ID) {
app = docker.build(DOCKER_HUB_ID + '/' + DOCKER_IMAGE_NAME)
app.push(env.BUILD_ID)
app.push('latest')
}
}
}
}
}
위와 같이 스테이지를 추가하고 저장하자.
withCredentials
메서드로 Credential의 값들을 변수에 받을 수 있다. 다른 추가적인 방법이 필요할 시 Docker Pipeline Plugin 공식문서는 LINK를 참고하여 Jenkinsfile
을 작성하자.
위의 stage
를 보면 push
가 두번 일어나게 된다. 하나는 latest
로 일어나고 하나는 빌드 번호로 일어나게 된다. 이렇게 두 번 push
하면 버전관리가 되고, 최신 버전을 latest
태그를 이용해 쉽게 받아올 수 있게 된다.
이제 변경 내용을 push
해보자.
1
2
3
$ git add .
$ git commit -m "chore: Jenkinsfile Docker Image Build & Push 추가"
$ git push
Jenkins를 확인해 보자.
추가한 stage
들이 잘 실행 된 것을 확인할 수 있다.
또한 Docker Hub에 접속해 확인해 보면 dev-cicd-test
라는 의도한 이름으로 Docker Image가 push
된 것을 확인할 수 있다.
생성된 Docker Image 삭제
하지만 조금의 문제가 있다. Jenkins에서 Docker Image Build가 여러번 진행되다 보면 이번 버전의 이미지가 storage에 계속해서 남아있어 용량을 차지하게 된다.
매번 Docker Images들의 목록을 관리할 수는 없기에 Jenkins에서 Docker Image Push가 완료되고 나면 이전 태그의 이미지들은 삭제시켜주는 작업을 자동화 해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
stage('Build & Push Docker Image') {
steps {
echo 'Build & Push Docker Image'
withCredentials([usernamePassword(
credentialsId: DOCKER_HUB_CREDENTIAL_ID,
usernameVariable: 'DOCKER_HUB_ID',
passwordVariable: 'DOCKER_HUB_PW')]) {
script {
docker.withRegistry(DOCKER_HUB_FULL_URL,
DOCKER_HUB_CREDENTIAL_ID) {
app = docker.build(DOCKER_HUB_ID + '/' + DOCKER_IMAGE_NAME)
app.push(env.BUILD_ID)
app.push('latest')
}
}
sh(script: """
docker rmi \$(docker images -q \
--filter \"before=${DOCKER_HUB_ID}/${DOCKER_IMAGE_NAME}:latest\" \
${DOCKER_HUB_URL}/${DOCKER_HUB_ID}/${DOCKER_IMAGE_NAME})
""", returnStatus: true)
}
}
}
}
위처럼 이번 빌드에서 만들어진 이미지 이전 이미지들을 모두 삭제하는 명령어를 stage에 추가해준다. returnStatus
는 true
로 설정했는데, 만약 첫 빌드였다면 이전의 이미지가 존재하지 않아 exit 1
이 리턴된다. 따라서 삭제가 실패해도 무시하고 stage
를 진행하기 위해 추가한 옵션이다.
이제 push
를 다음과 같이 진행한다.
1
2
3
$ git add .
$ git commit -m "chore: Jenkinsfile Docker Image 삭제 추가"
$ git push
.
명령어 추가 후 빌드를 하면 이전 태그의 이미지들은 삭제되고 현재 버전의 이미지만 스토리지에 남게된다. (위의 사진은 추가된 예시이며 실제로는 dev-cicd-test
로 이미지 이름이 나와야 정상입니다.)
위와 같이 Jenkinsfile
을 작성했으면 이제 커밋과 푸시를 진행하자.
1
2
3
$ git add .
$ git commit -m "chore: Jenkinsfile Docker Image 삭제 추가"
$ git push
push
를 진행하고 Jenkins를 확인하면 빌드가 잘 진행된 것을 확인할 수 있다.
최종 Jenkinsfile
은 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
pipeline {
agent any
environment {
GPG_SECRET_KEY = credentials('GPG_SECRET_KEY')
}
stages {
stage('Set Variables') {
steps {
echo 'Set Variables'
script {
// BASIC
PROJECT_NAME = 'cicd-test'
REPOSITORY_URL = '[원격 Repository URL]'
PROD_BRANCH = 'main'
DEV_BRANCH = 'develop'
BRANCH_NAME = env.BRANCH_NAME
OPERATION_ENV = BRANCH_NAME.equals(PROD_BRANCH) ? 'prod' : 'dev'
// DOCKER
DOCKER_HUB_URL = 'registry.hub.docker.com'
DOCKER_HUB_FULL_URL = 'https://' + DOCKER_HUB_URL
DOCKER_HUB_CREDENTIAL_ID = 'DOCKER_HUB_CREDENTIAL'
DOCKER_IMAGE_NAME = OPERATION_ENV + '-' + PROJECT_NAME
}
}
}
stage('Git Checkout') {
steps {
echo 'Checkout Remote Repository'
git branch: "${env.BRANCH_NAME}",
url: REPOSITORY_URL
}
}
stage('Git Secret Reveal') {
steps {
echo 'Git Secret Reveal'
sh(script:
('gpg --batch --import ' + GPG_SECRET_KEY + ' && '
+ ' git secret reveal -f'))
}
}
stage('Build') {
steps {
echo 'Build With gradlew'
sh '''
./gradlew clean build
'''
}
}
stage('Build & Push Docker Image') {
steps {
echo 'Build & Push Docker Image'
withCredentials([usernamePassword(
credentialsId: DOCKER_HUB_CREDENTIAL_ID,
usernameVariable: 'DOCKER_HUB_ID',
passwordVariable: 'DOCKER_HUB_PW')]) {
script {
docker.withRegistry(DOCKER_HUB_FULL_URL,
DOCKER_HUB_CREDENTIAL_ID) {
app = docker.build(DOCKER_HUB_ID + '/' + DOCKER_IMAGE_NAME)
app.push(env.BUILD_ID)
app.push('latest')
}
sh(script: """
docker rmi \$(docker images -q \
--filter \"before=${DOCKER_HUB_ID}/${DOCKER_IMAGE_NAME}:latest\" \
${DOCKER_HUB_URL}/${DOCKER_HUB_ID}/${DOCKER_IMAGE_NAME})
""", returnStatus: true)
}
}
}
}
}
}
최종적으로 작성된 Jenkinsfile
은 위와 같다.
이제 Docker Hub에 이미지도 Push했으니 다음 포스트 에서는 실제로 서버에 접속해 배포를 진행해 보자.
댓글남기기