cloudwithbass

[Jenkins] 젠킨스 파이프라인: 깃 푸시부터 인수 테스트까지 본문

Docker and Jenkins

[Jenkins] 젠킨스 파이프라인: 깃 푸시부터 인수 테스트까지

여영클 2024. 7. 24. 20:55

Continuous Delivery with Docker and Jenkins의 챕터 5까지 학습하며 만든 최종 Jenkinsfile입니다.

 

  • git push하면 자동으로 파이프라인을 빌드하도록 콘솔에서 트리거를 구성했습니다.
  • Jenkins 복습을 위해 스스로 지금까지 공부한 내용에 대해 설명하려고 합니다.
  • 전체 Jenkinsfile의 코드부터 첨부한 후, 부분마다 제 설명을 덧붙이겠습니다.

목차


    1. 전체 Jenkinsfile

    pipeline {
        agent {
            docker {
                    image 'dminus251/jenkins-docker-agent:using_socket'
                    args '--privileged -v /var/run/docker.sock:/var/run/docker.sock'
                    label 'docker-node-agent'
            }
        }
        environment {
            DOCKER_CREDENTIALS_ID = 'dminus251' // 저장한 자격 증명의 ID를 입력합니다.
        }
        stages {
            stage('Check Docker Installation') {
                steps {
                    script {
                        sh 'which docker'
                        sh 'docker --version'
                    }
                }
            }
            stage('Compile') {
                steps {
                    sh './gradlew compileJava'
                }
            }
            stage('Unit Test') {
                steps {
                    sh './gradlew test'
                }
            }
            stage('Code Coverage') {
                steps {
                    sh './gradlew jacocoTestReport'
                    sh './gradlew jacocoTestCoverageVerification'
                }
            }
            stage('Static Code Analysis') {
                steps {
                    sh './gradlew checkstyleMain'
                }
            }
            stage('Build Jar') {
                steps {
                    sh './gradlew build'
                }
            }
            stage('Docker Build') {
                steps {
                    script {
                        sh 'docker build -t dminus251/calculator:latest .'
                    }
                }
            }
            stage('Docker Login') {
                steps {
                    script {
                        // Docker Hub에 로그인
                        withCredentials([usernamePassword(credentialsId: env.DOCKER_CREDENTIALS_ID, usernameVariable: 'DOCKER_USERNAME', passwordVariable: 'DOCKER_PASSWORD')]) {
                            sh 'echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin'
                        }
                    }
                }
            }
            stage("Docker push"){
              steps{
                sh "docker push dminus251/calculator:latest"
              }
            }
            stage("Deploy to staging"){
              steps{
                sh "docker run -d --rm -p 8765:8081 --name calcForStaging dminus251/calculator:latest"
              }
            }
            stage("Acceptance test"){
              steps{
                sleep 30 //docker run이 확실히 실행될 때까지 기다림
                sh "docker logs calcForStaging"
                sh "chmod +x acceptance_test.sh && ./acceptance_test.sh"
              }
            }
        }
        //까지 stages
        post{
            always{
              sh "docker stop calcForStaging"
            }
        }
    }

     


    2. agent

    pipeline {
        agent {
            docker {
                    image 'dminus251/jenkins-docker-agent:using_socket'
                    args '--privileged -v /var/run/docker.sock:/var/run/docker.sock'
                    label 'docker-node-agent'
            }
        }
    • 저는 처음에 동적 프로비저닝 도커 에이전트를 사용했었는데, 도커인도커 사용을 위해 영구 도커 에이전트로 agent를 다시 구성했습니다.
    • 이 글에서 3일 동안 도커인도커를 구성한 과정을 확인할 수 있습니다.
    • 도커인도커 통신을 위해 /var/run/docker.sock을 마운트했으며, privileged 권한을 부여했습니다.
    • dminus251/jenkins-docker-agent:using_socket을 만든 Dockerfile은 다음과 같습니다.
    #Dockerfile for dminus251/jenkins-docker-agent:using_socket
    FROM gradle:jdk17
    
    # Docker 클라이언트 설치
    RUN apt-get update && \
        apt-get install -y docker.io
    
    RUN groupadd -g 1001 newdocker && usermod -aG newdocker root
    
    USER root

    docker.sock 사용을 위해 docker.sock의 소유 그룹 ID인 1001을 newdocker라는 그룹으로 추가했고, root 사용자를 newdocker 그룹에 추가했습니다.


    3. environment

     environment {
            DOCKER_CREDENTIALS_ID = 'dminus251' // 저장한 자격 증명의 ID를 입력합니다.
        }
    • Docker push 스테이지에서 도커 허브에 이미지를 푸시하기 위해 필요한 환경 변수입니다.
    • 젠킨스 콘솔의 Credentials 메뉴에서 도커 허브의 계정명, ID, PASSWORD를 저장해야 합니다.
    • 이후 Docker push 스테이지에서 더욱 자세히 설명하겠습니다.

    4. stages

    4-1. Check Docker Installation

        stages {
            stage('Check Docker Installation') {
                steps {
                    script {
                        sh 'which docker'
                        sh 'docker --version'
                    }
                }
            }

    도커가 설치된 이미지를 젠킨스의 에이전트로 사용했는데요, 성공적으로 도커 명령을 사용할 수 있는지 확인하기 위해 docker 명령어의 위치와 docker version을 출력합니다.


    4-2. Compile, Unit Test

    stage('Compile') {
                steps {
                    sh './gradlew compileJava'
                }
            }
            stage('Unit Test') {
                steps {
                    sh './gradlew test'
                }
            }

     

    • Compile 스테이지에서 자바 application을 컴파일하고, Unit Test 스테이지에서 단위 테스트를 실행합니다.
    • 이 글에서 더 자세한 내용을 확인할 수 있습니다.

    4-3. Code Coverage, Static Code Analysis

     stage('Code Coverage') {
                steps {
                    sh './gradlew jacocoTestReport'
                    sh './gradlew jacocoTestCoverageVerification'
                }
            }
            stage('Static Code Analysis') {
                steps {
                    sh './gradlew checkstyleMain'
                }
            }
    • 각각 코드 커버리지와 정적 코드 분석을 실시합니다.
    • 이 글에서 더 자세한 내용을 확인할 수 있습니다.

    4-4. Build Jar, Docker Build

    stage('Build Jar') {
                steps {
                    sh './gradlew build'
                }
            }
            stage('Docker Build') {
                steps {
                    script {
                        sh 'docker build -t dminus251/calculator:latest .'
                    }
                }
            }
    • Build Jar 스테이지에선 프로젝트를 빌드하고, 그 프로젝트의 아티팩트인 jar 파일을 생성합니다.
    • Docer Build 스테이지에선 현재 디렉터리의 Dockerfile에 작성된 내용을 dminus251/calculator:latest 이미지로 빌드합니다.
    • Dockerfile의 내용은 다음과 같습니다.
    # Dockerfile for dminus251/calculator:latest
    FROM openjdk:17-slim
    
    # JAR 파일을 이미지에 복사
    COPY build/libs/calculator-0.0.1-SNAPSHOT.jar app.jar
    RUN apt-get update && \
        apt-get upgrade -y && \
        apt-get install -y curl
    # 컨테이너가 시작될 때 JAR 파일을 실행하도록 설정
    ENTRYPOINT ["java", "-jar", "app.jar"]
    • 자바 애플리케이션 실행을 위해 베이스 이미지로 openjdk:17를 사용했습니다.
    • 이 컨테이너는 jar 파일을 복사한 후 실행합니다.

    4-5. Docker Login

     stage('Docker Login') {
                steps {
                    script {
                        // Docker Hub에 로그인
                        withCredentials([usernamePassword(credentialsId: env.DOCKER_CREDENTIALS_ID, usernameVariable: 'DOCKER_USERNAME', passwordVariable: 'DOCKER_PASSWORD')]) {
                            sh 'echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin'
                        }
                    }
                }
            }
    • 도커 허브에 dminus251/calculator:latest 이미지를 푸시하기 위해 도커 허브에 로그인합니다.
    • DOCKER_CREDENTIALS_ID 환경 변수는  3. environment에서 선언했습니다.
    • 젠킨스가 이 변수를 참조해서 젠킨스 설정 내 Credentials에서 DOCKER_USERNAMEDOCKER_PASSWORD 값을 가져옵니다.
    • echo $DOCKER_PASSWORD | docker login -u $DOCKER_USERNAME --password-stdin에서는 파이프를 이용해  echo $DOCKER_PASSWORD의 결과값을 파이프 뒤 명령의 입력값으로 사용합니다. 
    • 이 과정을 거쳐 도커 허브에 정상적으로 로그인이 가능합니다.

    4-6. Docker push, Deploy to staging

    stage("Docker push"){
              steps{
                sh "docker push dminus251/calculator:latest"
              }
            }
            stage("Deploy to staging"){
              steps{
                sh "docker run -d --rm -p 8765:8081 --name calcForStaging dminus251/calculator:latest"
              }
            }
    • 도커 허브에 이미지를 푸시하고, 인수 테스트를 위해 그 이미지의 도커 컨테이너를 실행합니다.
    • -p 8765:8081 옵션을 통해 localhost의 8765포트로 이 컨테이너에 접근할 수 있습니다.
    • 이 컨테이너의 이름을 calcForStaging으로 지정합니다.

    4-8. Acceptance test

            stage("Acceptance test"){
              steps{
                sleep 30 //docker run이 확실히 실행될 때까지 기다림
                sh "docker logs calcForStaging"
                sh "chmod +x acceptance_test.sh && ./acceptance_test.sh"
              }
            }
        }
        //까지 stages
    • 도커 컨테이너가 확실히 실행될 때까지 기다립니다.
    • docker logs 명령을 통해 컨테이너가 실행되는 포트, 컨테이너 실행 여부 등을 확인할 수 있습니다.
      calcForStaging 컨테이너는 8081 포트에서 실행되는 것을 확인했습니다.
    • acceptance_test.sh 파일에 실행 권한을 부여하고, 이 쉘 스크립트를 실행합니다.
    • acceptance_test.sh의 내용은 다음과 같습니다.
    #!/bin/bash
    
    #curl 결과를 result 변수에 저장
    result=$(docker exec calcForStaging curl -s "http://localhost:8081/sum?a=1&b=2")
    
    # 기대값을 설정: 100+172=272
    expected=3
    
    # 결과값이 기대값과 동일한지 체크
    if [ "$result" -eq "$expected" ]; then
        exit 0 #pass
    else
        echo "Test failed. expected $expected, but result is $result"
        exit 1
    fi

     

    docker exec calcForStaging curl -s "http://localhost:8081/sum?a=1&b=2" 명령을 수행해 그 값을 result 변수에 저장합니다.


    5. curl 관련 에러

    여기서 고생한 점이 두 가지있는데요

     

    1. curl 명령어를 실행할 땐 쌍따옴표 사용을 습관화해야겠습니다.

    • curl -s http://localhost:8081/sum?a=1&b=2에서 &를 백그라운드 실행 연산자로 이식됩니다.
    • 따라서 요청 주소가 올바르지 않게 됩니다.

     

    2. 원래는 result 값을 $(curl -s "http://localhost:8081/sum?a=1&b=2")로 지정하려 했으나, Connection refused 에러가 지속적으로 발생했습니다.

    두 명령의 차이점은 다음과 같습니다.

    • $(docker exec calcForStaging curl -s "http://localhost:8081/sum?a=1&b=2")
      • calcForStaging 컨테이너 내에서 curl 명령을 수행합니다. 즉, localhost는 calcForStaging 컨테이너가 됩니다.
    • $(curl -s "http://localhost:8081/sum?a=1&b=2")
      • 저는 wsl에서 도커가 실행 중이므로 localhost는 wsl이 됩니다. 즉, wsl의 8081포트에 curl 요청을 보내게 됩니다.
      • localhost의 8765 포트가 컨테이너의 8081 포트에 매핑되어 있으므로 $(curl -s "http://localhost:8765/sum?a=1&b=2")를 사용해야 합니다.
      • 아래 사진처럼 localhost:8765로 접근 시 정상적으로 a와 b의 합이 반환됩니다.


    6. post

     post{
            always{
              sh "docker stop calcForStaging"
            }
        }
    }
    • post 섹션에는 파이프라인 작업이 끝난 후 실행할 작업들을 정의합니다.
    • 제 경우 젠킨스 파이프라인 빌드가 실패할 때를 대비해서 calcForStaging 컨테이너를 중지합니다.
    • 4-6 Docker run에서 컨테이너를 실행할 때 --rm 옵션을 사용했습니다. 이 옵션을 사용하면 컨테이너가 중지될 때 볼륨과 컨테이너가 함께 삭제됩니다.