Jenkins로 Spring Boot 프로젝트 배포 및 테스트

최병훈·2024년 11월 12일
post-thumbnail

1. 이미지 빌드 및 업로드

1)Spring Boot 프로젝트를 생성하고 코드를 작성

  • Controller를 작성: CalculatorController.java

    import lombok.RequiredArgsConstructor;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequiredArgsConstructor
    public class CalculatorController {
        private final Calculator calculator;
    
        @RequestMapping("/")
        String sum(@RequestParam("a") Integer a, @RequestParam("b") Integer b) {
            return String.valueOf(calculator.sum(a, b));
        }
    }
  • Service를 작성: Calculator.java

    import org.springframework.stereotype.Service;
    
    @Service
    public class Calculator {
        public Integer sum(Integer a, Integer b){
            return a+b;
        }
    }

2)프로젝트를 실행하고 테스트

3)gradle 프로젝트 빌드

  • 빌드는 실행 가능한 상태로 만들어주는 작업
  • 빌드 도구에 따라 빌드하는 명령어는 다름
    gradle의 경우: ./gradlew clean build
    maven의 경우: ./mvnw clean build
  • 빌드 결과는 build/libs 디렉토리에 저장: 실행할 애플리케이션(.jar)이 build 디렉토리에 존재

4)이미지 빌드

  • Dockerfile 을 프로젝트 디렉토리에 생성하고 작성

    FROM bellsoft/liberica-openjdk-alpine:23
    
    ARG JAR_FILE=build/libs/*.jar
    
    COPY ${JAR_FILE} app.jar
    
    EXPOSE 8080
    
    ENTRYPOINT ["java", "-jar", "app.jar"]
  • 이미지 빌드

    docker build -t 이미지이름 .
    docker build -t calculator .

  • 컨테이너 실행 및 확인
    docker run -d --name calculator-container -p 8080:8080 calculator
    docker ps
  • 브라우저에서 : http://localhost:8080/?a=9&b=11
    9 + 11 의 결과인 20 이 출력되는 것을 확인

    Dockerfile 을 통해 image가 build되고, container로 실행시켰을 때, 잘 동작하는 것이 확인됨.

5)빌드 내용을 stage에 추가

  • Jenkinsfile을 수정

    pipeline {
        agent any
        stages {
            stage("Permission") {
                steps {
                    sh "chmod +x ./gradlew"
                }
            }
            stage("Compile") {
                steps {
                    sh "./gradlew compileJava"
                }
            }
            stage("Test") {
                steps {
                    sh "./gradlew test"
                }
            }
            stage("Test Code Coverage"){
                steps{
                    sh "./gradlew jacocoTestCoverageVerification"
                    sh "./gradlew jacocoTestReport"
                }
           }
           stage("Gradle Build"){
             steps{
                 sh "./gradlew clean build"
             }
           }
           stage("Docker Build"){
              steps{
                  sh "docker build -t jenkinspipeline ."
             }
           }
        }
    }
  • git push

    git commit -am "Docker Build Stage added"
    git push
  • Jenkins에서 확인 Docker Image Build 단계에서 실패

  • Jenkins에서 host docker 접근 권한 부여

  • 다시 push 하고 Jenkins에서 확인
    Dockerfile에 의해 image build가 완료되었다.

6)Docker Hub에 이미지를 푸시하기 위한 준비 작업

  • Docker Hub에서 토큰을 발급
  • 이미지를 저장하기 위한 Repository를 생성
    : yachae1101/calculator
    업로드 되는 도커 이미지는 저장소의 이름과 동일한 이름을 가져야 한다.
  • 토큰을 Jenkins의 Credential에 저장: Jenkinsfile에 Token을 직접 사용하면 에러가 발생하고 이후에는 git에 push가 안됨
    • Username And Password로 등록
      - Username : DockerHub의 계정 이름
      - Password : 토큰 값
      - ID : dockerhub-username-password

7)Docker Login

  • Jenkinsfile에 Credential ID를 변수로 등록

    environment{
       DOCKERHUB_CREDENTIALS = credentials("dockerhub-username-password")
    }
  • ImageBuild Stage는 저장소 이름으로 변경하고 로그인 작성

    stage("Docker Image Build"){
      steps{
          sh 'docker build -t yachae1101/calculator .'
      }
    }
    stage('Docker Hub Login'){
      steps{
          sh 'echo $DOCKERHUB_CREDENTIALS_PSW | docker login -u $DOCKERHUB_CREDENTIALS_USR --password-stdin'
      }
    }
  • 수정된 Jenkinsfile

    pipeline {
        agent any
        environment{
           DOCKERHUB_CREDENTIALS = credentials("dockerhub-username-password")
        }
        stages {
            stage("Permission") {
                steps {
                    sh "chmod +x ./gradlew"
                }
            }
            stage("Compile") {
                steps {
                    sh "./gradlew compileJava"
                }
            }
            stage("Test") {
                steps {
                    sh "./gradlew test"
                }
            }
            stage("Test Code Coverage"){
                steps{
                    sh "./gradlew jacocoTestCoverageVerification"
                    sh "./gradlew jacocoTestReport"
                }
           }
           stage("Gradle Build"){
             steps{
                 sh "./gradlew clean build"
             }
           }
           stage("Docker Image Build"){
             steps{
                 sh 'docker build -t yachae1101/calculator .'
             }
           }
           stage('Docker Hub Login'){
             steps{
                 sh 'echo $DOCKERHUB_CREDENTIALS_PSW | docker login -u $DOCKERHUB_CREDENTIALS_USR --password-stdin'
             }
           }
        }
    }
  • git push 후 확인

    git commit -am "Docker Hub Login Stage added"
    git push

    로그인 성공

8)Docker Image Push

  • docker hub 에 image push 하는 stage 추가

    stage('Docker Hub Push'){
      steps{
          sh 'docker push yachae1101/calculator:latest'
      }
    }
  • git push 후 확인

    git commit -am "Docker Hub Push Stage added"
    git push

  • docker hub에서 확인
    docker hub 에 빌드된 이미지가 push 되었다.

9) Jenkins 환경 변수를 활용하여 docker image에 tag 붙이기

젠킨스가 제공하는 환경 변수

  • env.VARNAME으로 사용이 가능, 이 변수는 전역 변수
    https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables

  • currentBuild 라는 환경 변수는 현재 빌드에만 해당되는 지역 변수
    number
    result
    currentResult
    duration
    keepLog
    displayName

  • BUILD_NUMBER 환경 변수를 이용해서 docker image에 매번 다른 tag를 붙여서 배포하도록 Jenkinsfile 수정

    pipeline {
        agent any
        environment{
           DOCKERHUB_CREDENTIALS = credentials("dockerhub-username-password")
        }
        stages {
            stage("Permission") {
                steps {
                    sh "chmod +x ./gradlew"
                }
            }
            stage("Compile") {
                steps {
                    sh "./gradlew compileJava"
                }
            }
            stage("Test") {
                steps {
                    sh "./gradlew test"
                }
            }
            stage("Test Code Coverage"){
                steps{
                    sh "./gradlew jacocoTestCoverageVerification"
                    sh "./gradlew jacocoTestReport"
                }
           }
           stage("Gradle Build"){
             steps{
                 sh "./gradlew clean build"
             }
           }
           stage("Docker Image Build"){
             steps{
                 sh "docker build -t yachae1101/calculator:${env.BUILD_NUMBER} ."
             }
           }
           stage('Docker Hub Login'){
             steps{
                 sh 'echo $DOCKERHUB_CREDENTIALS_PSW | docker login -u $DOCKERHUB_CREDENTIALS_USR --password-stdin'
             }
           }
           stage('Docker Hub Push'){
             steps{
                 sh "docker push yachae1101/calculator:${env.BUILD_NUMBER}"
             }
           }
        }
    }
  • git push 후 jenkins에서 확인
  • dockerhub에서 확인
    BUILD_NUMBER가 tag로 붙은 이미지가 업로드되었다.
  • 이렇게 하니까, image가 계속 쌓인다..

  • 그래서 이번 최신 image 2개만 남기고 다른 image 들은 삭제하는 과정을 Jenkinsfile에 추가

    stage('Clean Up Docker Images') {
        steps {
            script {
                def imageTag = "${env.BUILD_NUMBER}"
                def previousTag = (imageTag.toInteger() - 1).toString()
    
                // Delete older images locally, keeping only the current and previous build images
                sh """
                    docker images --filter=reference='yachae1101/calculator:*' --format '{{.Tag}}' | \
                    grep -Ev '^(${imageTag}|${previousTag})\$' | \
                    xargs -I {} docker rmi -f yachae1101/calculator:{}
                """
    
                // 환경 변수로 Docker Hub 사용자 이름과 API 토큰 설정
                withCredentials([usernamePassword(credentialsId: 'dockerhub-username-password', usernameVariable: 'DOCKERHUB_USR', passwordVariable: 'DOCKERHUB_TOKEN')]) {
                    // Clean up old images on Docker Hub using Docker Hub API with token
                    sh """
                        # Get the list of tags from Docker Hub and delete older images except the current and previous ones
                        curl -s -H "Authorization: JWT \$DOCKERHUB_TOKEN" \
                        "https://hub.docker.com/v2/repositories/yachae1101/calculator/tags/" | \
                        jq -r '.results[].name' | \
                        grep -Ev '^(${imageTag}|${previousTag})\$' | \
                        xargs -I {} curl -X DELETE -H "Authorization: JWT \$DOCKERHUB_TOKEN" \
                        "https://hub.docker.com/v2/repositories/yachae1101/calculator/tags/{}"
                    """
                }
            }
        }
    }
  • git push 하고 확인
  • ubuntu 에서 image 확인
    이미지가 2개씩만 남아있고 나머지는 삭제되었다.
    docker images
  • docker hub 에서 image 확인
    docker hub 에서도 최신 이미지 2개만 남아있다.
  • 가장 최신 이미지에 대해서 latest 태그를 유지하도록 Jenkinsfile 수정

    pipeline {
        agent any
        environment{
           DOCKERHUB_CREDENTIALS = credentials("dockerhub-username-password")
        }
        stages {
            stage("Permission") {
                steps {
                    sh "chmod +x ./gradlew"
                }
            }
            stage("Compile") {
                steps {
                    sh "./gradlew compileJava"
                }
            }
            stage("Test") {
                steps {
                    sh "./gradlew test"
                }
            }
            stage("Test Code Coverage"){
                steps{
                    sh "./gradlew jacocoTestCoverageVerification"
                    sh "./gradlew jacocoTestReport"
                }
           }
           stage("Gradle Build"){
             steps{
                 sh "./gradlew clean build"
             }
           }
           stage("Docker Image Build"){
             steps{
                 sh "docker build -t yachae1101/calculator:${env.BUILD_NUMBER} ."
                 sh "docker tag yachae1101/calculator:${env.BUILD_NUMBER} yachae1101/calculator:latest"
             }
           }
           stage('Docker Hub Login'){
             steps{
                 sh 'echo $DOCKERHUB_CREDENTIALS_PSW | docker login -u $DOCKERHUB_CREDENTIALS_USR --password-stdin'
             }
           }
           stage('Docker Hub Push'){
             steps{
                 sh "docker push yachae1101/calculator:${env.BUILD_NUMBER}"
                 sh "docker push yachae1101/calculator:latest"
             }
           }
            stage('Clean Up Docker Images') {
                steps {
                    script {
                        def imageTag = "${env.BUILD_NUMBER}"
                        def previousTag = (imageTag.toInteger() - 1).toString()
    
                        // Delete older images locally, keeping only the current and previous build images
                        sh """
                            docker images --filter=reference='yachae1101/calculator:*' --format '{{.Tag}}' | \
                            grep -Ev '^(${imageTag}|${previousTag}|latest)\$' | \
                            xargs -I {} docker rmi -f yachae1101/calculator:{}
                        """
    
                        // 환경 변수로 Docker Hub 사용자 이름과 API 토큰 설정
                        withCredentials([usernamePassword(credentialsId: 'dockerhub-username-password', usernameVariable: 'DOCKERHUB_USR', passwordVariable: 'DOCKERHUB_TOKEN')]) {
                            // Clean up old images on Docker Hub using Docker Hub API with token
                            sh """
                                # Get the list of tags from Docker Hub and delete older images except the current and previous ones
                                curl -s -H "Authorization: JWT \$DOCKERHUB_TOKEN" \
                                "https://hub.docker.com/v2/repositories/yachae1101/calculator/tags/" | \
                                jq -r '.results[].name' | \
                                grep -Ev '^(${imageTag}|${previousTag}|latest)\$' | \
                                xargs -I {} curl -X DELETE -H "Authorization: JWT \$DOCKERHUB_TOKEN" \
                                "https://hub.docker.com/v2/repositories/yachae1101/calculator/tags/{}"
                            """
                        }
                    }
                }
            }
        }
  • ubuntu 에서 이미지 확인

    docker images

    이미지가 2개씩만 남아있고, 가장 최신의 이미지에 대해서 latest 태그를 붙여서 유지

  • docker hub 에서 이미지 확인

2. 컨테이너로 배포 후 인수테스트

1) 컨테이너 배포

  • Jenkinsfile에 컨테이너를 생성하는 stage를 추가
    stage('Deploy'){
       steps{
           sh "docker run -d --rm -p 8765:8080 --name calculator yachae1101/calculator"
       }
    }
  • git push 후 확인
    git commit -am "Deploy Stage added"
    git push
  • Jenkins가 설치된 컴퓨터에 접속해서 docker ps 로 확인
    Jenkins에 의해 컨테이너가 실행되고 있다.

2)인수 테스트

  • spring 프로젝트의 루트 디렉토리에 스크립트 파일 생성: acceptance_test.sh

    #!/bin/bash
    test $(curl "localhost:8765/?a=1&b=2") -eq 3
  • 스크립트 파일을 실행하는 코드를 스테이지에 추가

    stage('Acceptance Test'){
      steps{
          sleep 60
          sh 'chmod +x acceptance_test.sh && ./acceptance_test.sh'
      }
    }

    sleep을 사용하는 이유 : docker run -d 가 비동기 방식으로 실행되므로 연속해서 테스트 하는 명령을 사용하게 되면 컨테이너가 만들어지기 전에 테스트를 해서 테스트 단계에서 실패로 판정하는 경우가 발생할 수 있다.

  • Jenkins가 설치된 컴퓨터에 접속해서 실행 중인 컨테이저 중지

    docker stop calculator
  • git push 후 jenkins에서 확인
    git add .
    git commit -m "Acceptance Test Stage added"
    git push

3)Post 로 clean up 과정 추가

  • Jenkinsfile 에 post로 clean up 과정을 추가한 jenkinsfile
    pipeline {
        agent any
        environment{
           DOCKERHUB_CREDENTIALS = credentials("dockerhub-username-password")
        }
        stages {
            stage("Permission") {
                steps {
                    sh "chmod +x ./gradlew"
                }
            }
            stage("Compile") {
                steps {
                    sh "./gradlew compileJava"
                }
            }
            stage("Test") {
                steps {
                    sh "./gradlew test"
                }
            }
            stage("Test Code Coverage"){
                steps{
                    sh "./gradlew jacocoTestCoverageVerification"
                    sh "./gradlew jacocoTestReport"
                }
           }
           stage("Gradle Build"){
             steps{
                 sh "./gradlew clean build"
             }
           }
           stage("Docker Image Build"){
             steps{
                 sh "docker build -t yachae1101/calculator:${env.BUILD_NUMBER} ."
             }
           }
           stage('Docker Hub Login'){
             steps{
                 sh 'echo $DOCKERHUB_CREDENTIALS_PSW | docker login -u $DOCKERHUB_CREDENTIALS_USR --password-stdin'
             }
           }
           stage('Docker Hub Push'){
             steps{
                 sh "docker push yachae1101/calculator:${env.BUILD_NUMBER}"
             }
           }
           stage('Deploy'){
              steps{
                  sh "docker run -d --rm -p 8765:8080 --name calculator yachae1101/calculator:${env.BUILD_NUMBER}"
              }
           }
           stage('Acceptance Test'){
             steps{
                 sleep 60
                 sh 'chmod +x acceptance_test.sh && ./acceptance_test.sh'
             }
           }
        }
        post{
           always{
               sh 'docker stop calculator'
           }
        }
    }
  • commit 후 push, jenkins에서 확인
    인수테스트를 수행한 후 컨테이너가 종료되었다.

0개의 댓글