💡 CICD 과정LINK의 개요에 따라 작성되었습니다.
순서를 따르면 순조롭게 진행할 수 있습니다.

Jenkins에서 Docker Image Build & Push하기

이제 buildjar파일을 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_NAMEmain 브랜치 일 때 prod-cicd-test, develop브랜치 일때 dev-cicd-test가 담기게 된다.

하는 김에 checkout stagerepository 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에 추가해준다. returnStatustrue로 설정했는데, 만약 첫 빌드였다면 이전의 이미지가 존재하지 않아 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했으니 다음 포스트 에서는 실제로 서버에 접속해 배포를 진행해 보자.

댓글남기기